import { createHash } from "node:crypto"; import { execFile } from "node:child_process"; import { access, lstat, readFile, symlink, writeFile } from "node:fs/promises"; import { DatabaseSync } from "node:sqlite"; import { promisify } from "node:util"; import { fileURLToPath } from "node:url"; import { expect, it as nativeTest, onTestFinished } from "vitest"; import { projectAnalysisError } from "../../../src/domain/analysisErrorProjection.js"; import { SqliteDatabaseProvider } from "../../../src/sqlite/SqliteDatabaseProvider.js"; import { spawnOwnedProviderProcess } from "../../../src/process/ProviderProcess.js"; import { PrivateRuntimeRoot } from "../../../src/process/PrivateRuntimeRoot.js"; import { waitForProviderProcessReady } from "../../fixtures/providerProcess.js"; import { createSqliteDatabaseFixture, SQLITE_NATIVE_LIMITS_AVAILABLE, } from "../../fixtures/sqlite/database.js"; import { createTestWorkspace, removeTestWorkspace, } from "../../support/workspace/workspaceFixture.js"; const it = nativeTest.runIf(SQLITE_NATIVE_LIMITS_AVAILABLE); const workerUrl = new URL( "../../../dist/sqlite/SqliteDatabaseWorker.js", import.meta.url, ); const createProvider = () => new SqliteDatabaseProvider(process.env, (spawn) => spawnOwnedProviderProcess({ ...spawn, arguments: spawn.arguments.map((argument, index) => index === 1 ? fileURLToPath(workerUrl) : argument, ), }), ); const createFixture = async (wal = false) => { const workspace = await createTestWorkspace("rea-sqlite-boundary-"); const database = createSqliteDatabaseFixture(workspace.root, wal); if (!wal) database.close(); onTestFinished(async () => { if (wal) database.close(); await removeTestWorkspace(workspace.root); }); return { workspace, database }; }; const expectPrivateRootRemoved = (path: string) => expect(access(path)).rejects.toMatchObject({ code: "ENOENT" }); it.each(["late-cancellation", "cleanup-failure"])( "preserves completed native-worker observations across %s", async (scenario) => { const { workspace, database } = await createFixture(); const original = await readFile(database.path); const controller = new AbortController(); let cleanupAllowed = false; let root: PrivateRuntimeRoot | undefined; const provider = new SqliteDatabaseProvider( process.env, (spawn) => spawnOwnedProviderProcess({ ...spawn, arguments: spawn.arguments.map((argument, index) => index === 1 ? fileURLToPath(workerUrl) : argument, ), }), async () => { root = await PrivateRuntimeRoot.create({ parent: workspace.root }); const acquired = root; return { path: acquired.path, close: async () => { if (scenario === "late-cancellation") controller.abort(); else if (!cleanupAllowed) throw new Error("Completed snapshot cleanup denied"); await acquired.close(); }, }; }, ); onTestFinished(async () => { cleanupAllowed = true; await provider.close(); }); const result = await provider.inspect( { path: database.path, table: "records", columns: ["id"], row_limit: 1 }, { signal: controller.signal }, ); if (scenario === "late-cancellation") { expect(controller.signal.aborted).toBe(true); expect(result).toMatchObject({ ok: true, value: { result: { rows: { columns: ["id"], returned_rows: 1 } } }, }); } else { expect(result).toMatchObject({ ok: false, error: { cleanupIncomplete: true, partialObservation: { operation: "inspect_sqlite_database", confidence: "observed", subject: { local_path: database.path, digest: { sha256: digestForTest(original) }, }, parameters: { path: database.path, table: "records", columns: ["id"], row_limit: 1, }, normalized_result: { rows: { columns: ["id"], returned_rows: 1 } }, }, }, }); if (root === undefined) throw new Error("Missing retained root"); await expect(access(root.path)).resolves.toBeUndefined(); cleanupAllowed = true; await provider.close(); await expectPrivateRootRemoved(root.path); } expect(await readFile(database.path)).toEqual(original); }, ); const digestForTest = (bytes: Buffer): string => createHash("sha256").update(bytes).digest("hex"); it("inspects committed WAL schema and rows while preserving every source artifact", async () => { const { database } = await createFixture(true); const paths = [database.path, database.walPath, `${database.path}-shm`]; const original = await Promise.all( paths.map(async (path) => ({ path, bytes: await readFile(path), stat: await lstat(path), })), ); const result = await createProvider().inspect({ path: database.path, table: "records", schema_scope: "database", row_limit: 1, }); if (!result.ok) throw result.error; const main = original[0]; const wal = original[1]; if (main === undefined || wal === undefined) throw new Error("Missing fixture identity"); expect(result.value.result).toMatchObject({ artifact: { path: database.path, bytes: main.bytes.length, sha256: createHash("sha256").update(main.bytes).digest("hex"), }, wal: { path: database.walPath, bytes: wal.bytes.length, sha256: createHash("sha256").update(wal.bytes).digest("hex"), }, schema: { completeness: "complete", tables: expect.arrayContaining([ { name: "records", sql: expect.stringContaining("CREATE TABLE"), root_page: expect.any(Number), kind: "table", without_rowid: false, strict: false, column_count: 7, columns_completeness: "complete", columns: expect.arrayContaining([ expect.objectContaining({ name: "generated_value", hidden: 2 }), ]), }, ]), indexes: [ expect.objectContaining({ name: "records_text", table: "records" }), ], views: [ { name: "record_names", sql: expect.stringContaining("SELECT text_value"), }, ], triggers: [ { name: "records_insert", table: "records", sql: expect.stringContaining("AFTER INSERT"), }, ], }, rows: { table: "records", returned_rows: 1, truncated: true, row_limit: 1, order: "unspecified", }, }); for (const artifact of original) { expect(await readFile(artifact.path)).toEqual(artifact.bytes); const after = await lstat(artifact.path); expect({ ino: after.ino, size: after.size, mtimeMs: after.mtimeMs, ctimeMs: after.ctimeMs, }).toEqual({ ino: artifact.stat.ino, size: artifact.stat.size, mtimeMs: artifact.stat.mtimeMs, ctimeMs: artifact.stat.ctimeMs, }); } }); it("preserves SQLite integer, binary, nonfinite, null, and text byte values", async () => { const { database } = await createFixture(); const result = await createProvider().inspect({ path: database.path, table: "records", row_limit: 10, }); if (!result.ok) throw result.error; expect(result.value.result).toMatchObject({ wal: null, rows: { columns: [ "id", "integer_value", "blob_value", "real_value", "nullable_value", "text_value", "generated_value", ], returned_rows: 3, truncated: false, values: [ [ { type: "integer", value: "1" }, { type: "integer", value: "9223372036854775807" }, { type: "blob", hex: "001fefff" }, { type: "real", value: "Infinity" }, { type: "null" }, { type: "text", value: "first é\0value", bytes_hex: Buffer.from("first é\0value").toString("hex"), }, { type: "text", value: "integer", bytes_hex: Buffer.from("integer").toString("hex"), }, ], [ { type: "integer", value: "2" }, { type: "integer", value: "-9223372036854775808" }, { type: "blob", hex: "" }, { type: "real", value: "-Infinity" }, { type: "text", value: "present", bytes_hex: Buffer.from("present").toString("hex"), }, { type: "text", value: "second", bytes_hex: Buffer.from("second").toString("hex"), }, { type: "text", value: "integer", bytes_hex: Buffer.from("integer").toString("hex"), }, ], [ { type: "integer", value: "3" }, { type: "integer", value: "0" }, { type: "blob", hex: "ff" }, { type: "real", value: 1.25 }, { type: "null" }, { type: "text", bytes_hex: "ff00c0af" }, { type: "text", value: "integer", bytes_hex: Buffer.from("integer").toString("hex"), }, ], ], }, }); expect(() => JSON.stringify(result.value.result)).not.toThrow(); }); it("selects an exact quoted table and refuses to execute table input as SQL", async () => { const { database } = await createFixture(); const provider = createProvider(); const selected = await provider.inspect({ path: database.path, table: database.quotedTable, row_limit: 1, }); if (!selected.ok) throw selected.error; expect(selected.value.result).toMatchObject({ rows: { table: database.quotedTable, columns: ["__proto__"], values: [ [ { type: "text", value: "exact selected table", bytes_hex: Buffer.from("exact selected table").toString("hex"), }, ], ], returned_rows: 1, truncated: false, }, }); const unknown = await provider.inspect({ path: database.path, table: 'records"; DROP TABLE records;--', row_limit: 1, }); expect(unknown).toMatchObject({ ok: false, error: { _tag: "AnalysisInputError" }, }); const after = await provider.inspect({ path: database.path, table: "records", row_limit: 10, }); if (!after.ok) throw after.error; expect(after.value.result).toMatchObject({ rows: { returned_rows: 3 } }); }); it("does not execute views when selecting rows", async () => { const { database } = await createFixture(); const result = await createProvider().inspect({ path: database.path, table: "record_names", row_limit: 1, }); expect(result).toMatchObject({ ok: false, error: { _tag: "AnalysisInputError" }, }); }); it("attributes table and column selection failures to their own arguments", async () => { const { database } = await createFixture(); const provider = createProvider(); onTestFinished(() => provider.close()); const projection = async (selection: { table: string; columns?: string[]; }) => { const result = await provider.inspect({ path: database.path, row_limit: 1, ...selection, }); if (result.ok) throw new Error("Expected a rejected selection"); return projectAnalysisError(result.error); }; expect( await projection({ table: "records", columns: ["nope"] }), ).toMatchObject({ details: { issues: [ { path: ["columns"], reason: "invalid_format", message: "Selected column does not exist with that exact name in table records: nope", }, ], }, }); expect(await projection({ table: "absent" })).toMatchObject({ details: { issues: [{ path: ["table"] }] }, }); const selected = await provider.inspect({ path: database.path, table: "records", columns: ["id"], row_limit: 1, }); if (!selected.ok) throw selected.error; expect(selected.value.result).toMatchObject({ rows: { columns: ["id"] } }); }); it("rejects malformed databases without altering the selected file", async () => { const workspace = await createTestWorkspace("rea-sqlite-invalid-"); onTestFinished(() => removeTestWorkspace(workspace.root)); const path = await workspace.write( "not-a-database.db", "selected non-SQLite bytes", ); const bytes = await readFile(path); const result = await createProvider().inspect({ path }); expect(result).toMatchObject({ ok: false, error: { _tag: "AnalysisInputError" }, }); expect(await readFile(path)).toEqual(bytes); }); nativeTest.runIf(process.platform !== "win32")( "rejects a symlink database", async () => { const { database, workspace } = await createFixture(); const alias = workspace.path("alias.db"); await symlink(database.path, alias); const provider = createProvider(); expect(await provider.inspect({ path: alias })).toMatchObject({ ok: false, error: { _tag: "AnalysisInputError" }, }); }, ); nativeTest( "rejects an actual open rollback transaction without changing its journal", async () => { const { database } = await createFixture(); const writer = new DatabaseSync(database.path); writer.exec( "BEGIN IMMEDIATE; INSERT INTO records(id, text_value) VALUES (4, 'uncommitted')", ); try { const main = await readFile(database.path); const journalPath = `${database.path}-journal`; const journal = await readFile(journalPath); expect(journal.length).toBeGreaterThan(0); expect( await createProvider().inspect({ path: database.path }), ).toMatchObject({ ok: false, error: { _tag: "AnalysisInputError" }, }); expect(await readFile(database.path)).toEqual(main); expect(await readFile(journalPath)).toEqual(journal); } finally { writer.exec("ROLLBACK"); writer.close(); } }, ); nativeTest.runIf(process.platform !== "win32")( "rejects FIFO input without waiting for a writer", async () => { const workspace = await createTestWorkspace("rea-sqlite-fifo-"); onTestFinished(() => removeTestWorkspace(workspace.root)); const path = workspace.path("database.fifo"); await promisify(execFile)("mkfifo", [path]); const result = await createProvider().inspect({ path }); expect(result).toMatchObject({ ok: false, error: { _tag: "AnalysisInputError" }, }); }, ); nativeTest( "honors a cancelled caller before opening the database", async () => { const { database } = await createFixture(); const controller = new AbortController(); controller.abort(); expect( await createProvider().inspect( { path: database.path }, { signal: controller.signal }, ), ).toMatchObject({ ok: false, error: { _tag: "AnalysisCancelledError" } }); }, ); nativeTest( "removes an acquired private root when cancelled before snapshot creation", async () => { const { database } = await createFixture(); const controller = new AbortController(); let ownedRoot = ""; const provider = new SqliteDatabaseProvider( {}, () => { throw new Error("Cancelled request must not launch SQLite"); }, async () => { const root = await PrivateRuntimeRoot.create({ prefix: "rea-sqlite-cancel-root-", }); ownedRoot = root.path; controller.abort(); return root; }, ); expect( await provider.inspect( { path: database.path }, { signal: controller.signal }, ), ).toMatchObject({ ok: false, error: { _tag: "AnalysisCancelledError" } }); await expectPrivateRootRemoved(ownedRoot); }, ); it.each(["UTF-8", "UTF-16le", "UTF-16be"] as const)( "preserves %s text, a leading BOM and empty table selection", async (encoding) => { const workspace = await createTestWorkspace("rea-sqlite-encoding-"); onTestFinished(() => removeTestWorkspace(workspace.root)); const path = workspace.path("encoding.db"); const writer = new DatabaseSync(path); writer.exec( `PRAGMA encoding='${encoding}'; CREATE TABLE "" ("1" TEXT, "0" TEXT)`, ); const value = "\uFEFFé𝄞\0selected"; writer .prepare('INSERT INTO "" VALUES (?, ?)') .run(value, "second numeric column"); writer.close(); const bytes = Buffer.from(value, encoding === "UTF-8" ? "utf8" : "utf16le"); if (encoding === "UTF-16be") bytes.swap16(); const result = await createProvider().inspect({ path, table: "", }); if (!result.ok) throw result.error; expect(result.value.result).toMatchObject({ database_encoding: encoding, rows: { table: "", columns: ["1", "0"], returned_rows: 1, values: [ [ { type: "text", value, bytes_hex: bytes.toString("hex"), encoding }, { type: "text", value: "second numeric column" }, ], ], }, }); }, ); it("retains an unavailable virtual module definition without executing it", async () => { const workspace = await createTestWorkspace("rea-sqlite-virtual-"); onTestFinished(() => removeTestWorkspace(workspace.root)); const path = workspace.path("virtual.db"); const writer = new DatabaseSync(path); writer.exec( "CREATE VIRTUAL TABLE optional_search USING fts5(body); INSERT INTO optional_search VALUES ('selected source')", ); writer.close(); const bytes = await readFile(path); const declaration = bytes.indexOf(Buffer.from("USING fts5")); if (declaration < 0) throw new Error("SQLite virtual module declaration not found"); bytes.write("zzzz", declaration + Buffer.byteLength("USING "), "utf8"); await writeFile(path, bytes); const provider = createProvider(); const result = await provider.inspect({ path, schema_scope: "database" }); if (!result.ok) throw result.error; expect(result.value.result).toMatchObject({ rows: null, schema: { tables: expect.arrayContaining([ expect.objectContaining({ name: "optional_search", kind: "virtual", sql: expect.stringContaining("USING zzzz"), columns: [], columns_completeness: "unsupported", }), ]), }, }); expect( await provider.inspect({ path, table: "optional_search" }), ).toMatchObject({ ok: false, error: { _tag: "AnalysisInputError" } }); expect(await readFile(path)).toEqual(bytes); }); nativeTest( "cancels an acquired SQLite worker and removes its private snapshot", async () => { const { database } = await createFixture(); const controller = new AbortController(); let ownedRoot = ""; let pid: number | undefined; const provider = new SqliteDatabaseProvider({}, async (spawn) => { ownedRoot = spawn.cwd ?? ""; const requestPath = spawn.arguments[2]; if (requestPath === undefined) throw new Error("SQLite worker arguments missing"); const launched = await spawnOwnedProviderProcess({ ...spawn, arguments: [ "--input-type=module", "-e", `await import(${JSON.stringify(workerUrl.href)}); process.stdout.write('ready\\n'); setInterval(() => {}, 1000);`, "sqlite-worker-test", requestPath, ], }); pid = launched.process.pid; await waitForProviderProcessReady(launched.process); await access(`${ownedRoot}/reply.json`); setImmediate(() => controller.abort()); return launched; }); const result = await provider.inspect( { path: database.path, table: "records" }, { signal: controller.signal }, ); expect(result).toMatchObject({ ok: false, error: { _tag: "AnalysisCancelledError" }, }); if (pid === undefined) throw new Error("SQLite worker not acquired"); const acquiredPid = pid; expect(() => process.kill(acquiredPid, 0)).toThrow(); await expectPrivateRootRemoved(ownedRoot); }, );