From af4a865e86d6fb1abca76bf88acfa490e151e0b6 Mon Sep 17 00:00:00 2001 From: Victor Fusco <1221933+vfusco@users.noreply.github.com> Date: Wed, 1 Jul 2026 21:45:27 -0300 Subject: [PATCH] fix(repository): make filters match their indexes --- internal/repository/postgres/output.go | 9 ++++++ internal/repository/postgres/util.go | 19 ++++++++++-- .../repository/repotest/output_test_cases.go | 29 +++++++++++++++---- 3 files changed, 49 insertions(+), 8 deletions(-) diff --git a/internal/repository/postgres/output.go b/internal/repository/postgres/output.go index 9c54ba735..b18f54b28 100644 --- a/internal/repository/postgres/output.go +++ b/internal/repository/postgres/output.go @@ -193,7 +193,16 @@ func (r *PostgresRepository) ListOutputs( } if f.VoucherAddress != nil { + // A destination-address filter is only meaningful for output methods + // that carry a destination (Voucher, DelegateCallVoucher); bytes 17..36 + // of other outputs are arbitrary payload. The selector IN list, with + // inline literals, is also what lets the planner prove the partial + // predicate of output_raw_data_address_idx. conditions = append(conditions, + SubstrBytea(table.Output.RawData, 1, 4).IN( + ByteaLiteral(voucherSelector), + ByteaLiteral(delegateCallVoucherSelector), + ), SubstrBytea(table.Output.RawData, 17, 20).EQ(postgres.Bytea(f.VoucherAddress.Bytes())), ) } diff --git a/internal/repository/postgres/util.go b/internal/repository/postgres/util.go index 37d6e0e96..5a6b5f42a 100644 --- a/internal/repository/postgres/util.go +++ b/internal/repository/postgres/util.go @@ -60,9 +60,22 @@ func countFromTx(ctx context.Context, tx pgx.Tx, countStmt postgres.SelectStatem return total, err } -// SubstrBytea returns a SUBSTR expression properly typed as ByteaExpression. +// SubstrBytea returns a substring expression properly typed as ByteaExpression. +// It must render exactly as `substring(col FROM n FOR m)` with inline literals: +// PostgreSQL matches expression indexes structurally (by function OID and +// argument tree), so the schema's `substring(... FROM ... FOR ...)` indexes are +// only usable when the query emits the same function with constant arguments. +// `SUBSTR(col, $1, $2)` is a different catalog function and bypasses them. func SubstrBytea(col postgres.ColumnBytea, from, count int64) postgres.ByteaExpression { qualified := pgx.Identifier{col.TableName(), col.Name()}.Sanitize() - raw := fmt.Sprintf("SUBSTR(%s, #from, #count)", qualified) - return postgres.RawBytea(raw, postgres.RawArgs{"#from": from, "#count": count}) + raw := fmt.Sprintf("substring(%s FROM %d FOR %d)", qualified, from, count) + return postgres.RawBytea(raw) +} + +// ByteaLiteral renders b as an inline bytea literal instead of a bind +// parameter. Inline literals are required where the planner must prove a +// partial-index predicate at plan time (e.g. output_raw_data_address_idx): +// a generic plan cannot prove implication from a parameterized IN list. +func ByteaLiteral(b []byte) postgres.ByteaExpression { + return postgres.RawBytea(fmt.Sprintf(`'\x%x'::bytea`, b)) } diff --git a/internal/repository/repotest/output_test_cases.go b/internal/repository/repotest/output_test_cases.go index 88304e350..49eae9f38 100644 --- a/internal/repository/repotest/output_test_cases.go +++ b/internal/repository/repotest/output_test_cases.go @@ -244,27 +244,46 @@ func (s *OutputSuite) TestListOutputs() { s.Run("FilterByVoucherAddress", func() { seed := Seed(s.Ctx, s.T(), s.Repo) - // VoucherAddress filter uses SUBSTR(raw_data, 17, 20) - // to extract a 20-byte address at bytes 17-36 (1-indexed) + // VoucherAddress filter matches substring(raw_data FROM 17 FOR 20) + // (the ABI head destination) but only on voucher-typed outputs: + // bytes 17-36 of other output types are arbitrary payload. + voucherSelector := []byte{0x23, 0x7a, 0x81, 0x6f} + delegateCallVoucherSelector := []byte{0x10, 0x32, 0x1e, 0x8b} + noticeSelector := []byte{0xc2, 0x58, 0xd6, 0xe5} + voucherAddr := UniqueAddress() rawWithVoucher := make([]byte, 64) + copy(rawWithVoucher[0:4], voucherSelector) copy(rawWithVoucher[16:36], voucherAddr.Bytes()) otherAddr := UniqueAddress() rawWithOther := make([]byte, 64) + copy(rawWithOther[0:4], voucherSelector) copy(rawWithOther[16:36], otherAddr.Bytes()) + // A notice whose payload happens to contain the searched address at + // bytes 17-36 must not match the voucher-address filter. + rawNoticeLookalike := make([]byte, 64) + copy(rawNoticeLookalike[0:4], noticeSelector) + copy(rawNoticeLookalike[16:36], voucherAddr.Bytes()) + + // Delegate-call vouchers carry a destination too and must match. + rawWithDelegateCall := make([]byte, 64) + copy(rawWithDelegateCall[0:4], delegateCallVoucherSelector) + copy(rawWithDelegateCall[16:36], voucherAddr.Bytes()) + s.storeAdvanceResult(seed.App.ID, 0, 0, - [][]byte{rawWithVoucher, rawWithOther}, nil) + [][]byte{rawWithVoucher, rawWithOther, rawNoticeLookalike, rawWithDelegateCall}, nil) outputs, total, err := s.Repo.ListOutputs( s.Ctx, seed.App.IApplicationAddress.String(), repository.OutputFilter{VoucherAddress: &voucherAddr}, repository.Pagination{Limit: 10}, false) s.Require().NoError(err) - s.Len(outputs, 1) - s.Equal(uint64(1), total) + s.Len(outputs, 2) + s.Equal(uint64(2), total) s.Equal(rawWithVoucher, outputs[0].RawData) + s.Equal(rawWithDelegateCall, outputs[1].RawData) }) s.Run("Pagination", func() {