diff --git a/server/alembic/versions/a1b8c6d4e2f9_material_items_project_list.py b/server/alembic/versions/a1b8c6d4e2f9_material_items_project_list.py index 4661c90..0fa5f06 100644 --- a/server/alembic/versions/a1b8c6d4e2f9_material_items_project_list.py +++ b/server/alembic/versions/a1b8c6d4e2f9_material_items_project_list.py @@ -32,7 +32,11 @@ def upgrade() -> None: sa.Column('code', sa.String(length=80), nullable=False, server_default=''), sa.Column('description', sa.String(length=300), nullable=False, server_default=''), sa.Column('unit', sa.String(length=20), nullable=False, server_default=''), - sa.Column('active', sa.Boolean(), nullable=False, server_default=sa.text('1')), + # sa.true(), not sa.text('1'): SQLite coerces integer 1 to boolean, + # Postgres refuses it (DatatypeMismatch) - found when this migration + # took down the wp.controls.dev api container on 2026-08-21. The + # location-taxonomy migration next door had it right all along. + sa.Column('active', sa.Boolean(), nullable=False, server_default=sa.true()), sa.Column('sort', sa.Integer(), nullable=False, server_default='0'), sa.Column('created_at', sa.DateTime(timezone=True), nullable=True), sa.PrimaryKeyConstraint('id'), diff --git a/tests/materials_check.py b/tests/materials_check.py index c477d28..c0a1c89 100644 --- a/tests/materials_check.py +++ b/tests/materials_check.py @@ -54,6 +54,20 @@ def main(): chk("locations and materials are both instances of it - not a copy beside it", "locList = WPListImport(" in suite and "matList = WPListImport(" in suite and "function locImport(dryRun){ locList.importText" in suite) + # The 2026-08-21 outage, pinned: a Boolean server_default of sa.text('1') + # passes on SQLite (which coerces 1) and crash-loops Postgres at deploy + # (DatatypeMismatch). Every migration must say sa.true()/sa.false(). + import re as _re + bad = [] + vdir = os.path.join(ROOT, "server", "alembic", "versions") + for fn in sorted(os.listdir(vdir)): + if not fn.endswith(".py"): + continue + for ln in open(os.path.join(vdir, fn), encoding="utf-8"): + if "Boolean" in ln and "server_default" in ln and not _re.search(r"server_default=sa\.(true|false)\(\)", ln): + bad.append("%s: %s" % (fn, ln.strip()[:90])) + chk("no migration gives a Boolean a non-portable server_default " + "(sa.true()/sa.false() only)", not bad, ascii_(bad[:3])) model = open(os.path.join(ROOT, "server", "models.py"), encoding="utf-8").read() mat_block = model[model.find("class MaterialItem"):model.find("class WpFile")] cols = re.findall(r"^\s+(\w+): Mapped", mat_block, re.M)