diff --git a/payments/db/sql_store.go b/payments/db/sql_store.go index 3637a981b..7f8f9e666 100644 --- a/payments/db/sql_store.go +++ b/payments/db/sql_store.go @@ -87,6 +87,25 @@ type SQLQueries interface { // DeleteFailedAttempts removes all failed HTLCs from the db for a // given payment. DeleteFailedAttempts(ctx context.Context, paymentID int64) error + + /* + Migration specific queries. + + These queries are used ONLY for the one-time migration from KV + to SQL. + */ + + // FetchPaymentsByIDsMig is a migration-only batch fetch that returns + // payment data along with HTLC attempt counts for structural + // validation. + FetchPaymentsByIDsMig(ctx context.Context, paymentIDs []int64) ([]sqlc.FetchPaymentsByIDsMigRow, error) + + // InsertPaymentMig is a migration-only variant of InsertPayment that + // allows setting fail_reason when inserting historical payments, since + // for real payments they have not failed at creation time and so no + // failure reason would exist yet. + InsertPaymentMig(ctx context.Context, arg sqlc.InsertPaymentMigParams) (int64, error) + } // BatchedSQLQueries is a version of the SQLQueries that's capable diff --git a/sqldb/sqlc/payments.sql.go b/sqldb/sqlc/payments.sql.go index b9ec31499..fe02be487 100644 --- a/sqldb/sqlc/payments.sql.go +++ b/sqldb/sqlc/payments.sql.go @@ -597,6 +597,72 @@ func (q *Queries) FetchPaymentsByIDs(ctx context.Context, paymentIds []int64) ([ return items, nil } +const fetchPaymentsByIDsMig = `-- name: FetchPaymentsByIDsMig :many +SELECT + p.id, + p.amount_msat, + p.created_at, + p.payment_identifier, + p.fail_reason, + COUNT(ha.id) AS htlc_attempt_count +FROM payments p +LEFT JOIN payment_htlc_attempts ha ON ha.payment_id = p.id +WHERE p.id IN (/*SLICE:payment_ids*/?) +GROUP BY p.id, p.amount_msat, p.created_at, p.payment_identifier, p.fail_reason +ORDER BY p.id ASC +` + +type FetchPaymentsByIDsMigRow struct { + ID int64 + AmountMsat int64 + CreatedAt time.Time + PaymentIdentifier []byte + FailReason sql.NullInt32 + HtlcAttemptCount int64 +} + +// Migration-specific batch fetch that returns payment data along with HTLC +// attempt counts for structural validation during KV to SQL migration. +func (q *Queries) FetchPaymentsByIDsMig(ctx context.Context, paymentIds []int64) ([]FetchPaymentsByIDsMigRow, error) { + query := fetchPaymentsByIDsMig + var queryParams []interface{} + if len(paymentIds) > 0 { + for _, v := range paymentIds { + queryParams = append(queryParams, v) + } + query = strings.Replace(query, "/*SLICE:payment_ids*/?", makeQueryParams(len(queryParams), len(paymentIds)), 1) + } else { + query = strings.Replace(query, "/*SLICE:payment_ids*/?", "NULL", 1) + } + rows, err := q.db.QueryContext(ctx, query, queryParams...) + if err != nil { + return nil, err + } + defer rows.Close() + var items []FetchPaymentsByIDsMigRow + for rows.Next() { + var i FetchPaymentsByIDsMigRow + if err := rows.Scan( + &i.ID, + &i.AmountMsat, + &i.CreatedAt, + &i.PaymentIdentifier, + &i.FailReason, + &i.HtlcAttemptCount, + ); 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 fetchRouteLevelFirstHopCustomRecords = `-- name: FetchRouteLevelFirstHopCustomRecords :many SELECT l.id, @@ -918,6 +984,51 @@ func (q *Queries) InsertPaymentIntent(ctx context.Context, arg InsertPaymentInte return id, err } +const insertPaymentMig = `-- name: InsertPaymentMig :one +/* ───────────────────────────────────────────── + Migration-specific queries + + These queries are used ONLY for the one-time migration from KV to SQL. + ───────────────────────────────────────────── +*/ + +INSERT INTO payments ( + amount_msat, + created_at, + payment_identifier, + fail_reason) +VALUES ( + $1, + $2, + $3, + $4 +) +RETURNING id +` + +type InsertPaymentMigParams struct { + AmountMsat int64 + CreatedAt time.Time + PaymentIdentifier []byte + FailReason sql.NullInt32 +} + +// Migration-specific payment insert that allows setting fail_reason. +// Normal InsertPayment forces fail_reason to NULL since new payments +// aren't failed yet. During migration, we're inserting historical data +// that may already be failed. +func (q *Queries) InsertPaymentMig(ctx context.Context, arg InsertPaymentMigParams) (int64, error) { + row := q.db.QueryRowContext(ctx, insertPaymentMig, + arg.AmountMsat, + arg.CreatedAt, + arg.PaymentIdentifier, + arg.FailReason, + ) + var id int64 + err := row.Scan(&id) + return id, err +} + const insertRouteHop = `-- name: InsertRouteHop :one INSERT INTO payment_route_hops ( htlc_attempt_index, diff --git a/sqldb/sqlc/querier.go b/sqldb/sqlc/querier.go index 3be738ba9..02f22971c 100644 --- a/sqldb/sqlc/querier.go +++ b/sqldb/sqlc/querier.go @@ -54,6 +54,9 @@ type Querier interface { // Used to avoid fetching redundant payment data when processing multiple // attempts for the same payment. FetchPaymentsByIDs(ctx context.Context, paymentIds []int64) ([]FetchPaymentsByIDsRow, error) + // Migration-specific batch fetch that returns payment data along with HTLC + // attempt counts for structural validation during KV to SQL migration. + 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. @@ -178,6 +181,11 @@ type Querier interface { InsertPaymentHopCustomRecord(ctx context.Context, arg InsertPaymentHopCustomRecordParams) error // Insert a payment intent for a given payment and return its ID. InsertPaymentIntent(ctx context.Context, arg InsertPaymentIntentParams) (int64, error) + // Migration-specific payment insert that allows setting fail_reason. + // Normal InsertPayment forces fail_reason to NULL since new payments + // aren't failed yet. During migration, we're inserting historical data + // that may already be failed. + InsertPaymentMig(ctx context.Context, arg InsertPaymentMigParams) (int64, error) InsertRouteHop(ctx context.Context, arg InsertRouteHopParams) (int64, error) InsertRouteHopAmp(ctx context.Context, arg InsertRouteHopAmpParams) error InsertRouteHopBlinded(ctx context.Context, arg InsertRouteHopBlindedParams) error diff --git a/sqldb/sqlc/queries/payments.sql b/sqldb/sqlc/queries/payments.sql index 419f7bf1a..46f84e494 100644 --- a/sqldb/sqlc/queries/payments.sql +++ b/sqldb/sqlc/queries/payments.sql @@ -368,3 +368,44 @@ VALUES ( -- name: FailPayment :execresult UPDATE payments SET fail_reason = $1 WHERE payment_identifier = $2; + +/* ───────────────────────────────────────────── + Migration-specific queries + + These queries are used ONLY for the one-time migration from KV to SQL. + ───────────────────────────────────────────── +*/ + +-- name: InsertPaymentMig :one +-- Migration-specific payment insert that allows setting fail_reason. +-- Normal InsertPayment forces fail_reason to NULL since new payments +-- aren't failed yet. During migration, we're inserting historical data +-- that may already be failed. +INSERT INTO payments ( + amount_msat, + created_at, + payment_identifier, + fail_reason) +VALUES ( + @amount_msat, + @created_at, + @payment_identifier, + @fail_reason +) +RETURNING id; + +-- name: FetchPaymentsByIDsMig :many +-- Migration-specific batch fetch that returns payment data along with HTLC +-- attempt counts for structural validation during KV to SQL migration. +SELECT + p.id, + p.amount_msat, + p.created_at, + p.payment_identifier, + p.fail_reason, + COUNT(ha.id) AS htlc_attempt_count +FROM payments p +LEFT JOIN payment_htlc_attempts ha ON ha.payment_id = p.id +WHERE p.id IN (sqlc.slice('payment_ids')/*SLICE:payment_ids*/) +GROUP BY p.id, p.amount_msat, p.created_at, p.payment_identifier, p.fail_reason +ORDER BY p.id ASC; \ No newline at end of file