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:
@@ -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", () => {
|
||||
|
||||
120
packages/core/src/__tests__/db-init-perf.test.ts
Normal file
120
packages/core/src/__tests__/db-init-perf.test.ts
Normal 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();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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 }>;
|
||||
|
||||
Reference in New Issue
Block a user