diff --git a/alembic/versions/0089_baseline.py b/alembic/versions/0089_baseline.py index b9d89d5..f961ee1 100644 --- a/alembic/versions/0089_baseline.py +++ b/alembic/versions/0089_baseline.py @@ -39,22 +39,31 @@ Four things still had to be added by hand, because they are not in the models: Extensions are database objects, not table metadata, so no model can carry them. `IF NOT EXISTS` because a re-run must not fail. -2. **The two settings singleton rows.** 0002 and 0003 did not only build - schema — they each inserted a row: +2. **Three seed inserts** — the two settings singletons (0002, 0003) and the + three hygiene system tags (0075). Some migrations did not only build schema; + they inserted rows the product needs in order to function, and nothing in + the application ever creates them. Every consumer reads them with + `scalar_one()`, which RAISES `NoResultFound` on an empty result rather than + returning None, so their absence is a crash and not a degradation. - INSERT INTO import_settings (id) VALUES (1) - INSERT INTO ml_settings (id) VALUES (1) + Distinguishing these from the other data statements in the chain is the + whole trick, and the rule turns out to be mechanical: - and nothing in the application ever creates them. `ImportSettings.load()` - and `MLSettings.load()` are `select(...).scalar_one()`, which RAISES - `NoResultFound` on an empty result rather than returning None. So a baseline - built from models alone would produce those two tables empty and crash a - fresh install on its first settings access. + * `INSERT ... VALUES (...)` with literal values is a SEED. It creates + something the product ships. It must be carried. + * `INSERT ... SELECT ... FROM ` is a BACKFILL. It derives rows + from rows that already exist, so on an empty database it inserts + nothing and carrying it would be pointless. 0034 (artist_visit), 0040 + and 0047 (series_chapter) are all of this shape and are correctly + absent here. - This is worth dwelling on, because it is invisible to every automated check - this project has: `baseline.yml` compares SCHEMA, and the schema would have - been a perfect match. Only running the application against a fresh database - finds it. + This category is invisible to every automated check this project has: + `baseline.yml` compares SCHEMA, and a baseline missing all three seeds still + produces a byte-identical schema and a perfectly green diff. What caught the + system tags was the integration suite — 36 tests failing on + `NoResultFound` — after a first version of this file shipped with only the + two settings rows. A first-run check against the real application is the + only thing that finds this class of defect. 3. **The `pgvector` import.** Autogenerate emits qualified `pgvector.sqlalchemy.vector.VECTOR(...)` references without importing the @@ -794,6 +803,26 @@ def upgrade() -> None: op.execute("INSERT INTO import_settings (id) VALUES (1)") op.execute("INSERT INTO ml_settings (id) VALUES (1)") + # The three hygiene system tags, from 0075. These are PRODUCT data, not + # operator configuration — 0075's own docstring says so: "the fix keys on + # SYSTEM tags the product ships". The presentation and process auto-apply + # sweeps look them up with scalar_one(), so without these rows those + # features raise NoResultFound rather than degrading. + # + # 0075 adopted an existing same-name general tag before inserting, because + # an operator might already have tagged `wip` by hand. That cannot happen + # on the empty database this file runs against, but the guard is kept: it + # costs nothing and makes the statement safe to re-run. + for _name in ("wip", "banner", "editor screenshot"): + op.execute( + sa.text( + "INSERT INTO tag (name, kind, is_system) " + "SELECT :name, 'general', true WHERE NOT EXISTS (" + " SELECT 1 FROM tag WHERE lower(name) = lower(:name)" + ")" + ).bindparams(name=_name) + ) + def downgrade() -> None: """Deliberately not implemented.