6868# uuids (see the "Data-preserving downgrade" note in the task brief).
6969LEGACY_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.
7374LEGACY_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+
114148def _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
186209def 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
208233def _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
247263def 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