Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changes/12907.fix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Reconcile indexes that differed between fresh installs and migrated databases, adding the two that back a foreign key and dropping the unused vfolder creator index
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,9 @@ def upgrade() -> None:


def downgrade() -> None:
op.drop_index("ix_prometheus_query_presets_category_id", table_name="prometheus_query_presets")
# drop_column below takes the column's index and foreign key with it;
# dropping either by name first only breaks on installs that never had
# that name.
op.drop_column("prometheus_query_presets", "category_id")
op.drop_column("prometheus_query_presets", "rank")
op.drop_column("prometheus_query_presets", "description")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,6 @@ def upgrade() -> None:


def downgrade() -> None:
op.drop_index("uq_role_invitations_active", table_name="role_invitations")
op.drop_index("ix_role_invitations_invitee_user_id", table_name="role_invitations")
# drop_table takes the table's indexes with it; dropping them by name first
# only breaks on installs that never had them.
op.drop_table("role_invitations")
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
"""reconcile index drift across install paths

A fresh install builds tables from the model metadata (`mgr schema oneshot`) and
stamps head without running a migration; an existing install replays the
migration files. Three indexes were created by migrations but never declared on
the models, so they existed only on migrated databases. This migration makes
every database agree with the models, in whichever direction each index earns.

Created here, because each backs a foreign key whose parent-side delete has to
find the referencing rows -- the index costs a write and saves a scan:
- ix_role_invitations_invitee_user_id (users delete cascades)
- ix_prometheus_query_presets_category_id (category delete sets null)

Dropped here, because nothing reads it: vfolders.creator_id carries no foreign
key and is never filtered, joined, or ordered by -- it is only written and
selected. Its creation has been removed from b4e7f1a2c3d5, so this drop is what
clears it from databases that already ran that revision.

The existence checks keep every statement a no-op where the database already
agrees.

Revision ID: b2f7c1a94e30
Revises: 3f9a1c7b2e04
Create Date: 2026-07-16

"""

from alembic import op

# revision identifiers, used by Alembic.
revision = "b2f7c1a94e30"
down_revision = "3f9a1c7b2e04"
# Part of: NEXT_RELEASE_VERSION
branch_labels = None
depends_on = None


def upgrade() -> None:
op.execute(
"CREATE INDEX IF NOT EXISTS ix_role_invitations_invitee_user_id"
" ON role_invitations (invitee_user_id)"
)
op.execute(
"CREATE INDEX IF NOT EXISTS ix_prometheus_query_presets_category_id"
" ON prometheus_query_presets (category_id)"
)
op.execute("DROP INDEX IF EXISTS ix_vfolders_creator_id")


def downgrade() -> None:
# Intentionally a no-op. This migration exists to make the two install paths
# agree with the models; undoing it would only put the drift back.
pass
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,6 @@ def upgrade() -> None:
"vfolders",
sa.Column("creator_id", GUID, nullable=True),
)
op.create_index(
op.f("ix_vfolders_creator_id"),
"vfolders",
["creator_id"],
unique=False,
)
# Backfill creator_id from users table by matching creator (email) β†’ users.email
op.execute(
sa.text(
Expand All @@ -38,5 +32,6 @@ def upgrade() -> None:


def downgrade() -> None:
op.drop_index(op.f("ix_vfolders_creator_id"), table_name="vfolders")
# drop_column takes the column's index with it; dropping it by name first
# only breaks on installs that never had it.
op.drop_column("vfolders", "creator_id")
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ class PrometheusQueryPresetRow(LifecycleTimestampsMixin, Base): # type: ignore[
GUID,
sa.ForeignKey("prometheus_query_preset_categories.id", ondelete="SET NULL"),
nullable=True,
index=True,
)
options: Mapped[PresetOptions] = mapped_column(
"options",
Expand Down
4 changes: 4 additions & 0 deletions src/ai/backend/manager/models/role_invitation/row.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ class RoleInvitationRow(Base): # type: ignore[misc]
GUID,
sa.ForeignKey("users.uuid", onupdate="CASCADE", ondelete="CASCADE"),
nullable=False,
# The ON DELETE CASCADE above has to find every row for a user, including
# accepted ones, so uq_role_invitations_active cannot serve it -- that
# index is partial (state != 'accepted').
index=True,
)
role_id: Mapped[uuid.UUID] = mapped_column(
"role_id",
Expand Down
Loading