feat(FN-4048): add FusionDB vacuum helper
Added a `FusionDB.vacuum()` helper to compact the SQLite database, with accompanying regression tests covering VACUUM behavior and error handling in `packages/core/src/db.ts` and its test file. Fusion-Task-Id: FN-4048
This commit is contained in:
@@ -11,7 +11,7 @@ import {
|
||||
} from "../db.js";
|
||||
import { DEFAULT_PROJECT_SETTINGS } from "../types.js";
|
||||
import { TaskStore } from "../store.js";
|
||||
import { mkdtempSync, existsSync, readFileSync, rmSync } from "node:fs";
|
||||
import { mkdtempSync, existsSync, readFileSync, rmSync, statSync } from "node:fs";
|
||||
import { join, dirname } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import { fileURLToPath } from "node:url";
|
||||
@@ -467,6 +467,64 @@ describe("Database", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("vacuum", () => {
|
||||
it("returns a no-op result for in-memory databases", () => {
|
||||
const memDb = new Database(fusionDir, { inMemory: true });
|
||||
memDb.init();
|
||||
|
||||
expect(memDb.vacuum()).toEqual({
|
||||
beforeBytes: 0,
|
||||
afterBytes: 0,
|
||||
durationMs: 0,
|
||||
});
|
||||
|
||||
memDb.close();
|
||||
});
|
||||
|
||||
it("runs disk-backed compaction and preserves stored rows", () => {
|
||||
const now = new Date().toISOString();
|
||||
db.prepare(
|
||||
"INSERT INTO tasks (id, description, \"column\", createdAt, updatedAt) VALUES (?, ?, ?, ?, ?)",
|
||||
).run("FN-VACUUM", "vacuum task", "todo", now, now);
|
||||
|
||||
for (let i = 0; i < 100; i += 1) {
|
||||
db.prepare(
|
||||
"INSERT INTO activityLog (id, timestamp, type, taskId, taskTitle, details, metadata) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||||
).run(`vac-${i}`, now, "task:updated", "FN-VACUUM", "vacuum task", `entry-${i}`, null);
|
||||
}
|
||||
|
||||
const dbFile = join(fusionDir, "fusion.db");
|
||||
const expectedBeforeBytes = existsSync(dbFile) ? statSync(dbFile).size : 0;
|
||||
const result = db.vacuum();
|
||||
|
||||
expect(result.beforeBytes).toBe(expectedBeforeBytes);
|
||||
expect(typeof result.beforeBytes).toBe("number");
|
||||
expect(typeof result.afterBytes).toBe("number");
|
||||
expect(typeof result.durationMs).toBe("number");
|
||||
expect(result.durationMs).toBeGreaterThanOrEqual(0);
|
||||
|
||||
const stored = db.prepare("SELECT id FROM tasks WHERE id = ?").get("FN-VACUUM") as
|
||||
| { id: string }
|
||||
| undefined;
|
||||
expect(stored?.id).toBe("FN-VACUUM");
|
||||
const expectedAfterBytes = existsSync(dbFile) ? statSync(dbFile).size : 0;
|
||||
expect(result.afterBytes).toBe(expectedAfterBytes);
|
||||
});
|
||||
|
||||
it("throws a descriptive error when checkpointing fails", () => {
|
||||
const checkpointSpy = vi
|
||||
.spyOn(db, "walCheckpoint")
|
||||
.mockImplementation(() => {
|
||||
throw new Error("checkpoint exploded");
|
||||
});
|
||||
|
||||
expect(() => db.vacuum()).toThrow(
|
||||
/Database vacuum maintenance failed during WAL checkpoint.*checkpoint exploded/,
|
||||
);
|
||||
checkpointSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe("transactions", () => {
|
||||
it("commits on success", () => {
|
||||
db.transaction(() => {
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
import { DatabaseSync } from "./sqlite-adapter.js";
|
||||
import { isAbsolute, join } from "node:path";
|
||||
import { mkdirSync, existsSync } from "node:fs";
|
||||
import { mkdirSync, existsSync, statSync } from "node:fs";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { DEFAULT_PROJECT_SETTINGS } from "./types.js";
|
||||
@@ -22,6 +22,13 @@ import type { SteeringComment, TaskComment } from "./types.js";
|
||||
/** A prepared SQL statement wrapping the node:sqlite StatementSync type. */
|
||||
export type Statement = ReturnType<DatabaseSync["prepare"]>;
|
||||
|
||||
/** Result payload for explicit database compaction via `VACUUM`. */
|
||||
export interface VacuumResult {
|
||||
beforeBytes: number;
|
||||
afterBytes: number;
|
||||
durationMs: number;
|
||||
}
|
||||
|
||||
// ── JSON Helpers ─────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
@@ -1298,6 +1305,50 @@ export class Database {
|
||||
return rebuilt.status === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run WAL truncation + VACUUM and report compaction stats.
|
||||
*
|
||||
* In-memory databases no-op and return zeroed stats. Disk-backed databases
|
||||
* sample file size before/after compaction, run `wal_checkpoint(TRUNCATE)`,
|
||||
* and then run `VACUUM` while the connection is in EXCLUSIVE locking mode to
|
||||
* prevent concurrent writes from other connections during maintenance.
|
||||
*/
|
||||
vacuum(): VacuumResult {
|
||||
if (this.inMemory) {
|
||||
return { beforeBytes: 0, afterBytes: 0, durationMs: 0 };
|
||||
}
|
||||
|
||||
const beforeBytes = existsSync(this.dbPath) ? statSync(this.dbPath).size : 0;
|
||||
const startedAt = Date.now();
|
||||
|
||||
this.db.exec("PRAGMA locking_mode=EXCLUSIVE");
|
||||
|
||||
try {
|
||||
try {
|
||||
this.walCheckpoint("TRUNCATE");
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
throw new Error(`Database vacuum maintenance failed during WAL checkpoint (dbPath=${this.dbPath}): ${message}`);
|
||||
}
|
||||
|
||||
try {
|
||||
this.db.exec("VACUUM");
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
throw new Error(`Database vacuum maintenance failed during VACUUM (dbPath=${this.dbPath}): ${message}`);
|
||||
}
|
||||
|
||||
const afterBytes = existsSync(this.dbPath) ? statSync(this.dbPath).size : 0;
|
||||
return {
|
||||
beforeBytes,
|
||||
afterBytes,
|
||||
durationMs: Date.now() - startedAt,
|
||||
};
|
||||
} finally {
|
||||
this.db.exec("PRAGMA locking_mode=NORMAL");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the database: create tables if they don't exist
|
||||
* and seed meta values.
|
||||
|
||||
@@ -104,7 +104,7 @@ export {
|
||||
export type { DistributedTaskIdAllocator } from "./distributed-task-id.js";
|
||||
export { Database, createDatabase, toJson, toJsonNullable, fromJson } from "./db.js";
|
||||
export { DatabaseSync } from "./sqlite-adapter.js";
|
||||
export type { Statement } from "./db.js";
|
||||
export type { Statement, VacuumResult } from "./db.js";
|
||||
export { ArchiveDatabase } from "./archive-db.js";
|
||||
export { detectLegacyData, migrateFromLegacy, getMigrationStatus } from "./db-migrate.js";
|
||||
export { GlobalSettingsStore, resolveGlobalDir } from "./global-settings.js";
|
||||
|
||||
Reference in New Issue
Block a user