textmachine/platform/internal/pgstore/credits.sql.go

340 lines
12 KiB
Go

// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
// source: credits.sql
package pgstore
import (
"context"
"time"
"textmachine/platform/internal/money"
)
const accountExists = `-- name: AccountExists :one
select true from users where id = $1
`
// No balance row is TWO different facts, and answering both with "insufficient credit" told an
// operator who mistyped an account id that the account was broke (PD-82). Postgres refuses FOR
// UPDATE on the nullable side of an outer join, so there is no one-query form of this question.
func (q *Queries) AccountExists(ctx context.Context, id string) (bool, error) {
row := q.db.QueryRow(ctx, accountExists, id)
var column_1 bool
err := row.Scan(&column_1)
return column_1, err
}
const balance = `-- name: Balance :one
select coalesce(b.balance_micro_usd, 0)::bigint as balance_micro_usd
from users u left join account_balances b on b.user_id = u.id
where u.id = $1
`
// Money. Every amount here is whole micro-USD and stays integral through the generated layer.
//
// ⚠ The ::bigint casts on the AGGREGATES are load-bearing and were arrived at by measurement, not
// taste. `sum(bigint)` is `numeric` in Postgres, and sqlc types a coalesced aggregate WITHOUT a cast
// as `interface{}` — money with no type at all, which is the worst of the three outcomes. With the
// cast it is `int64`: integral, and one explicit conversion away from money.MicroUSD in the domain
// layer. What sqlc will NOT do either way is apply the `*.*_micro_usd` override to a computed
// column — that override reaches real table columns only (measured; an alias-targeted override does
// not reach them either). So the aggregates below are the one place in this file where the money
// TYPE is restored by hand, and credits.go says so at each site.
// What the account may still spend, read from the cache that every ledger write updates in its own
// transaction.
//
// ⚠ The coalesce is not a simplification, it is a CORRECTION forced by measurement. The column is
// `not null` in the table, so sqlc types it non-nullable and does not notice that the LEFT JOIN can
// still produce NULL — an account that exists with no ledger rows. Left as `b.balance_micro_usd`,
// the generated Scan would ERROR on exactly that account instead of answering "no credit". The two
// facts stay distinguishable without the pointer: a missing ACCOUNT is still no row at all, because
// the outer `from users` decides that, and a missing BALANCE row is the zero this coalesce supplies.
func (q *Queries) Balance(ctx context.Context, userID string) (int64, error) {
row := q.db.QueryRow(ctx, balance, userID)
var balance_micro_usd int64
err := row.Scan(&balance_micro_usd)
return balance_micro_usd, err
}
const creditHeldBy = `-- name: CreditHeldBy :one
select coalesce((select r1.book_id from reservations r1
where r1.user_id = $1 and r1.state = 'open'
and r1.book_id <> $2
group by r1.book_id
order by sum(r1.amount_micro_usd) desc, min(r1.opened_at) limit 1), '')::text as book_id,
coalesce((select sum(r2.amount_micro_usd) from reservations r2
where r2.user_id = $1 and r2.state = 'open'
and r2.book_id <> $2), 0)::bigint as held_micro_usd
`
type CreditHeldByParams struct {
UserID string
ExceptBookID string
}
type CreditHeldByRow struct {
BookID string
HeldMicroUsd int64
}
// ⚠ The book with the LARGEST hold, not the oldest: naming a book whose $0.03 hold was merely opened
// first, beside another holding $9.60, sends the user to cancel a run that frees nothing. Ties go to
// the older one, so the answer is stable between two reads.
func (q *Queries) CreditHeldBy(ctx context.Context, arg CreditHeldByParams) (CreditHeldByRow, error) {
row := q.db.QueryRow(ctx, creditHeldBy, arg.UserID, arg.ExceptBookID)
var i CreditHeldByRow
err := row.Scan(&i.BookID, &i.HeldMicroUsd)
return i, err
}
const deleteBook = `-- name: DeleteBook :exec
delete from books where id = $1
`
func (q *Queries) DeleteBook(ctx context.Context, id string) error {
_, err := q.db.Exec(ctx, deleteBook, id)
return err
}
const deleteClosedReservationsOfBook = `-- name: DeleteClosedReservationsOfBook :exec
delete from reservations where book_id = $1 and state <> 'open'
`
// Closed reservations carry no financial fact the ledger does not already hold. An OPEN one blocks
// the book delete through the foreign key, which is the point.
func (q *Queries) DeleteClosedReservationsOfBook(ctx context.Context, bookID string) error {
_, err := q.db.Exec(ctx, deleteClosedReservationsOfBook, bookID)
return err
}
const insertLedgerEntry = `-- name: InsertLedgerEntry :execrows
insert into credit_ledger (user_id, kind, amount_micro_usd, source, source_id, note, created_at)
values ($1, $2, $3, $4, $5,
$6, $7)
on conflict (user_id, source, source_id) do nothing
`
type InsertLedgerEntryParams struct {
UserID string
Kind string
Amount money.MicroUSD
Source string
SourceID string
Note string
Now time.Time
}
// The ledger is APPEND-ONLY in the code that writes it, not in the schema: no statement in this
// package updates or deletes a row here, and TestTheLedgerIsAppendOnlyInTheCodeThatWritesIt walks
// every SQL string to keep that true.
func (q *Queries) InsertLedgerEntry(ctx context.Context, arg InsertLedgerEntryParams) (int64, error) {
result, err := q.db.Exec(ctx, insertLedgerEntry,
arg.UserID,
arg.Kind,
arg.Amount,
arg.Source,
arg.SourceID,
arg.Note,
arg.Now,
)
if err != nil {
return 0, err
}
return result.RowsAffected(), nil
}
const lockAccountBalance = `-- name: LockAccountBalance :one
select balance_micro_usd from account_balances where user_id = $1 for update
`
// Every money operation starts here, so they all take their locks in the same order.
// ⚠ Named LockAccountBalance and not LockBalance: sqlc derives an unexported const from the query
// name, and `lockBalance` is already the hand-written helper this one serves. Same for
// LockBookForUpdate, MarkReservationClosed and InsertLedgerEntry.
func (q *Queries) LockAccountBalance(ctx context.Context, userID string) (money.MicroUSD, error) {
row := q.db.QueryRow(ctx, lockAccountBalance, userID)
var balance_micro_usd money.MicroUSD
err := row.Scan(&balance_micro_usd)
return balance_micro_usd, err
}
const lockAttemptUnit = `-- name: LockAttemptUnit :one
select unit_name from run_attempts where id = $1 for update
`
// The reconciler's belief that an attempt never spawned is re-checked HERE, in the same transaction
// as the money, against the row rather than against the sweep's snapshot.
func (q *Queries) LockAttemptUnit(ctx context.Context, id int64) (*string, error) {
row := q.db.QueryRow(ctx, lockAttemptUnit, id)
var unit_name *string
err := row.Scan(&unit_name)
return unit_name, err
}
const lockBookForUpdate = `-- name: LockBookForUpdate :one
select id from books where id = $1 for update
`
// The FIRST lock of every transaction in this package that touches more than one of:
// books → runs → run_attempts → account_balances → reservations. A missing book is not an error
// here: the caller's own statements find nothing and decide what that means.
func (q *Queries) LockBookForUpdate(ctx context.Context, id string) (string, error) {
row := q.db.QueryRow(ctx, lockBookForUpdate, id)
var id_2 string
err := row.Scan(&id_2)
return id_2, err
}
const markReservationClosed = `-- name: MarkReservationClosed :one
update reservations set state = $1, closed_at = $2::timestamptz
where engine_run_id = $3 and state = 'open'
returning amount_micro_usd
`
type MarkReservationClosedParams struct {
State string
Now time.Time
EngineRunID string
}
// The state guard is what makes the unlocked owner read above safe: a reservation closed by someone
// else in between makes this update match nothing.
func (q *Queries) MarkReservationClosed(ctx context.Context, arg MarkReservationClosedParams) (money.MicroUSD, error) {
row := q.db.QueryRow(ctx, markReservationClosed, arg.State, arg.Now, arg.EngineRunID)
var amount_micro_usd money.MicroUSD
err := row.Scan(&amount_micro_usd)
return amount_micro_usd, err
}
const moveBalance = `-- name: MoveBalance :exec
insert into account_balances (user_id, balance_micro_usd, updated_at)
values ($1, $2, $3)
on conflict (user_id) do update
set balance_micro_usd = account_balances.balance_micro_usd + excluded.balance_micro_usd,
updated_at = excluded.updated_at
`
type MoveBalanceParams struct {
UserID string
Amount money.MicroUSD
Now time.Time
}
// Written in the SAME transaction as the ledger row above, always. A cache that can lag its source
// is a second answer about money.
func (q *Queries) MoveBalance(ctx context.Context, arg MoveBalanceParams) error {
_, err := q.db.Exec(ctx, moveBalance, arg.UserID, arg.Amount, arg.Now)
return err
}
const openReservation = `-- name: OpenReservation :execrows
insert into reservations (engine_run_id, user_id, book_id, amount_micro_usd, ceiling_micro_usd, state, opened_at)
select $1, $2, $3, $4,
$4, 'open', $5
from books where id = $3 and owner_id = $2
`
type OpenReservationParams struct {
EngineRunID string
UserID string
BookID string
Amount money.MicroUSD
Now time.Time
}
// The book must belong to the account being charged. The foreign key only proves the book exists,
// which is not the same question — hence the `select ... from books where owner_id`, whose zero rows
// ARE the refusal.
func (q *Queries) OpenReservation(ctx context.Context, arg OpenReservationParams) (int64, error) {
result, err := q.db.Exec(ctx, openReservation,
arg.EngineRunID,
arg.UserID,
arg.BookID,
arg.Amount,
arg.Now,
)
if err != nil {
return 0, err
}
return result.RowsAffected(), nil
}
const openReservations = `-- name: OpenReservations :many
select engine_run_id, book_id, amount_micro_usd, ceiling_micro_usd, opened_at
from reservations where user_id = $1 and state = 'open' order by opened_at
`
type OpenReservationsRow struct {
EngineRunID string
BookID string
AmountMicroUsd money.MicroUSD
CeilingMicroUsd money.MicroUSD
OpenedAt time.Time
}
// Holds that were taken and never closed. Without this they are money that is gone from the balance
// and invisible to everything that could give it back.
func (q *Queries) OpenReservations(ctx context.Context, userID string) ([]OpenReservationsRow, error) {
rows, err := q.db.Query(ctx, openReservations, userID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []OpenReservationsRow
for rows.Next() {
var i OpenReservationsRow
if err := rows.Scan(
&i.EngineRunID,
&i.BookID,
&i.AmountMicroUsd,
&i.CeilingMicroUsd,
&i.OpenedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const readAccount = `-- name: ReadAccount :one
select coalesce((select ab.balance_micro_usd from account_balances ab
where ab.user_id = $1), 0)::bigint as balance_micro_usd,
coalesce((select sum(cl.amount_micro_usd) from credit_ledger cl
where cl.user_id = $1), 0)::bigint as ledger_sum_micro_usd,
coalesce((select sum(r.amount_micro_usd) from reservations r
where r.user_id = $1 and r.state = 'open'), 0)::bigint as reserved_micro_usd
from users u where u.id = $1
`
type ReadAccountRow struct {
BalanceMicroUsd int64
LedgerSumMicroUsd int64
ReservedMicroUsd int64
}
// The three are read in ONE snapshot on purpose: reading them separately reports drift that a
// concurrent grant caused between the queries. LedgerSum exists to be COMPARED with Balance.
func (q *Queries) ReadAccount(ctx context.Context, userID string) (ReadAccountRow, error) {
row := q.db.QueryRow(ctx, readAccount, userID)
var i ReadAccountRow
err := row.Scan(&i.BalanceMicroUsd, &i.LedgerSumMicroUsd, &i.ReservedMicroUsd)
return i, err
}
const reservationOwner = `-- name: ReservationOwner :one
select user_id from reservations where engine_run_id = $1
`
func (q *Queries) ReservationOwner(ctx context.Context, engineRunID string) (string, error) {
row := q.db.QueryRow(ctx, reservationOwner, engineRunID)
var user_id string
err := row.Scan(&user_id)
return user_id, err
}