From d5b163558dbad71c590508e063e662da6fe14e41 Mon Sep 17 00:00:00 2001 From: diegosouzapw Date: Sun, 31 May 2026 00:33:51 -0300 Subject: [PATCH] fix(db): resolve 077 migration version collision blocking getDbInstance 077_api_key_stream_default_mode.sql and 077_quota_pools.sql both claimed prefix 077, so getMigrationFiles() threw a version-collision error and getDbInstance() failed at every startup (app would not boot; all DB-touching unit tests were red on release/v3.8.8). Renumber the dependency-free, idempotent quota_pools migration 077 -> 085 (no other migration references quota_pools/quota_allocations), keep the non-idempotent api_key_stream_default_mode ALTER at 077, add a retroactive isSchemaAlreadyApplied guard (case 085) for DBs that already applied it under 077, and add a regression test enforcing unique migration prefixes. --- CHANGELOG.md | 11 +++ src/lib/db/migrationRunner.ts | 5 ++ ...77_quota_pools.sql => 085_quota_pools.sql} | 10 ++- .../db-migration-version-uniqueness.test.ts | 68 +++++++++++++++++++ 4 files changed, 93 insertions(+), 1 deletion(-) rename src/lib/db/migrations/{077_quota_pools.sql => 085_quota_pools.sql} (67%) create mode 100644 tests/unit/db-migration-version-uniqueness.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 5dd79c7d74..c40c6d435e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,6 +53,17 @@ `INSPECTOR_TLS_INTERCEPT`, `INSPECTOR_SYSTEM_PROXY_GUARD_MINUTES`, `INSPECTOR_MAX_BODY_KB`, `INSPECTOR_MASK_SECRETS`, `INSPECTOR_LLM_HOSTS_EXTRA`, `INSPECTOR_INTERNAL_INGEST_TOKEN`). +### Fixed + +- **db/migrations:** resolve a `077` migration version collision + (`077_api_key_stream_default_mode.sql` vs `077_quota_pools.sql`) that made + `getMigrationFiles()` throw and blocked `getDbInstance()` at startup (app would + not boot; every DB-touching test was red). Renumbered the dependency-free, + idempotent `quota_pools` migration to `085`, kept the non-idempotent + `api_key_stream_default_mode` `ALTER` at `077`, added a retroactive + `isSchemaAlreadyApplied` guard (case `085`), and a regression test enforcing + unique migration prefixes. + ### ✨ New Features - **notion:** add Notion as an MCP context source — 6 tools (`notion_search`, `notion_list_databases`, `notion_get_database`, `notion_query_database`, `notion_read`, `notion_append_blocks`) scoped under `read:notion` / `write:notion`, with dashboard "Context Sources" tab, settings API, and token persistence in `key_value` table (#2959) diff --git a/src/lib/db/migrationRunner.ts b/src/lib/db/migrationRunner.ts index d91e2984ac..3cf9a60856 100644 --- a/src/lib/db/migrationRunner.ts +++ b/src/lib/db/migrationRunner.ts @@ -444,6 +444,11 @@ function isSchemaAlreadyApplied( // mid-file and skip the CREATE INDEX that follows, leaving the index // missing on DBs that re-execute the script after a partial first run. return hasColumn(db, "memories", "needs_reindex"); + case "085": + // Retroactive guard for quota_pools migration renumbered from 077 → 085 + // (077 collided with 077_api_key_stream_default_mode). DBs that already + // applied quota_pools under the old 077 number should not re-run as 085. + return hasTable(db, "quota_pools") && hasTable(db, "quota_allocations"); default: return false; } diff --git a/src/lib/db/migrations/077_quota_pools.sql b/src/lib/db/migrations/085_quota_pools.sql similarity index 67% rename from src/lib/db/migrations/077_quota_pools.sql rename to src/lib/db/migrations/085_quota_pools.sql index 255a126071..d5fb173f4a 100644 --- a/src/lib/db/migrations/077_quota_pools.sql +++ b/src/lib/db/migrations/085_quota_pools.sql @@ -1,4 +1,12 @@ --- Migration 073: quota_pools + quota_allocations +-- Migration 085: quota_pools + quota_allocations +-- +-- Renumbered from 077 → 085 (#2900 sibling fix): 077 collided with +-- 077_api_key_stream_default_mode.sql, which made getMigrationFiles() throw a +-- version-collision error and blocked getDbInstance() at startup. quota_pools +-- has no dependents (no other migration references these tables) and is fully +-- idempotent, so it can safely move to the next free number. DBs that already +-- applied it under the old 077 number are guarded in isSchemaAlreadyApplied +-- (case "085"). -- -- Creates the two tables that persist quota-sharing pools and per-API-key -- allocations within each pool. Idempotent: safe to run more than once. diff --git a/tests/unit/db-migration-version-uniqueness.test.ts b/tests/unit/db-migration-version-uniqueness.test.ts new file mode 100644 index 0000000000..08efb38551 --- /dev/null +++ b/tests/unit/db-migration-version-uniqueness.test.ts @@ -0,0 +1,68 @@ +/** + * Regression guard for the migration version collision that blocked + * release/v3.8.8: `077_api_key_stream_default_mode.sql` and `077_quota_pools.sql` + * both claimed prefix 077. getMigrationFiles() throws on such a collision, which + * made getDbInstance() fail at startup and turned every DB-touching test red. + * + * quota_pools was renumbered 077 → 085 (it is dependency-free and idempotent; + * api_key_stream_default_mode is a non-idempotent ALTER and stays at 077). + * + * This test asserts — purely from the filesystem, without booting the DB — that + * no two live migration files share a numeric prefix (mirroring the runner's + * own collision check, minus any SUPERSEDED_DUPLICATE_MIGRATIONS allowlisted + * renamed pairs). It prevents the collision from being reintroduced. + */ +import test from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const MIGRATIONS_DIR = path.join( + path.dirname(fileURLToPath(import.meta.url)), + "..", + "..", + "src", + "lib", + "db", + "migrations" +); + +function migrationFiles(): Array<{ version: string; name: string }> { + return fs + .readdirSync(MIGRATIONS_DIR) + .filter((f) => f.endsWith(".sql")) + .map((filename) => { + const m = filename.match(/^(\d+)_(.+)\.sql$/); + return m ? { version: m[1], name: m[2] } : null; + }) + .filter((x): x is { version: string; name: string } => x !== null); +} + +test("no two migration files share the same numeric prefix", () => { + const byVersion = new Map(); + for (const f of migrationFiles()) { + if (!byVersion.has(f.version)) byVersion.set(f.version, []); + byVersion.get(f.version)!.push(f.name); + } + const collisions = [...byVersion.entries()] + .filter(([, names]) => names.length > 1) + .map(([version, names]) => `${version} → [${names.join(", ")}]`); + assert.deepEqual( + collisions, + [], + `Migration version collision(s) detected: ${collisions.join("; ")}. ` + + `Each migration must have a unique numeric prefix (rename to the next free number).` + ); +}); + +test("quota_pools lives at 085 (renumbered from the 077 collision)", () => { + const files = migrationFiles(); + const quotaPools = files.find((f) => f.name === "quota_pools"); + assert.ok(quotaPools, "quota_pools migration must exist"); + assert.equal(quotaPools.version, "085", "quota_pools must be renumbered to 085"); + // The standalone, non-idempotent column add stays at 077. + const streamDefault = files.find((f) => f.name === "api_key_stream_default_mode"); + assert.ok(streamDefault, "api_key_stream_default_mode migration must exist"); + assert.equal(streamDefault.version, "077", "api_key_stream_default_mode stays at 077"); +});