Postgres schema and migrations #2
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Part of #1.
Goal
Create the database schema tireless persists to, as sqlx migrations, plus the
decision about how queries are checked at compile time.
crates/tireless-data/declares the
migratefeature andstore.rsrefers to amigrations/directory that does not exist, so this is the first missing piece of stage 1.
Three tables, matching the domain types in
tireless-entities: tracked repos,jobs, and agent runs. The claim protocol (design.md §4.2) needs jobs to carry
claimed_byandclaim_expires_atand to be indexed for the claim query;enqueue idempotency (§4.4) needs a unique key on
(forge, owner, repo, number).One decision must be made here and stated in the PR, because it is expensive
to reverse.
sqlxis configured with themacrosfeature, soquery!checksSQL at compile time against either a live
DATABASE_URLor committed offlinemetadata. CI has neither today. Either commit
.sqlxoffline data and addcargo sqlx prepare --checkto the workflow, or use runtimequery_asand dropthe
macrosfeature. Both are defensible; pick one, say why in the PR, and beconsistent.
Files
crates/tireless-data/migrations/0001_init.sql— new; the three tables, theirindexes and constraints
crates/tireless-data/src/store.rs— embed the migrator, run migrations onstartup
crates/tireless-data/Cargo.toml— adjust the sqlx feature set if the runtimequery path is chosen
.gitea/workflows/deploy.yaml— add the offline-data check, if that path ischosen
Steps
0001_init.sqlcoveringtracked_repo,jobandagent_run. Mirrorthe field names in
tireless_entitiesso the mapping stays obvious.(forge, owner, repo, number)that makesenqueue an upsert.
filtered by kind.
adding a
JobKindvariant should be a migration, not a type alteration thatlocks the table.
PgStoreand run it on connect.Acceptance
cargo test --workspacecargo clippy --all-targets --all-features -- -D warningsa no-op.
Out of scope
JobStoretrait implementation — that is its own child, and it depends onthis one.
behaviour lands in stage 2.
PgConfig/Database. Auth is ident-mappedmTLS (design.md §4.1); a password appearing here means the ident mapping is
wrong and should be fixed there instead.
Done in
6545d19.The decision this issue asked for
Runtime queries, not
query!. No.sqlxoffline metadata, noDATABASE_URLneeded to build, no
cargo sqlx prepare --checkin CI.lairball— the other house project on this cluster — already does exactlythis: 19 uses of
sqlx::query(, zero macros, no.sqlxdirectory, no Postgresin CI. But the deciding argument is specific to tireless. Compile-time checking
makes a database a build dependency: regenerating
.sqlxafter any querychange needs a live Postgres. This crate is meant to be modified by a 27B model
working unattended (design.md §2.4), and a build that fails without a database
it cannot provision is a build that model cannot fix — its documented failure
mode being to improvise.
The cost is real and named in
store.rs: a malformed query is caught by a testagainst a real database rather than by
cargo build. That is why the schematests below exist.
What the schema does that isn't obvious
The live-issue index is partial, and getting it wrong is silent in both
directions. A plain
unique (forge, owner, repo, number)— which is what"enqueue is an upsert keyed on …" suggests — would forbid re-running a terminal
job, and would forbid the discovery lane outright, since discovery recurs
against one tracking issue on a cooldown (§2.6). Partial on the non-terminal
states gives at most one live job per issue plus unlimited history.
The claim index orders by
created_atalone, leavingkindas a filter.Leading with
kind— which is what this issue's step 3 proposed — makes theplanner sort every pending row on every claim, because
LIMIT 1can otherwisestop at the first match:
(kind, created_at)(created_at)The gap grows with the backlog rather than staying fixed.
INCLUDE (kind)wasmeasured too and dropped:
FOR UPDATEvisits the heap regardless.A spelling that would have been permanent.
rename_all = "snake_case"turnsForge::GitHubintogit_hub— in the database, the JSON API and thegenerated TypeScript. Renamed to
githubwhile nothing is persisted and GitHubsupport is still disabled. There is a test.
Check constraints are tested against the domain enums. Adding a
JobKindvariant without a migration would otherwise fail as a constraint violation on
the first job of that kind — in production, unattended. Now it fails
cargo test.Verified, not assumed
Against Postgres 18 (same major as the house cluster):
matters because all three units migrate on start and a deploy restarts them
together;
including
git_hubnow being rejected;loser skipping rather than blocking — the property §4.2 rests on.
The live tests are
#[ignore]d rather than skipping on a missing variable, so agreen
cargo testnever implies the schema was exercised.CLAUDE.mdsays howto run them, and that an applied migration must never be edited — sqlx keeps a
checksum per version, so editing one makes every deployed database refuse to
start.
Left alone
No
JobStoreimpl (that is #3), no forge work, no reconciliation or lease-expirybehaviour — the columns exist, including
labels_synced_atfor the pollersplit in §6.4, but nothing reads them yet. No password field went near
Database.