fix: make migration 168 idempotent for from-scratch upgrades

Migration 168 used bare add_column/drop_column, so `alembic upgrade head`
from an empty DB failed at 168 ("column revoked_at already exists"): the
0_initial_schema migration bootstraps via Base.metadata.create_all (the live
ORM shape), which already includes the hardened refresh_tokens columns, and
168's unguarded ALTERs then collided.

Guard every step (ADD COLUMN IF NOT EXISTS, DROP COLUMN IF EXISTS, existence-
checked backfill) so 168 no-ops on already-hardened (create_all) databases
while still fully transforming legacy is_revoked-shape databases. Matches the
guarded style already used by migrations 92/149/169. Net schema effect is
unchanged. Restores a clean from-scratch `alembic upgrade head` to head.
This commit is contained in:
CREDO23 2026-06-26 11:35:30 +02:00
parent 34216d05de
commit 3b31352e19

View file

@ -6,8 +6,6 @@ Revises: 167
from collections.abc import Sequence
import sqlalchemy as sa
from alembic import op
revision: str = "168"
@ -17,50 +15,52 @@ depends_on: str | Sequence[str] | None = None
def upgrade() -> None:
op.add_column(
"refresh_tokens",
sa.Column("revoked_at", sa.TIMESTAMP(timezone=True), nullable=True),
op.execute(
"ALTER TABLE refresh_tokens "
"ADD COLUMN IF NOT EXISTS revoked_at TIMESTAMP WITH TIME ZONE"
)
op.add_column(
"refresh_tokens",
sa.Column("absolute_expiry", sa.TIMESTAMP(timezone=True), nullable=True),
op.execute(
"ALTER TABLE refresh_tokens "
"ADD COLUMN IF NOT EXISTS absolute_expiry TIMESTAMP WITH TIME ZONE"
)
op.execute(
"""
UPDATE refresh_tokens
SET revoked_at = NOW()
WHERE is_revoked = TRUE
DO $$
BEGIN
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'refresh_tokens'
AND column_name = 'is_revoked'
) THEN
UPDATE refresh_tokens SET revoked_at = NOW() WHERE is_revoked = TRUE;
END IF;
END $$;
"""
)
op.alter_column(
"refresh_tokens",
"token_hash",
existing_type=sa.String(length=256),
type_=sa.String(length=64),
existing_nullable=False,
)
op.drop_column("refresh_tokens", "is_revoked")
op.execute("ALTER TABLE refresh_tokens ALTER COLUMN token_hash TYPE VARCHAR(64)")
op.execute("ALTER TABLE refresh_tokens DROP COLUMN IF EXISTS is_revoked")
def downgrade() -> None:
op.add_column(
"refresh_tokens",
sa.Column("is_revoked", sa.Boolean(), nullable=False, server_default="false"),
op.execute(
"ALTER TABLE refresh_tokens "
"ADD COLUMN IF NOT EXISTS is_revoked BOOLEAN NOT NULL DEFAULT false"
)
op.execute(
"""
UPDATE refresh_tokens
SET is_revoked = TRUE
WHERE revoked_at IS NOT NULL
DO $$
BEGIN
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'refresh_tokens'
AND column_name = 'revoked_at'
) THEN
UPDATE refresh_tokens SET is_revoked = TRUE WHERE revoked_at IS NOT NULL;
END IF;
END $$;
"""
)
op.alter_column("refresh_tokens", "is_revoked", server_default=None)
op.alter_column(
"refresh_tokens",
"token_hash",
existing_type=sa.String(length=64),
type_=sa.String(length=256),
existing_nullable=False,
)
op.drop_column("refresh_tokens", "absolute_expiry")
op.drop_column("refresh_tokens", "revoked_at")
op.execute("ALTER TABLE refresh_tokens ALTER COLUMN is_revoked DROP DEFAULT")
op.execute("ALTER TABLE refresh_tokens ALTER COLUMN token_hash TYPE VARCHAR(256)")
op.execute("ALTER TABLE refresh_tokens DROP COLUMN IF EXISTS absolute_expiry")
op.execute("ALTER TABLE refresh_tokens DROP COLUMN IF EXISTS revoked_at")