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>
This commit is contained in:
yasshmittal 2026-10-13 18:33:14 +05:30 committed by GitHub
parent 928b4d5fe4
commit d2e16b2a49
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 56 additions and 2 deletions

View File

@ -394,7 +394,7 @@ const readRows = (
for (const name of columns)
if (!available.has(name))
throw new SqliteInspectionFailure(
"selection",
"column-selection",
`Selected column does not exist with that exact name in table ${input.table}: ${name}`,
);
const projection = columns

View File

@ -12,6 +12,7 @@ export class SqliteInspectionFailure extends Error {
readonly reason:
| "format"
| "selection"
| "column-selection"
| "unsupported"
| "unavailable"
| "output-limit"

View File

@ -36,6 +36,7 @@ const replySchema = z.discriminatedUnion("ok", [
reason: z.enum([
"format",
"selection",
"column-selection",
"unsupported",
"unavailable",
"output-limit",
@ -49,6 +50,14 @@ export type SqliteWorkerLauncher = NonNullable<
Parameters<typeof runOwnedCommand>[2]
>["launcher"];
// Each input rejection names the caller argument that requires correction.
const rejectedArgument = {
format: "path",
unsupported: "path",
selection: "table",
"column-selection": "columns",
} as const;
const rejectedReply = (
reply: z.output<typeof replySchema> & { ok: false },
capturedOutput: AnalysisCapturedOutput,
@ -83,7 +92,7 @@ const rejectedReply = (
);
return new AnalysisInputError(operation, { capturedOutput }, [
{
path: [reply.reason === "selection" ? "table" : "path"],
path: [rejectedArgument[reply.reason]],
reason: "invalid_format",
message: reply.message,
},

View File

@ -5,6 +5,7 @@ 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";
@ -356,6 +357,49 @@ it("does not execute views when selecting rows", async () => {
});
});
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));