Skip to content
Merged
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
14 changes: 13 additions & 1 deletion packages/loopover-miner/lib/migrate-cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,14 +81,26 @@ function migrateStore({ name, resolveDbPath, open }, env) {
versionAfter,
};
} catch (error) {
// applySchemaMigrations applies AND stamps each migration in its OWN transaction, so a failure part-way
// through a multi-migration sequence leaves the file at the LAST fully-applied version -- genuinely AHEAD
// of versionBefore. Re-read the real on-disk version instead of reporting a misleading "nothing changed".
// Guarded by its own try: the failure may itself be an unreadable/corrupt file (the same reason
// versionBefore can still be null here), in which case the pre-failure reading is all we can honestly
// report.
let versionAfter = versionBefore;
try {
versionAfter = peekSchemaVersion(dbPath);
} catch {
versionAfter = versionBefore;
}
return {
name,
dbPath,
ok: false,
status: "failed",
detail: error instanceof Error ? error.message : String(error),
versionBefore,
versionAfter: versionBefore,
versionAfter,
};
}
}
Expand Down
54 changes: 54 additions & 0 deletions test/unit/miner-migrate-cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { afterEach, describe, expect, it, vi } from "vitest";
import { runMigrate, runMigrateChecks } from "../../packages/loopover-miner/lib/migrate-cli.js";
import { initPortfolioQueueStore, resolvePortfolioQueueDbPath } from "../../packages/loopover-miner/lib/portfolio-queue.js";
import { resolveEventLedgerDbPath } from "../../packages/loopover-miner/lib/event-ledger.js";
import { applySchemaMigrations, BASELINE_SCHEMA_VERSION } from "../../packages/loopover-miner/lib/schema-version.js";

const roots: string[] = [];

Expand Down Expand Up @@ -141,6 +142,59 @@ describe("loopover-miner migrate (#4871)", () => {
]);
});

it("REGRESSION: reports the REAL post-failure version when a migration fails part-way through a multi-migration sequence (#6767)", () => {
const env = tempEnv();
const dbPath = join(dirname(resolvePortfolioQueueDbPath(env)), "partial-migration.sqlite3");
mkdirSync(dirname(dbPath), { recursive: true });
// Seed a real, openable file stamped at the baseline version -- this is what versionBefore reads.
const seed = new DatabaseSync(dbPath);
try {
applySchemaMigrations(seed, []);
} finally {
seed.close();
}

const results = runMigrateChecks(env, [
{
name: "partial-migration",
resolveDbPath: () => dbPath,
open: () => {
const db = new DatabaseSync(dbPath);
try {
// applySchemaMigrations applies AND stamps each migration in its own transaction: the first one
// COMMITS (stamping BASELINE+1) and the second throws, so the file really is left at BASELINE+1.
applySchemaMigrations(db, [
(migrationDb: DatabaseSync) => migrationDb.exec("CREATE TABLE first_ok (id INTEGER)"),
() => {
throw new Error("second migration boom");
},
]);
} finally {
db.close();
}
// Unreachable: applySchemaMigrations always throws for this fixture. Present only to satisfy the
// store contract's `open(dbPath) => { close() }` return type.
return { close: () => {} };
},
},
]);

const result = results[0];
expect(result).toMatchObject({ name: "partial-migration", ok: false, status: "failed" });
expect(result?.versionBefore).toBe(BASELINE_SCHEMA_VERSION);
// The bug: the catch branch hardcoded `versionAfter: versionBefore`, reporting "nothing changed" even
// though the first migration had already committed to disk.
expect(result?.versionAfter).toBe(BASELINE_SCHEMA_VERSION + 1);

// ...and the reported version is the one actually on disk, not an assumption.
const verifyDb = new DatabaseSync(dbPath, { readOnly: true });
try {
expect(verifyDb.prepare("PRAGMA user_version").get()?.user_version).toBe(BASELINE_SCHEMA_VERSION + 1);
} finally {
verifyDb.close();
}
});

it("runMigrate prints human-readable text (exit 0) and machine JSON with --json, and exits 1 when a store fails", () => {
const healthyEnv = tempEnv();
const log = vi.spyOn(console, "log").mockImplementation(() => {});
Expand Down