340 lines
12 KiB
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
|
|
}
|