// 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 }