Consolidate migrations 001-080 into a checkpoint baseline#542
Conversation
…plied
Discovered while generating the v080 checkpoint baseline (see follow-up
commit): replaying every migration into a pristine apache/age container
with psql -v ON_ERROR_STOP=1 aborted at 058 with
ERROR: syntax error at or near "CREATE"
LINE 14: ... 'SELECT * FROM cypher(''knowledge_graph'', $$ CREATE VLA...
Two stacked defects, both previously invisible:
1. Dollar-quote collision (parse-time, deterministic). The EXECUTE format
string embedded `$$ CREATE VLABEL ... $$` inside a DO $$ block; the
inner $$ terminates the outer block body, so the file has never parsed
at all. Production never noticed because migrate-db.sh treats a failed
migration as "skip, retry on next run" — it has failed silently on
every platform start since the file was written. Confirmed on the live
dev database: schema_migrations records versions 57 and 59 with no 58.
2. Even without the quoting bug, the statement was invalid: openCypher has
no CREATE VLABEL statement. Label precreation is AGE's
ag_catalog.create_vlabel()/create_elabel() API, whose parameters are
cstring — a text variable needs an explicit ::cstring cast or function
resolution fails. (This second failure mode surfaced during the first
rewrite attempt, masked by the migration's own EXCEPTION handlers:
"function ag_catalog.create_vlabel(unknown, text) does not exist".)
Consequences of it never running: every fresh install has relied on lazy
label auto-creation during ingestion — the exact concurrent-DDL race
('relation "Source" already exists') this migration was written to
eliminate — and migration 078's MATCH on :Ontology assumed a precreated
label that 058 never actually delivered.
The rewrite keeps the IF NOT EXISTS guards and EXCEPTION handlers, and
creates labels via PERFORM ag_catalog.create_vlabel('knowledge_graph',
lbl::cstring) / create_elabel(...). Verified in the checkpoint replay: it
creates all 45 missing labels on a fresh graph with zero failures, and it
now shows as pending-warm on the dev database, so existing deployments
finally apply it on their next platform start.
Safe to modify in place — "never modify existing migrations" protects
migrations that have been applied somewhere; this one never was, anywhere.
Claude-Session: https://claude.ai/code/session_018VLKzWianGCKvY7CPbow8Z
Executes ADR-210's 'periodically consolidate' plan. schema/00_baseline.sql is now a generated checkpoint of the full resting state at version 080: - Generated, not hand-merged: schema/scripts/checkpoint.sh replays the old baseline + all 78 migration files into a throwaway apache/age container via raw psql, then dumps relational DDL and seed data with pg_dump. Graph-seeding cypher migrations (014, 044, 058, 078) are carried into the baseline verbatim because AGE graphs cannot round-trip pg_dump. - Verified faithful: a second container built from the candidate alone matches the replayed original on full schema dump, AGE label catalog, per-table row counts, and normalized seed data. - Migrations 001-080 move to schema/migrations/archived/. The baseline records versions 1-80 in schema_migrations, so fresh installs skip them; migrate-db.sh scans archived/ first, so pre-checkpoint databases keep their incremental upgrade path. Next migration: 081. - Warm migration 058 stays in migrations-warm/ for pre-checkpoint deployments (none ever applied it; see previous commit). The kg-postgres image bakes 00_baseline.sql at build time and must be rebuilt/republished with the next release. Claude-Session: https://claude.ai/code/session_018VLKzWianGCKvY7CPbow8Z
The checkpoint baseline is pg_dump output, which the docs generator did not fully understand: multi-word types (character varying, timestamp without time zone) were truncated to their first word, and constraints emitted as separate ALTER TABLE ... ADD CONSTRAINT statements (how pg_dump writes PKs, FKs, UNIQUEs) were dropped entirely. Both are now parsed and folded back into the column tables. With FK edges available, each schema section now opens with a mermaid erDiagram of its relationships — rendered natively by GitHub and by mkdocs-material (superfences already configured), published to GitHub Pages by the existing docs workflow, regenerated via make docs-schema. Table counts in the regenerated page drop (42->40 kg_api, 15->14 kg_auth) because the old textual parser counted tables that later migrations dropped or renamed; the checkpoint reflects the true resting state. Claude-Session: https://claude.ai/code/session_018VLKzWianGCKvY7CPbow8Z
Code ReviewScope per request: hand-written logic (checkpoint.sh, migrate-db.sh scan, 058 rewrite, generate-schema-docs.py). Baseline content not relitigated (A/B verification accepted); structure skimmed. Medium — checkpoint.sh is already stale for its stated purpose (future re-pins)Location: Problem: This PR moves migrations to
Why it matters: The header says the script is "kept for future re-pins", but it broke in the same commit that used it. Whoever runs checkpoint v2 hits both. Suggestion: Update the seed paths to Medium (pre-existing, adjacent) — migrate-db.sh's skip-and-retry path is unreachable under
|
- checkpoint.sh: GRAPH_SEED_MIGRATIONS now points into migrations/archived/ (this PR moved the files it referenced), and migration_files() filters unexpanded globs so replay works with an empty migrations/ directory. Validated end-to-end from the post-checkpoint repo state: replay picks up only warm 058 and the regenerated candidate verifies faithful. - migrate-db.sh: the skip-and-retry path was dead code — under set -e a bare command-substitution assignment aborts the script on psql failure before the exit code is examined. Capture it with an || arm. Matters now that pre-checkpoint databases apply batches of archived migrations. - 058 + baseline: label-creation failures escalate NOTICE -> WARNING so a future signature drift is visible instead of swallowed. - baseline seed-data section: re-inject the CHANGE-IN-PRODUCTION warning for development-default credentials (lost from migration 020's comment during consolidation); generator emits it for future re-pins. - generate-schema-docs.py: parse ALTER COLUMN ... SET DEFAULT nextval(...) (27 serial defaults were missing from the reference); disambiguate ER diagram entities whose names collide across schemas. Claude-Session: https://claude.ai/code/session_018VLKzWianGCKvY7CPbow8Z
Summary
schema/00_baseline.sqlis now a generated consolidation of migrations 001–080; the old files move toschema/migrations/archived/and the next migration is 081.ALTER TABLE … ADD CONSTRAINTPKs/FKs) and emits per-schema mermaid ER diagrams, published by the existing GitHub Pages workflow, regenerated viamake docs-schema.Finding: migration 058 never ran
While replaying all 78 migration files into a pristine
apache/agecontainer withpsql -v ON_ERROR_STOP=1, the replay aborted at058_precreate_graph_labels.sql:Two stacked defects, both invisible in production:
EXECUTE format(...)string embeds$$ CREATE VLABEL … $$inside aDO $$block — the inner$$terminates the outer block, so the file has never parsed.migrate-db.shtreats failures as "skip, retry next run", so it failed silently on every platform start since the file was written. Confirmed on the live dev database:schema_migrationsrecords versions 57 and 59, no 58.CREATE VLABEL; label precreation is AGE'sag_catalog.create_vlabel()/create_elabel()API, whosecstringparameters need an explicit cast from atextvariable (the first rewrite attempt surfacedfunction ag_catalog.create_vlabel(unknown, text) does not exist, silently swallowed by the migration's own EXCEPTION handlers).Consequence: every fresh install has relied on lazy label auto-creation during ingestion — the exact concurrent-DDL race (
relation "Source" already exists) 058 was written to eliminate — and 078'sMATCH (:Ontology …)assumed a precreated label that never was. The fixed version creates all 45 missing labels cleanly in replay, and shows as pending-warm on existing deployments, so they finally apply it on next platform start. Modified in place deliberately: "never modify existing migrations" protects applied migrations; this one never applied.How the checkpoint was built
schema/scripts/checkpoint.sh(kept for future re-pins):apache/age:release_PG18_1.7.0container (graph_accel mounted) via raw psql, no operator tooling.schema_migrationsrows 1–80 recorded by the baseline.Upgrade-path semantics
archived/re-runs.migrate-db.shscansarchived/beforemigrations/, so a DB at e.g. version 60 still applies 61–80, then 081+.Test plan
checkpoint.sh verify— all four A/B diffs cleanmigrate-db.sh --dry-runand--dry-run --warmagainst the live pre-checkpoint dev DB behave correctlymake docs-schemaregenerates; table counts now match the real database (the old textual parser over-counted dropped/renamed tables)00_baseline.sqlat build time)https://claude.ai/code/session_018VLKzWianGCKvY7CPbow8Z