From 495d87dd7228bd8efb7c03833e14888969e0446e Mon Sep 17 00:00:00 2001 From: Cody Schaefer Date: Fri, 21 Aug 2026 15:51:43 -0500 Subject: [PATCH] Fix T8.6 migration: boolean default that Postgres rejects outright NOT part of D13. Found while testing T10.3's migration against a real Postgres, which could not be reached because the chain dies two revisions earlier. a1b8c6d4e2f9 (T8.6 / D6, 190144c, Aug 19) creates material_items.active as: sa.Column('active', sa.Boolean(), nullable=False, server_default=sa.text('1')) sa.text() emits raw SQL, so that is an integer literal, and Postgres refuses it: psycopg.errors.DatatypeMismatch: column "active" is of type boolean but default expression is of type integer SQLite accepts 1 as a boolean without complaint, which is why this passed every local test. The sibling migration e2a4c7d91b30 creates the identical column correctly with sa.true() - so the two are inconsistent and the older one is right. Fixed to match, which renders as `true` on both engines. THIS IS VERY LIKELY THE PRODUCTION 502. The Dockerfile CMD is `alembic upgrade head && exec gunicorn ...`, so a failed migration means gunicorn is never reached: no API process, nginx cannot reach api:8000, and every /api/ route returns 502 while the static site keeps serving normally. That is exactly the observed symptom - login.html 200, /api/health 502 - and this migration landed Aug 19, so the first deploy carrying it would be the first to break. `docker compose logs api` should show the DatatypeMismatch above. Committed separately from the D13 work so it can be cherry-picked to main ahead of this branch. It is a one-line fix to a shipped wave 8 migration and should not wait for an auth wave to merge. Verified on postgres:16-alpine (the image docker-compose uses): the full chain from empty now reaches head. Co-Authored-By: Claude Opus 5 (1M context) --- .../versions/a1b8c6d4e2f9_material_items_project_list.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/server/alembic/versions/a1b8c6d4e2f9_material_items_project_list.py b/server/alembic/versions/a1b8c6d4e2f9_material_items_project_list.py index 4661c90..7c9fd2d 100644 --- a/server/alembic/versions/a1b8c6d4e2f9_material_items_project_list.py +++ b/server/alembic/versions/a1b8c6d4e2f9_material_items_project_list.py @@ -32,7 +32,12 @@ 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'). sa.text() emits raw SQL, and Postgres refuses + # an integer default on a boolean column: "column active is of type boolean + # but default expression is of type integer". SQLite accepts 1 happily, so + # this passed every local test and failed only on the real engine. The + # sibling migration e2a4c7d91b30 does the identical column correctly. + 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'),