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.
73+ LEGACY_INDEXES = '_legacy_indexes'
74+
7175
7276def _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+
88114def _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
127153def _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
196247def 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