sqldb: Change payment_intent relationship to payment table

Previously a one(intent)-to-many(payment) relationship it is now
changed to a one-to-one relationship because a payment request
only can have 1 payment related to it. Looking into the future
with BOLT12 offers, the fetched invoice from the offer could be
stored here as well and the relationship would still hold.
This commit is contained in:
ziggie committed 2026-02-25 08:02:08 +01:00
1 parent f8241748a1
commit 27071acb6b
4 files changed
+50 -49

No files matched your search

+40 -36
View File
@@ -2,43 +2,12 @@
-- Payment System Schema Migration
-- ─────────────────────────────────────────────
-- This migration creates the complete payment system schema including:
-- - Payment intents (BOLT 11/12 invoices, offers)
-- - Payment intents (only BOLT 11 invoices for now)
-- - Payment attempts and HTLC tracking
-- - Route hops and custom TLV records
-- - Resolution tracking for settled/failed payments
-- ─────────────────────────────────────────────
-- ─────────────────────────────────────────────
-- Payment Intents Table
-- ─────────────────────────────────────────────
-- Stores the descriptor of what the payment is paying for.
-- Depending on the type, the payload might contain:
-- - BOLT 11 invoice data
-- - BOLT 12 offer data
-- - NULL for legacy hash-only/keysend style payments
-- ─────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS payment_intents (
-- Primary key for the intent record
id INTEGER PRIMARY KEY,
-- The type of intent (e.g. 0 = bolt11_invoice, 1 = bolt12_offer)
-- Uses SMALLINT (int16) for efficient storage of enum values
intent_type SMALLINT NOT NULL,
-- The serialized payload for the payment intent
-- Content depends on type - could be invoice, offer, or NULL
intent_payload BLOB
);
-- Index for efficient querying by intent type
CREATE INDEX IF NOT EXISTS idx_payment_intents_type
ON payment_intents(intent_type);
-- Unique constraint for deduplication of payment intents
CREATE UNIQUE INDEX IF NOT EXISTS idx_payment_intents_unique
ON payment_intents(intent_type, intent_payload);
-- ─────────────────────────────────────────────
-- Payments Table
-- ─────────────────────────────────────────────
@@ -55,10 +24,6 @@ CREATE TABLE IF NOT EXISTS payments (
-- Primary key for the payment record
id INTEGER PRIMARY KEY,
-- Optional reference to the payment intent this payment was derived from
-- Links to BOLT 11 invoice, BOLT 12 offer, etc.
intent_id BIGINT REFERENCES payment_intents (id),
-- The amount of the payment in millisatoshis
amount_msat BIGINT NOT NULL,
@@ -84,6 +49,45 @@ CREATE TABLE IF NOT EXISTS payments (
CREATE INDEX IF NOT EXISTS idx_payments_created_at
ON payments(created_at);
-- ─────────────────────────────────────────────
-- Payment Intents Table
-- ─────────────────────────────────────────────
-- Stores the descriptor of what the payment is paying for.
-- Depending on the type, the payload might contain:
-- - BOLT 11 invoice data
-- - BOLT 12 offer data
-- - NULL for legacy hash-only/keysend style payments
-- ─────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS payment_intents (
-- Primary key for the intent record
id INTEGER PRIMARY KEY,
-- Reference to the payment this intent belongs to (one-to-one relationship)
-- When the payment is deleted, the intent is automatically deleted
payment_id BIGINT NOT NULL REFERENCES payments (id) ON DELETE CASCADE,
-- The type of intent (e.g. 0 = bolt11_invoice, 1 = bolt12_invoice)
-- Uses SMALLINT (int16) for efficient storage of enum values
intent_type SMALLINT NOT NULL,
-- The serialized payload for the payment intent
-- Content depends on type - could be invoice, offer, or NULL
intent_payload BLOB,
-- Ensure one-to-one relationship: each payment has at most one intent.
-- Currently we only support one intent per payment this makes sure we do
-- not accidentally pay the same request multiple times. This currently
-- only has bolt 11 payment requests/invoices. But in the future this can
-- also include BOLT 12 offers/invoices.
CONSTRAINT idx_payment_intents_payment_id_unique
UNIQUE (payment_id)
);
-- Index for efficient querying by intent type
CREATE INDEX IF NOT EXISTS idx_payment_intents_type
ON payment_intents(intent_type);
-- ─────────────────────────────────────────────
-- Payment HTLC Attempts Table
-- ─────────────────────────────────────────────
+1 -1
View File
@@ -211,7 +211,6 @@ type MigrationTracker struct {
type Payment struct {
ID int64
IntentID sql.NullInt64
AmountMsat int64
CreatedAt time.Time
PaymentIdentifier []byte
@@ -264,6 +263,7 @@ type PaymentHtlcAttemptResolution struct {
type PaymentIntent struct {
ID int64
PaymentID int64
IntentType int16
IntentPayload []byte
}
+6 -9
View File
@@ -317,11 +317,11 @@ func (q *Queries) FetchHtlcAttemptsForPayments(ctx context.Context, paymentIds [
const fetchPayment = `-- name: FetchPayment :one
SELECT
p.id, p.intent_id, p.amount_msat, p.created_at, p.payment_identifier, p.fail_reason,
p.id, p.amount_msat, p.created_at, p.payment_identifier, p.fail_reason,
i.intent_type AS "intent_type",
i.intent_payload AS "intent_payload"
FROM payments p
LEFT JOIN payment_intents i ON i.id = p.intent_id
LEFT JOIN payment_intents i ON i.payment_id = p.id
WHERE p.payment_identifier = $1
`
@@ -336,7 +336,6 @@ func (q *Queries) FetchPayment(ctx context.Context, paymentIdentifier []byte) (F
var i FetchPaymentRow
err := row.Scan(
&i.Payment.ID,
&i.Payment.IntentID,
&i.Payment.AmountMsat,
&i.Payment.CreatedAt,
&i.Payment.PaymentIdentifier,
@@ -398,11 +397,11 @@ func (q *Queries) FetchPaymentLevelFirstHopCustomRecords(ctx context.Context, pa
const fetchPaymentsByIDs = `-- name: FetchPaymentsByIDs :many
SELECT
p.id, p.intent_id, p.amount_msat, p.created_at, p.payment_identifier, p.fail_reason,
p.id, p.amount_msat, p.created_at, p.payment_identifier, p.fail_reason,
i.intent_type AS "intent_type",
i.intent_payload AS "intent_payload"
FROM payments p
LEFT JOIN payment_intents i ON i.id = p.intent_id
LEFT JOIN payment_intents i ON i.payment_id = p.id
WHERE p.id IN (/*SLICE:payment_ids*/?)
`
@@ -433,7 +432,6 @@ func (q *Queries) FetchPaymentsByIDs(ctx context.Context, paymentIds []int64) ([
var i FetchPaymentsByIDsRow
if err := rows.Scan(
&i.Payment.ID,
&i.Payment.IntentID,
&i.Payment.AmountMsat,
&i.Payment.CreatedAt,
&i.Payment.PaymentIdentifier,
@@ -510,11 +508,11 @@ const filterPayments = `-- name: FilterPayments :many
*/
SELECT
p.id, p.intent_id, p.amount_msat, p.created_at, p.payment_identifier, p.fail_reason,
p.id, p.amount_msat, p.created_at, p.payment_identifier, p.fail_reason,
i.intent_type AS "intent_type",
i.intent_payload AS "intent_payload"
FROM payments p
LEFT JOIN payment_intents i ON i.id = p.intent_id
LEFT JOIN payment_intents i ON i.payment_id = p.id
WHERE (
p.id > $1 OR
$1 IS NULL
@@ -572,7 +570,6 @@ func (q *Queries) FilterPayments(ctx context.Context, arg FilterPaymentsParams)
var i FilterPaymentsRow
if err := rows.Scan(
&i.Payment.ID,
&i.Payment.IntentID,
&i.Payment.AmountMsat,
&i.Payment.CreatedAt,
&i.Payment.PaymentIdentifier,
+3 -3
View File
@@ -9,7 +9,7 @@ SELECT
i.intent_type AS "intent_type",
i.intent_payload AS "intent_payload"
FROM payments p
LEFT JOIN payment_intents i ON i.id = p.intent_id
LEFT JOIN payment_intents i ON i.payment_id = p.id
WHERE (
p.id > sqlc.narg('index_offset_get') OR
sqlc.narg('index_offset_get') IS NULL
@@ -37,7 +37,7 @@ SELECT
i.intent_type AS "intent_type",
i.intent_payload AS "intent_payload"
FROM payments p
LEFT JOIN payment_intents i ON i.id = p.intent_id
LEFT JOIN payment_intents i ON i.payment_id = p.id
WHERE p.payment_identifier = $1;
-- name: FetchPaymentsByIDs :many
@@ -46,7 +46,7 @@ SELECT
i.intent_type AS "intent_type",
i.intent_payload AS "intent_payload"
FROM payments p
LEFT JOIN payment_intents i ON i.id = p.intent_id
LEFT JOIN payment_intents i ON i.payment_id = p.id
WHERE p.id IN (sqlc.slice('payment_ids')/*SLICE:payment_ids*/);
-- name: CountPayments :one