Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion internal/interpreter/function_exprs.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,23 @@ func meta(
return "", err
}

// Check the cache first, so that we don't query the store again
// for a (account, key) pair we already fetched
if value, ok := s.CachedAccountsMeta[*account][*key]; ok {
return value, nil
}

// Note: we do not cache negative lookups, so a key that is absent from
// the store is queried again (and errors again) on every meta() call
meta, fetchMetaErr := s.Store.GetAccountsMetadata(s.ctx, MetadataQuery{
*account: []string{*key},
})
if fetchMetaErr != nil {
return "", QueryMetadataError{WrappedError: fetchMetaErr}
}
s.CachedAccountsMeta = meta
// Merge the fetched metadata into the cache instead of replacing it,
// so that previously cached entries are preserved
s.CachedAccountsMeta.Merge(meta)

// body
accountMeta := s.CachedAccountsMeta[*account]
Expand Down
8 changes: 8 additions & 0 deletions internal/utils/__snapshots__/pretty_csv_test.snap
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,11 @@
| b | 12345 |
| very-very-very-long-key | |
---

[TestPrettyCsvRaggedRows - 1]
| Account | Asset  | Balance |
| alice | EUR/2 | 1 |
| bob | | |
| charlie | USD/1234 | |
| dave | BTC | 3 |
---
19 changes: 13 additions & 6 deletions internal/utils/pretty_csv.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import (
"github.com/formancehq/numscript/internal/ansi"
)

// Fails if the header is shorter than any of the rows
// Rows shorter than the header are padded with empty cells;
// cells beyond the header length are ignored for padding purposes
func CsvPretty(
header []string,
rows [][]string,
Expand All @@ -34,9 +35,10 @@ func CsvPretty(
maxLen := len(fieldName)

for _, row := range rows {
// panics if row[fieldIndex] is out of bounds
// thus we must never have unlabeled cols
maxLen = max(maxLen, len(row[fieldIndex]))
// rows shorter than the header are treated as having empty cells
if fieldIndex < len(row) {
maxLen = max(maxLen, len(row[fieldIndex]))
}
}

maxLengths[fieldIndex] = maxLen
Expand All @@ -61,10 +63,15 @@ func CsvPretty(
// -- Print rows
for _, row := range rows {
var partialRow []string
for index, fieldName := range row {
for index := range header {
// missing cells in ragged rows are rendered as empty strings
var fieldValue string
if index < len(row) {
fieldValue = row[index]
}
partialRow = append(partialRow, fmt.Sprintf("| %-*s ",
maxLengths[index],
fieldName,
fieldValue,
))
}
partialRow = append(partialRow, "|")
Expand Down
15 changes: 15 additions & 0 deletions internal/utils/pretty_csv_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,21 @@ func TestPrettyCsv(t *testing.T) {
snaps.MatchSnapshot(t, out)
}

func TestPrettyCsvRaggedRows(t *testing.T) {
// rows shorter (or longer) than the header must not panic;
// missing cells are rendered as empty strings
out := utils.CsvPretty([]string{
"Account", "Asset", "Balance",
}, [][]string{
{"alice", "EUR/2", "1"},
{"bob"},
{"charlie", "USD/1234"},
{"dave", "BTC", "3", "extra-cell"},
}, true)

snaps.MatchSnapshot(t, out)
}

func TestPrettyCsvMap(t *testing.T) {
out := utils.CsvPrettyMap("Name", "Value", map[string]string{
"a": "0",
Expand Down
104 changes: 104 additions & 0 deletions numscript_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -539,6 +539,110 @@ send [USD/2 10] (

}

func TestMetaFunctionCachesQueries(t *testing.T) {
parseResult := numscript.Parse(`vars {
string $a = meta(@acc1, "key1")
string $b = meta(@acc2, "key2")
string $a_again = meta(@acc1, "key1")
}
set_tx_meta("a", $a)
set_tx_meta("b", $b)
set_tx_meta("a_again", $a_again)
`)

require.Empty(t, parseResult.GetParsingErrors(), "There should not be parsing errors")

store := scopedMetaStore{
meta: interpreter.AccountsMetadata{
"acc1": {"key1": "value1"},
"acc2": {"key2": "value2"},
},
}

res, err := parseResult.Run(context.Background(), numscript.VariablesMap{}, &store)
require.Nil(t, err)

// merging the second query into the cache must not clobber
// the previously cached acc1 metadata
require.Equal(t,
numscript.Metadata{
"a": interpreter.String("value1"),
"b": interpreter.String("value2"),
"a_again": interpreter.String("value1"),
},
res.Metadata)

// acc1's "key1" is cached after the first meta() call,
// so the third meta() call must not query the store again
require.Equal(t,
[]numscript.MetadataQuery{
{"acc1": {"key1"}},
{"acc2": {"key2"}},
},
store.GetMetadataCalls)
}

func TestMetaFunctionMissingKeyStillErrors(t *testing.T) {
parseResult := numscript.Parse(`vars {
string $a = meta(@acc1, "key1")
string $b = meta(@acc1, "missing_key")
}
set_tx_meta("a", $a)
set_tx_meta("b", $b)
`)

require.Empty(t, parseResult.GetParsingErrors(), "There should not be parsing errors")

store := scopedMetaStore{
meta: interpreter.AccountsMetadata{
"acc1": {"key1": "value1"},
},
}

// even though acc1's metadata is (partially) cached,
// a key that is absent from the store must still error
_, err := parseResult.Run(context.Background(), numscript.VariablesMap{}, &store)
require.NotNil(t, err)

notFoundErr, ok := err.(interpreter.MetadataNotFound)
require.True(t, ok, "expected a MetadataNotFound error, got: %v", err)
require.Equal(t, "acc1", notFoundErr.Account)
require.Equal(t, "missing_key", notFoundErr.Key)
}

// A store that, unlike StaticStore, only returns the queried
// (account, key) pairs, and records the metadata queries it receives
type scopedMetaStore struct {
meta interpreter.AccountsMetadata
GetMetadataCalls []numscript.MetadataQuery
}

func (s *scopedMetaStore) GetBalances(ctx context.Context, q interpreter.BalanceQuery) (interpreter.Balances, error) {
return interpreter.StaticStore{}.GetBalances(ctx, q)
}

func (s *scopedMetaStore) GetAccountsMetadata(_ context.Context, q interpreter.MetadataQuery) (interpreter.AccountsMetadata, error) {
s.GetMetadataCalls = append(s.GetMetadataCalls, q)

out := interpreter.AccountsMetadata{}
for account, keys := range q {
for _, key := range keys {
value, ok := s.meta[account][key]
if !ok {
continue
}

accountMeta, ok := out[account]
if !ok {
accountMeta = interpreter.AccountMetadata{}
out[account] = accountMeta
}
accountMeta[key] = value
}
}
return out, nil
}

type ObservableStore struct {
StaticStore interpreter.StaticStore
GetBalancesCalls []numscript.BalanceQuery
Expand Down