From 1b5c04e1eb2760476beee31461690281aeb90f47 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Fri, 23 May 2025 16:16:31 +0200 Subject: [PATCH] feat: add unbounded partial addresses matching over address filters --- go.mod | 4 ++-- internal/storage/ledger/accounts_test.go | 10 +++++++++ .../ledger/resource_aggregated_balances.go | 8 +++++-- internal/storage/ledger/resource_volumes.go | 4 +++- internal/storage/ledger/transactions.go | 22 ++++++++----------- internal/storage/ledger/transactions_test.go | 9 ++++++++ internal/storage/ledger/utils.go | 17 +++++++------- tools/generator/go.mod | 4 ++-- 8 files changed, 50 insertions(+), 28 deletions(-) diff --git a/go.mod b/go.mod index ca7154193f..36d5044cda 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,8 @@ module github.com/formancehq/ledger -go 1.23.0 +go 1.24 -toolchain go1.23.3 +toolchain go1.24.2 replace github.com/formancehq/ledger/pkg/client => ./pkg/client diff --git a/internal/storage/ledger/accounts_test.go b/internal/storage/ledger/accounts_test.go index 1a0095ef02..321778057b 100644 --- a/internal/storage/ledger/accounts_test.go +++ b/internal/storage/ledger/accounts_test.go @@ -179,6 +179,16 @@ func TestAccountsList(t *testing.T) { require.NoError(t, err) require.Len(t, accounts.Data, 3) }) + t.Run("list using filter on address and unbounded length", func(t *testing.T) { + t.Parallel() + accounts, err := store.Accounts().Paginate(ctx, common.OffsetPaginatedQuery[any]{ + Options: common.ResourceQuery[any]{ + Builder: query.Match("address", "account:..."), + }, + }) + require.NoError(t, err) + require.Len(t, accounts.Data, 3) + }) t.Run("list using filter on multiple address", func(t *testing.T) { t.Parallel() accounts, err := store.Accounts().Paginate(ctx, common.OffsetPaginatedQuery[any]{ diff --git a/internal/storage/ledger/resource_aggregated_balances.go b/internal/storage/ledger/resource_aggregated_balances.go index 0ee48a2ea2..d97ee27303 100644 --- a/internal/storage/ledger/resource_aggregated_balances.go +++ b/internal/storage/ledger/resource_aggregated_balances.go @@ -74,7 +74,9 @@ func (h aggregatedBalancesResourceRepositoryHandler) BuildDataset(query common.R Where("effective_date <= ?", query.PIT) } - if query.UseFilter("address", isPartialAddress) { + if query.UseFilter("address", func(value any) bool { + return isPartialAddress(value.(string)) + }) { subQuery := h.store.db.NewSelect(). TableExpr(h.store.GetPrefixedRelationName("accounts")). Column("address_array"). @@ -108,7 +110,9 @@ func (h aggregatedBalancesResourceRepositoryHandler) BuildDataset(query common.R ColumnExpr("(input, output)::"+h.store.GetPrefixedRelationName("volumes")+" as volumes"). Where("ledger = ?", h.store.ledger.Name) - if query.UseFilter("metadata") || query.UseFilter("address", isPartialAddress) { + if query.UseFilter("metadata") || query.UseFilter("address", func(value any) bool { + return isPartialAddress(value.(string)) + }) { subQuery := h.store.db.NewSelect(). TableExpr(h.store.GetPrefixedRelationName("accounts")). Column("address"). diff --git a/internal/storage/ledger/resource_volumes.go b/internal/storage/ledger/resource_volumes.go index 58ba11e667..c019700472 100644 --- a/internal/storage/ledger/resource_volumes.go +++ b/internal/storage/ledger/resource_volumes.go @@ -66,7 +66,9 @@ func (h volumesResourceHandler) BuildDataset(query common.RepositoryHandlerBuild var selectVolumes *bun.SelectQuery - needAddressSegments := query.UseFilter("address", isPartialAddress) + needAddressSegments := query.UseFilter("address", func(value any) bool { + return isPartialAddress(value.(string)) + }) if !query.UsePIT() && !query.UseOOT() { selectVolumes = h.store.db.NewSelect(). Column("asset", "input", "output"). diff --git a/internal/storage/ledger/transactions.go b/internal/storage/ledger/transactions.go index 782be89ca4..d5c2ca26eb 100644 --- a/internal/storage/ledger/transactions.go +++ b/internal/storage/ledger/transactions.go @@ -297,26 +297,22 @@ func (store *Store) DeleteTransactionMetadata(ctx context.Context, id uint64, ke func filterAccountAddressOnTransactions(address string, source, destination bool) string { src := strings.Split(address, ":") - needSegmentCheck := false - for _, segment := range src { - needSegmentCheck = segment == "" - if needSegmentCheck { - break - } - } - - if needSegmentCheck { - m := map[string]any{ - fmt.Sprint(len(src)): nil, - } + if isPartialAddress(address) { + m := map[string]any{} parts := make([]string, 0) for i, segment := range src { if len(segment) == 0 { continue } + if i == len(src)-1 && segment == "..." { + break + } m[fmt.Sprint(i)] = segment } + if src[len(src)-1] != "..." { + m[fmt.Sprint(len(src))] = nil + } data, err := json.Marshal([]any{m}) if err != nil { @@ -345,4 +341,4 @@ func filterAccountAddressOnTransactions(address string, source, destination bool parts = append(parts, fmt.Sprintf("destinations @> '%s'", string(data))) } return strings.Join(parts, " or ") -} \ No newline at end of file +} diff --git a/internal/storage/ledger/transactions_test.go b/internal/storage/ledger/transactions_test.go index 90ee693c3b..27b1ceda55 100644 --- a/internal/storage/ledger/transactions_test.go +++ b/internal/storage/ledger/transactions_test.go @@ -777,6 +777,15 @@ func TestTransactionsList(t *testing.T) { }, expected: []ledger.Transaction{tx5, tx4, tx3}, }, + { + name: "address filter using segment and unbounded segment list", + query: common.ColumnPaginatedQuery[any]{ + Options: common.ResourceQuery[any]{ + Builder: query.Match("account", "users:..."), + }, + }, + expected: []ledger.Transaction{tx5, tx4, tx3}, + }, { name: "filter using metadata", query: common.ColumnPaginatedQuery[any]{ diff --git a/internal/storage/ledger/utils.go b/internal/storage/ledger/utils.go index 8f7d89a94f..713ecf4349 100644 --- a/internal/storage/ledger/utils.go +++ b/internal/storage/ledger/utils.go @@ -5,13 +5,16 @@ import ( "strings" ) -func isSegmentedAddress(address string) bool { +func isPartialAddress(address string) bool { src := strings.Split(address, ":") - for _, segment := range src { + for index, segment := range src { if segment == "" { return true } + if segment == "..." && index == len(src)-1 { + return true + } } return false @@ -22,10 +25,12 @@ func filterAccountAddress(address, key string) string { if isPartialAddress(address) { src := strings.Split(address, ":") - parts = append(parts, fmt.Sprintf("jsonb_array_length(%s_array) = %d", key, len(src))) + if src[len(src)-1] != "" { + parts = append(parts, fmt.Sprintf("jsonb_array_length(%s_array) = %d", key, len(src))) + } for i, segment := range src { - if len(segment) == 0 { + if len(segment) == 0 || segment == "..." { continue } parts = append(parts, fmt.Sprintf("%s_array @@ ('$[%d] == \"%s\"')::jsonpath", key, i, segment)) @@ -37,10 +42,6 @@ func filterAccountAddress(address, key string) string { return strings.Join(parts, " and ") } -func isPartialAddress(address any) bool { - return isSegmentedAddress(address.(string)) -} - func explodeAddress(address string) map[string]any { parts := strings.Split(address, ":") ret := make(map[string]any, len(parts)+1) diff --git a/tools/generator/go.mod b/tools/generator/go.mod index bf194e05b0..9a7e274a41 100644 --- a/tools/generator/go.mod +++ b/tools/generator/go.mod @@ -1,8 +1,8 @@ module github.com/formancehq/ledger/tools/generator -go 1.23.0 +go 1.24 -toolchain go1.23.3 +toolchain go1.24.3 replace github.com/formancehq/ledger => ../..