feat(FN-4094): cache schema compatibility checks with fingerprint fast path

Adds schema fingerprinting to cache schema compatibility checks, enabling a fast path that skips unnecessary re-initialization work on startup, with performance tests covering the new code paths and updated storage documentation.

Fusion-Task-Id: FN-4094
This commit is contained in:
Fusion
2026-05-12 07:15:20 -07:00
committed by gsxdsm
parent 7ca8d5ab50
commit 5bdf92b26e
6 changed files with 286 additions and 34 deletions

View File

@@ -11,8 +11,8 @@ function readDbSource(): string {
describe("architecture schema compatibility", () => {
it("invokes ensureSchemaCompatibility() from init()", () => {
const source = readDbSource();
expect(source).toMatch(/private ensureSchemaCompatibility\(\): void/);
expect(source).toMatch(/this\.migrate\(\);\s*[\s\S]*?this\.ensureSchemaCompatibility\(\);/);
expect(source).toMatch(/private ensureSchemaCompatibility\(options: SchemaCompatibilityOptions = \{\}\): void/);
expect(source).toMatch(/this\.migrate\(\);\s*[\s\S]*?this\.ensureSchemaCompatibility\([^)]*\);\s*[\s\S]*?this\.ensureRoutinesSchemaCompatibility\([^)]*\);\s*[\s\S]*?this\.ensureInsightRunsSchemaCompatibility\([^)]*\);\s*[\s\S]*?this\.ensureEvalTaskResultsSchemaCompatibility\([^)]*\);/);
});
it("restores missing declared columns for SCHEMA_SQL tables", () => {

View File

@@ -0,0 +1,120 @@
import { describe, expect, it, vi } from "vitest";
import { Database, SCHEMA_COMPAT_FINGERPRINT } from "../db.js";
function createInMemoryDatabase(): Database {
return new Database("/tmp/fn-db-init-perf", { inMemory: true });
}
function getMetaValue(db: Database, key: string): string | null {
const row = db.prepare("SELECT value FROM __meta WHERE key = ?").get(key) as { value: string } | undefined;
return row?.value ?? null;
}
function getColumnNames(db: Database, table: string): string[] {
return (db.prepare(`PRAGMA table_info(${table})`).all() as Array<{ name: string }>).map((column) => column.name);
}
function median(values: number[]): number {
const sorted = [...values].sort((left, right) => left - right);
const middle = Math.floor(sorted.length / 2);
return sorted.length % 2 === 0
? (sorted[middle - 1] + sorted[middle]) / 2
: sorted[middle];
}
describe("Database.init() schema compatibility performance", () => {
it("writes schemaCompatFingerprint to __meta for a fresh database", () => {
const db = createInMemoryDatabase();
try {
db.init();
expect(getMetaValue(db, "schemaCompatFingerprint")).toBe(SCHEMA_COMPAT_FINGERPRINT);
} finally {
db.close();
}
});
it("skips ALTER TABLE work and keeps PRAGMA table_info calls under a strict ceiling on unchanged-schema re-init", () => {
const db = createInMemoryDatabase();
try {
db.init();
const execSpy = vi.spyOn((db as any).db, "exec");
const prepareSpy = vi.spyOn((db as any).db, "prepare");
db.init();
const alterTableStatements = execSpy.mock.calls.filter(([sql]) => sql.includes("ALTER TABLE"));
expect(alterTableStatements).toHaveLength(0);
const pragmaTableInfoCalls = prepareSpy.mock.calls.filter(([sql]) => sql.includes("PRAGMA table_info("));
// Current-schema re-init still probes tasks twice from migrate()'s legacy guard;
// the fingerprint hit should prevent the broader schema-compatibility sweep.
expect(pragmaTableInfoCalls.length).toBeLessThanOrEqual(2);
} finally {
db.close();
}
});
it("restores a missing declared column when the fingerprint is absent", () => {
const db = createInMemoryDatabase();
try {
db.init();
db.exec("ALTER TABLE tasks DROP COLUMN modifiedFiles");
db.exec("DELETE FROM __meta WHERE key = 'schemaCompatFingerprint'");
expect(getColumnNames(db, "tasks")).not.toContain("modifiedFiles");
db.init();
expect(getColumnNames(db, "tasks")).toContain("modifiedFiles");
expect(getMetaValue(db, "schemaCompatFingerprint")).toBe(SCHEMA_COMPAT_FINGERPRINT);
} finally {
db.close();
}
});
it("restores a missing declared column when the fingerprint is stale", () => {
const db = createInMemoryDatabase();
try {
db.init();
db.exec("ALTER TABLE tasks DROP COLUMN modifiedFiles");
db.exec("INSERT OR REPLACE INTO __meta (key, value) VALUES ('schemaCompatFingerprint', 'stale-fingerprint')");
expect(getColumnNames(db, "tasks")).not.toContain("modifiedFiles");
db.init();
expect(getColumnNames(db, "tasks")).toContain("modifiedFiles");
expect(getMetaValue(db, "schemaCompatFingerprint")).toBe(SCHEMA_COMPAT_FINGERPRINT);
} finally {
db.close();
}
});
it("keeps repeated unchanged-schema init() calls comfortably below the coarse perf guard", () => {
const db = createInMemoryDatabase();
try {
db.init();
const durationsMs: number[] = [];
for (let index = 0; index < 50; index += 1) {
const startedAt = process.hrtime.bigint();
db.init();
const endedAt = process.hrtime.bigint();
durationsMs.push(Number(endedAt - startedAt) / 1_000_000);
}
// Coarse local/CI-safe guard: unchanged-schema re-init should stay well below
// tens of milliseconds once the fingerprint short-circuits reconciliation.
expect(median(durationsMs)).toBeLessThan(50);
} finally {
db.close();
}
});
});

View File

@@ -985,6 +985,7 @@ describe("Migration: pre-33 DB upgrade", () => {
`);
db1.exec("DROP TABLE project_insight_runs");
db1.exec("ALTER TABLE project_insight_runs_legacy RENAME TO project_insight_runs");
db1.exec("DELETE FROM __meta WHERE key = 'schemaCompatFingerprint'");
// Verify lifecycle column is gone
const colsBefore = db1.prepare("PRAGMA table_info(project_insight_runs)").all() as Array<{ name: string }>;