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

353 lines
11 KiB
Go

// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
// source: exports.sql
package pgstore
import (
"context"
"time"
"github.com/jackc/pgx/v5/pgtype"
)
const expireReadyExports = `-- name: ExpireReadyExports :many
update exports set state = 'expired'
where id in (select e.id from exports e
where e.state = 'ready' and e.expires_at <= $1
order by e.expires_at limit $2)
returning id
`
type ExpireReadyExportsParams struct {
Now *time.Time
Lim int32
}
// The GC's first question: which links have lapsed. The ROW moves here and nothing else does — a
// link must stop working at the moment it was promised to, and that is a statement the row makes.
//
// ⚠ `path` is KEPT, not cleared, and that is the difference between a leak and a retry. Clearing it
// in the same breath as the state made the unlink a ONE-SHOT act: a removal that failed — a full
// disk, a read-only mount, a crash between the two — left a file that nothing could ever name
// again, because every other read of this table selects `ready`. The path now outlives the state
// change and `ForgetExportPath` takes it away once the bytes are actually gone (`UnlinkedExports`).
func (q *Queries) ExpireReadyExports(ctx context.Context, arg ExpireReadyExportsParams) ([]string, error) {
rows, err := q.db.Query(ctx, expireReadyExports, arg.Now, arg.Lim)
if err != nil {
return nil, err
}
defer rows.Close()
var items []string
for rows.Next() {
var id string
if err := rows.Scan(&id); err != nil {
return nil, err
}
items = append(items, id)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const failExport = `-- name: FailExport :execrows
update exports
set state = 'failed', failure_code = $1, finished_at = $2
where id = $3 and state = 'pending'
`
type FailExportParams struct {
FailureCode *string
FinishedAt *time.Time
ID string
}
func (q *Queries) FailExport(ctx context.Context, arg FailExportParams) (int64, error) {
result, err := q.db.Exec(ctx, failExport, arg.FailureCode, arg.FinishedAt, arg.ID)
if err != nil {
return 0, err
}
return result.RowsAffected(), nil
}
const failStalePendingExports = `-- name: FailStalePendingExports :many
update exports
set state = 'failed', failure_code = $1, finished_at = $2
where id in (select e.id from exports e
where e.state = 'pending'
and ((e.started_at is not null and e.started_at <= $3)
or (e.started_at is null and e.requested_at <= $4))
order by e.requested_at limit $5)
returning id
`
type FailStalePendingExportsParams struct {
FailureCode *string
Now *time.Time
StartedCutoff *time.Time
QueuedCutoff time.Time
Lim int32
}
// The GC's second question: which builds nobody is coming back for. The queue does not retry this
// kind (jobs.ExportArgs), so nothing else would ever end these polls — and a poll that never ends is
// the one thing the canon forbids of this resource.
//
// ⚠ TWO CLOCKS, not one, and the second one was an adversarial pass's finding. A row a worker HAS
// picked up is judged from `started_at`: silence longer than one job's whole life means its process
// is gone. A row still IN THE QUEUE is judged from `requested_at` against a much longer grace,
// because one queue serves spawns, parses and builds — four parses ahead of it are a wait, not a
// fault, and burying such a build tells its user it was interrupted when it had not begun. The long
// grace still ends the poll, which is what the canon actually requires.
func (q *Queries) FailStalePendingExports(ctx context.Context, arg FailStalePendingExportsParams) ([]string, error) {
rows, err := q.db.Query(ctx, failStalePendingExports,
arg.FailureCode,
arg.Now,
arg.StartedCutoff,
arg.QueuedCutoff,
arg.Lim,
)
if err != nil {
return nil, err
}
defer rows.Close()
var items []string
for rows.Next() {
var id string
if err := rows.Scan(&id); err != nil {
return nil, err
}
items = append(items, id)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const finishExport = `-- name: FinishExport :execrows
update exports
set state = 'ready', path = $1, size_bytes = $2,
complete = $3, finished_at = $4,
expires_at = $5
where id = $6 and state = 'pending'
`
type FinishExportParams struct {
Path *string
SizeBytes pgtype.Int8
Complete pgtype.Bool
FinishedAt *time.Time
ExpiresAt *time.Time
ID string
}
// `state = 'pending'` in the WHERE is the whole of the worker's write safety: a build that comes
// back after its export was already failed by the stale sweep must not resurrect the row and
// publish a link the poll has already reported as finished.
func (q *Queries) FinishExport(ctx context.Context, arg FinishExportParams) (int64, error) {
result, err := q.db.Exec(ctx, finishExport,
arg.Path,
arg.SizeBytes,
arg.Complete,
arg.FinishedAt,
arg.ExpiresAt,
arg.ID,
)
if err != nil {
return 0, err
}
return result.RowsAffected(), nil
}
const forgetExportPath = `-- name: ForgetExportPath :exec
update exports set path = null where id = $1
`
// The bytes are gone; the row stops naming them. Only after the unlink, or the retry above would
// have nothing to find.
func (q *Queries) ForgetExportPath(ctx context.Context, id string) error {
_, err := q.db.Exec(ctx, forgetExportPath, id)
return err
}
const insertExport = `-- name: InsertExport :exec
insert into exports (id, book_id, format, state, requested_at)
values ($1, $2, $3, 'pending', $4)
`
type InsertExportParams struct {
ID string
BookID string
Format string
RequestedAt time.Time
}
// The storage half of the export door (canon §createExport/§getExport).
//
// Every read joins `books.owner_id` rather than carrying an owner of its own: an export belongs to
// whoever the book belongs to, and one carrier of that fact is what keeps an authorization check
// from disagreeing with itself (API1 BOLA).
func (q *Queries) InsertExport(ctx context.Context, arg InsertExportParams) error {
_, err := q.db.Exec(ctx, insertExport,
arg.ID,
arg.BookID,
arg.Format,
arg.RequestedAt,
)
return err
}
const readExportForBuild = `-- name: ReadExportForBuild :one
select e.id, e.book_id, e.format, e.state, b.workdir,
exists (select 1 from chapters c where c.book_id = b.id) as has_tree
from exports e join books b on b.id = e.book_id
where e.id = $1
`
type ReadExportForBuildRow struct {
ID string
BookID string
Format string
State string
Workdir string
HasTree bool
}
// The worker's read: no owner, because the worker is not a caller — it was handed an id by the
// door that already checked ownership, and a job the queue re-drives after a restart has no
// session behind it at all.
//
// ⚠ It asks whether the book HAS A CHAPTER TREE, and the question moved here from the door on
// 04.09 by ratification: the canon forbids the door to refuse by a book's state («Nothing about a
// book's state conflicts with exporting it»), so the request is always accepted and the answer is
// given as the export's own outcome. The worker needs the fact because a book nobody has cut has
// nothing to build from and must NOT be sent to the engine — see exports.Service.Build.
func (q *Queries) ReadExportForBuild(ctx context.Context, id string) (ReadExportForBuildRow, error) {
row := q.db.QueryRow(ctx, readExportForBuild, id)
var i ReadExportForBuildRow
err := row.Scan(
&i.ID,
&i.BookID,
&i.Format,
&i.State,
&i.Workdir,
&i.HasTree,
)
return i, err
}
const readExportForOwner = `-- name: ReadExportForOwner :one
select e.id, e.book_id, e.format, e.state, e.path, e.size_bytes, e.failure_code, e.complete,
e.requested_at, e.finished_at, e.expires_at, b.revision
from exports e join books b on b.id = e.book_id
where e.id = $1 and e.book_id = $2 and b.owner_id = $3
`
type ReadExportForOwnerParams struct {
ID string
BookID string
OwnerID string
}
type ReadExportForOwnerRow struct {
ID string
BookID string
Format string
State string
Path *string
SizeBytes pgtype.Int8
FailureCode *string
Complete pgtype.Bool
RequestedAt time.Time
FinishedAt *time.Time
ExpiresAt *time.Time
Revision int64
}
// The book's revision travels with the row because the canon stamps every book-scoped response
// with it, and an export read is one — read in the SAME statement so the two cannot be a
// transaction apart.
func (q *Queries) ReadExportForOwner(ctx context.Context, arg ReadExportForOwnerParams) (ReadExportForOwnerRow, error) {
row := q.db.QueryRow(ctx, readExportForOwner, arg.ID, arg.BookID, arg.OwnerID)
var i ReadExportForOwnerRow
err := row.Scan(
&i.ID,
&i.BookID,
&i.Format,
&i.State,
&i.Path,
&i.SizeBytes,
&i.FailureCode,
&i.Complete,
&i.RequestedAt,
&i.FinishedAt,
&i.ExpiresAt,
&i.Revision,
)
return i, err
}
const stampExportStart = `-- name: StampExportStart :execrows
update exports set started_at = $1
where id = $2 and state = 'pending' and started_at is null
`
type StampExportStartParams struct {
Now *time.Time
ID string
}
// The worker says it has the build, and this is also what makes the claim EXCLUSIVE.
//
// Two guards, and each answers a different way of being wrong. `state = 'pending'` refuses a row the
// stale sweep has already ended — zero rows means the verdict is in and this worker must not build.
// `started_at is null` refuses a SECOND worker on a row the first is still building: without it both
// would build to the SAME path (it is derived from the export's id), and the loser's cleanup would
// delete the file the winner had just published. Unreachable while River works one job of a kind at
// a time; the day a second replica is deployed it is the whole difference between a duplicate build
// and a deleted artifact.
func (q *Queries) StampExportStart(ctx context.Context, arg StampExportStartParams) (int64, error) {
result, err := q.db.Exec(ctx, stampExportStart, arg.Now, arg.ID)
if err != nil {
return 0, err
}
return result.RowsAffected(), nil
}
const unlinkedExports = `-- name: UnlinkedExports :many
select id, path from exports
where state = 'expired' and path is not null
order by finished_at limit $1
`
type UnlinkedExportsRow struct {
ID string
Path *string
}
// Artifacts whose row has lapsed and whose bytes are still there. The retry of an unlink that did
// not happen — see ExpireReadyExports for why the path outlives the state.
func (q *Queries) UnlinkedExports(ctx context.Context, lim int32) ([]UnlinkedExportsRow, error) {
rows, err := q.db.Query(ctx, unlinkedExports, lim)
if err != nil {
return nil, err
}
defer rows.Close()
var items []UnlinkedExportsRow
for rows.Next() {
var i UnlinkedExportsRow
if err := rows.Scan(&i.ID, &i.Path); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}