Skip to content

Commit d8bce72

Browse files
committed
fix: recreate unique constraints and not-null on converted fk columns
1 parent 5cc3ef8 commit d8bce72

1 file changed

Lines changed: 47 additions & 29 deletions

File tree

alembic/versions/c1d2e3f4a5b6_convert_ids_to_integer.py

Lines changed: 47 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,9 @@
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.
71+
# Shadow table recording indexes and unique constraints on FK columns that
72+
# DROP COLUMN removes, so they can be recreated on the converted columns
73+
# (upgrade) and restored (downgrade) for the earlier migrations' DROP INDEX.
7374
LEGACY_INDEXES = '_legacy_indexes'
7475

7576

@@ -100,17 +101,50 @@ def _fk_name(child: str, column: str) -> str:
100101
return f'{child}_{column}_fkey'
101102

102103

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."""
104+
def _indexes_on_column(bind, table: str, column: str) -> list[tuple[str, str, bool]]:
105+
"""(name, comma-joined columns, is_unique) of every index or unique
106+
constraint containing the column (PG implements unique constraints as
107+
unique indexes)."""
105108
inspector = sa.inspect(bind)
106109
out = []
107110
for idx in inspector.get_indexes(table):
108111
cols = idx["column_names"] or []
109-
if column in cols and len(cols) == 1:
110-
out.append((idx["name"], bool(idx.get("unique", False))))
112+
if column in cols:
113+
out.append((idx["name"], ",".join(cols), bool(idx.get("unique", False))))
111114
return out
112115

113116

117+
def _record_indexes(bind, child: str, column: str) -> None:
118+
"""DROP COLUMN removes every index on the column; record them first."""
119+
indexes = _indexes_on_column(bind, child, column)
120+
if not indexes:
121+
return
122+
op.execute(sa.text(
123+
f'CREATE TABLE IF NOT EXISTS {LEGACY_INDEXES} '
124+
f'(table_name TEXT, index_name TEXT, column_names TEXT, is_unique BOOLEAN)'
125+
))
126+
for name, columns, unique in indexes:
127+
op.execute(sa.text(
128+
f"INSERT INTO {LEGACY_INDEXES} (table_name, index_name, column_names, is_unique) "
129+
f"VALUES ('{child}', '{name}', '{columns}', {unique})"
130+
))
131+
132+
133+
def _recreate_indexes(bind, child: str) -> None:
134+
"""Recreate the recorded indexes/unique constraints on a table once all
135+
of their columns have been converted (or restored)."""
136+
if not sa.inspect(bind).has_table(LEGACY_INDEXES):
137+
return
138+
rows = bind.execute(sa.text(
139+
f'SELECT index_name, column_names, is_unique FROM {LEGACY_INDEXES} '
140+
f"WHERE table_name = '{child}'"
141+
)).fetchall()
142+
for name, columns, unique in dict.fromkeys(rows):
143+
op.execute(sa.text(
144+
f"CREATE {'UNIQUE ' if unique else ''}INDEX {name} ON {child} ({columns})"
145+
))
146+
147+
114148
def _convert_pk(bind, table: str) -> None:
115149
"""Add _id integer identity column, backfill via row_number over the
116150
old uuid PK, record the uuid -> int mapping, drop the uuid PK, rename."""
@@ -162,25 +196,14 @@ def _convert_fk(bind, child: str, column: str, parent: str) -> None:
162196
f'UPDATE {child} SET _fk = l.new_id FROM {LEGACY_IDS} l '
163197
f'WHERE l.table_name = \'{parent}\' AND {child}.{column} = l.legacy_uuid'
164198
))
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-
))
199+
_record_indexes(bind, child, column)
178200
op.execute(sa.text(f'ALTER TABLE {child} DROP COLUMN {column}'))
179201
op.execute(sa.text(f'ALTER TABLE {child} RENAME COLUMN _fk TO {column}'))
180202
op.execute(sa.text(
181203
f'ALTER TABLE {child} ADD CONSTRAINT {_fk_name(child, column)} '
182204
f'FOREIGN KEY ({column}) REFERENCES {parent} (id)'
183205
))
206+
op.execute(sa.text(f'ALTER TABLE {child} ALTER COLUMN {column} SET NOT NULL'))
184207

185208

186209
def upgrade() -> None:
@@ -197,12 +220,14 @@ def upgrade() -> None:
197220
for fk_column, parent in FK_MAP.get(table, []):
198221
if parent in tables:
199222
_convert_fk(bind, table, fk_column, parent)
223+
_recreate_indexes(bind, table)
200224
_convert_pk(bind, table)
201225

202226
# api_keys may exist in dev databases even though it has no migration
203227
if 'api_keys' in tables:
204228
_convert_pk(bind, 'api_keys')
205229
_convert_fk(bind, 'api_keys', 'tenant_id', 'tenants')
230+
_recreate_indexes(bind, 'api_keys')
206231

207232

208233
def _restore_pk(bind, table: str) -> None:
@@ -232,16 +257,7 @@ def _restore_fk(bind, child: str, column: str, parent: str) -> None:
232257
f'ALTER TABLE {child} ADD CONSTRAINT {_fk_name(child, column)} '
233258
f'FOREIGN KEY ({column}) REFERENCES {parent} (id)'
234259
))
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-
))
260+
op.execute(sa.text(f'ALTER TABLE {child} ALTER COLUMN {column} SET NOT NULL'))
245261

246262

247263
def downgrade() -> None:
@@ -269,9 +285,11 @@ def downgrade() -> None:
269285
for fk_column, parent in FK_MAP.get(table, []):
270286
if parent in tables:
271287
_restore_fk(bind, table, fk_column, parent)
288+
_recreate_indexes(bind, table)
272289

273290
if 'api_keys' in tables:
274291
_restore_fk(bind, 'api_keys', 'tenant_id', 'tenants')
292+
_recreate_indexes(bind, 'api_keys')
275293

276294
op.execute(sa.text(f'DROP TABLE IF EXISTS {LEGACY_INDEXES}'))
277295
op.execute(sa.text(f'DROP TABLE IF EXISTS {LEGACY_IDS}'))

0 commit comments

Comments
 (0)