diff --git a/config_builder.go b/config_builder.go index 0f563d6f267..1fcff0c0c26 100644 --- a/config_builder.go +++ b/config_builder.go @@ -1246,6 +1246,7 @@ func (d *DefaultDatabaseBuilder) BuildDatabase( ) sqlInvoiceDB := invoices.NewSQLStore( + &invoices.SQLStoreConfig{QueryCfg: queryCfg}, invoiceExecutor, clock.NewDefaultClock(), ) diff --git a/docs/release-notes/release-notes-0.21.0.md b/docs/release-notes/release-notes-0.21.0.md index e59f4b61f03..4c9f95da934 100644 --- a/docs/release-notes/release-notes-0.21.0.md +++ b/docs/release-notes/release-notes-0.21.0.md @@ -211,6 +211,26 @@ is fully populated. This new behaviour can be opted out of via the new `--db.sync-graph-cache-load` option. +* [Invoice pagination queries no longer use + `OFFSET`](https://github.com/lightningnetwork/lnd/pull/10700). The five + invoice filter queries previously used `LIMIT+OFFSET` for internal batching, + which requires the database to scan and discard all preceding rows on every + page. All pagination is now cursor-based (`WHERE id >= cursor`), making every + page an efficient primary-key range scan regardless of how deep into the + result set the query is. + +* [Eliminate N+1 per-invoice queries in the SQL invoice + store](https://github.com/lightningnetwork/lnd/pull/10701). The four + paginated list methods (`FetchPendingInvoices`, `InvoicesSettledSince`, + `InvoicesAddedSince`, `QueryInvoices`) previously issued multiple separate + DB round-trips per invoice row (features, HTLCs, HTLC custom records, AMP + sub-invoices, AMP sub-invoice HTLCs). Each method now collects all invoice + IDs for a page and loads the ancillary data in a small set of `WHERE id IN + (…)` batch queries, reducing the total round-trips per page from `O(n)` to + `O(1)`. + The single-invoice lookup path (`LookupInvoice`, `UpdateInvoice`) is + unchanged. + * [Replace the catch-all `FilterInvoices` SQL query with five focused, index-friendly queries](https://github.com/lightningnetwork/lnd/pull/10601) (`FetchPendingInvoices`, `FilterInvoicesBySettleIndex`, diff --git a/invoices/invoiceregistry_test.go b/invoices/invoiceregistry_test.go index 785da115ee4..0ce7897c54d 100644 --- a/invoices/invoiceregistry_test.go +++ b/invoices/invoiceregistry_test.go @@ -155,7 +155,15 @@ func TestInvoiceRegistry(t *testing.T) { testClock := clock.NewTestClock(testTime) - return invpkg.NewSQLStore(executor, testClock), testClock + queryCfg := sqldb.DefaultSQLiteConfig() + if !sqlite { + queryCfg = sqldb.DefaultPostgresConfig() + } + + return invpkg.NewSQLStore( + &invpkg.SQLStoreConfig{QueryCfg: queryCfg}, + executor, testClock, + ), testClock } for _, test := range testList { diff --git a/invoices/invoices_test.go b/invoices/invoices_test.go index 0f7e473ebff..5a0ebb7dba4 100644 --- a/invoices/invoices_test.go +++ b/invoices/invoices_test.go @@ -254,9 +254,14 @@ func TestInvoices(t *testing.T) { // that we also cover query pagination. const testPaginationLimit = 3 + queryCfg := &sqldb.QueryConfig{ + MaxPageSize: testPaginationLimit, + MaxBatchSize: testPaginationLimit, + } + return invpkg.NewSQLStore( + &invpkg.SQLStoreConfig{QueryCfg: queryCfg}, executor, testClock, - invpkg.WithPaginationLimit(testPaginationLimit), ) } diff --git a/invoices/kv_sql_migration_test.go b/invoices/kv_sql_migration_test.go index 709a3c8e106..473a8b095f8 100644 --- a/invoices/kv_sql_migration_test.go +++ b/invoices/kv_sql_migration_test.go @@ -58,8 +58,15 @@ func TestMigrationWithChannelDB(t *testing.T) { testClock := clock.NewTestClock(time.Unix(1, 0)) - return invpkg.NewSQLStore(invoiceExecutor, testClock), - genericExecutor + queryCfg := sqldb.DefaultSQLiteConfig() + if !sqlite { + queryCfg = sqldb.DefaultPostgresConfig() + } + + return invpkg.NewSQLStore( + &invpkg.SQLStoreConfig{QueryCfg: queryCfg}, + invoiceExecutor, testClock, + ), genericExecutor } migrationTest := func(t *testing.T, kvStore *channeldb.DB, diff --git a/invoices/sql_migration_test.go b/invoices/sql_migration_test.go index 3005909ee87..fcef40799a4 100644 --- a/invoices/sql_migration_test.go +++ b/invoices/sql_migration_test.go @@ -283,7 +283,15 @@ func TestMigrateSingleInvoiceRapid(t *testing.T) { testClock := clock.NewTestClock(time.Unix(1, 0)) - return NewSQLStore(executor, testClock) + queryCfg := sqldb.DefaultSQLiteConfig() + if !sqlite { + queryCfg = sqldb.DefaultPostgresConfig() + } + + return NewSQLStore( + &SQLStoreConfig{QueryCfg: queryCfg}, + executor, testClock, + ) } // Define property-based test using rapid. diff --git a/invoices/sql_store.go b/invoices/sql_store.go index 0628a5d5445..0f8d569eaa1 100644 --- a/invoices/sql_store.go +++ b/invoices/sql_store.go @@ -21,10 +21,6 @@ import ( ) const ( - // defaultQueryPaginationLimit is used in the LIMIT clause of the SQL - // queries to limit the number of rows returned. - defaultQueryPaginationLimit = 100 - // invoiceProgressLogInterval is the interval we use limiting the // logging output of invoice processing. invoiceProgressLogInterval = 30 * time.Second @@ -46,6 +42,11 @@ var ( invoiceCreatedBeforeDefault = time.Date( 9999, 12, 31, 23, 59, 59, 0, time.UTC, ) + + // errMaxInvoicesReached is a sentinel error returned by the + // QueryInvoices processItem callback to signal that the NumMaxInvoices + // limit has been reached and pagination can stop early. + errMaxInvoicesReached = errors.New("max invoices reached") ) // SQLInvoiceQueries is an interface that defines the set of operations that can @@ -118,6 +119,25 @@ type SQLInvoiceQueries interface { //nolint:interfacebloat GetInvoiceHTLCs(ctx context.Context, invoiceID int64) ([]sqlc.InvoiceHtlc, error) + // Batch variants used by the paginated list callers to replace N+1 + // per-invoice queries with a single IN-list query per page. + GetInvoiceFeaturesForInvoices(ctx context.Context, + invoiceIDs []int64) ([]sqlc.InvoiceFeature, error) + + GetInvoiceHTLCsForInvoices(ctx context.Context, + invoiceIDs []int64) ([]sqlc.InvoiceHtlc, error) + + GetInvoiceHTLCCustomRecordsForInvoices(ctx context.Context, + invoiceIDs []int64) ( + []sqlc.GetInvoiceHTLCCustomRecordsForInvoicesRow, error) + + FetchAMPSubInvoicesForInvoices(ctx context.Context, + invoiceIDs []int64) ([]sqlc.AmpSubInvoice, error) + + FetchAMPSubInvoiceHTLCsForInvoices(ctx context.Context, + invoiceIDs []int64) ( + []sqlc.FetchAMPSubInvoiceHTLCsForInvoicesRow, error) + UpdateInvoiceState(ctx context.Context, arg sqlc.UpdateInvoiceStateParams) (sql.Result, error) @@ -207,52 +227,238 @@ type BatchedSQLInvoiceQueries interface { sqldb.BatchedTx[SQLInvoiceQueries] } +// SQLStoreConfig holds the configuration for the SQLStore. +type SQLStoreConfig struct { + // QueryCfg holds configuration values for SQL queries, including both + // the page size for cursor-based pagination and the batch size for + // IN-list batch loading. + QueryCfg *sqldb.QueryConfig +} + // SQLStore represents a storage backend. type SQLStore struct { + cfg *SQLStoreConfig db BatchedSQLInvoiceQueries clock clock.Clock - opts SQLStoreOptions } -// SQLStoreOptions holds the options for the SQL store. -type SQLStoreOptions struct { - paginationLimit int -} +// NewSQLStore creates a new SQLStore instance given a open +// BatchedSQLInvoiceQueries storage backend. +func NewSQLStore(cfg *SQLStoreConfig, db BatchedSQLInvoiceQueries, + clock clock.Clock) *SQLStore { -// defaultSQLStoreOptions returns the default options for the SQL store. -func defaultSQLStoreOptions() SQLStoreOptions { - return SQLStoreOptions{ - paginationLimit: defaultQueryPaginationLimit, + return &SQLStore{ + cfg: cfg, + db: db, + clock: clock, } } -// SQLStoreOption is a functional option that can be used to optionally modify -// the behavior of the SQL store. -type SQLStoreOption func(*SQLStoreOptions) +// invoiceDetailsData holds pre-loaded ancillary data for a page of invoice +// rows, eliminating N+1 per-invoice queries when iterating large result sets. +type invoiceDetailsData struct { + // features maps invoice_id → feature rows. + features map[int64][]sqlc.InvoiceFeature -// WithPaginationLimit sets the pagination limit for the SQL store queries that -// paginate results. -func WithPaginationLimit(limit int) SQLStoreOption { - return func(o *SQLStoreOptions) { - o.paginationLimit = limit - } + // htlcs maps invoice_id → HTLC rows (non-AMP invoices only). + htlcs map[int64][]sqlc.InvoiceHtlc + + // customRecords maps invoice_htlcs.id (the table primary key, distinct + // from the channel-level htlc_id) to the custom records for that HTLC. + // Shared between the AMP and non-AMP code paths. + customRecords map[int64][]sqlc.GetInvoiceHTLCCustomRecordsForInvoicesRow + + // ampSubInvoices maps invoice_id → AMP sub-invoice rows. + ampSubInvoices map[int64][]sqlc.AmpSubInvoice + + // ampHTLCs maps invoice_id → AMP sub-invoice HTLC rows. + ampHTLCs map[int64][]sqlc.FetchAMPSubInvoiceHTLCsForInvoicesRow } -// NewSQLStore creates a new SQLStore instance given a open -// BatchedSQLInvoiceQueries storage backend. -func NewSQLStore(db BatchedSQLInvoiceQueries, - clock clock.Clock, options ...SQLStoreOption) *SQLStore { +// batchLoadInvoiceFeatures fetches invoice features for all IDs in one or more +// IN-list queries and stores the results in data.features. +func batchLoadInvoiceFeatures(ctx context.Context, cfg *sqldb.QueryConfig, + db SQLInvoiceQueries, ids []int64, data *invoiceDetailsData) error { + + return sqldb.ExecuteBatchQuery( + ctx, cfg, ids, + func(id int64) int64 { return id }, + func(ctx context.Context, batch []int64) ( + []sqlc.InvoiceFeature, error) { + + return db.GetInvoiceFeaturesForInvoices(ctx, batch) + }, + func(_ context.Context, row sqlc.InvoiceFeature) error { + data.features[row.InvoiceID] = append( + data.features[row.InvoiceID], row, + ) + + return nil + }, + ) +} + +// batchLoadInvoiceHTLCs fetches non-AMP invoice HTLCs for all IDs and stores +// them in data.htlcs. +func batchLoadInvoiceHTLCs(ctx context.Context, cfg *sqldb.QueryConfig, + db SQLInvoiceQueries, ids []int64, data *invoiceDetailsData) error { + + return sqldb.ExecuteBatchQuery( + ctx, cfg, ids, + func(id int64) int64 { return id }, + func(ctx context.Context, batch []int64) ( + []sqlc.InvoiceHtlc, error) { + + return db.GetInvoiceHTLCsForInvoices(ctx, batch) + }, + func(_ context.Context, row sqlc.InvoiceHtlc) error { + data.htlcs[row.InvoiceID] = append( + data.htlcs[row.InvoiceID], row, + ) + + return nil + }, + ) +} + +// batchLoadInvoiceHTLCCustomRecords fetches HTLC custom records for the given +// invoice IDs and stores them in data.customRecords, keyed by +// invoice_htlcs.id. Covers both AMP and non-AMP HTLCs. +func batchLoadInvoiceHTLCCustomRecords(ctx context.Context, + cfg *sqldb.QueryConfig, db SQLInvoiceQueries, ids []int64, + data *invoiceDetailsData) error { + + //nolint:ll + return sqldb.ExecuteBatchQuery( + ctx, cfg, ids, + func(id int64) int64 { return id }, + func(ctx context.Context, batch []int64) ( + []sqlc.GetInvoiceHTLCCustomRecordsForInvoicesRow, + error) { + + return db.GetInvoiceHTLCCustomRecordsForInvoices( + ctx, batch, + ) + }, + func(_ context.Context, + row sqlc.GetInvoiceHTLCCustomRecordsForInvoicesRow) error { - opts := defaultSQLStoreOptions() - for _, applyOption := range options { - applyOption(&opts) + data.customRecords[row.HtlcID] = append( + data.customRecords[row.HtlcID], row, + ) + + return nil + }, + ) +} + +// batchLoadAMPSubInvoices fetches AMP sub-invoices for the given invoice IDs +// and stores them in data.ampSubInvoices. +func batchLoadAMPSubInvoices(ctx context.Context, cfg *sqldb.QueryConfig, + db SQLInvoiceQueries, ids []int64, data *invoiceDetailsData) error { + + return sqldb.ExecuteBatchQuery( + ctx, cfg, ids, + func(id int64) int64 { return id }, + func(ctx context.Context, batch []int64) ( + []sqlc.AmpSubInvoice, error) { + + return db.FetchAMPSubInvoicesForInvoices(ctx, batch) + }, + func(_ context.Context, row sqlc.AmpSubInvoice) error { + data.ampSubInvoices[row.InvoiceID] = append( + data.ampSubInvoices[row.InvoiceID], row, + ) + + return nil + }, + ) +} + +// batchLoadAMPSubInvoiceHTLCs fetches AMP sub-invoice HTLCs for the given +// invoice IDs and stores them in data.ampHTLCs. +func batchLoadAMPSubInvoiceHTLCs(ctx context.Context, cfg *sqldb.QueryConfig, + db SQLInvoiceQueries, ids []int64, data *invoiceDetailsData) error { + + return sqldb.ExecuteBatchQuery( + ctx, cfg, ids, + func(id int64) int64 { return id }, + func(ctx context.Context, batch []int64) ( + []sqlc.FetchAMPSubInvoiceHTLCsForInvoicesRow, error) { + + return db.FetchAMPSubInvoiceHTLCsForInvoices(ctx, batch) + }, + func(_ context.Context, + row sqlc.FetchAMPSubInvoiceHTLCsForInvoicesRow) error { + + data.ampHTLCs[row.InvoiceID] = append( + data.ampHTLCs[row.InvoiceID], row, + ) + + return nil + }, + ) +} + +// batchLoadInvoiceDetailsData pre-loads all ancillary data for a page of +// invoice IDs in a small constant number of IN-list queries, replacing the +// per-invoice N+1 query pattern. It is designed to satisfy the +// CollectAndBatchDataQueryFunc signature used by +// ExecuteCollectAndBatchWithSharedDataQuery. +func batchLoadInvoiceDetailsData(ctx context.Context, cfg *sqldb.QueryConfig, + db SQLInvoiceQueries, ids []int64) (*invoiceDetailsData, error) { + + //nolint:ll + data := &invoiceDetailsData{ + features: make(map[int64][]sqlc.InvoiceFeature), + htlcs: make(map[int64][]sqlc.InvoiceHtlc), + customRecords: make(map[int64][]sqlc.GetInvoiceHTLCCustomRecordsForInvoicesRow), + ampSubInvoices: make(map[int64][]sqlc.AmpSubInvoice), + ampHTLCs: make(map[int64][]sqlc.FetchAMPSubInvoiceHTLCsForInvoicesRow), } - return &SQLStore{ - db: db, - clock: clock, - opts: opts, + if len(ids) == 0 { + return data, nil } + + // Load features for all invoices. + err := batchLoadInvoiceFeatures(ctx, cfg, db, ids, data) + if err != nil { + return nil, fmt.Errorf("failed to batch-load invoice "+ + "features: %w", err) + } + + // Load non-AMP HTLCs. For AMP invoice IDs this query returns no rows, + // so there is no correctness concern in querying all IDs at once. + if err := batchLoadInvoiceHTLCs(ctx, cfg, db, ids, data); err != nil { + return nil, fmt.Errorf("failed to batch-load invoice "+ + "HTLCs: %w", err) + } + + // Load HTLC custom records for all invoices — both AMP and non-AMP + // HTLCs can carry custom records and the same query covers both. + if err := batchLoadInvoiceHTLCCustomRecords( + ctx, cfg, db, ids, data, + ); err != nil { + return nil, fmt.Errorf("failed to batch-load HTLC custom "+ + "records: %w", err) + } + + // Load AMP sub-invoices. For non-AMP invoice IDs this query returns no + // rows. + if err := batchLoadAMPSubInvoices(ctx, cfg, db, ids, data); err != nil { + return nil, fmt.Errorf("failed to batch-load AMP "+ + "sub-invoices: %w", err) + } + + // Load AMP sub-invoice HTLCs. + err = batchLoadAMPSubInvoiceHTLCs(ctx, cfg, db, ids, data) + if err != nil { + return nil, fmt.Errorf("failed to batch-load AMP "+ + "sub-invoice HTLCs: %w", err) + } + + return data, nil } func makeInsertInvoiceParams(invoice *Invoice, paymentHash lntypes.Hash) ( @@ -517,7 +723,7 @@ func fetchInvoice(ctx context.Context, db SQLInvoiceQueries, ref InvoiceRef) ( // Fetch the rest of the invoice data and fill the invoice struct. _, invoice, err := fetchInvoiceData( - ctx, db, sqlInvoice, setID, fetchAmpHtlcs, + ctx, db, sqlInvoice, setID, fetchAmpHtlcs, nil, ) if err != nil { return nil, err @@ -771,32 +977,52 @@ func (i *SQLStore) FetchPendingInvoices(ctx context.Context) ( readTxOpt := sqldb.ReadTxOpt() err := i.db.ExecTx(ctx, readTxOpt, func(db SQLInvoiceQueries) error { - return queryWithLimit(func(offset int) (int, error) { - params := sqlc.FetchPendingInvoicesParams{ - NumOffset: int32(offset), - NumLimit: int32(i.opts.paginationLimit), - } + queryFunc := func(ctx context.Context, cursor int64, + limit int32) ([]sqlc.Invoice, error) { - rows, err := db.FetchPendingInvoices(ctx, params) + rows, err := db.FetchPendingInvoices(ctx, + sqlc.FetchPendingInvoicesParams{ + IDCursor: cursor, + NumLimit: limit, + }, + ) if err != nil && !errors.Is(err, sql.ErrNoRows) { - return 0, fmt.Errorf("unable to get invoices "+ - "from db: %w", err) + return nil, fmt.Errorf("unable to get "+ + "invoices from db: %w", err) } - // Load all the information for the invoices. - for _, row := range rows { + return rows, nil + } + + return sqldb.ExecuteCollectAndBatchWithSharedDataQuery( + ctx, i.cfg.QueryCfg, int64(0), + queryFunc, + func(row sqlc.Invoice) int64 { return row.ID }, + func(row sqlc.Invoice) (int64, error) { + return row.ID, nil + }, + func(ctx context.Context, ids []int64) ( + *invoiceDetailsData, error) { + + return batchLoadInvoiceDetailsData( + ctx, i.cfg.QueryCfg, db, ids, + ) + }, + func(ctx context.Context, row sqlc.Invoice, + batchData *invoiceDetailsData) error { + hash, invoice, err := fetchInvoiceData( - ctx, db, row, nil, true, + ctx, db, row, nil, true, batchData, ) if err != nil { - return 0, err + return err } invoices[*hash] = *invoice - } - return len(rows), nil - }, i.opts.paginationLimit) + return nil + }, + ) }, func() { invoices = make(map[lntypes.Hash]Invoice) }) @@ -830,28 +1056,48 @@ func (i *SQLStore) InvoicesSettledSince(ctx context.Context, idx uint64) ( readTxOpt := sqldb.ReadTxOpt() err := i.db.ExecTx(ctx, readTxOpt, func(db SQLInvoiceQueries) error { - err := queryWithLimit(func(offset int) (int, error) { - // settle_index is always provided here so the - // invoices_settle_index_idx index can be used. - params := sqlc.FilterInvoicesBySettleIndexParams{ - SettleIndexGet: sqldb.SQLInt64(idx + 1), - NumOffset: int32(offset), - NumLimit: int32(i.opts.paginationLimit), - } - - rows, err := db.FilterInvoicesBySettleIndex(ctx, params) + // settle_index is always provided so the + // invoices_settle_index_idx index is used. + queryFunc := func(ctx context.Context, cursor int64, + limit int32) ([]sqlc.Invoice, error) { + + rows, err := db.FilterInvoicesBySettleIndex(ctx, + sqlc.FilterInvoicesBySettleIndexParams{ + SettleIndexGet: sqldb.SQLInt64(idx + 1), + IDCursor: cursor, + NumLimit: limit, + }, + ) if err != nil && !errors.Is(err, sql.ErrNoRows) { - return 0, fmt.Errorf("unable to get invoices "+ - "from db: %w", err) + return nil, fmt.Errorf("unable to get "+ + "invoices from db: %w", err) } - // Load all the information for the invoices. - for _, row := range rows { + return rows, nil + } + + err := sqldb.ExecuteCollectAndBatchWithSharedDataQuery( + ctx, i.cfg.QueryCfg, int64(0), + queryFunc, + func(row sqlc.Invoice) int64 { return row.ID }, + func(row sqlc.Invoice) (int64, error) { + return row.ID, nil + }, + func(ctx context.Context, ids []int64) ( + *invoiceDetailsData, error) { + + return batchLoadInvoiceDetailsData( + ctx, i.cfg.QueryCfg, db, ids, + ) + }, + func(ctx context.Context, row sqlc.Invoice, + batchData *invoiceDetailsData) error { + _, invoice, err := fetchInvoiceData( - ctx, db, row, nil, true, + ctx, db, row, nil, true, batchData, ) if err != nil { - return 0, fmt.Errorf("unable to fetch "+ + return fmt.Errorf("unable to fetch "+ "invoice(id=%d) from db: %w", row.ID, err) } @@ -869,10 +1115,10 @@ func (i *SQLStore) InvoicesSettledSince(ctx context.Context, idx uint64) ( lastLogTime = time.Now() } - } - return len(rows), nil - }, i.opts.paginationLimit) + return nil + }, + ) if err != nil { return err } @@ -888,10 +1134,13 @@ func (i *SQLStore) InvoicesSettledSince(ctx context.Context, idx uint64) ( return err } + // Collect invoice IDs and build sqlc.Invoice wrappers for the + // AMP settled rows so we can batch-load their ancillary data. + ampIDs := make([]int64, 0, len(ampInvoices)) + ampSQLInvoices := make([]sqlc.Invoice, 0, len(ampInvoices)) for _, ampInvoice := range ampInvoices { - // Convert the row to a sqlc.Invoice so we can use the - // existing fetchInvoiceData function. - sqlInvoice := sqlc.Invoice{ + ampIDs = append(ampIDs, ampInvoice.ID) + ampSQLInvoices = append(ampSQLInvoices, sqlc.Invoice{ ID: ampInvoice.ID, Hash: ampInvoice.Hash, Preimage: ampInvoice.Preimage, @@ -909,12 +1158,26 @@ func (i *SQLStore) InvoicesSettledSince(ctx context.Context, idx uint64) ( IsHodl: ampInvoice.IsHodl, IsKeysend: ampInvoice.IsKeysend, CreatedAt: ampInvoice.CreatedAt.UTC(), - } + }) + } + + // Batch-load ancillary data for all AMP settled invoices. + ampBatchData, err := batchLoadInvoiceDetailsData( + ctx, i.cfg.QueryCfg, db, ampIDs, + ) + if err != nil { + return fmt.Errorf("unable to batch-load AMP invoice "+ + "data: %w", err) + } + + for idx2, ampInvoice := range ampInvoices { + sqlInvoice := ampSQLInvoices[idx2] // Fetch the state and HTLCs for this AMP sub invoice. _, invoice, err := fetchInvoiceData( ctx, db, sqlInvoice, (*[32]byte)(ampInvoice.SetID), true, + ampBatchData, ) if err != nil { return fmt.Errorf("unable to fetch "+ @@ -977,28 +1240,49 @@ func (i *SQLStore) InvoicesAddedSince(ctx context.Context, idx uint64) ( readTxOpt := sqldb.ReadTxOpt() err := i.db.ExecTx(ctx, readTxOpt, func(db SQLInvoiceQueries) error { - return queryWithLimit(func(offset int) (int, error) { - // id is always provided here so the primary-key - // index is used for this range scan. - params := sqlc.FilterInvoicesByAddIndexParams{ - AddIndexGet: int64(idx + 1), - NumOffset: int32(offset), - NumLimit: int32(i.opts.paginationLimit), - } - - rows, err := db.FilterInvoicesByAddIndex(ctx, params) + // id is always provided here so the primary-key index is used + // for this range scan. The initial cursor is idx+1 so the first + // page fetches invoices with id >= idx+1 (inclusive). After + // each row the cursor advances to row.ID + 1 for the next page. + queryFunc := func(ctx context.Context, cursor int64, + limit int32) ([]sqlc.Invoice, error) { + + rows, err := db.FilterInvoicesByAddIndex(ctx, + sqlc.FilterInvoicesByAddIndexParams{ + AddIndexGet: cursor, + NumLimit: limit, + }, + ) if err != nil && !errors.Is(err, sql.ErrNoRows) { - return 0, fmt.Errorf("unable to get invoices "+ - "from db: %w", err) + return nil, fmt.Errorf("unable to get "+ + "invoices from db: %w", err) } - // Load all the information for the invoices. - for _, row := range rows { + return rows, nil + } + + return sqldb.ExecuteCollectAndBatchWithSharedDataQuery( + ctx, i.cfg.QueryCfg, int64(idx+1), + queryFunc, + func(row sqlc.Invoice) int64 { return row.ID + 1 }, + func(row sqlc.Invoice) (int64, error) { + return row.ID, nil + }, + func(ctx context.Context, ids []int64) ( + *invoiceDetailsData, error) { + + return batchLoadInvoiceDetailsData( + ctx, i.cfg.QueryCfg, db, ids, + ) + }, + func(ctx context.Context, row sqlc.Invoice, + batchData *invoiceDetailsData) error { + _, invoice, err := fetchInvoiceData( - ctx, db, row, nil, true, + ctx, db, row, nil, true, batchData, ) if err != nil { - return 0, err + return err } result = append(result, *invoice) @@ -1013,10 +1297,10 @@ func (i *SQLStore) InvoicesAddedSince(ctx context.Context, idx uint64) ( lastLogTime = time.Now() } - } - return len(rows), nil - }, i.opts.paginationLimit) + return nil + }, + ) }, func() { result = nil }) @@ -1064,77 +1348,120 @@ func (i *SQLStore) QueryInvoices(ctx context.Context, readTxOpt := sqldb.ReadTxOpt() err := i.db.ExecTx(ctx, readTxOpt, func(db SQLInvoiceQueries) error { - return queryWithLimit(func(offset int) (int, error) { - var ( - rows []sqlc.Invoice - err error - limit = int32(i.opts.paginationLimit) - ) + // For reverse queries the cursor is an inclusive upper bound on + // id (id <= cursor); after each row it advances to row.ID - 1. + // Start at IndexOffset - 1, or MaxInt64 to begin from the most + // recent invoice. + // For forward queries the cursor is an inclusive lower bound + // (id >= cursor); after each row it advances to row.ID + 1. + // Start at IndexOffset + 1 so the invoice at IndexOffset itself + // is excluded (matching the old behaviour). + // + //nolint:ll + var ( + initialCursor int64 + extractCursor sqldb.CursorExtractFunc[sqlc.Invoice, int64] + pageQueryFunc sqldb.PagedQueryFunc[int64, sqlc.Invoice] + ) - if q.Reversed { - // For reverse queries the upper id bound is - // always provided. When no offset is given we - // start from the most recently added invoice. - addIndexLet := int64(math.MaxInt64) - if q.IndexOffset != 0 { - // The invoice at IndexOffset must not - // appear in the results. - addIndexLet = int64(q.IndexOffset) - 1 - } + if q.Reversed { + initialCursor = int64(math.MaxInt64) + if q.IndexOffset != 0 { + initialCursor = int64(q.IndexOffset) - 1 + } - params := sqlc.FilterInvoicesReverseParams{ - AddIndexLet: addIndexLet, - PendingOnly: q.PendingOnly, - CreatedAfter: createdAfter, - CreatedBefore: createdBefore, - NumOffset: int32(offset), - NumLimit: limit, - } + extractCursor = func(row sqlc.Invoice) int64 { + return row.ID - 1 + } - rows, err = db.FilterInvoicesReverse( - ctx, params, + pageQueryFunc = func(ctx context.Context, cursor int64, + limit int32) ([]sqlc.Invoice, error) { + + rows, err := db.FilterInvoicesReverse(ctx, + sqlc.FilterInvoicesReverseParams{ + AddIndexLet: cursor, + PendingOnly: q.PendingOnly, + CreatedAfter: createdAfter, + CreatedBefore: createdBefore, + NumLimit: limit, + }, ) - } else { - // For forward queries the lower id bound is - // always provided. IndexOffset 0 means "start - // from the very first invoice" (id >= 1). - params := sqlc.FilterInvoicesForwardParams{ - AddIndexGet: int64(q.IndexOffset) + 1, - PendingOnly: q.PendingOnly, - CreatedAfter: createdAfter, - CreatedBefore: createdBefore, - NumOffset: int32(offset), - NumLimit: limit, + if err != nil && + !errors.Is(err, sql.ErrNoRows) { + + return nil, fmt.Errorf("unable to get "+ + "invoices from db: %w", err) } - rows, err = db.FilterInvoicesForward( - ctx, params, - ) + return rows, nil } + } else { + initialCursor = int64(q.IndexOffset) + 1 - if err != nil && !errors.Is(err, sql.ErrNoRows) { - return 0, fmt.Errorf("unable to get invoices "+ - "from db: %w", err) + extractCursor = func(row sqlc.Invoice) int64 { + return row.ID + 1 + } + + pageQueryFunc = func(ctx context.Context, cursor int64, + limit int32) ([]sqlc.Invoice, error) { + + rows, err := db.FilterInvoicesForward(ctx, + sqlc.FilterInvoicesForwardParams{ + AddIndexGet: cursor, + PendingOnly: q.PendingOnly, + CreatedAfter: createdAfter, + CreatedBefore: createdBefore, + NumLimit: limit, + }, + ) + if err != nil && + !errors.Is(err, sql.ErrNoRows) { + + return nil, fmt.Errorf("unable to get "+ + "invoices from db: %w", err) + } + + return rows, nil } + } + + err := sqldb.ExecuteCollectAndBatchWithSharedDataQuery( + ctx, i.cfg.QueryCfg, initialCursor, + pageQueryFunc, + extractCursor, + func(row sqlc.Invoice) (int64, error) { + return row.ID, nil + }, + func(ctx context.Context, ids []int64) ( + *invoiceDetailsData, error) { + + return batchLoadInvoiceDetailsData( + ctx, i.cfg.QueryCfg, db, ids, + ) + }, + func(ctx context.Context, row sqlc.Invoice, + batchData *invoiceDetailsData) error { - // Load all the information for the invoices. - for _, row := range rows { _, invoice, err := fetchInvoiceData( - ctx, db, row, nil, true, + ctx, db, row, nil, true, batchData, ) if err != nil { - return 0, err + return err } invoices = append(invoices, *invoice) - if len(invoices) == int(q.NumMaxInvoices) { - return 0, nil + return errMaxInvoicesReached } - } - return len(rows), nil - }, i.opts.paginationLimit) + return nil + }, + ) + if errors.Is(err, errMaxInvoicesReached) { + return nil + } + + return err }, func() { invoices = nil }) @@ -1653,9 +1980,13 @@ func (i *SQLStore) DeleteCanceledInvoices(ctx context.Context) error { // invoice is AMP and the setID is not nil, then it will also fetch the AMP // state and HTLCs for the given setID, otherwise for all AMP sub invoices of // the invoice. If fetchAmpHtlcs is true, it will also fetch the AMP HTLCs. +// +// When batchData is non-nil the function reads ancillary data from the +// pre-loaded maps instead of issuing per-invoice DB queries, replacing the +// N+1 pattern in paginated callers. func fetchInvoiceData(ctx context.Context, db SQLInvoiceQueries, - row sqlc.Invoice, setID *[32]byte, fetchAmpHtlcs bool) (*lntypes.Hash, - *Invoice, error) { + row sqlc.Invoice, setID *[32]byte, fetchAmpHtlcs bool, + batchData *invoiceDetailsData) (*lntypes.Hash, *Invoice, error) { // Unmarshal the common data. hash, invoice, err := unmarshalInvoice(row) @@ -1664,7 +1995,38 @@ func fetchInvoiceData(ctx context.Context, db SQLInvoiceQueries, "invoice(id=%d) from db: %w", row.ID, err) } - // Fetch the invoice features. + if batchData != nil { + // Fast path: build from pre-loaded batch data. + invoice.Terms.Features = buildFeaturesFromBatch( + batchData.features[row.ID], + ) + + if invoice.IsAMP() { + ampState, ampHtlcs, err := buildAMPStateFromBatch( + batchData, row.ID, setID, fetchAmpHtlcs, + ) + if err != nil { + return nil, nil, err + } + + invoice.AMPState = ampState + invoice.Htlcs = ampHtlcs + } else { + htlcs, err := buildHTLCsFromBatch(batchData, row.ID) + if err != nil { + return nil, nil, err + } + + if len(htlcs) > 0 { + invoice.Htlcs = htlcs + } + } + + return hash, invoice, nil + } + + // Slow path: issue per-invoice DB queries (used for single-invoice + // lookups where batch loading is not applicable). features, err := getInvoiceFeatures(ctx, db, row.ID) if err != nil { return nil, nil, err @@ -1702,6 +2064,214 @@ func fetchInvoiceData(ctx context.Context, db SQLInvoiceQueries, return hash, invoice, nil } +// buildFeaturesFromBatch builds a FeatureVector from pre-loaded feature rows. +func buildFeaturesFromBatch(rows []sqlc.InvoiceFeature) *lnwire.FeatureVector { + features := lnwire.EmptyFeatureVector() + for _, row := range rows { + features.Set(lnwire.FeatureBit(row.Feature)) + } + + return features +} + +// buildHTLCsFromBatch reconstructs the HTLC map for a non-AMP invoice from +// pre-loaded batch data. +func buildHTLCsFromBatch(data *invoiceDetailsData, invoiceID int64) ( + map[CircuitKey]*InvoiceHTLC, error) { + + htlcRows := data.htlcs[invoiceID] + if len(htlcRows) == 0 { + return nil, nil + } + + htlcs := make(map[CircuitKey]*InvoiceHTLC, len(htlcRows)) + + for _, row := range htlcRows { + circuitKey, htlc, err := unmarshalInvoiceHTLC(row) + if err != nil { + return nil, fmt.Errorf("unable to unmarshal "+ + "htlc(%d): %w", row.ID, err) + } + + crRows := data.customRecords[row.ID] + cr := make(record.CustomSet, len(crRows)) + for _, crRow := range crRows { + value := crRow.Value + if value == nil { + value = []byte{} + } + cr[uint64(crRow.Key)] = value + } + htlc.CustomRecords = cr + + htlcs[circuitKey] = htlc + } + + return htlcs, nil +} + +// buildAMPStateFromBatch reconstructs the AMP state and optional HTLC set for +// an AMP invoice from pre-loaded batch data. When setID is non-nil only the +// matching sub-invoice is included, mirroring the behaviour of fetchAmpState. +func buildAMPStateFromBatch(data *invoiceDetailsData, invoiceID int64, + setID *[32]byte, fetchHtlcs bool) (AMPInvoiceState, HTLCSet, error) { + + ampState := make(AMPInvoiceState) + + for _, row := range data.ampSubInvoices[invoiceID] { + if setID != nil && !bytes.Equal(row.SetID, setID[:]) { + continue + } + + if len(row.SetID) != 32 { + return nil, nil, fmt.Errorf("invalid set id "+ + "length: %d", len(row.SetID)) + } + + var rowSetID [32]byte + copy(rowSetID[:], row.SetID) + + var settleDate time.Time + if row.SettledAt.Valid { + settleDate = row.SettledAt.Time.Local() + } + + ampState[rowSetID] = InvoiceStateAMP{ + State: HtlcState(row.State), + SettleIndex: uint64(row.SettleIndex.Int64), + SettleDate: settleDate, + InvoiceKeys: make(map[models.CircuitKey]struct{}), + } + } + + if !fetchHtlcs { + return ampState, nil, nil + } + + ampHtlcs := make(map[models.CircuitKey]*InvoiceHTLC) + + for _, row := range data.ampHTLCs[invoiceID] { + if setID != nil && !bytes.Equal(row.SetID, setID[:]) { + continue + } + + uint64ChanID, err := strconv.ParseUint(row.ChanID, 10, 64) + if err != nil { + return nil, nil, err + } + + chanID := lnwire.NewShortChanIDFromInt(uint64ChanID) + + if row.HtlcID < 0 { + return nil, nil, fmt.Errorf("invalid HTLC ID "+ + "value: %v", row.HtlcID) + } + + circuitKey := CircuitKey{ + ChanID: chanID, + HtlcID: uint64(row.HtlcID), + } + + htlc := &InvoiceHTLC{ + Amt: lnwire.MilliSatoshi(row.AmountMsat), + AcceptHeight: uint32(row.AcceptHeight), + AcceptTime: row.AcceptTime.Local(), + Expiry: uint32(row.ExpiryHeight), + State: HtlcState(row.State), + } + + if row.TotalMppMsat.Valid { + htlc.MppTotalAmt = lnwire.MilliSatoshi( + row.TotalMppMsat.Int64, + ) + } + + if row.ResolveTime.Valid { + htlc.ResolveTime = row.ResolveTime.Time.Local() + } + + var ( + rootShare [32]byte + rowSetID [32]byte + ) + + if len(row.RootShare) != 32 { + return nil, nil, fmt.Errorf("invalid root share "+ + "length: %d", len(row.RootShare)) + } + copy(rootShare[:], row.RootShare) + + if len(row.SetID) != 32 { + return nil, nil, fmt.Errorf("invalid set ID "+ + "length: %d", len(row.SetID)) + } + copy(rowSetID[:], row.SetID) + + if row.ChildIndex < 0 || row.ChildIndex > math.MaxUint32 { + return nil, nil, fmt.Errorf("invalid child index "+ + "value: %v", row.ChildIndex) + } + + ampRecord := record.NewAMP( + rootShare, rowSetID, uint32(row.ChildIndex), + ) + htlc.AMP = &InvoiceHtlcAMPData{Record: *ampRecord} + + if len(row.Hash) != 32 { + return nil, nil, fmt.Errorf("invalid hash "+ + "length: %d", len(row.Hash)) + } + copy(htlc.AMP.Hash[:], row.Hash) + + if row.Preimage != nil { + preimage, err := lntypes.MakePreimage(row.Preimage) + if err != nil { + return nil, nil, err + } + htlc.AMP.Preimage = &preimage + } + + // row.ID is invoice_htlcs.id — the key for custom records. + crRows := data.customRecords[row.ID] + cr := make(record.CustomSet, len(crRows)) + for _, crRow := range crRows { + value := crRow.Value + if value == nil { + value = []byte{} + } + cr[uint64(crRow.Key)] = value + } + htlc.CustomRecords = cr + + ampHtlcs[circuitKey] = htlc + } + + // Populate InvoiceKeys and AmtPaid for each set in ampState. + for sid := range ampState { + var amtPaid lnwire.MilliSatoshi + invoiceKeys := make(map[models.CircuitKey]struct{}) + + for key, htlc := range ampHtlcs { + if htlc.AMP.Record.SetID() != sid { + continue + } + + invoiceKeys[key] = struct{}{} + + if htlc.State != HtlcStateCanceled { + amtPaid += htlc.Amt + } + } + + setState := ampState[sid] + setState.InvoiceKeys = invoiceKeys + setState.AmtPaid = amtPaid + ampState[sid] = setState + } + + return ampState, ampHtlcs, nil +} + // getInvoiceFeatures fetches the invoice features for the given invoice id. func getInvoiceFeatures(ctx context.Context, db SQLInvoiceQueries, invoiceID int64) (*lnwire.FeatureVector, error) { @@ -1891,22 +2461,3 @@ func unmarshalInvoiceHTLC(row sqlc.InvoiceHtlc) (CircuitKey, return circuitKey, htlc, nil } - -// queryWithLimit is a helper method that can be used to query the database -// using a limit and offset. The passed query function should return the number -// of rows returned and an error if any. -func queryWithLimit(query func(int) (int, error), limit int) error { - offset := 0 - for { - rows, err := query(offset) - if err != nil { - return err - } - - if rows < limit { - return nil - } - - offset += limit - } -} diff --git a/itest/lnd_invoice_migration_test.go b/itest/lnd_invoice_migration_test.go index d7974a679c1..13e0924836f 100644 --- a/itest/lnd_invoice_migration_test.go +++ b/itest/lnd_invoice_migration_test.go @@ -68,7 +68,13 @@ func openNativeSQLInvoiceDB(ht *lntest.HarnessTest, }, ) + queryCfg := sqldb.DefaultSQLiteConfig() + if hn.Cfg.DBBackend != node.BackendSqlite { + queryCfg = sqldb.DefaultPostgresConfig() + } + return invoices.NewSQLStore( + &invoices.SQLStoreConfig{QueryCfg: queryCfg}, executor, clock.NewDefaultClock(), ) } diff --git a/sqldb/sqlc/amp_invoices.sql.go b/sqldb/sqlc/amp_invoices.sql.go index 3176b3cbfdb..20128936bf8 100644 --- a/sqldb/sqlc/amp_invoices.sql.go +++ b/sqldb/sqlc/amp_invoices.sql.go @@ -8,6 +8,7 @@ package sqlc import ( "context" "database/sql" + "strings" "time" ) @@ -88,6 +89,86 @@ func (q *Queries) FetchAMPSubInvoiceHTLCs(ctx context.Context, arg FetchAMPSubIn return items, nil } +const fetchAMPSubInvoiceHTLCsForInvoices = `-- name: FetchAMPSubInvoiceHTLCsForInvoices :many +SELECT + amp.set_id, amp.root_share, amp.child_index, amp.hash, amp.preimage, + invoice_htlcs.id, invoice_htlcs.chan_id, invoice_htlcs.htlc_id, invoice_htlcs.amount_msat, invoice_htlcs.total_mpp_msat, invoice_htlcs.accept_height, invoice_htlcs.accept_time, invoice_htlcs.expiry_height, invoice_htlcs.state, invoice_htlcs.resolve_time, invoice_htlcs.invoice_id +FROM amp_sub_invoice_htlcs amp +INNER JOIN invoice_htlcs ON amp.htlc_id = invoice_htlcs.id +WHERE amp.invoice_id IN (/*SLICE:invoice_ids*/?) +ORDER BY invoice_htlcs.invoice_id ASC +` + +type FetchAMPSubInvoiceHTLCsForInvoicesRow struct { + SetID []byte + RootShare []byte + ChildIndex int64 + Hash []byte + Preimage []byte + ID int64 + ChanID string + HtlcID int64 + AmountMsat int64 + TotalMppMsat sql.NullInt64 + AcceptHeight int32 + AcceptTime time.Time + ExpiryHeight int32 + State int16 + ResolveTime sql.NullTime + InvoiceID int64 +} + +// Batch version of FetchAMPSubInvoiceHTLCs for a set of invoice IDs. +func (q *Queries) FetchAMPSubInvoiceHTLCsForInvoices(ctx context.Context, invoiceIds []int64) ([]FetchAMPSubInvoiceHTLCsForInvoicesRow, error) { + query := fetchAMPSubInvoiceHTLCsForInvoices + var queryParams []interface{} + if len(invoiceIds) > 0 { + for _, v := range invoiceIds { + queryParams = append(queryParams, v) + } + query = strings.Replace(query, "/*SLICE:invoice_ids*/?", makeQueryParams(len(queryParams), len(invoiceIds)), 1) + } else { + query = strings.Replace(query, "/*SLICE:invoice_ids*/?", "NULL", 1) + } + rows, err := q.db.QueryContext(ctx, query, queryParams...) + if err != nil { + return nil, err + } + defer rows.Close() + var items []FetchAMPSubInvoiceHTLCsForInvoicesRow + for rows.Next() { + var i FetchAMPSubInvoiceHTLCsForInvoicesRow + if err := rows.Scan( + &i.SetID, + &i.RootShare, + &i.ChildIndex, + &i.Hash, + &i.Preimage, + &i.ID, + &i.ChanID, + &i.HtlcID, + &i.AmountMsat, + &i.TotalMppMsat, + &i.AcceptHeight, + &i.AcceptTime, + &i.ExpiryHeight, + &i.State, + &i.ResolveTime, + &i.InvoiceID, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const fetchAMPSubInvoices = `-- name: FetchAMPSubInvoices :many SELECT set_id, state, created_at, settled_at, settle_index, invoice_id FROM amp_sub_invoices @@ -133,6 +214,54 @@ func (q *Queries) FetchAMPSubInvoices(ctx context.Context, arg FetchAMPSubInvoic return items, nil } +const fetchAMPSubInvoicesForInvoices = `-- name: FetchAMPSubInvoicesForInvoices :many +SELECT set_id, state, created_at, settled_at, settle_index, invoice_id +FROM amp_sub_invoices +WHERE invoice_id IN (/*SLICE:invoice_ids*/?) +ORDER BY invoice_id ASC +` + +// Batch version of FetchAMPSubInvoices for a set of invoice IDs. +func (q *Queries) FetchAMPSubInvoicesForInvoices(ctx context.Context, invoiceIds []int64) ([]AmpSubInvoice, error) { + query := fetchAMPSubInvoicesForInvoices + var queryParams []interface{} + if len(invoiceIds) > 0 { + for _, v := range invoiceIds { + queryParams = append(queryParams, v) + } + query = strings.Replace(query, "/*SLICE:invoice_ids*/?", makeQueryParams(len(queryParams), len(invoiceIds)), 1) + } else { + query = strings.Replace(query, "/*SLICE:invoice_ids*/?", "NULL", 1) + } + rows, err := q.db.QueryContext(ctx, query, queryParams...) + if err != nil { + return nil, err + } + defer rows.Close() + var items []AmpSubInvoice + for rows.Next() { + var i AmpSubInvoice + if err := rows.Scan( + &i.SetID, + &i.State, + &i.CreatedAt, + &i.SettledAt, + &i.SettleIndex, + &i.InvoiceID, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const fetchSettledAMPSubInvoices = `-- name: FetchSettledAMPSubInvoices :many SELECT a.set_id, diff --git a/sqldb/sqlc/invoices.sql.go b/sqldb/sqlc/invoices.sql.go index 9743f2b648c..217c4750ad0 100644 --- a/sqldb/sqlc/invoices.sql.go +++ b/sqldb/sqlc/invoices.sql.go @@ -8,6 +8,7 @@ package sqlc import ( "context" "database/sql" + "strings" "time" ) @@ -69,20 +70,23 @@ SELECT invoices.id, invoices.hash, invoices.preimage, invoices.settle_index, invoices.settled_at, invoices.memo, invoices.amount_msat, invoices.cltv_delta, invoices.expiry, invoices.payment_addr, invoices.payment_request, invoices.payment_request_hash, invoices.state, invoices.amount_paid_msat, invoices.is_amp, invoices.is_hodl, invoices.is_keysend, invoices.created_at FROM invoices WHERE state IN (0, 3) -- 0 = ContractOpen, 3 = ContractAccepted + AND id > $1 ORDER BY id ASC -LIMIT $2 OFFSET $1 +LIMIT $2 ` type FetchPendingInvoicesParams struct { - NumOffset int32 - NumLimit int32 + IDCursor int64 + NumLimit int32 } // FetchPendingInvoices returns all invoices in a pending state (open or // accepted). The invoices_state_idx index on the state column makes this a -// fast index scan rather than a full table scan. +// fast index scan rather than a full table scan. id_cursor is an exclusive +// lower bound on the primary key used for cursor-based pagination; the caller +// must supply 0 when starting from the beginning. func (q *Queries) FetchPendingInvoices(ctx context.Context, arg FetchPendingInvoicesParams) ([]Invoice, error) { - rows, err := q.db.QueryContext(ctx, fetchPendingInvoices, arg.NumOffset, arg.NumLimit) + rows, err := q.db.QueryContext(ctx, fetchPendingInvoices, arg.IDCursor, arg.NumLimit) if err != nil { return nil, err } @@ -129,21 +133,21 @@ SELECT FROM invoices WHERE id >= $1 ORDER BY id ASC -LIMIT $3 OFFSET $2 +LIMIT $2 ` type FilterInvoicesByAddIndexParams struct { AddIndexGet int64 - NumOffset int32 NumLimit int32 } // FilterInvoicesByAddIndex returns invoices whose add_index (primary key id) // is greater than or equal to the given value, ordered by id. Because id is // the primary key, this is always an efficient range scan on the clustered -// index. +// index. For cursor-based pagination the caller advances add_index_get to +// last_returned_id + 1 on each subsequent page. func (q *Queries) FilterInvoicesByAddIndex(ctx context.Context, arg FilterInvoicesByAddIndexParams) ([]Invoice, error) { - rows, err := q.db.QueryContext(ctx, filterInvoicesByAddIndex, arg.AddIndexGet, arg.NumOffset, arg.NumLimit) + rows, err := q.db.QueryContext(ctx, filterInvoicesByAddIndex, arg.AddIndexGet, arg.NumLimit) if err != nil { return nil, err } @@ -189,22 +193,25 @@ SELECT invoices.id, invoices.hash, invoices.preimage, invoices.settle_index, invoices.settled_at, invoices.memo, invoices.amount_msat, invoices.cltv_delta, invoices.expiry, invoices.payment_addr, invoices.payment_request, invoices.payment_request_hash, invoices.state, invoices.amount_paid_msat, invoices.is_amp, invoices.is_hodl, invoices.is_keysend, invoices.created_at FROM invoices WHERE settle_index >= $1 + AND id > $2 ORDER BY id ASC -LIMIT $3 OFFSET $2 +LIMIT $3 ` type FilterInvoicesBySettleIndexParams struct { SettleIndexGet sql.NullInt64 - NumOffset int32 + IDCursor int64 NumLimit int32 } // FilterInvoicesBySettleIndex returns settled invoices whose settle_index is // greater than or equal to the given value, ordered by id. The caller must // always supply a concrete lower bound so the invoices_settle_index_idx index -// can be used. +// can be used. id_cursor is an exclusive lower bound on the primary key used +// for cursor-based pagination; the caller must supply 0 when starting from +// the beginning. func (q *Queries) FilterInvoicesBySettleIndex(ctx context.Context, arg FilterInvoicesBySettleIndexParams) ([]Invoice, error) { - rows, err := q.db.QueryContext(ctx, filterInvoicesBySettleIndex, arg.SettleIndexGet, arg.NumOffset, arg.NumLimit) + rows, err := q.db.QueryContext(ctx, filterInvoicesBySettleIndex, arg.SettleIndexGet, arg.IDCursor, arg.NumLimit) if err != nil { return nil, err } @@ -254,7 +261,7 @@ WHERE id >= $1 AND created_at >= $3 AND created_at < $4 ORDER BY id ASC -LIMIT $6 OFFSET $5 +LIMIT $5 ` type FilterInvoicesForwardParams struct { @@ -262,25 +269,25 @@ type FilterInvoicesForwardParams struct { PendingOnly interface{} CreatedAfter time.Time CreatedBefore time.Time - NumOffset int32 NumLimit int32 } -// FilterInvoicesForward returns invoices in ascending id order starting from -// add_index_get. All parameters are non-nullable so the planner always sees -// plain range predicates and can use the primary-key index. The caller is -// responsible for supplying Go-side defaults when a filter is not needed: +// FilterInvoicesForward returns invoices in ascending id order. All parameters +// are non-nullable so the planner always sees plain range predicates and can +// use the primary-key index. For cursor-based pagination the caller advances +// add_index_get to last_returned_id + 1 on each subsequent page. The caller +// is responsible for supplying Go-side defaults when a filter is not needed: // -// created_after → time.Unix(0, 0).UTC() (epoch – before any invoice) -// created_before → time.Date(9999, …) (far future – no upper cap) -// pending_only → false (include all states) +// add_index_get → 1 (first valid invoice id) +// created_after → time.Unix(0, 0).UTC() (epoch – before any invoice) +// created_before → time.Date(9999, …) (far future – no upper cap) +// pending_only → false (include all states) func (q *Queries) FilterInvoicesForward(ctx context.Context, arg FilterInvoicesForwardParams) ([]Invoice, error) { rows, err := q.db.QueryContext(ctx, filterInvoicesForward, arg.AddIndexGet, arg.PendingOnly, arg.CreatedAfter, arg.CreatedBefore, - arg.NumOffset, arg.NumLimit, ) if err != nil { @@ -332,7 +339,7 @@ WHERE id <= $1 AND created_at >= $3 AND created_at < $4 ORDER BY id DESC -LIMIT $6 OFFSET $5 +LIMIT $5 ` type FilterInvoicesReverseParams struct { @@ -340,20 +347,20 @@ type FilterInvoicesReverseParams struct { PendingOnly interface{} CreatedAfter time.Time CreatedBefore time.Time - NumOffset int32 NumLimit int32 } // FilterInvoicesReverse is the descending counterpart of FilterInvoicesForward. -// It returns invoices in descending id order up to and including add_index_let. -// See FilterInvoicesForward for the expected Go-side defaults. +// It returns invoices in descending id order. For cursor-based pagination the +// caller advances add_index_let to last_returned_id - 1 on each subsequent +// page; pass math.MaxInt64 to start from the most recent invoice. See +// FilterInvoicesForward for the expected Go-side defaults. func (q *Queries) FilterInvoicesReverse(ctx context.Context, arg FilterInvoicesReverseParams) ([]Invoice, error) { rows, err := q.db.QueryContext(ctx, filterInvoicesReverse, arg.AddIndexLet, arg.PendingOnly, arg.CreatedAfter, arg.CreatedBefore, - arg.NumOffset, arg.NumLimit, ) if err != nil { @@ -540,9 +547,50 @@ func (q *Queries) GetInvoiceFeatures(ctx context.Context, invoiceID int64) ([]In return items, nil } +const getInvoiceFeaturesForInvoices = `-- name: GetInvoiceFeaturesForInvoices :many +SELECT feature, invoice_id +FROM invoice_features +WHERE invoice_id IN (/*SLICE:invoice_ids*/?) +ORDER BY invoice_id ASC +` + +// Batch version of GetInvoiceFeatures for a set of invoice IDs. +func (q *Queries) GetInvoiceFeaturesForInvoices(ctx context.Context, invoiceIds []int64) ([]InvoiceFeature, error) { + query := getInvoiceFeaturesForInvoices + var queryParams []interface{} + if len(invoiceIds) > 0 { + for _, v := range invoiceIds { + queryParams = append(queryParams, v) + } + query = strings.Replace(query, "/*SLICE:invoice_ids*/?", makeQueryParams(len(queryParams), len(invoiceIds)), 1) + } else { + query = strings.Replace(query, "/*SLICE:invoice_ids*/?", "NULL", 1) + } + rows, err := q.db.QueryContext(ctx, query, queryParams...) + if err != nil { + return nil, err + } + defer rows.Close() + var items []InvoiceFeature + for rows.Next() { + var i InvoiceFeature + if err := rows.Scan(&i.Feature, &i.InvoiceID); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const getInvoiceHTLCCustomRecords = `-- name: GetInvoiceHTLCCustomRecords :many SELECT ihcr.htlc_id, key, value -FROM invoice_htlcs ih JOIN invoice_htlc_custom_records ihcr ON ih.id=ihcr.htlc_id +FROM invoice_htlcs ih JOIN invoice_htlc_custom_records ihcr ON ih.id=ihcr.htlc_id WHERE ih.invoice_id = $1 ` @@ -575,6 +623,53 @@ func (q *Queries) GetInvoiceHTLCCustomRecords(ctx context.Context, invoiceID int return items, nil } +const getInvoiceHTLCCustomRecordsForInvoices = `-- name: GetInvoiceHTLCCustomRecordsForInvoices :many +SELECT ihcr.htlc_id, key, value +FROM invoice_htlcs ih JOIN invoice_htlc_custom_records ihcr ON ih.id=ihcr.htlc_id +WHERE ih.invoice_id IN (/*SLICE:invoice_ids*/?) +ORDER BY ihcr.htlc_id ASC +` + +type GetInvoiceHTLCCustomRecordsForInvoicesRow struct { + HtlcID int64 + Key int64 + Value []byte +} + +// Batch version of GetInvoiceHTLCCustomRecords for a set of invoice IDs. +func (q *Queries) GetInvoiceHTLCCustomRecordsForInvoices(ctx context.Context, invoiceIds []int64) ([]GetInvoiceHTLCCustomRecordsForInvoicesRow, error) { + query := getInvoiceHTLCCustomRecordsForInvoices + var queryParams []interface{} + if len(invoiceIds) > 0 { + for _, v := range invoiceIds { + queryParams = append(queryParams, v) + } + query = strings.Replace(query, "/*SLICE:invoice_ids*/?", makeQueryParams(len(queryParams), len(invoiceIds)), 1) + } else { + query = strings.Replace(query, "/*SLICE:invoice_ids*/?", "NULL", 1) + } + rows, err := q.db.QueryContext(ctx, query, queryParams...) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetInvoiceHTLCCustomRecordsForInvoicesRow + for rows.Next() { + var i GetInvoiceHTLCCustomRecordsForInvoicesRow + if err := rows.Scan(&i.HtlcID, &i.Key, &i.Value); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const getInvoiceHTLCs = `-- name: GetInvoiceHTLCs :many SELECT id, chan_id, htlc_id, amount_msat, total_mpp_msat, accept_height, accept_time, expiry_height, state, resolve_time, invoice_id FROM invoice_htlcs @@ -616,6 +711,59 @@ func (q *Queries) GetInvoiceHTLCs(ctx context.Context, invoiceID int64) ([]Invoi return items, nil } +const getInvoiceHTLCsForInvoices = `-- name: GetInvoiceHTLCsForInvoices :many +SELECT id, chan_id, htlc_id, amount_msat, total_mpp_msat, accept_height, accept_time, expiry_height, state, resolve_time, invoice_id +FROM invoice_htlcs +WHERE invoice_id IN (/*SLICE:invoice_ids*/?) +ORDER BY invoice_id ASC +` + +// Batch version of GetInvoiceHTLCs for a set of invoice IDs. +func (q *Queries) GetInvoiceHTLCsForInvoices(ctx context.Context, invoiceIds []int64) ([]InvoiceHtlc, error) { + query := getInvoiceHTLCsForInvoices + var queryParams []interface{} + if len(invoiceIds) > 0 { + for _, v := range invoiceIds { + queryParams = append(queryParams, v) + } + query = strings.Replace(query, "/*SLICE:invoice_ids*/?", makeQueryParams(len(queryParams), len(invoiceIds)), 1) + } else { + query = strings.Replace(query, "/*SLICE:invoice_ids*/?", "NULL", 1) + } + rows, err := q.db.QueryContext(ctx, query, queryParams...) + if err != nil { + return nil, err + } + defer rows.Close() + var items []InvoiceHtlc + for rows.Next() { + var i InvoiceHtlc + if err := rows.Scan( + &i.ID, + &i.ChanID, + &i.HtlcID, + &i.AmountMsat, + &i.TotalMppMsat, + &i.AcceptHeight, + &i.AcceptTime, + &i.ExpiryHeight, + &i.State, + &i.ResolveTime, + &i.InvoiceID, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const getKVInvoicePaymentHashByAddIndex = `-- name: GetKVInvoicePaymentHashByAddIndex :one SELECT hash FROM invoice_payment_hashes diff --git a/sqldb/sqlc/querier.go b/sqldb/sqlc/querier.go index a739f5cffe7..ff9226744af 100644 --- a/sqldb/sqlc/querier.go +++ b/sqldb/sqlc/querier.go @@ -39,7 +39,11 @@ type Querier interface { FailAttempt(ctx context.Context, arg FailAttemptParams) error FailPayment(ctx context.Context, arg FailPaymentParams) (sql.Result, error) FetchAMPSubInvoiceHTLCs(ctx context.Context, arg FetchAMPSubInvoiceHTLCsParams) ([]FetchAMPSubInvoiceHTLCsRow, error) + // Batch version of FetchAMPSubInvoiceHTLCs for a set of invoice IDs. + FetchAMPSubInvoiceHTLCsForInvoices(ctx context.Context, invoiceIds []int64) ([]FetchAMPSubInvoiceHTLCsForInvoicesRow, error) FetchAMPSubInvoices(ctx context.Context, arg FetchAMPSubInvoicesParams) ([]AmpSubInvoice, error) + // Batch version of FetchAMPSubInvoices for a set of invoice IDs. + FetchAMPSubInvoicesForInvoices(ctx context.Context, invoiceIds []int64) ([]AmpSubInvoice, error) // Fetch all inflight attempts with their payment data using pagination. // Returns attempt data joined with payment and intent data to avoid separate queries. FetchAllInflightAttempts(ctx context.Context, arg FetchAllInflightAttemptsParams) ([]PaymentHtlcAttempt, error) @@ -64,31 +68,40 @@ type Querier interface { FetchPaymentsByIDsMig(ctx context.Context, paymentIds []int64) ([]FetchPaymentsByIDsMigRow, error) // FetchPendingInvoices returns all invoices in a pending state (open or // accepted). The invoices_state_idx index on the state column makes this a - // fast index scan rather than a full table scan. + // fast index scan rather than a full table scan. id_cursor is an exclusive + // lower bound on the primary key used for cursor-based pagination; the caller + // must supply 0 when starting from the beginning. FetchPendingInvoices(ctx context.Context, arg FetchPendingInvoicesParams) ([]Invoice, error) FetchRouteLevelFirstHopCustomRecords(ctx context.Context, htlcAttemptIndices []int64) ([]PaymentAttemptFirstHopCustomRecord, error) FetchSettledAMPSubInvoices(ctx context.Context, arg FetchSettledAMPSubInvoicesParams) ([]FetchSettledAMPSubInvoicesRow, error) // FilterInvoicesByAddIndex returns invoices whose add_index (primary key id) // is greater than or equal to the given value, ordered by id. Because id is // the primary key, this is always an efficient range scan on the clustered - // index. + // index. For cursor-based pagination the caller advances add_index_get to + // last_returned_id + 1 on each subsequent page. FilterInvoicesByAddIndex(ctx context.Context, arg FilterInvoicesByAddIndexParams) ([]Invoice, error) // FilterInvoicesBySettleIndex returns settled invoices whose settle_index is // greater than or equal to the given value, ordered by id. The caller must // always supply a concrete lower bound so the invoices_settle_index_idx index - // can be used. + // can be used. id_cursor is an exclusive lower bound on the primary key used + // for cursor-based pagination; the caller must supply 0 when starting from + // the beginning. FilterInvoicesBySettleIndex(ctx context.Context, arg FilterInvoicesBySettleIndexParams) ([]Invoice, error) - // FilterInvoicesForward returns invoices in ascending id order starting from - // add_index_get. All parameters are non-nullable so the planner always sees - // plain range predicates and can use the primary-key index. The caller is - // responsible for supplying Go-side defaults when a filter is not needed: - // created_after → time.Unix(0, 0).UTC() (epoch – before any invoice) - // created_before → time.Date(9999, …) (far future – no upper cap) - // pending_only → false (include all states) + // FilterInvoicesForward returns invoices in ascending id order. All parameters + // are non-nullable so the planner always sees plain range predicates and can + // use the primary-key index. For cursor-based pagination the caller advances + // add_index_get to last_returned_id + 1 on each subsequent page. The caller + // is responsible for supplying Go-side defaults when a filter is not needed: + // add_index_get → 1 (first valid invoice id) + // created_after → time.Unix(0, 0).UTC() (epoch – before any invoice) + // created_before → time.Date(9999, …) (far future – no upper cap) + // pending_only → false (include all states) FilterInvoicesForward(ctx context.Context, arg FilterInvoicesForwardParams) ([]Invoice, error) // FilterInvoicesReverse is the descending counterpart of FilterInvoicesForward. - // It returns invoices in descending id order up to and including add_index_let. - // See FilterInvoicesForward for the expected Go-side defaults. + // It returns invoices in descending id order. For cursor-based pagination the + // caller advances add_index_let to last_returned_id - 1 on each subsequent + // page; pass math.MaxInt64 to start from the most recent invoice. See + // FilterInvoicesForward for the expected Go-side defaults. FilterInvoicesReverse(ctx context.Context, arg FilterInvoicesReverseParams) ([]Invoice, error) FilterPayments(ctx context.Context, arg FilterPaymentsParams) ([]FilterPaymentsRow, error) FilterPaymentsDesc(ctx context.Context, arg FilterPaymentsDescParams) ([]FilterPaymentsDescRow, error) @@ -117,8 +130,14 @@ type Querier interface { // the primary key of amp_sub_invoices table. GetInvoiceBySetID(ctx context.Context, setID []byte) ([]Invoice, error) GetInvoiceFeatures(ctx context.Context, invoiceID int64) ([]InvoiceFeature, error) + // Batch version of GetInvoiceFeatures for a set of invoice IDs. + GetInvoiceFeaturesForInvoices(ctx context.Context, invoiceIds []int64) ([]InvoiceFeature, error) GetInvoiceHTLCCustomRecords(ctx context.Context, invoiceID int64) ([]GetInvoiceHTLCCustomRecordsRow, error) + // Batch version of GetInvoiceHTLCCustomRecords for a set of invoice IDs. + GetInvoiceHTLCCustomRecordsForInvoices(ctx context.Context, invoiceIds []int64) ([]GetInvoiceHTLCCustomRecordsForInvoicesRow, error) GetInvoiceHTLCs(ctx context.Context, invoiceID int64) ([]InvoiceHtlc, error) + // Batch version of GetInvoiceHTLCs for a set of invoice IDs. + GetInvoiceHTLCsForInvoices(ctx context.Context, invoiceIds []int64) ([]InvoiceHtlc, error) GetKVInvoicePaymentHashByAddIndex(ctx context.Context, addIndex int64) ([]byte, error) GetMigration(ctx context.Context, version int32) (time.Time, error) GetNodeAddresses(ctx context.Context, nodeID int64) ([]GetNodeAddressesRow, error) diff --git a/sqldb/sqlc/queries/amp_invoices.sql b/sqldb/sqlc/queries/amp_invoices.sql index 1184fd2a418..73637d4067d 100644 --- a/sqldb/sqlc/queries/amp_invoices.sql +++ b/sqldb/sqlc/queries/amp_invoices.sql @@ -66,6 +66,23 @@ WHERE a.invoice_id = $1 AND a.set_id = $2 AND a.htlc_id = ( SELECT id FROM invoice_htlcs AS i WHERE i.chan_id = $3 AND i.htlc_id = $4 ); +-- name: FetchAMPSubInvoicesForInvoices :many +-- Batch version of FetchAMPSubInvoices for a set of invoice IDs. +SELECT * +FROM amp_sub_invoices +WHERE invoice_id IN (sqlc.slice('invoice_ids')/*SLICE:invoice_ids*/) +ORDER BY invoice_id ASC; + +-- name: FetchAMPSubInvoiceHTLCsForInvoices :many +-- Batch version of FetchAMPSubInvoiceHTLCs for a set of invoice IDs. +SELECT + amp.set_id, amp.root_share, amp.child_index, amp.hash, amp.preimage, + invoice_htlcs.* +FROM amp_sub_invoice_htlcs amp +INNER JOIN invoice_htlcs ON amp.htlc_id = invoice_htlcs.id +WHERE amp.invoice_id IN (sqlc.slice('invoice_ids')/*SLICE:invoice_ids*/) +ORDER BY invoice_htlcs.invoice_id ASC; + -- name: InsertAMPSubInvoice :exec INSERT INTO amp_sub_invoices ( set_id, state, created_at, settled_at, settle_index, invoice_id diff --git a/sqldb/sqlc/queries/invoices.sql b/sqldb/sqlc/queries/invoices.sql index 1ec96acf1d7..76afe50eab9 100644 --- a/sqldb/sqlc/queries/invoices.sql +++ b/sqldb/sqlc/queries/invoices.sql @@ -50,46 +50,55 @@ ON i.id = a.invoice_id AND a.set_id = $1; -- name: FetchPendingInvoices :many -- FetchPendingInvoices returns all invoices in a pending state (open or -- accepted). The invoices_state_idx index on the state column makes this a --- fast index scan rather than a full table scan. +-- fast index scan rather than a full table scan. id_cursor is an exclusive +-- lower bound on the primary key used for cursor-based pagination; the caller +-- must supply 0 when starting from the beginning. SELECT invoices.* FROM invoices WHERE state IN (0, 3) -- 0 = ContractOpen, 3 = ContractAccepted + AND id > @id_cursor ORDER BY id ASC -LIMIT @num_limit OFFSET @num_offset; +LIMIT @num_limit; -- name: FilterInvoicesBySettleIndex :many -- FilterInvoicesBySettleIndex returns settled invoices whose settle_index is -- greater than or equal to the given value, ordered by id. The caller must -- always supply a concrete lower bound so the invoices_settle_index_idx index --- can be used. +-- can be used. id_cursor is an exclusive lower bound on the primary key used +-- for cursor-based pagination; the caller must supply 0 when starting from +-- the beginning. SELECT invoices.* FROM invoices WHERE settle_index >= @settle_index_get + AND id > @id_cursor ORDER BY id ASC -LIMIT @num_limit OFFSET @num_offset; +LIMIT @num_limit; -- name: FilterInvoicesByAddIndex :many -- FilterInvoicesByAddIndex returns invoices whose add_index (primary key id) -- is greater than or equal to the given value, ordered by id. Because id is -- the primary key, this is always an efficient range scan on the clustered --- index. +-- index. For cursor-based pagination the caller advances add_index_get to +-- last_returned_id + 1 on each subsequent page. SELECT invoices.* FROM invoices WHERE id >= @add_index_get ORDER BY id ASC -LIMIT @num_limit OFFSET @num_offset; +LIMIT @num_limit; -- name: FilterInvoicesForward :many --- FilterInvoicesForward returns invoices in ascending id order starting from --- add_index_get. All parameters are non-nullable so the planner always sees --- plain range predicates and can use the primary-key index. The caller is --- responsible for supplying Go-side defaults when a filter is not needed: --- created_after → time.Unix(0, 0).UTC() (epoch – before any invoice) --- created_before → time.Date(9999, …) (far future – no upper cap) --- pending_only → false (include all states) +-- FilterInvoicesForward returns invoices in ascending id order. All parameters +-- are non-nullable so the planner always sees plain range predicates and can +-- use the primary-key index. For cursor-based pagination the caller advances +-- add_index_get to last_returned_id + 1 on each subsequent page. The caller +-- is responsible for supplying Go-side defaults when a filter is not needed: +-- add_index_get → 1 (first valid invoice id) +-- created_after → time.Unix(0, 0).UTC() (epoch – before any invoice) +-- created_before → time.Date(9999, …) (far future – no upper cap) +-- pending_only → false (include all states) SELECT invoices.* FROM invoices @@ -98,12 +107,14 @@ WHERE id >= @add_index_get AND created_at >= @created_after AND created_at < @created_before ORDER BY id ASC -LIMIT @num_limit OFFSET @num_offset; +LIMIT @num_limit; -- name: FilterInvoicesReverse :many -- FilterInvoicesReverse is the descending counterpart of FilterInvoicesForward. --- It returns invoices in descending id order up to and including add_index_let. --- See FilterInvoicesForward for the expected Go-side defaults. +-- It returns invoices in descending id order. For cursor-based pagination the +-- caller advances add_index_let to last_returned_id - 1 on each subsequent +-- page; pass math.MaxInt64 to start from the most recent invoice. See +-- FilterInvoicesForward for the expected Go-side defaults. SELECT invoices.* FROM invoices @@ -112,7 +123,7 @@ WHERE id <= @add_index_let AND created_at >= @created_after AND created_at < @created_before ORDER BY id DESC -LIMIT @num_limit OFFSET @num_offset; +LIMIT @num_limit; -- name: UpdateInvoiceState :execresult UPDATE invoices @@ -186,9 +197,30 @@ INSERT INTO invoice_htlc_custom_records ( -- name: GetInvoiceHTLCCustomRecords :many SELECT ihcr.htlc_id, key, value -FROM invoice_htlcs ih JOIN invoice_htlc_custom_records ihcr ON ih.id=ihcr.htlc_id +FROM invoice_htlcs ih JOIN invoice_htlc_custom_records ihcr ON ih.id=ihcr.htlc_id WHERE ih.invoice_id = $1; +-- name: GetInvoiceFeaturesForInvoices :many +-- Batch version of GetInvoiceFeatures for a set of invoice IDs. +SELECT * +FROM invoice_features +WHERE invoice_id IN (sqlc.slice('invoice_ids')/*SLICE:invoice_ids*/) +ORDER BY invoice_id ASC; + +-- name: GetInvoiceHTLCsForInvoices :many +-- Batch version of GetInvoiceHTLCs for a set of invoice IDs. +SELECT * +FROM invoice_htlcs +WHERE invoice_id IN (sqlc.slice('invoice_ids')/*SLICE:invoice_ids*/) +ORDER BY invoice_id ASC; + +-- name: GetInvoiceHTLCCustomRecordsForInvoices :many +-- Batch version of GetInvoiceHTLCCustomRecords for a set of invoice IDs. +SELECT ihcr.htlc_id, key, value +FROM invoice_htlcs ih JOIN invoice_htlc_custom_records ihcr ON ih.id=ihcr.htlc_id +WHERE ih.invoice_id IN (sqlc.slice('invoice_ids')/*SLICE:invoice_ids*/) +ORDER BY ihcr.htlc_id ASC; + -- name: InsertKVInvoiceKeyAndAddIndex :exec INSERT INTO invoice_payment_hashes ( id, add_index