From 866efbd7fb6c3b1c564abd4dcc9b167bf3b162e1 Mon Sep 17 00:00:00 2001 From: ziggie Date: Fri, 20 Feb 2026 19:15:21 +0100 Subject: [PATCH] sqldb/sqlc: add targeted invoice queries The existing FilterInvoices query uses optional parameters via the pattern `(col >= param OR param IS NULL)` for every filter. SQLite cannot use indexes with this pattern because the OR prevents the query planner from determining at plan time which rows satisfy the condition, resulting in a full table scan regardless of the available indexes (invoices_state_idx, invoices_settle_index_idx, and the primary-key clustered index on id). Additionally, the conditional ORDER BY: ORDER BY CASE WHEN reverse = FALSE ... THEN id ELSE NULL END ASC, CASE WHEN reverse = TRUE ... THEN id ELSE NULL END DESC prevents the planner from using the index ordering and forces an explicit sort. Add five focused replacements, each with a plain sargable predicate and a direct ORDER BY so the planner can always choose an index scan: - FetchPendingInvoices: WHERE state IN (0, 3) - FilterInvoicesBySettleIndex: WHERE settle_index >= $1 - FilterInvoicesByAddIndex: WHERE id >= $1 - FilterInvoicesForward: WHERE id >= $1 ... ORDER BY id ASC - FilterInvoicesReverse: WHERE id <= $1 ... ORDER BY id DESC FilterInvoicesForward and FilterInvoicesReverse accept non-nullable timestamp parameters (created_after, created_before) so the planner always sees plain range predicates on created_at. Callers supply Go-side defaults when no date filter is needed, following the same convention already used by the payments query. FilterInvoices is kept in this commit so all existing call sites continue to compile. It will be removed once all callers have been migrated. --- sqldb/sqlc/invoices.sql.go | 342 +++++++++++++++++++++++++++++++- sqldb/sqlc/querier.go | 26 +++ sqldb/sqlc/queries/invoices.sql | 77 ++++++- 3 files changed, 435 insertions(+), 10 deletions(-) diff --git a/sqldb/sqlc/invoices.sql.go b/sqldb/sqlc/invoices.sql.go index 178e70d49..d9d57cae7 100644 --- a/sqldb/sqlc/invoices.sql.go +++ b/sqldb/sqlc/invoices.sql.go @@ -64,15 +64,74 @@ func (q *Queries) DeleteInvoice(ctx context.Context, arg DeleteInvoiceParams) (s ) } +const fetchPendingInvoices = `-- name: FetchPendingInvoices :many +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 +ORDER BY id ASC +LIMIT $2 OFFSET $1 +` + +type FetchPendingInvoicesParams struct { + NumOffset int32 + 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. +func (q *Queries) FetchPendingInvoices(ctx context.Context, arg FetchPendingInvoicesParams) ([]Invoice, error) { + rows, err := q.db.QueryContext(ctx, fetchPendingInvoices, arg.NumOffset, arg.NumLimit) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Invoice + for rows.Next() { + var i Invoice + if err := rows.Scan( + &i.ID, + &i.Hash, + &i.Preimage, + &i.SettleIndex, + &i.SettledAt, + &i.Memo, + &i.AmountMsat, + &i.CltvDelta, + &i.Expiry, + &i.PaymentAddr, + &i.PaymentRequest, + &i.PaymentRequestHash, + &i.State, + &i.AmountPaidMsat, + &i.IsAmp, + &i.IsHodl, + &i.IsKeysend, + &i.CreatedAt, + ); 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 filterInvoices = `-- name: FilterInvoices :many 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 ( - id >= $1 OR + id >= $1 OR $1 IS NULL ) AND ( - id <= $2 OR + id <= $2 OR $2 IS NULL ) AND ( settle_index >= $3 OR @@ -81,18 +140,18 @@ WHERE ( settle_index <= $4 OR $4 IS NULL ) AND ( - state = $5 OR + state = $5 OR $5 IS NULL ) AND ( created_at >= $6 OR $6 IS NULL ) AND ( - created_at < $7 OR + created_at < $7 OR $7 IS NULL ) AND ( CASE WHEN $8 = TRUE THEN (state = 0 OR state = 3) - ELSE TRUE + ELSE TRUE END ) ORDER BY @@ -175,6 +234,279 @@ func (q *Queries) FilterInvoices(ctx context.Context, arg FilterInvoicesParams) return items, nil } +const filterInvoicesByAddIndex = `-- name: FilterInvoicesByAddIndex :many +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 id >= $1 +ORDER BY id ASC +LIMIT $3 OFFSET $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. +func (q *Queries) FilterInvoicesByAddIndex(ctx context.Context, arg FilterInvoicesByAddIndexParams) ([]Invoice, error) { + rows, err := q.db.QueryContext(ctx, filterInvoicesByAddIndex, arg.AddIndexGet, arg.NumOffset, arg.NumLimit) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Invoice + for rows.Next() { + var i Invoice + if err := rows.Scan( + &i.ID, + &i.Hash, + &i.Preimage, + &i.SettleIndex, + &i.SettledAt, + &i.Memo, + &i.AmountMsat, + &i.CltvDelta, + &i.Expiry, + &i.PaymentAddr, + &i.PaymentRequest, + &i.PaymentRequestHash, + &i.State, + &i.AmountPaidMsat, + &i.IsAmp, + &i.IsHodl, + &i.IsKeysend, + &i.CreatedAt, + ); 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 filterInvoicesBySettleIndex = `-- name: FilterInvoicesBySettleIndex :many +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 +ORDER BY id ASC +LIMIT $3 OFFSET $2 +` + +type FilterInvoicesBySettleIndexParams struct { + SettleIndexGet sql.NullInt64 + NumOffset int32 + 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. +func (q *Queries) FilterInvoicesBySettleIndex(ctx context.Context, arg FilterInvoicesBySettleIndexParams) ([]Invoice, error) { + rows, err := q.db.QueryContext(ctx, filterInvoicesBySettleIndex, arg.SettleIndexGet, arg.NumOffset, arg.NumLimit) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Invoice + for rows.Next() { + var i Invoice + if err := rows.Scan( + &i.ID, + &i.Hash, + &i.Preimage, + &i.SettleIndex, + &i.SettledAt, + &i.Memo, + &i.AmountMsat, + &i.CltvDelta, + &i.Expiry, + &i.PaymentAddr, + &i.PaymentRequest, + &i.PaymentRequestHash, + &i.State, + &i.AmountPaidMsat, + &i.IsAmp, + &i.IsHodl, + &i.IsKeysend, + &i.CreatedAt, + ); 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 filterInvoicesForward = `-- name: FilterInvoicesForward :many +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 id >= $1 + AND (NOT $2 OR state IN (0, 3)) -- 0 = ContractOpen, 3 = ContractAccepted + AND created_at >= $3 + AND created_at < $4 +ORDER BY id ASC +LIMIT $6 OFFSET $5 +` + +type FilterInvoicesForwardParams struct { + AddIndexGet int64 + 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: +// +// 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 { + return nil, err + } + defer rows.Close() + var items []Invoice + for rows.Next() { + var i Invoice + if err := rows.Scan( + &i.ID, + &i.Hash, + &i.Preimage, + &i.SettleIndex, + &i.SettledAt, + &i.Memo, + &i.AmountMsat, + &i.CltvDelta, + &i.Expiry, + &i.PaymentAddr, + &i.PaymentRequest, + &i.PaymentRequestHash, + &i.State, + &i.AmountPaidMsat, + &i.IsAmp, + &i.IsHodl, + &i.IsKeysend, + &i.CreatedAt, + ); 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 filterInvoicesReverse = `-- name: FilterInvoicesReverse :many +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 id <= $1 + AND (NOT $2 OR state IN (0, 3)) -- 0 = ContractOpen, 3 = ContractAccepted + AND created_at >= $3 + AND created_at < $4 +ORDER BY id DESC +LIMIT $6 OFFSET $5 +` + +type FilterInvoicesReverseParams struct { + AddIndexLet int64 + 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. +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 { + return nil, err + } + defer rows.Close() + var items []Invoice + for rows.Next() { + var i Invoice + if err := rows.Scan( + &i.ID, + &i.Hash, + &i.Preimage, + &i.SettleIndex, + &i.SettledAt, + &i.Memo, + &i.AmountMsat, + &i.CltvDelta, + &i.Expiry, + &i.PaymentAddr, + &i.PaymentRequest, + &i.PaymentRequestHash, + &i.State, + &i.AmountPaidMsat, + &i.IsAmp, + &i.IsHodl, + &i.IsKeysend, + &i.CreatedAt, + ); 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 getInvoice = `-- name: GetInvoice :many SELECT i.id, i.hash, i.preimage, i.settle_index, i.settled_at, i.memo, i.amount_msat, i.cltv_delta, i.expiry, i.payment_addr, i.payment_request, i.payment_request_hash, i.state, i.amount_paid_msat, i.is_amp, i.is_hodl, i.is_keysend, i.created_at diff --git a/sqldb/sqlc/querier.go b/sqldb/sqlc/querier.go index d26f845d0..6e0011a7d 100644 --- a/sqldb/sqlc/querier.go +++ b/sqldb/sqlc/querier.go @@ -31,8 +31,34 @@ type Querier interface { DeleteZombieChannel(ctx context.Context, arg DeleteZombieChannelParams) (sql.Result, error) FetchAMPSubInvoiceHTLCs(ctx context.Context, arg FetchAMPSubInvoiceHTLCsParams) ([]FetchAMPSubInvoiceHTLCsRow, error) FetchAMPSubInvoices(ctx context.Context, arg FetchAMPSubInvoicesParams) ([]AmpSubInvoice, 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. + FetchPendingInvoices(ctx context.Context, arg FetchPendingInvoicesParams) ([]Invoice, error) FetchSettledAMPSubInvoices(ctx context.Context, arg FetchSettledAMPSubInvoicesParams) ([]FetchSettledAMPSubInvoicesRow, error) FilterInvoices(ctx context.Context, arg FilterInvoicesParams) ([]Invoice, 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. + 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. + 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(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. + FilterInvoicesReverse(ctx context.Context, arg FilterInvoicesReverseParams) ([]Invoice, error) GetAMPInvoiceID(ctx context.Context, setID []byte) (int64, error) GetChannelAndNodesBySCID(ctx context.Context, arg GetChannelAndNodesBySCIDParams) (GetChannelAndNodesBySCIDRow, error) GetChannelByOutpointWithPolicies(ctx context.Context, arg GetChannelByOutpointWithPoliciesParams) (GetChannelByOutpointWithPoliciesRow, error) diff --git a/sqldb/sqlc/queries/invoices.sql b/sqldb/sqlc/queries/invoices.sql index db1f46e61..ff23a6cf0 100644 --- a/sqldb/sqlc/queries/invoices.sql +++ b/sqldb/sqlc/queries/invoices.sql @@ -65,15 +65,82 @@ FROM invoices i INNER JOIN amp_sub_invoices a 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. +SELECT + invoices.* +FROM invoices +WHERE state IN (0, 3) -- 0 = ContractOpen, 3 = ContractAccepted +ORDER BY id ASC +LIMIT @num_limit OFFSET @num_offset; + +-- 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. +SELECT + invoices.* +FROM invoices +WHERE settle_index >= @settle_index_get +ORDER BY id ASC +LIMIT @num_limit OFFSET @num_offset; + +-- 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. +SELECT + invoices.* +FROM invoices +WHERE id >= @add_index_get +ORDER BY id ASC +LIMIT @num_limit OFFSET @num_offset; + +-- 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) +SELECT + invoices.* +FROM invoices +WHERE id >= @add_index_get + AND (NOT @pending_only OR state IN (0, 3)) -- 0 = ContractOpen, 3 = ContractAccepted + AND created_at >= @created_after + AND created_at < @created_before +ORDER BY id ASC +LIMIT @num_limit OFFSET @num_offset; + +-- 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. +SELECT + invoices.* +FROM invoices +WHERE id <= @add_index_let + AND (NOT @pending_only OR state IN (0, 3)) -- 0 = ContractOpen, 3 = ContractAccepted + AND created_at >= @created_after + AND created_at < @created_before +ORDER BY id DESC +LIMIT @num_limit OFFSET @num_offset; + -- name: FilterInvoices :many SELECT invoices.* FROM invoices WHERE ( - id >= sqlc.narg('add_index_get') OR + id >= sqlc.narg('add_index_get') OR sqlc.narg('add_index_get') IS NULL ) AND ( - id <= sqlc.narg('add_index_let') OR + id <= sqlc.narg('add_index_let') OR sqlc.narg('add_index_let') IS NULL ) AND ( settle_index >= sqlc.narg('settle_index_get') OR @@ -82,18 +149,18 @@ WHERE ( settle_index <= sqlc.narg('settle_index_let') OR sqlc.narg('settle_index_let') IS NULL ) AND ( - state = sqlc.narg('state') OR + state = sqlc.narg('state') OR sqlc.narg('state') IS NULL ) AND ( created_at >= sqlc.narg('created_after') OR sqlc.narg('created_after') IS NULL ) AND ( - created_at < sqlc.narg('created_before') OR + created_at < sqlc.narg('created_before') OR sqlc.narg('created_before') IS NULL ) AND ( CASE WHEN sqlc.narg('pending_only') = TRUE THEN (state = 0 OR state = 3) - ELSE TRUE + ELSE TRUE END ) ORDER BY