45 lines
1.6 KiB
Python
45 lines
1.6 KiB
Python
"""Small utilities shared by migrations.
|
|
|
|
Kept here rather than copied into each one because a migration that has already
|
|
run is awkward to change later — the fewer places the same fix has to be applied,
|
|
the better.
|
|
"""
|
|
|
|
from alembic import op
|
|
import sqlalchemy as sa
|
|
|
|
|
|
def drop_foreign_key_on(table: str, column: str) -> None:
|
|
"""Drop the foreign key on a column, whatever the database called it.
|
|
|
|
Alembic's autogenerate writes `op.drop_constraint(None, ...)` for a
|
|
constraint it created without a name, and that cannot be emitted: a
|
|
constraint with no name is one Alembic cannot address. Every downgrade
|
|
containing it fails, which means the migration chain is not reversible —
|
|
discovered by trying it rather than by anything failing in normal use.
|
|
|
|
Hard-coding the name Postgres happens to generate would work on one database
|
|
and not on another created at a different time, so the name is looked up.
|
|
Absent, nothing happens: a downgrade should not fail because the thing it
|
|
wants to remove is already gone.
|
|
"""
|
|
name = op.get_bind().execute(
|
|
sa.text(
|
|
"""
|
|
SELECT con.conname
|
|
FROM pg_constraint con
|
|
JOIN pg_attribute att
|
|
ON att.attrelid = con.conrelid
|
|
AND att.attnum = ANY(con.conkey)
|
|
WHERE con.conrelid = CAST(:table AS regclass)
|
|
AND con.contype = 'f'
|
|
AND att.attname = :column
|
|
LIMIT 1
|
|
"""
|
|
),
|
|
{"table": table, "column": column},
|
|
).scalar()
|
|
|
|
if name:
|
|
op.drop_constraint(name, table, type_="foreignkey")
|