Skip to content

Commit 8ca3ba4

Browse files
committed
fix: preserve fk constraint names and indexes in id conversion migration
1 parent 40e1fa5 commit 8ca3ba4

1 file changed

Lines changed: 57 additions & 5 deletions

File tree

alembic/versions/c1d2e3f4a5b6_convert_ids_to_integer.py

Lines changed: 57 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,10 @@
6868
# uuids (see the "Data-preserving downgrade" note in the task brief).
6969
LEGACY_IDS = '_legacy_ids'
7070

71+
# Shadow table recording indexes on FK columns that DROP COLUMN removes,
72+
# so the downgrade can recreate them for the earlier migrations' DROP INDEX.
73+
LEGACY_INDEXES = '_legacy_indexes'
74+
7175

7276
def _columns(bind) -> dict[str, list[str]]:
7377
inspector = sa.inspect(bind)
@@ -85,6 +89,28 @@ def _fk_constraint_name(bind, table: str, column: str) -> str | None:
8589
return None
8690

8791

92+
def _fk_name(child: str, column: str) -> str:
93+
"""Constraint names as created by the preceding migrations, so that
94+
`alembic downgrade` further down the chain can still drop them by
95+
name: multitenant FKs are named fk_<table>_tenant_id (8efb2c8c7b20),
96+
the initial schemas' unnamed constraints get PostgreSQL's default
97+
<table>_<column>_fkey (b0de87aaeb97)."""
98+
if column == 'tenant_id':
99+
return f'fk_{child}_tenant_id'
100+
return f'{child}_{column}_fkey'
101+
102+
103+
def _indexes_on_column(bind, table: str, column: str) -> list[tuple[str, bool]]:
104+
"""(name, is_unique) of single-column indexes containing the column."""
105+
inspector = sa.inspect(bind)
106+
out = []
107+
for idx in inspector.get_indexes(table):
108+
cols = idx["column_names"] or []
109+
if column in cols and len(cols) == 1:
110+
out.append((idx["name"], bool(idx.get("unique", False))))
111+
return out
112+
113+
88114
def _convert_pk(bind, table: str) -> None:
89115
"""Add _id integer identity column, backfill via row_number over the
90116
old uuid PK, record the uuid -> int mapping, drop the uuid PK, rename."""
@@ -126,18 +152,33 @@ def _convert_pk(bind, table: str) -> None:
126152

127153
def _convert_fk(bind, child: str, column: str, parent: str) -> None:
128154
"""Remap a child FK column to the parent's new integer ids."""
155+
# The constraint is usually already gone: dropping a parent's PK with
156+
# CASCADE removed every incoming FK. Drop by name only if it survives.
157+
fk_name = _fk_constraint_name(bind, child, column)
158+
if fk_name:
159+
op.execute(sa.text(f'ALTER TABLE {child} DROP CONSTRAINT {fk_name}'))
129160
op.execute(sa.text(f'ALTER TABLE {child} ADD COLUMN _fk INTEGER'))
130161
op.execute(sa.text(
131162
f'UPDATE {child} SET _fk = l.new_id FROM {LEGACY_IDS} l '
132163
f'WHERE l.table_name = \'{parent}\' AND {child}.{column} = l.legacy_uuid'
133164
))
134-
fk_name = _fk_constraint_name(bind, child, column)
135-
if fk_name:
136-
op.execute(sa.text(f'ALTER TABLE {child} DROP CONSTRAINT {fk_name}'))
165+
# DROP COLUMN removes indexes on the column; record them so the
166+
# downgrade can recreate them for the earlier migrations' DROP INDEX.
167+
indexes = _indexes_on_column(bind, child, column)
168+
if indexes:
169+
op.execute(sa.text(
170+
f'CREATE TABLE IF NOT EXISTS {LEGACY_INDEXES} '
171+
f'(table_name TEXT, index_name TEXT, column_name TEXT, is_unique BOOLEAN)'
172+
))
173+
for name, unique in indexes:
174+
op.execute(sa.text(
175+
f"INSERT INTO {LEGACY_INDEXES} (table_name, index_name, column_name, is_unique) "
176+
f"VALUES ('{child}', '{name}', '{column}', {unique})"
177+
))
137178
op.execute(sa.text(f'ALTER TABLE {child} DROP COLUMN {column}'))
138179
op.execute(sa.text(f'ALTER TABLE {child} RENAME COLUMN _fk TO {column}'))
139180
op.execute(sa.text(
140-
f'ALTER TABLE {child} ADD CONSTRAINT {child}_{column}_fkey '
181+
f'ALTER TABLE {child} ADD CONSTRAINT {_fk_name(child, column)} '
141182
f'FOREIGN KEY ({column}) REFERENCES {parent} (id)'
142183
))
143184

@@ -188,9 +229,19 @@ def _restore_fk(bind, child: str, column: str, parent: str) -> None:
188229
op.execute(sa.text(f'ALTER TABLE {child} DROP COLUMN {column}'))
189230
op.execute(sa.text(f'ALTER TABLE {child} RENAME COLUMN _fk TO {column}'))
190231
op.execute(sa.text(
191-
f'ALTER TABLE {child} ADD CONSTRAINT {child}_{column}_fkey '
232+
f'ALTER TABLE {child} ADD CONSTRAINT {_fk_name(child, column)} '
192233
f'FOREIGN KEY ({column}) REFERENCES {parent} (id)'
193234
))
235+
# Recreate indexes on the restored column so the earlier migrations'
236+
# downgrades can drop them by name again.
237+
rows = bind.execute(sa.text(
238+
f'SELECT index_name, is_unique FROM {LEGACY_INDEXES} '
239+
f"WHERE table_name = '{child}' AND column_name = '{column}'"
240+
)).fetchall()
241+
for name, unique in rows:
242+
op.execute(sa.text(
243+
f"CREATE {'UNIQUE ' if unique else ''}INDEX {name} ON {child} ({column})"
244+
))
194245

195246

196247
def downgrade() -> None:
@@ -222,4 +273,5 @@ def downgrade() -> None:
222273
if 'api_keys' in tables:
223274
_restore_fk(bind, 'api_keys', 'tenant_id', 'tenants')
224275

276+
op.execute(sa.text(f'DROP TABLE IF EXISTS {LEGACY_INDEXES}'))
225277
op.execute(sa.text(f'DROP TABLE IF EXISTS {LEGACY_IDS}'))

0 commit comments

Comments
 (0)