Skip to content

Commit 3f0c880

Browse files
jopemachineclaude
andcommitted
fix(BA-6914): give the three drifting FKs one name on both install paths
Three migrations create a foreign key under a hardcoded name while the models left it unnamed, so the metadata naming convention generated a different one. The key is created and enforced either way -- only the name differed, depending on whether the database came from `mgr schema oneshot` or from replaying migrations. That is not cosmetic: each of those downgrades drops the constraint by the hardcoded name and aborted on a fresh install. Converge on the hardcoded names rather than the convention ones. The migrations' downgrades already expect them and work today on migrated databases, so this direction needs no edit to released migrations, and it sidesteps the convention name for prometheus_query_presets, which exceeds PostgreSQL's 63-character limit and comes back truncated with a hash suffix. Naming a constraint explicitly is not novel here -- see fk_deployment_policies_endpoint. The rename looks the current name up rather than hardcoding it, so it does not depend on SQLAlchemy's truncation algorithm and is a no-op where the name already matches. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 04b8764 commit 3f0c880

4 files changed

Lines changed: 78 additions & 3 deletions

File tree

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
"""reconcile fk constraint name drift across install paths
2+
3+
Three migrations create a foreign key under a hardcoded name, while the models
4+
declared the same key without one and let the metadata naming convention
5+
generate it. The constraint is created and enforced either way -- only the name
6+
differed, depending on whether the database came from `mgr schema oneshot` or
7+
from replaying migrations.
8+
9+
The models now name all three explicitly, matching what the migrations create
10+
and what their downgrades already drop. This migration renames the constraint on
11+
databases that were built from the convention.
12+
13+
The convention name is not hardcoded here: for prometheus_query_presets it
14+
exceeds PostgreSQL's 63-character limit and comes back truncated with a hash
15+
suffix, which would tie this migration to SQLAlchemy's truncation algorithm.
16+
Look the current name up instead and rename only when it differs, which also
17+
makes this a no-op on databases that already agree.
18+
19+
Revision ID: c8d1e5b73f92
20+
Revises: b2f7c1a94e30
21+
Create Date: 2026-07-16
22+
23+
"""
24+
25+
import sqlalchemy as sa
26+
from alembic import op
27+
28+
# revision identifiers, used by Alembic.
29+
revision = "c8d1e5b73f92"
30+
down_revision = "b2f7c1a94e30"
31+
# Part of: NEXT_RELEASE_VERSION
32+
branch_labels = None
33+
depends_on = None
34+
35+
# (table, column, name the migrations create and their downgrades drop)
36+
_TARGETS = [
37+
("kernels", "image_id", "fk_kernels_image_id"),
38+
("login_sessions", "login_client_type_id", "fk_login_sessions_login_client_type_id"),
39+
("prometheus_query_presets", "category_id", "fk_prometheus_query_presets_category_id"),
40+
]
41+
42+
43+
def _rename_fk_on(conn: sa.engine.Connection, table: str, column: str, target: str) -> None:
44+
inspector = sa.inspect(conn)
45+
if table not in inspector.get_table_names():
46+
return
47+
for fk in inspector.get_foreign_keys(table):
48+
if fk["constrained_columns"] != [column]:
49+
continue
50+
current = fk["name"]
51+
if not current or current == target:
52+
return
53+
op.execute(f'ALTER TABLE {table} RENAME CONSTRAINT "{current}" TO "{target}"')
54+
return
55+
56+
57+
def upgrade() -> None:
58+
conn = op.get_bind()
59+
for table, column, target in _TARGETS:
60+
_rename_fk_on(conn, table, column, target)
61+
62+
63+
def downgrade() -> None:
64+
# Intentionally a no-op. This migration exists to make the two install paths
65+
# agree; undoing it would only put the drift back -- and the convention name
66+
# it came from is not reconstructible here.
67+
pass

src/ai/backend/manager/models/kernel/row.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ class KernelRow(Base): # type: ignore[misc]
229229
image_id: Mapped[ImageID | None] = mapped_column(
230230
"image_id",
231231
GUID(ImageID),
232-
sa.ForeignKey("images.id", ondelete="SET NULL"),
232+
sa.ForeignKey("images.id", name="fk_kernels_image_id", ondelete="SET NULL"),
233233
nullable=True,
234234
index=True,
235235
)

src/ai/backend/manager/models/login_session/row.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,11 @@ class LoginSessionRow(Base): # type: ignore[misc]
4343
login_client_type_id: Mapped[uuid.UUID | None] = mapped_column(
4444
"login_client_type_id",
4545
GUID,
46-
sa.ForeignKey("login_client_types.id", ondelete="SET NULL"),
46+
sa.ForeignKey(
47+
"login_client_types.id",
48+
name="fk_login_sessions_login_client_type_id",
49+
ondelete="SET NULL",
50+
),
4751
nullable=True,
4852
index=True,
4953
)

src/ai/backend/manager/models/prometheus_query_preset/row.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,11 @@ class PrometheusQueryPresetRow(Base): # type: ignore[misc]
4444
category_id: Mapped[uuid.UUID | None] = mapped_column(
4545
"category_id",
4646
GUID,
47-
sa.ForeignKey("prometheus_query_preset_categories.id", ondelete="SET NULL"),
47+
sa.ForeignKey(
48+
"prometheus_query_preset_categories.id",
49+
name="fk_prometheus_query_presets_category_id",
50+
ondelete="SET NULL",
51+
),
4852
nullable=True,
4953
index=True,
5054
)

0 commit comments

Comments
 (0)