diff --git a/src/lib/db/migrationRunner.ts b/src/lib/db/migrationRunner.ts index c5470b6b1e..83d6a90420 100644 --- a/src/lib/db/migrationRunner.ts +++ b/src/lib/db/migrationRunner.ts @@ -465,13 +465,13 @@ function isSchemaAlreadyApplied( // exists the rebuild ran — skip re-executing the rename/copy/drop, which // would fail on the missing proxy_assignments_pre117 table. return hasColumn(db, "proxy_assignments", "position"); - // Retroactive guard for the 135/136 renumber (#8523 landed onto slots already taken - // by #8908/#9515): a DB that ran these under the old numbers already has the column, - // and a bare ALTER TABLE ADD COLUMN would throw on the re-run under the new number. + // Retroactive schema guards for migrations renumbered after release-branch collisions. case "137": return hasColumn(db, "version_manager", "auto_restart_adopted"); case "138": return hasColumn(db, "upstream_proxy_config", "fallback_backend"); + case "139": + return hasTable(db, "ccr_blocks"); default: return false; } diff --git a/src/lib/db/migrationRunner/constants.ts b/src/lib/db/migrationRunner/constants.ts index 9f678386c7..697fe3b31d 100644 --- a/src/lib/db/migrationRunner/constants.ts +++ b/src/lib/db/migrationRunner/constants.ts @@ -69,6 +69,12 @@ export const RENAMED_MIGRATION_COMPATIBILITY = [ toVersion: "059", toName: "manifest_routing", }, + { + fromVersion: "134", + fromName: "ccr_blocks", + toVersion: "139", + toName: "ccr_blocks", + }, ] as const; export const LEGACY_VERSION_SLOT_MIGRATIONS = [ diff --git a/src/lib/db/migrations/134_ccr_blocks.sql b/src/lib/db/migrations/139_ccr_blocks.sql similarity index 100% rename from src/lib/db/migrations/134_ccr_blocks.sql rename to src/lib/db/migrations/139_ccr_blocks.sql diff --git a/tests/unit/db-ccr-migration-renumber-134.test.ts b/tests/unit/db-ccr-migration-renumber-134.test.ts new file mode 100644 index 0000000000..dd31c3f1bc --- /dev/null +++ b/tests/unit/db-ccr-migration-renumber-134.test.ts @@ -0,0 +1,79 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import Database from "better-sqlite3"; + +const migrationsDir = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-ccr-migration-")); +const originalMigrationsDir = process.env.OMNIROUTE_MIGRATIONS_DIR; +process.env.OMNIROUTE_MIGRATIONS_DIR = migrationsDir; + +fs.writeFileSync( + path.join(migrationsDir, "134_proxy_logs_egress_ip.sql"), + "ALTER TABLE proxy_logs ADD COLUMN egress_ip TEXT;" +); +fs.writeFileSync( + path.join(migrationsDir, "139_ccr_blocks.sql"), + "CREATE TABLE ccr_blocks (principal_id TEXT PRIMARY KEY);" +); + +const { runMigrations } = await import("../../src/lib/db/migrationRunner.ts"); + +function createLegacyDb(appliedName: string) { + const db = new Database(":memory:"); + db.exec(` + CREATE TABLE proxy_logs (id TEXT PRIMARY KEY); + CREATE TABLE ccr_blocks (principal_id TEXT PRIMARY KEY); + CREATE TABLE _omniroute_migrations ( + version TEXT PRIMARY KEY, + name TEXT NOT NULL, + applied_at TEXT NOT NULL DEFAULT (datetime('now')) + ); + `); + db.prepare("INSERT INTO _omniroute_migrations (version, name) VALUES (?, ?)").run( + "134", + appliedName + ); + return db; +} + +test.after(() => { + fs.rmSync(migrationsDir, { recursive: true, force: true }); + if (originalMigrationsDir === undefined) delete process.env.OMNIROUTE_MIGRATIONS_DIR; + else process.env.OMNIROUTE_MIGRATIONS_DIR = originalMigrationsDir; +}); + +test("renumbered CCR migration frees 134 for proxy_logs on existing databases", () => { + const db = createLegacyDb("ccr_blocks"); + try { + assert.equal(runMigrations(db), 1); + assert.deepEqual( + db.prepare("SELECT version, name FROM _omniroute_migrations ORDER BY version").all(), + [ + { version: "134", name: "proxy_logs_egress_ip" }, + { version: "139", name: "ccr_blocks" }, + ] + ); + const columns = db.prepare("PRAGMA table_info(proxy_logs)").all() as Array<{ name: string }>; + assert.ok(columns.some((column) => column.name === "egress_ip")); + } finally { + db.close(); + } +}); + +test("renumbered CCR migration marks an existing table without recreating it", () => { + const db = createLegacyDb("proxy_logs_egress_ip"); + try { + assert.equal(runMigrations(db), 1); + assert.deepEqual( + db.prepare("SELECT version, name FROM _omniroute_migrations ORDER BY version").all(), + [ + { version: "134", name: "proxy_logs_egress_ip" }, + { version: "139", name: "ccr_blocks" }, + ] + ); + } finally { + db.close(); + } +}); diff --git a/tests/unit/db-migrationrunner-constants-split.test.ts b/tests/unit/db-migrationrunner-constants-split.test.ts index 7c87fac93f..7da7d426ad 100644 --- a/tests/unit/db-migrationrunner-constants-split.test.ts +++ b/tests/unit/db-migrationrunner-constants-split.test.ts @@ -70,8 +70,8 @@ describe("migrationRunner/constants — exact small-table snapshots", () => { // ── large tables — count + shape + spot-checks (corruption guard) ───────────── describe("migrationRunner/constants — large-table integrity", () => { - it("RENAMED_MIGRATION_COMPATIBILITY has 10 well-formed entries", () => { - assert.equal(RENAMED_MIGRATION_COMPATIBILITY.length, 10); + it("RENAMED_MIGRATION_COMPATIBILITY has 11 well-formed entries", () => { + assert.equal(RENAMED_MIGRATION_COMPATIBILITY.length, 11); for (const e of RENAMED_MIGRATION_COMPATIBILITY) { assert.equal(typeof e.fromVersion, "string"); assert.equal(typeof e.fromName, "string"); @@ -91,6 +91,12 @@ describe("migrationRunner/constants — large-table integrity", () => { // both manifest_routing collisions (052→059 and 056→059) must survive const manifest = RENAMED_MIGRATION_COMPATIBILITY.filter((e) => e.toName === "manifest_routing"); assert.deepEqual(manifest.map((e) => e.fromVersion).sort(), ["052", "056"]); + assert.deepEqual(RENAMED_MIGRATION_COMPATIBILITY.at(-1), { + fromVersion: "134", + fromName: "ccr_blocks", + toVersion: "139", + toName: "ccr_blocks", + }); }); it("PHYSICAL_SCHEMA_SENTINELS has 15 well-formed entries incl. the newest 064", () => {