feat(db): move fetch_url.last_fetched_at to a runtime fetch_state table (SA) - #147
Conversation
…le (SA) Dataset-separation SA / acceptance criterion #6: engine runtime must not mutate dataset-owned metadata tables, and operational timestamps belong in runtime tables. `fetch_url` is dataset-owned (projected from the dataset's sources.yaml), yet `levels fetch` wrote `last_fetched_at` into it on every run. Relocate that timestamp to a new runtime-only `fetch_state` table. - Migration 0076 creates fetch_state (fetch_url_id PK/FK CASCADE, last_fetched_at) and drops fetch_url.last_fetched_at. Schema-only (no DML — passes the >0074 schema-only guard); the old churn value is not carried over (no readers; fetch repopulates it next run). The CREATE TABLE mirrors SQLAlchemy create_all()'s separate-clause form so the migrated schema and the ORM introspect identically (keeps schema-parity green). DROP COLUMN is safe on prod (Debian 12+/13 ships SQLite >= 3.40; the column is in no index/FK). - models.py: drop FetchUrl.last_fetched_at, add the FetchState runtime model (not dataset-owned; absent from CONTRACT_CSVS, never exported/synced). - fetch.py: the writer upserts fetch_state by fetch_url id instead of mutating the fetch_url row. - layout.py: drop the fetch_url entry from EXCLUDED_COLUMNS (only the reach geom/gradient sidecar columns remain); sweep the EXCLUDED-churn comments in metadata_csv.py / export_metadata.py / import_metadata.py to cite reach.geom. - Regenerate live_schema.sql + the migration manifest; document the fetch_state table in database-schema.md and remove the fetch_url.last_fetched_at row; fix the ORM table count (24->25 / 25->26 live) in CLAUDE.md, README.md, database-schema.md; regenerate schema-overview.svg (also picks up 0075's unknown_station_policy, which had drifted). - Make scripts/regenerate_schema_svg.sh portable (BSD mktemp has no --suffix). fetch_url.last_fetched_at was already EXCLUDED from fetch_url.csv, so the CSV and `generate-sources --check` are byte-identical — no kayak_data change, no pin bump. init-db creates fetch_state via create_all; migrate creates it via 0076. Tests: a real-DB test that a fetch upserts fetch_state.last_fetched_at (and never fetch_url); the #145 optional-vs-preserved sync test re-anchored on a non-optional column now that fetch_url has no excluded column; schema-parity / schema-doc-sync / migrate stay green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Adversarial review findings:
No functional blockers found in the migration/fetch path after live DB verification. Verification performed:
|
Adversarial review — PR #147 (move
|
…leak (review) Two P3 review findings on #147: - tests/fixtures/live_schema.sql carried SQLAlchemy's trailing space after every column comma, so regenerating the fetch_url block (DROP COLUMN) added new trailing-whitespace lines that tripped `git diff --check`. The whole fixture had always had them (157 lines) — only the changed lines surfaced. Make regenerate_schema_snapshot.sh pipe its output through `sed 's/[[:space:]]*$//'` so the snapshot is clean and stays clean across regens, and regenerate the fixture (now zero trailing-whitespace lines; schema-parity unchanged). - regenerate_schema_svg.sh's `TMP_DB="$(mktemp)".db` created one temp file then pointed SQLite at a second path (`.db` appended); the trap only removed the second, leaking the original. Use `mktemp -d` (portable on BSD + GNU) and put schema.db inside it, trapping `rm -rf` on the dir — no leak. Full gate green (ruff/format/mypy, pytest -m "not slow" = 1384, wheel-smoke); `git diff --check main` clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Both P3 findings addressed in P3 — trailing whitespace in P3 — SVG temp-file leak. Switched Verified: full gate green (ruff/format/mypy, Thanks for the careful real-prod-DB verification of the migration + the deploy-sequencing note (pull+migrate as one chain while the pipeline is idle, with a pre-migrate backup — same as #143/0075). Still not merging — yours. |
What
Dataset-separation SA, second sub-slice (AC #6). Relocates the operational timestamp
last_fetched_atoff the dataset-ownedfetch_urltable onto a new runtime-onlyfetch_statetable, so engine runtime (levels fetch) no longer mutates a dataset-owned table.Why
AC #6: "Engine runtime … never create or mutate dataset-owned metadata rows/columns outside
sync-metadataor an explicit schema migration; operational timestamps and caches live in runtime tables."fetch_urlis projected from the dataset'ssources.yaml;fetch.pywrotelast_fetched_atinto it every run. (It's the exact churn column #145 had to special-case in the OPTIONAL-vs-EXCLUDED distinction.)Change
CREATE TABLE fetch_state (fetch_url_id PK→fetch_url ON DELETE CASCADE, last_fetched_at)+ALTER TABLE fetch_url DROP COLUMN last_fetched_at. Schema-only (no DML → passes the>0074schema-only guard). The CREATE TABLE mirrorscreate_all()'s separate PK/FK-clause form so the migrated schema and the ORM introspect identically (schema-parity).DROP COLUMNis safe on prod (Debian 12+/13 → SQLite ≥ 3.40; the column is in no index/FK).models.py— dropFetchUrl.last_fetched_at; add theFetchStateruntime model (not dataset-owned; absent fromCONTRACT_CSVS, never exported/synced).fetch.py— the writer upsertsfetch_stateby fetch_url id instead of mutating thefetch_urlrow.layout.py— drop thefetch_urlentry fromEXCLUDED_COLUMNS(only the reach geom/gradient sidecar columns remain); comment sweep inmetadata_csv.py/export_metadata.py/import_metadata.py.live_schema.sql+ manifest; documentfetch_stateindatabase-schema.md(remove thefetch_url.last_fetched_atrow); fix the ORM table count 24→25 / 25→26 live inCLAUDE.md/README.md/database-schema.md; regenerateschema-overview.svg(also clears 0075'sunknown_station_policydrift); makeregenerate_schema_svg.shportable (BSDmktemphas no--suffix).No kayak_data change
fetch_url.last_fetched_atwas already EXCLUDED fromfetch_url.csv, so removing it leaves the CSV andgenerate-sources --checkbyte-identical — no dataset PR, no pin bump.init-dbcreatesfetch_stateviacreate_all;migratecreates it via 0076.Verification
pytest -m "not slow"= 1384 passed, wheel-smoke.../DB— createsfetch_state, dropslast_fetched_at, zero FK violations;init-db(create_all) andmigrate(0076) produce identical schemas (schema-parity)._process_work_item(upserts a single row;fetch_urlhas no such column).generate-sources --checkbyte-identical,validate-datasetOK.Scope / next
No editor/PHP impact (the column had zero readers). Next SA sub-slices (separate PRs): SA-3 CI writer-boundary guard + convert the maintenance-tool writers; then the reverse-sync teardown.
🤖 Generated with Claude Code