Files
rea/tests/boundary/sqlite/sqliteDatabaseProvider.test.ts
T
yasshmittalandClaude Opus 5.5 d2e16b2a49 fix(sqlite): attribute unknown column selections to columns (#2774)
Every worker "selection" rejection was projected onto the table argument,
including an exact column name that does not exist in an existing table.
Give column rejections their own worker reason and map each input reason
to the caller argument it requires correcting. Table rejections keep the
table path.

Fixes #2613

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 21:03:14 +08:00

640 lines
20 KiB
TypeScript

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);
},
);