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:
parent
928b4d5fe4
commit
d2e16b2a49
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ export class SqliteInspectionFailure extends Error {
|
|||
readonly reason:
|
||||
| "format"
|
||||
| "selection"
|
||||
| "column-selection"
|
||||
| "unsupported"
|
||||
| "unavailable"
|
||||
| "output-limit"
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
Loading…
Reference in New Issue