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) <noreply@anthropic.com>
This commit is contained in:
2026-08-21 15:51:43 -05:00
parent c5540ce6da
commit 495d87dd72

View File

@@ -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'),