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