invoices+sqldb/sqlc: replace offset-based pagination with cursor-based
What changed, and why it matters
This commit is a performance optimization, not a security fix. It replaces slow database pagination that skipped rows using OFFSET with faster cursor-based pagination using primary-key ranges. On nodes with very large invoice histories this can dramatically reduce CPU usage and query time, but it does not close any vulnerability that an attacker could exploit.
Treat as a routine performance improvement. No security response is required, but operators with large invoice databases should benefit from lower CPU usage after upgrading.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The change removes LIMIT+OFFSET pagination from five invoice SQL queries (FetchPendingInvoices, FilterInvoicesBySettleIndex, FilterInvoicesByAddIndex, FilterInvoicesForward, FilterInvoicesReverse) and replaces it with cursor-based pagination on the primary key id. OFFSET caused SQLite to build a temporary B-tree for each page, making later pages O(offset+limit); the new approach performs a single PK seek plus a forward scan of page_size rows. The Go callers in invoices/sql_store.go now track a cursor (last returned id ± 1) and stop when a page is smaller than the limit. SQL inclusive bounds (>= / <=) are preserved so observable query semantics remain unchanged. The helper queryWithLimit is removed because it has no callers left.
Changed components
invoices/sql_store.gosqldb/sqlc/invoices.sql.gosqldb/sqlc/querier.gosqldb/sqlc/queries/invoices.sqlInspect captured patch +160 / −128
diff --git a/invoices/sql_store.go b/invoices/sql_store.go
index 0628a5d..3d377cb 100644
--- a/invoices/sql_store.go
+++ b/invoices/sql_store.go
@@ -771,15 +771,17 @@ 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) {
+ var cursor int64
+ limit := int32(i.opts.paginationLimit)
+ for {
params := sqlc.FetchPendingInvoicesParams{
- NumOffset: int32(offset),
- NumLimit: int32(i.opts.paginationLimit),
+ IDCursor: cursor,
+ NumLimit: limit,
}
rows, err := db.FetchPendingInvoices(ctx, params)
if err != nil && !errors.Is(err, sql.ErrNoRows) {
- return 0, fmt.Errorf("unable to get invoices "+
+ return fmt.Errorf("unable to get invoices "+
"from db: %w", err)
}
@@ -789,14 +791,17 @@ func (i *SQLStore) FetchPendingInvoices(ctx context.Context) (
ctx, db, row, nil, true,
)
if err != nil {
- return 0, err
+ return err
}
invoices[*hash] = *invoice
+ cursor = row.ID
}
- return len(rows), nil
- }, i.opts.paginationLimit)
+ if int32(len(rows)) < limit {
+ return nil
+ }
+ }
}, func() {
invoices = make(map[lntypes.Hash]Invoice)
})
@@ -830,18 +835,20 @@ 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) {
+ var cursor int64
+ limit := int32(i.opts.paginationLimit)
+ for {
// 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),
+ IDCursor: cursor,
+ NumLimit: limit,
}
rows, err := db.FilterInvoicesBySettleIndex(ctx, params)
if err != nil && !errors.Is(err, sql.ErrNoRows) {
- return 0, fmt.Errorf("unable to get invoices "+
+ return fmt.Errorf("unable to get invoices "+
"from db: %w", err)
}
@@ -851,12 +858,13 @@ func (i *SQLStore) InvoicesSettledSince(ctx context.Context, idx uint64) (
ctx, db, row, nil, true,
)
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)
}
invoices = append(invoices, *invoice)
+ cursor = row.ID
processedCount++
if time.Since(lastLogTime) >=
@@ -871,10 +879,9 @@ func (i *SQLStore) InvoicesSettledSince(ctx context.Context, idx uint64) (
}
}
- return len(rows), nil
- }, i.opts.paginationLimit)
- if err != nil {
- return err
+ if int32(len(rows)) < limit {
+ break
+ }
}
// Now fetch all the AMP sub invoices that were settled since
@@ -977,18 +984,21 @@ 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.
+ // id is always provided here so the primary-key index is used
+ // for this range scan. The cursor starts at idx+1 so the first
+ // page fetches invoices with id >= idx+1. After each page the
+ // cursor advances to last_id + 1.
+ cursor := int64(idx + 1)
+ limit := int32(i.opts.paginationLimit)
+ for {
params := sqlc.FilterInvoicesByAddIndexParams{
- AddIndexGet: int64(idx + 1),
- NumOffset: int32(offset),
- NumLimit: int32(i.opts.paginationLimit),
+ AddIndexGet: cursor,
+ NumLimit: limit,
}
rows, err := db.FilterInvoicesByAddIndex(ctx, params)
if err != nil && !errors.Is(err, sql.ErrNoRows) {
- return 0, fmt.Errorf("unable to get invoices "+
+ return fmt.Errorf("unable to get invoices "+
"from db: %w", err)
}
@@ -998,10 +1008,11 @@ func (i *SQLStore) InvoicesAddedSince(ctx context.Context, idx uint64) (
ctx, db, row, nil, true,
)
if err != nil {
- return 0, err
+ return err
}
result = append(result, *invoice)
+ cursor = row.ID + 1
processedCount++
if time.Since(lastLogTime) >=
@@ -1015,8 +1026,10 @@ func (i *SQLStore) InvoicesAddedSince(ctx context.Context, idx uint64) (
}
}
- return len(rows), nil
- }, i.opts.paginationLimit)
+ if int32(len(rows)) < limit {
+ return nil
+ }
+ }
}, func() {
result = nil
})
@@ -1064,30 +1077,39 @@ 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) {
+ limit := int32(i.opts.paginationLimit)
+
+ // For reverse queries the cursor is an inclusive upper bound on
+ // id (id <= cursor); after each page it advances to
+ // last_returned_id - 1. Start at IndexOffset, or MaxInt64 to
+ // begin from the most recent invoice.
+ // For forward queries the cursor is an inclusive lower bound
+ // (id >= cursor); after each page it advances to
+ // last_returned_id + 1. Start at IndexOffset + 1 so the invoice
+ // at IndexOffset itself is excluded (matching the old
+ // behaviour).
+ var cursor int64
+ if q.Reversed {
+ cursor = int64(math.MaxInt64)
+ if q.IndexOffset != 0 {
+ cursor = int64(q.IndexOffset) - 1
+ }
+ } else {
+ cursor = int64(q.IndexOffset) + 1
+ }
+
+ for {
var (
- rows []sqlc.Invoice
- err error
- limit = int32(i.opts.paginationLimit)
+ rows []sqlc.Invoice
+ err error
)
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
- }
-
params := sqlc.FilterInvoicesReverseParams{
- AddIndexLet: addIndexLet,
+ AddIndexLet: cursor,
PendingOnly: q.PendingOnly,
CreatedAfter: createdAfter,
CreatedBefore: createdBefore,
- NumOffset: int32(offset),
NumLimit: limit,
}
@@ -1095,15 +1117,11 @@ func (i *SQLStore) QueryInvoices(ctx context.Context,
ctx, params,
)
} 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,
+ AddIndexGet: cursor,
PendingOnly: q.PendingOnly,
CreatedAfter: createdAfter,
CreatedBefore: createdBefore,
- NumOffset: int32(offset),
NumLimit: limit,
}
@@ -1113,7 +1131,7 @@ func (i *SQLStore) QueryInvoices(ctx context.Context,
}
if err != nil && !errors.Is(err, sql.ErrNoRows) {
- return 0, fmt.Errorf("unable to get invoices "+
+ return fmt.Errorf("unable to get invoices "+
"from db: %w", err)
}
@@ -1123,18 +1141,25 @@ func (i *SQLStore) QueryInvoices(ctx context.Context,
ctx, db, row, nil, true,
)
if err != nil {
- return 0, err
+ return err
}
invoices = append(invoices, *invoice)
+ if q.Reversed {
+ cursor = row.ID - 1
+ } else {
+ cursor = row.ID + 1
+ }
if len(invoices) == int(q.NumMaxInvoices) {
- return 0, nil
+ return nil
}
}
- return len(rows), nil
- }, i.opts.paginationLimit)
+ if int32(len(rows)) < limit {
+ return nil
+ }
+ }
}, func() {
invoices = nil
})
@@ -1891,22 +1916,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/sqldb/sqlc/invoices.sql.go b/sqldb/sqlc/invoices.sql.go
index 9743f2b..c7bb8d1 100644
--- a/sqldb/sqlc/invoices.sql.go
+++ b/sqldb/sqlc/invoices.sql.go
@@ -69,20 +69,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 +132,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 +192,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 +260,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 +268,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 +338,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 +346,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 {
diff --git a/sqldb/sqlc/querier.go b/sqldb/sqlc/querier.go
index a739f5c..a810c60 100644
--- a/sqldb/sqlc/querier.go
+++ b/sqldb/sqlc/querier.go
@@ -64,31 +64,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)
diff --git a/sqldb/sqlc/queries/invoices.sql b/sqldb/sqlc/queries/invoices.sql
index 1ec96ac..9152513 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
Why this scored 27/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.