Skip to content

fix(BA-6913): reconcile indexes that only exist on migrated databases#12907

Draft
jopemachine wants to merge 5 commits into
mainfrom
fix/BA-6913-fresh-install-missing-indexes
Draft

fix(BA-6913): reconcile indexes that only exist on migrated databases#12907
jopemachine wants to merge 5 commits into
mainfrom
fix/BA-6913-fresh-install-missing-indexes

Conversation

@jopemachine

@jopemachine jopemachine commented Jul 16, 2026

Copy link
Copy Markdown
Member

📚 Stacked PRs

Merge in order, bottom-up. Each PR is based on the one above it, so its diff shows only its own layer.

Problem

The schema is defined twice, and nothing checks that the two agree:

fresh install existing install
command mgr schema oneshot alembic upgrade head
schema from SQLAlchemy model metadatacreate_all → stamp head the migration files
migrations run none at all all

Three migrations create an index the matching model never declared, so it exists only on migrated databases. Verified on a DB built at head by oneshot: none of the three is present, and no later migration drops them.

Convergence has two directions

Which way each index goes depends on whether it earns its write cost, so each was checked individually rather than fixed by pattern:

column FK filtered / joined / ordered by? direction
role_invitations.invitee_user_id yesON DELETE CASCADE the cascade must scan for them create
prometheus_query_presets.category_id yesON DELETE SET NULL the category delete must scan for them create
vfolders.creator_id no never — only written and selected, nowhere in src/ai/backend/ drop

role_invitations.invitee_user_id is the one that bites: on a fresh install every user deletion scans role_invitations. uq_role_invitations_active cannot substitute — it is partial (WHERE state != 'accepted'), so it excludes exactly the rows a cascade must find.

vfolders.creator_id goes the other way. Nothing reads it, so the index b4e7f1a2c3d5 created costs a write on every vfolder and saves nothing. Declaring it on the model to match would have spread the cost to every install; instead its creation is removed and migrated databases have it dropped.

Fix

  1. Declare the two FK-backed indexes on the models (index=True reproduces the migrations' exact ix_<table>_<column> names).
  2. Remove the create_index from b4e7f1a2c3d5.
  3. Add b2f7c1a94e30 to reconcile databases already out there — creates the two, drops the one. Existence checks make every statement a no-op where the DB already agrees; its downgrade is a no-op, since undoing it would only put the drift back.
  4. Drop the redundant drop_index calls in all three: the following drop_column/drop_table removes the index anyway, so naming it first only broke installs that never had it. This is independent of the model changeb4e7f1a2c3d5's downgrade passes on a fresh install with no such index.

Verification

All against real databases; the dev DB was only ever read.

Fresh install — exactly the intended set: the two FK indexes present, ix_vfolders_creator_id absent.

Old oneshot install (schema built from main's models, stamped at the old head aa27f1d5cd35):

FK indexes useless vfolder index
before upgrade head 0 of 2 0
after 2 of 2 0

Migrated DB carrying the useless index:

FK indexes useless vfolder index
before upgrade head 2 of 2 1
after 2 of 2 0

Re-running after a downgrade produces no errors.

Edited b4e7f1a2c3d5 round-trip: downgrade drops creator_id; re-upgrade restores the column and does not create the index.

Downgrade on a fresh install (stamp the revision, run only its downgrade):

migration main this PR
ad7acfe8aa1c FAILED — index "ix_role_invitations_invitee_user_id" does not exist OK
b4e7f1a2c3d5 FAILED — index "ix_vfolders_creator_id" does not exist OK
a3b4c5d6e7f8 FAILED — index "ix_prometheus_query_presets_category_id" does not exist still fails ¹

¹ Honest result: clearing the index error on a3b4c5d6e7f8 exposes a second, different defect underneath — its downgrade also drops fk_prometheus_query_presets_category_id, but a fresh install names that constraint by the metadata convention, which exceeds PostgreSQL's 63-char identifier limit and comes out truncated and hash-suffixed as fk_prometheus_query_presets_category_id_prometheus_quer_8a51. That is the FK-naming drift class (BA-6914, also affects cd067180a8b1 and c7f2a8e31b04) — those constraints are enforced on both paths, only the names differ. This PR does not claim to fix it.

Found while surveying the downgrade chain for #12881.

🤖 Generated with Claude Code

The schema is defined twice: a fresh install builds tables from the model
metadata via `mgr schema oneshot` and stamps head without running a single
migration, while an existing install replays the migration files. Nothing
checks that the two agree, and for three indexes they did not -- the
migrations create them, the models never declared them, so only migrated
databases ever had them.

The worst of the three covers role_invitations.invitee_user_id, whose FK
to users is ON DELETE CASCADE: without it a fresh install scans the table
on every user deletion. uq_role_invitations_active cannot stand in for it
because it is partial (state != 'accepted') and so misses exactly the rows
a cascade has to find.

Declare all three on the models, and add a migration so databases already
deployed by oneshot pick them up; IF NOT EXISTS keeps it a no-op where they
are already present.

Also drop the redundant drop_index calls that made this visible: the
following drop_column/drop_table removes the index anyway, so naming it
first only broke downgrades on installs that never had it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added size:M 30~100 LoC comp:manager Related to Manager component require:db-migration Automatically set when alembic migrations are added or updated labels Jul 16, 2026
jopemachine and others added 2 commits July 16, 2026 11:17
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Converging the two schema paths only makes sense in the direction the
index earns. role_invitations.invitee_user_id and
prometheus_query_presets.category_id each back a foreign key whose
parent-side delete (CASCADE / SET NULL) has to find the referencing rows,
so declaring them costs a write and saves a scan.

vfolders.creator_id has neither: no foreign key, and nothing in the tree
filters, joins, or orders by it -- it is only ever written and selected.
Declaring it would add write cost to every vfolder for no reader, so leave
the model as it was. Whether the migration should have created that index
at all is a separate question from this one.

The redundant drop_index removal in b4e7f1a2c3d5 stays: drop_column takes
the index with it whether or not the index is there, so that downgrade
passes on a fresh install either way.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@jopemachine jopemachine changed the title fix(BA-6913): declare indexes that only the migrations created fix(BA-6913): declare the FK indexes that only the migrations created Jul 16, 2026
jopemachine and others added 2 commits July 16, 2026 11:39
Converging the two schema paths only makes sense in the direction the index
earns. vfolders.creator_id carries no foreign key and is never filtered,
joined, or ordered by anywhere in the tree -- it is only written and
selected -- so the index it got in b4e7f1a2c3d5 costs a write on every
vfolder and saves nothing. Rather than declare it on the model to match,
stop creating it, and drop it from the databases that already ran that
revision.

The reconcile migration now moves each index in whichever direction it
earns: it creates the two that back a foreign key whose parent-side delete
must find the referencing rows, and drops this one.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@jopemachine jopemachine changed the title fix(BA-6913): declare the FK indexes that only the migrations created fix(BA-6913): reconcile indexes that only exist on migrated databases Jul 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp:manager Related to Manager component require:db-migration Automatically set when alembic migrations are added or updated size:M 30~100 LoC

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant