From d2e16b2a4998818be5f29219467b97cc15ae8061 Mon Sep 17 00:00:00 2001 From: yasshmittal Date: Tue, 13 Oct 2026 18:33:14 +0530 Subject: [PATCH] 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 --- src/sqlite/SqliteDatabaseInspection.ts | 2 +- src/sqlite/SqliteDatabaseLimits.ts | 1 + src/sqlite/SqliteDatabaseWorkerExecution.ts | 11 ++++- .../sqlite/sqliteDatabaseProvider.test.ts | 44 +++++++++++++++++++ 4 files changed, 56 insertions(+), 2 deletions(-) diff --git a/src/sqlite/SqliteDatabaseInspection.ts b/src/sqlite/SqliteDatabaseInspection.ts index f5f92bdf4..510fbaafc 100644 --- a/src/sqlite/SqliteDatabaseInspection.ts +++ b/src/sqlite/SqliteDatabaseInspection.ts @@ -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 diff --git a/src/sqlite/SqliteDatabaseLimits.ts b/src/sqlite/SqliteDatabaseLimits.ts index 99df7ed9d..408fc4bc3 100644 --- a/src/sqlite/SqliteDatabaseLimits.ts +++ b/src/sqlite/SqliteDatabaseLimits.ts @@ -12,6 +12,7 @@ export class SqliteInspectionFailure extends Error { readonly reason: | "format" | "selection" + | "column-selection" | "unsupported" | "unavailable" | "output-limit" diff --git a/src/sqlite/SqliteDatabaseWorkerExecution.ts b/src/sqlite/SqliteDatabaseWorkerExecution.ts index 9474a6d27..cba180399 100644 --- a/src/sqlite/SqliteDatabaseWorkerExecution.ts +++ b/src/sqlite/SqliteDatabaseWorkerExecution.ts @@ -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[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 & { 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, }, diff --git a/tests/boundary/sqlite/sqliteDatabaseProvider.test.ts b/tests/boundary/sqlite/sqliteDatabaseProvider.test.ts index 24dadc128..b5adf8297 100644 --- a/tests/boundary/sqlite/sqliteDatabaseProvider.test.ts +++ b/tests/boundary/sqlite/sqliteDatabaseProvider.test.ts @@ -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));