Collapse alembic 0001..0089 into one baseline #245

Merged
bvandeusen merged 2 commits from dev into main 2026-09-01 01:30:52 -04:00
Showing only changes of commit aa71cbbdbf - Show all commits
+42 -13
View File
@@ -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 <table>` 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.