Skip to content

fix: handle sqlite legacy budget column drops#2876

Closed
danpiths wants to merge 1 commit intographite-base/2876from
fix/04-21-fix_handle_sqlite_legacy_budget_column_drops
Closed

fix: handle sqlite legacy budget column drops#2876
danpiths wants to merge 1 commit intographite-base/2876from
fix/04-21-fix_handle_sqlite_legacy_budget_column_drops

Conversation

@danpiths
Copy link
Copy Markdown
Collaborator

Summary

Fixes SQLite startup failures and runtime 500s caused by legacy budget_id
columns lingering on governance_virtual_keys,
governance_virtual_key_provider_configs, and governance_teams after the
multi-budget ownership migration.

The V1.5.0 migrations (add_multi_budget_tables,
add_team_budgets_to_budgets_table) moved budget ownership from single-FK
columns (budget_id) on parent tables to multi-budget ownership via
governance_budgets.{virtual_key_id, provider_config_id, team_id}. However, the
legacy budget_id columns were dropped with a fire-and-forget
ALTER TABLE ... DROP COLUMN IF EXISTS that silently failed on SQLite when the
column was referenced by a foreign key definition. This left stale columns and
FK constraints in the schema, causing FOREIGN KEY constraint failed errors at
request time when budgets were reconciled during team/VK updates.

Changes

  • Added SQLite-safe legacy column drop (sqliteDropLegacyBudgetColumn):
    Uses a dump-data / drop-original / create-clean / restore-data strategy to
    remove budget_id without ever renaming the original table. This avoids a
    SQLite behavior where ALTER TABLE RENAME propagates into FK references in
    other tables, corrupting them.
  • Added cross-dialect dispatcher (dropLegacyBudgetColumn): Routes to the
    SQLite rebuild path or standard GORM DropColumn for Postgres. Verifies the
    column is actually gone afterward and fails the migration if not.
  • Reordered migration steps: Legacy column drops now happen before FK
    constraint creation (CreateConstraint). On SQLite, CreateConstraint
    triggers internal table rebuilds; doing it after the legacy column is gone
    avoids FK reference corruption from rename propagation.
  • Replaced fire-and-forget drops: The original
    _ = tx.Exec("ALTER TABLE ... DROP COLUMN IF EXISTS budget_id") calls are
    replaced with dropLegacyBudgetColumn(tx, tableName) which fails the
    migration on error instead of silently swallowing it.
  • Added migration regression tests: Two new test cases
    (TestMigrationAddMultiBudgetTables_DropsLegacyBudgetColumnsAndBackfillsOwners,
    TestMigrationAddTeamBudgetsToBudgetsTable_DropsLegacyBudgetColumnAndBackfillsOwners)
    simulate legacy DBs with budget_id columns still present and verify both the
    column drop and the ownership backfill.
  • Added test helpers: setupLegacyBudgetOwnerMigrationDB,
    insertProviderConfigRaw, insertTeamRaw for constructing legacy-shaped test
    databases.

Type of change

  • Bug fix
  • Feature
  • Refactor
  • Documentation
  • Chore/CI

Affected areas

  • Core (Go)
  • Transports (HTTP)
  • Providers/Integrations
  • Plugins
  • UI (React)
  • Docs

How to test

# Run the migration tests (includes the new regression tests)
go test ./framework/configstore/... -v -run 'TestMigrationAdd(MultiBudgetTables|TeamBudgetsToBudgetsTable)_Drops'

# Run the full configstore test suite
go test ./framework/configstore/...

Manual validation with a pre-V1.5.0 SQLite database:

  1. Restore a SQLite config.db backup from before the multi-budget migration
  2. Start Bifrost — migrations should complete without errors
  3. Verify the legacy columns are gone:
    sqlite3 ~/.config/bifrost/config.db "PRAGMA table_info(governance_virtual_keys);" | grep budget_id
    sqlite3 ~/.config/bifrost/config.db "PRAGMA table_info(governance_teams);" | grep budget_id
    # Both should return empty
  4. Verify no FK references are corrupted:
    sqlite3 ~/.config/bifrost/config.db "SELECT sql FROM sqlite_master WHERE sql LIKE '%legacy_budget%';"
    # Should return empty
  5. Update a team's budgets and a virtual key's budgets via the UI or API —
    should succeed without FK errors

Screenshots/Recordings

N/A — backend-only migration fix.

Breaking changes

  • Yes
  • No

Related issues

N/A

Security considerations

None. This change only affects database schema migration logic during startup.
No auth, secrets, PII, or sandboxing changes.

Checklist

  • I read docs/contributing/README.md and followed the guidelines
  • I added/updated tests where appropriate
  • I updated documentation where needed
  • I verified builds succeed (Go and UI)
  • I verified the CI pipeline passes locally if applicable

@danpiths danpiths requested a review from akshaydeo April 20, 2026 22:10
@CLAassistant
Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@danpiths danpiths mentioned this pull request Apr 20, 2026
18 tasks
Copy link
Copy Markdown
Collaborator Author

Warning

This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
Learn more

This stack of pull requests is managed by Graphite. Learn more about stacking.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 20, 2026

📝 Walkthrough

Summary by CodeRabbit

  • Chores

    • Updated database migrations to safely remove legacy budget identifier columns, with a SQLite-safe rebuild pathway, verification to ensure removal, and FK constraints created only after legacy columns are removed; improved error handling and logging during column drops.
  • Tests

    • Added end-to-end tests that seed legacy schemas, validate legacy column removal, backfill new budget ownership fields, check foreign-key integrity, and assert idempotent migration behavior.

Walkthrough

Added SQLite-aware migration logic to remove legacy budget_id columns by rebuilding tables via temporary dumps and verification; non-SQLite dialects use GORM's DropColumn path. Migration order changed to remove legacy columns before creating new foreign-key constraints; tests added to cover legacy-schema scenarios and FK integrity.

Changes

Cohort / File(s) Summary
Database Migration Implementation
framework/configstore/migrations.go
Introduced dropLegacyBudgetColumn dispatcher and SQLite-specific rebuild strategy that uses PRAGMA table_info, creates a temporary dump table (excluding budget_id), copies data, drops and recreates the table from the GORM model, restores data, verifies removal. Reordered migrations to drop legacy budget_id columns before adding new Budgets FKs. Added slices import and slices.Contains usage for column checks.
Migration Tests & Helpers
framework/configstore/migrations_test.go
Added legacy-schema test scaffold setupLegacyBudgetOwnerMigrationDB, raw insert helpers (insertProviderConfigRaw, insertTeamRaw), and two tests verifying legacy budget_id columns are dropped and owner/backfill logic runs for virtual keys, provider configs, and teams. Adjusted an existing calendar-aligned idempotency assertion and added FK-integrity check assertNoCorruptedFKReferences.

Sequence Diagram

sequenceDiagram
    participant Migr as Migration
    participant SQLite as SQLiteDB
    participant GORM as GORMMigrator
    participant Dump as DumpTable

    Migr->>SQLite: PRAGMA table_info(table)\n(check for budget_id)
    SQLite-->>Migr: column metadata
    alt budget_id exists (SQLite path)
        Migr->>SQLite: CREATE TABLE __dump ... (exclude budget_id)
        Migr->>SQLite: INSERT INTO __dump SELECT ... (non-budget_id cols)
        SQLite-->>Migr: data copied
        Migr->>SQLite: DROP TABLE original
        Migr->>SQLite: CREATE TABLE original (from GORM model)
        Migr->>SQLite: INSERT INTO original SELECT ... FROM __dump
        Migr->>SQLite: DROP TABLE __dump
        Migr->>SQLite: PRAGMA table_info(original)\n(verify budget_id gone)
        SQLite-->>Migr: verification result
    else non-SQLite (GORM path)
        Migr->>GORM: DropColumn(table, "budget_id")
        GORM-->>Migr: operation result
        Migr->>GORM: Verify column absent
        GORM-->>Migr: verification result
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

A rabbit taps the schema floor, 🐇
Copies rows and drops the door,
Rebuilds the table, checks with care,
Bids budget_id a fond farewell there. ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 42.86% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: adding SQLite-safe handling for legacy budget column drops, which is the core fix addressed in the PR.
Description check ✅ Passed The description follows the template structure with all critical sections completed: Summary, Changes, Type of change, Affected areas, How to test, Breaking changes, and Checklist. All required information is present and comprehensive.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/04-21-fix_handle_sqlite_legacy_budget_column_drops

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.11.4)

level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain main module or its selected dependencies"


Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 20, 2026

Confidence Score: 5/5

Safe to merge; the fix is well-reasoned, the PRAGMA/connection-pool approach is correct, and all remaining findings are minor style suggestions.

All issues found are P2: a single-slash comment typo and missing idempotency test coverage for the new regression tests. The core logic — dump table strategy, PRAGMA foreign_keys OFF with SetMaxOpenConns(1), correct ordering of drops before CreateConstraint — is sound and well-tested.

No files require special attention.

Important Files Changed

Filename Overview
framework/configstore/migrations.go Adds SQLite-safe legacy budget_id column drop helpers (dump→drop→recreate→restore) with cross-dialect dispatcher; reorders column drops before FK constraint creation; one minor comment typo on line 178.
framework/configstore/migrations_test.go Adds regression tests and helpers for legacy budget_id column drops; missing idempotency test coverage for the two new migration test cases.

Reviews (2): Last reviewed commit: "fix: handle sqlite legacy budget column ..." | Re-trigger Greptile

Comment thread framework/configstore/migrations.go Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@framework/configstore/migrations_test.go`:
- Around line 2091-2093: The test currently adds plain budget_id columns but
must model the legacy FK; change the three ALTER TABLE statements in
migrations_test.go to add budget_id VARCHAR(255) REFERENCES
governance_budgets(id) so the fixture mirrors the broken production schema, then
after running the migration query PRAGMA foreign_key_list for each table (use a
small fkRow struct with fields Table/From/To), e.g. run db.Raw(`PRAGMA
foreign_key_list(governance_virtual_keys)`).Scan(&fks) and assert the result
contains fkRow{Table: "governance_budgets", From: "budget_id", To: "id"} (repeat
for the other two tables) to validate post-migration FK metadata still points at
governance_budgets.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e4afafd4-ef4b-435b-b6f7-490d2f62cbbe

📥 Commits

Reviewing files that changed from the base of the PR and between 958d965 and 535e6c7.

📒 Files selected for processing (2)
  • framework/configstore/migrations.go
  • framework/configstore/migrations_test.go

Comment thread framework/configstore/migrations_test.go Outdated
@danpiths danpiths force-pushed the fix/04-21-fix_handle_sqlite_legacy_budget_column_drops branch from 535e6c7 to 7df8625 Compare April 20, 2026 22:22
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
framework/configstore/migrations.go (2)

6319-6351: ⚠️ Potential issue | 🟠 Major

Add preflight check for duplicate team references to the same budget before backfill.

The scalar subquery at line 6344 (SELECT id FROM governance_teams WHERE governance_teams.budget_id = governance_budgets.id) assumes each budget is referenced by at most one team. If multiple teams reference the same budget in legacy data, PostgreSQL will fail the migration with a "more than one row" error, and SQLite will silently pick one team, losing the other associations when the budget_id column is dropped. Add a GROUP BY/HAVING preflight check alongside the existing cross-owner check:

Suggested preflight query
SELECT COUNT(*) FROM governance_teams t
GROUP BY t.budget_id
HAVING COUNT(*) > 1

If duplicates exist, fail the migration with a clear error message requiring manual resolution.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@framework/configstore/migrations.go` around lines 6319 - 6351, Add a
preflight duplicate-team-per-budget check before the UPDATE backfill: run a
grouped query against governance_teams (GROUP BY budget_id HAVING COUNT(*) > 1)
using tx.Raw (similar to the existing conflictCount query), scan the result into
a count/variable and, if >0, return an error that clearly states duplicate team
references to the same budget exist and must be resolved manually; place this
check before the tx.Exec(...) UPDATE that uses the scalar subquery so the
migration fails fast instead of producing a SQL error or silently losing
associations.

6172-6200: ⚠️ Potential issue | 🟠 Major

Add duplicate-reference preflights to all three legacy budget_id backfills.

Lines 6174 (VirtualKey), 6189 (ProviderConfig), and 6350 (Team) use scalar subqueries that assume each legacy budget_id maps to exactly one owner. If stale data has the same budget referenced by multiple VirtualKeys, multiple ProviderConfigs, or multiple Teams—or by any combination—PostgreSQL will fail with "more than one row returned by a subquery" while SQLite may silently pick one row, leaving governance_budgets rows with arbitrary or missing ownership after the legacy columns drop.

Add a preflight for each that checks for duplicates within the same table (GROUP BY budget_id HAVING COUNT(*) > 1) and aborts before the backfill runs. The team migration already has a cross-owner conflict check (line 6330); extend it with a same-owner duplicate check. Apply the same pattern to VirtualKey and ProviderConfig backfills.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@framework/configstore/migrations.go` around lines 6172 - 6200, The three
legacy budget_id backfills (the tx.Exec blocks that update governance_budgets
using scalar subqueries over governance_virtual_keys,
governance_virtual_key_provider_configs, and governance_teams) must each run a
preflight duplicate check to avoid "more than one row returned" errors: before
running the UPDATE, run a query against the source table (for VirtualKey:
governance_virtual_keys / TableVirtualKey; for ProviderConfig:
governance_virtual_key_provider_configs / TableVirtualKeyProviderConfig; for
Team: governance_teams) that SELECTs budget_id GROUP BY budget_id HAVING
COUNT(*) > 1 and if any rows are returned abort the migration with a clear error
message; also extend the existing team preflight (the cross-owner conflict check
around the team backfill) to include this same-table duplicate check so
same-owner duplicates are caught. Ensure the checks run only when the legacy
budget_id column exists (same mg.HasColumn guards) and return a descriptive
fmt.Errorf if duplicates are found.
🧹 Nitpick comments (1)
framework/configstore/migrations_test.go (1)

2280-2337: Consider adding a brief comment noting these tests are SQLite-specific.

Both migration tests run exclusively against SQLite (via setupLegacyBudgetOwnerMigrationDB). While this is appropriate given the bug is SQLite-specific and Postgres uses GORM's native DropColumn, a brief comment would help future maintainers understand the scope.

📝 Suggested documentation
+// TestMigrationAddMultiBudgetTables_DropsLegacyBudgetColumnsAndBackfillsOwners
+// exercises the SQLite-specific table rebuild path for dropping legacy budget_id
+// columns. Postgres uses GORM's native DropColumn and is covered by existing tests.
 func TestMigrationAddMultiBudgetTables_DropsLegacyBudgetColumnsAndBackfillsOwners(t *testing.T) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@framework/configstore/migrations_test.go` around lines 2280 - 2337, The two
migration tests
TestMigrationAddMultiBudgetTables_DropsLegacyBudgetColumnsAndBackfillsOwners and
TestMigrationAddTeamBudgetsToBudgetsTable_DropsLegacyBudgetColumnAndBackfillsOwners
run against a SQLite-specific test DB created by
setupLegacyBudgetOwnerMigrationDB; add a one-line comment above each test (or
above the shared setup call) stating that these tests target SQLite only because
the bug is SQLite rename/DropColumn-specific and Postgres uses GORM's native
DropColumn behavior, so maintainers understand the DB scope when reading these
tests.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@framework/configstore/migrations.go`:
- Around line 6319-6351: Add a preflight duplicate-team-per-budget check before
the UPDATE backfill: run a grouped query against governance_teams (GROUP BY
budget_id HAVING COUNT(*) > 1) using tx.Raw (similar to the existing
conflictCount query), scan the result into a count/variable and, if >0, return
an error that clearly states duplicate team references to the same budget exist
and must be resolved manually; place this check before the tx.Exec(...) UPDATE
that uses the scalar subquery so the migration fails fast instead of producing a
SQL error or silently losing associations.
- Around line 6172-6200: The three legacy budget_id backfills (the tx.Exec
blocks that update governance_budgets using scalar subqueries over
governance_virtual_keys, governance_virtual_key_provider_configs, and
governance_teams) must each run a preflight duplicate check to avoid "more than
one row returned" errors: before running the UPDATE, run a query against the
source table (for VirtualKey: governance_virtual_keys / TableVirtualKey; for
ProviderConfig: governance_virtual_key_provider_configs /
TableVirtualKeyProviderConfig; for Team: governance_teams) that SELECTs
budget_id GROUP BY budget_id HAVING COUNT(*) > 1 and if any rows are returned
abort the migration with a clear error message; also extend the existing team
preflight (the cross-owner conflict check around the team backfill) to include
this same-table duplicate check so same-owner duplicates are caught. Ensure the
checks run only when the legacy budget_id column exists (same mg.HasColumn
guards) and return a descriptive fmt.Errorf if duplicates are found.

---

Nitpick comments:
In `@framework/configstore/migrations_test.go`:
- Around line 2280-2337: The two migration tests
TestMigrationAddMultiBudgetTables_DropsLegacyBudgetColumnsAndBackfillsOwners and
TestMigrationAddTeamBudgetsToBudgetsTable_DropsLegacyBudgetColumnAndBackfillsOwners
run against a SQLite-specific test DB created by
setupLegacyBudgetOwnerMigrationDB; add a one-line comment above each test (or
above the shared setup call) stating that these tests target SQLite only because
the bug is SQLite rename/DropColumn-specific and Postgres uses GORM's native
DropColumn behavior, so maintainers understand the DB scope when reading these
tests.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9be99ad3-94d7-46d2-b8c5-5457a46b26ae

📥 Commits

Reviewing files that changed from the base of the PR and between 535e6c7 and 7df8625.

📒 Files selected for processing (2)
  • framework/configstore/migrations.go
  • framework/configstore/migrations_test.go

@akshaydeo akshaydeo closed this Apr 20, 2026
@akshaydeo akshaydeo changed the base branch from 04-21-v1.5.0-prerelease4_cut to graphite-base/2876 April 20, 2026 23:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants