From cbee7600c2b0598643513a0294b4504e0024ed25 Mon Sep 17 00:00:00 2001 From: Taras Kornichuk Date: Sun, 25 Oct 2026 16:57:28 +0200 Subject: [PATCH 1/3] feat(decode): keep row values out of CompiledQueryDecodeError The error now carries dialect, collection, column and the Schema path, kind and expected type of each issue. The Schema error moves from cause to a non-enumerable schemaError property, so message, toString, JSON, inspect and Cause.pretty print no row values. Adds formatDecodeError with an opt-in verbose flag. --- src/ch/compile.ts | 22 +++-- src/ch/decode-error.test.ts | 93 +++++++++++++++++++ src/ch/decode-error.ts | 164 ++++++++++++++++++++++++++++++++++ src/core.ts | 2 + src/database/database.test.ts | 12 +++ src/database/database.ts | 11 ++- 6 files changed, 290 insertions(+), 14 deletions(-) create mode 100644 src/ch/decode-error.test.ts create mode 100644 src/ch/decode-error.ts diff --git a/src/ch/compile.ts b/src/ch/compile.ts index 0aaaac1..db8d160 100644 --- a/src/ch/compile.ts +++ b/src/ch/compile.ts @@ -28,20 +28,14 @@ import { checkedLiteral, clickhouseDialect, currentDialect, withDialect, type Di import { Effect, Option, Schema } from "effect" import { QueryBuilderDefect, QueryBuilderError } from "./errors" import { withSubqueryCompiler } from "./subquery-context" +import { decodeErrorFromCause, type CompiledQueryDecodeError } from "./decode-error" import { tenantBoundOf, tenantColumnOf, tenantPredicatesOf, withTenantBound, type TenantPredicate } from "./tenant" // `QueryBuilderError` moved to ./errors so `expr.ts` can raise it too; still // exported from here, which is where every caller imports it from. export { QueryBuilderError } from "./errors" -export class CompiledQueryDecodeError extends Schema.TaggedError()( - "@maple-dev/effect-orm/CompiledQueryDecodeError", - { - message: Schema.String, - rowIndex: Schema.Number, - cause: Schema.optional(Schema.Unknown), - }, -) {} +export { CompiledQueryDecodeError } from "./decode-error" export class CompiledQueryEncodeError extends Schema.TaggedError()( "@maple-dev/effect-orm/CompiledQueryEncodeError", @@ -444,10 +438,12 @@ const makeCompiledQuery = ( decode(row).pipe( Effect.mapError( (cause) => - new CompiledQueryDecodeError({ - message: `Compiled query row ${index} did not match its declared output schema`, + decodeErrorFromCause({ + prefix: `Compiled query row ${index} did not match its declared output schema`, rowIndex: index, cause, + dialect, + collection, }), ), ), @@ -514,10 +510,12 @@ const makeCompiledQuery = ( Effect.map(Option.some), Effect.mapError( (cause) => - new CompiledQueryDecodeError({ - message: "Compiled query row 0 did not match its declared output schema", + decodeErrorFromCause({ + prefix: "Compiled query row 0 did not match its declared output schema", rowIndex: 0, cause, + dialect, + collection, }), ), ) diff --git a/src/ch/decode-error.test.ts b/src/ch/decode-error.test.ts new file mode 100644 index 0000000..f219580 --- /dev/null +++ b/src/ch/decode-error.test.ts @@ -0,0 +1,93 @@ +import { describe, expect, it } from "@effect/vitest" +import { Cause, Effect, Schema } from "effect" +import { inspect } from "node:util" +import { compileCHUnsafe, rawCompiledQuery } from "./compile" +import { formatDecodeError } from "./decode-error" +import * as CH from "./index" + +// A wire value that must never reach a log line. +const SECRET = "+1-555-0100" + +const phoneQuery = () => + rawCompiledQuery<{ readonly phone: number }>({ + reason: "test-fixture", + justification: "Synthetic SQL asserting decode-error formatting, not a product query.", + tenantScope: "single-tenant", + sql: "SELECT phone FROM contacts", + rowSchema: Schema.Struct({ phone: Schema.Number }), + }) + +describe("CompiledQueryDecodeError", () => { + it.effect("names the column path and the expected type in its message", () => + Effect.gen(function* () { + const error = yield* Effect.flip(phoneQuery().decodeRows([{ phone: SECRET }])) + + expect(error.message).toBe('Compiled query row 0 did not match its declared output schema: at ["phone"] expected number') + expect(error.column).toBe("phone") + expect(error.issues).toEqual([{ path: ["phone"], kind: "InvalidType", expected: "number" }]) + }), + ) + + // The ClickHouse DateTime codec quotes the text it could not parse, so the + // raw Schema error holds the value. None of the ways to log the error may. + it.effect("keeps the row value out of message, toString, JSON, inspect and Cause.pretty", () => + Effect.gen(function* () { + const compiled = rawCompiledQuery<{ readonly at: unknown }>({ + reason: "test-fixture", + justification: "Synthetic SQL asserting decode-error formatting, not a product query.", + tenantScope: "single-tenant", + sql: "SELECT at FROM contacts", + rowSchema: Schema.Struct({ at: CH.dateTime.schema }), + }) + const error = yield* Effect.flip(compiled.decodeRows([{ at: SECRET }])) + + expect(error.schemaError?.message).toContain(SECRET) + for (const printed of [ + error.message, + String(error), + JSON.stringify(error), + inspect(error, { depth: 20 }), + Cause.pretty(Cause.fail(error)), + formatDecodeError(error), + ]) { + expect(printed).not.toContain(SECRET) + } + expect(error.issues).toEqual([{ path: ["at"], kind: "Encoding", expected: "DateTime.Utc" }]) + }), + ) + + it.effect("carries the dialect and the table of the query", () => + Effect.gen(function* () { + const contacts = CH.table("contacts", { OrgId: CH.string, phone: CH.uint32 }, { tenantColumn: "OrgId" }) + const compiled = compileCHUnsafe( + CH.from(contacts) + .select(($) => ({ phone: $.phone })) + .where(($) => [$.OrgId.eq("org")]), + {}, + ) + const error = yield* Effect.flip(compiled.decodeRows([{ phone: SECRET }])) + + expect(error.dialect).toBe("clickhouse") + expect(error.collection).toBe("contacts") + expect(formatDecodeError(error)).toBe( + `CompiledQueryDecodeError (clickhouse, contacts): row 0: at ["phone"] expected a finite number`, + ) + }), + ) + + it.effect("prints the raw Schema error only when verbose is set", () => + Effect.gen(function* () { + const compiled = rawCompiledQuery<{ readonly at: unknown }>({ + reason: "test-fixture", + justification: "Synthetic SQL asserting decode-error formatting, not a product query.", + tenantScope: "single-tenant", + sql: "SELECT at FROM contacts", + rowSchema: Schema.Struct({ at: CH.dateTime.schema }), + }) + const error = yield* Effect.flip(compiled.decodeRows([{ at: SECRET }])) + + expect(formatDecodeError(error)).not.toContain(SECRET) + expect(formatDecodeError(error, { verbose: true })).toContain(SECRET) + }), + ) +}) diff --git a/src/ch/decode-error.ts b/src/ch/decode-error.ts new file mode 100644 index 0000000..7dffe55 --- /dev/null +++ b/src/ch/decode-error.ts @@ -0,0 +1,164 @@ +// BOUNDARY: This module turns a Schema parse failure of a wire row into an +// error that is safe to log. +// +// A row is user data: a phone number, an email, a token. The Schema error that +// a failed decode produces can quote that data — a codec's own message +// (`"" is not a ClickHouse DateTime`), a filter that returns a message +// built from its input, `reportInput`. `Cause.pretty` and `util.inspect` print +// an `Error.cause` chain, so carrying the Schema error as `cause` put the value +// in every log line that printed the failure. +// +// So the error keeps only what the schema says, never what the row held: the +// path of each issue, its kind, and the expected type read from the schema +// AST. The Schema error itself stays on the error as a non-enumerable property, +// out of `toString`, `JSON.stringify`, `inspect` and `Cause.pretty`. + +import { Schema, SchemaIssue } from "effect" + +/** One decode issue: where it is in the row and what the schema expected there. */ +export const DecodeIssue = Schema.Struct({ + /** The path from the row to the value, as Schema reports it. The first segment is the column alias. */ + path: Schema.Array(Schema.Union([Schema.String, Schema.Number])), + /** The Schema issue tag: `InvalidType`, `MissingKey`, `Filter`, `Encoding`, … */ + kind: Schema.String, + /** What the schema expected, from the schema AST. Absent when the schema does not say. */ + expected: Schema.optional(Schema.String), +}) +export type DecodeIssue = typeof DecodeIssue.Type + +export class CompiledQueryDecodeError extends Schema.TaggedError()( + "@maple-dev/effect-orm/CompiledQueryDecodeError", + { + message: Schema.String, + rowIndex: Schema.Number, + /** The dialect the query was compiled for or ran on, when known. */ + dialect: Schema.optional(Schema.String), + /** The table the statement reads, when it has a single one. */ + collection: Schema.optional(Schema.String), + /** The column alias of the first issue. */ + column: Schema.optional(Schema.String), + issues: Schema.Array(DecodeIssue), + }, +) { + /** + * The Schema error the decode failed with. It can hold row values, so it is + * not enumerable and is not the error's `cause`: logging the error does not + * print it. Read it here, or with `formatDecodeError(error, { verbose: true })`. + */ + declare readonly schemaError: Schema.SchemaError | undefined +} + +/** Options for {@link formatDecodeError}. */ +export interface FormatDecodeErrorOptions { + /** + * Append the raw Schema error message. It can contain row values: use it for + * local debugging, not for shared logs. + */ + readonly verbose?: boolean +} + +/** + * A one-line summary of a decode failure: dialect, table, row index, and the + * path and expected type of each issue. It holds no row values unless + * `verbose` is set. + */ +export const formatDecodeError = (error: CompiledQueryDecodeError, options?: FormatDecodeErrorOptions): string => { + const context = [error.dialect, error.collection].filter((part) => part !== undefined) + const head = `CompiledQueryDecodeError${context.length > 0 ? ` (${context.join(", ")})` : ""}: row ${error.rowIndex}` + const issues = error.issues.length > 0 ? `: ${describeIssues(error.issues)}` : "" + const raw = options?.verbose === true && error.schemaError !== undefined ? `\n${error.schemaError.message}` : "" + return `${head}${issues}${raw}` +} + +/** @internal Build the error from a failed row decode. */ +export const decodeErrorFromCause = (input: { + readonly prefix: string + readonly rowIndex: number + readonly cause: unknown + readonly dialect?: string | undefined + readonly collection?: string | undefined +}): CompiledQueryDecodeError => { + const schemaError = Schema.isSchemaError(input.cause) ? input.cause : undefined + const issues = schemaError === undefined ? [] : collectIssues(schemaError.issue, [], undefined) + const first = issues[0]?.path[0] + const error = new CompiledQueryDecodeError({ + message: issues.length > 0 ? `${input.prefix}: ${describeIssues(issues)}` : input.prefix, + rowIndex: input.rowIndex, + ...(input.dialect !== undefined ? { dialect: input.dialect } : undefined), + ...(input.collection !== undefined ? { collection: input.collection } : undefined), + ...(typeof first === "string" ? { column: first } : undefined), + issues, + }) + Object.defineProperty(error, "schemaError", { value: schemaError, enumerable: false }) + return error +} + +const describeIssues = (issues: ReadonlyArray): string => + issues + .map((issue) => { + const at = issue.path.length > 0 ? `at ${formatPath(issue.path)} ` : "" + return issue.expected !== undefined ? `${at}expected ${issue.expected}` : `${at}${issue.kind}` + }) + .join("; ") + +// Path segments are schema keys (column aliases, field names, array indexes), not values. +const formatPath = (path: ReadonlyArray): string => + `[${path.map((segment) => (typeof segment === "number" ? String(segment) : JSON.stringify(segment))).join("][")}]` + +const pathSegment = (key: PropertyKey): string | number => (typeof key === "symbol" ? String(key) : key) + +/** + * Walk the issue tree to its leaves. Reads only the tree's shape and the schema + * AST: never `input`, never a message an issue carries. + */ +const collectIssues = ( + issue: SchemaIssue.Issue, + path: ReadonlyArray, + enclosing: SchemaIssue.Issue | undefined, +): ReadonlyArray => { + switch (issue._tag) { + case "Pointer": + return collectIssues(issue.issue, [...path, ...issue.path.map(pathSegment)], undefined) + case "Composite": + return issue.issues.flatMap((child) => collectIssues(child, path, issue)) + case "AnyOf": + return issue.issues.length > 0 + ? issue.issues.flatMap((child) => collectIssues(child, path, issue)) + : [leaf(path, issue._tag, expectedOf(issue.ast))] + case "Encoding": + // A transformation failed. Its own message can quote the value; the AST + // it decodes to says what was expected. + return [leaf(path, issue._tag, expectedOf(issue.ast))] + case "Filter": { + const annotated = issue.filter.annotations?.expected + const expected = + typeof annotated === "string" + ? annotated + : enclosing !== undefined && "ast" in enclosing + ? expectedOf(enclosing.ast) + : undefined + return [leaf(path, issue._tag, expected)] + } + case "InvalidType": + return [leaf(path, issue._tag, expectedOf(issue.ast))] + case "OneOf": + return [leaf(path, issue._tag, expectedOf(issue.ast))] + case "MissingKey": + case "UnexpectedKey": + case "InvalidValue": + case "Forbidden": + return [leaf(path, issue._tag, undefined)] + } +} + +const leaf = (path: ReadonlyArray, kind: string, expected: string | undefined): DecodeIssue => + expected === undefined ? { path, kind } : { path, kind, expected } + +/** + * What a schema AST expects, as Schema's own formatter words it. The issue is + * built here with no input, so the text comes from the AST alone. + */ +const expectedOf = (ast: SchemaIssue.InvalidType["ast"]): string | undefined => { + const text = SchemaIssue.defaultLeafHook(new SchemaIssue.InvalidType(ast)) + return text.startsWith("Expected ") ? text.slice("Expected ".length) : text +} diff --git a/src/core.ts b/src/core.ts index b32adcf..f510114 100644 --- a/src/core.ts +++ b/src/core.ts @@ -148,6 +148,8 @@ export { CompiledQueryEncodeError, } from "./ch/compile" +export { formatDecodeError, type DecodeIssue, type FormatDecodeErrorOptions } from "./ch/decode-error" + export { type Dialect, type DialectClauses, diff --git a/src/database/database.test.ts b/src/database/database.test.ts index 072673f..43140a9 100644 --- a/src/database/database.test.ts +++ b/src/database/database.test.ts @@ -103,6 +103,18 @@ layer(Live, { excludeTestServices: true })("Database on PGlite", (it) => { }), ) + it.effect("a query decode error names the dialect and the column, not the value", () => + Effect.gen(function* () { + const Row = Schema.Struct({ phone: Schema.Number }) + const error = yield* Effect.flip(Db.query(Db.sql`SELECT '+1-555-0100' AS phone`, Row)) + assert(error instanceof CH.CompiledQueryDecodeError) + expect(error.dialect).toBe("postgres") + expect(error.column).toBe("phone") + expect(error.message).toBe('row 0 did not match the schema: at ["phone"] expected number') + expect(Cause.pretty(Cause.fail(error))).not.toContain("555-0100") + }), + ) + it.effect("refuses a query compiled for another dialect", () => Effect.gen(function* () { // The root compile is ClickHouse's. diff --git a/src/database/database.ts b/src/database/database.ts index aaf89d4..effde88 100644 --- a/src/database/database.ts +++ b/src/database/database.ts @@ -11,7 +11,8 @@ import { Cause, Context, Effect, Exit, Layer, Option, Schedule, Schema } from "e import { dual } from "effect/Function" import * as SqlClient from "effect/sql/SqlClient" import { isSqlError } from "effect/sql/SqlError" -import { compileCH, compileUnion, CompiledQueryDecodeError, type CompiledQuery } from "../ch/compile" +import { compileCH, compileUnion, type CompiledQueryDecodeError, type CompiledQuery } from "../ch/compile" +import { decodeErrorFromCause } from "../ch/decode-error" import { noTransactions, type Dialect, type IsolationLevel, type TransactionSettings } from "../ch/dialect" import type { QueryBuilderError } from "../ch/errors" import type { AnyInsert } from "../ch/insert" @@ -263,7 +264,13 @@ export const fromSqlClient = (sql: SqlClient.SqlClient, options: FromSqlClientOp return Effect.forEach(wire, (row, rowIndex) => decode(row).pipe( Effect.mapError( - (cause) => new CompiledQueryDecodeError({ message: `row ${rowIndex} did not match the schema`, rowIndex, cause }), + (cause) => + decodeErrorFromCause({ + prefix: `row ${rowIndex} did not match the schema`, + rowIndex, + cause, + dialect: dialect.name, + }), ), ), ) From 46828b3c1a4a0172c9400a2c69347f69138f7be2 Mon Sep 17 00:00:00 2001 From: Taras Kornichuk Date: Sun, 25 Oct 2026 16:57:28 +0200 Subject: [PATCH 2/3] docs(decode): document safe decode errors and formatDecodeError --- .changeset/safe-decode-errors.md | 5 +++++ docs/decoding-results.md | 34 ++++++++++++++++++++++++++++++-- docs/reference.md | 18 ++++++++++++++++- docs/troubleshooting.md | 7 +++++-- src/docs-examples.test.ts | 19 ++++++++++++++++++ 5 files changed, 78 insertions(+), 5 deletions(-) create mode 100644 .changeset/safe-decode-errors.md diff --git a/.changeset/safe-decode-errors.md b/.changeset/safe-decode-errors.md new file mode 100644 index 0000000..56c285a --- /dev/null +++ b/.changeset/safe-decode-errors.md @@ -0,0 +1,5 @@ +--- +"@maple-dev/effect-orm": minor +--- + +`CompiledQueryDecodeError` is safe to log. It carries `dialect`, `collection`, `column` and `issues` (the Schema path, issue kind and expected type of each issue), and its `message` names them: `Compiled query row 0 did not match its declared output schema: at ["phone"] expected number`. No field holds a row value, so `toString()`, `JSON.stringify`, `util.inspect` and `Cause.pretty` print none. The Schema error moves from `cause` to the non-enumerable `schemaError` property, because it can quote the value (the ClickHouse `DateTime` codec did). New `formatDecodeError(error, { verbose? })` gives a one-line summary for a log; `verbose: true` appends the raw Schema message. Breaking for code that read `error.cause` or built the error by hand: read `error.schemaError`, and pass `issues`. diff --git a/docs/decoding-results.md b/docs/decoding-results.md index bbf76c7..4df6718 100644 --- a/docs/decoding-results.md +++ b/docs/decoding-results.md @@ -154,8 +154,12 @@ const error = await Effect.runPromise(Effect.flip(compiled.decodeRows([{ name: 4 error._tag // "@maple-dev/effect-orm/CompiledQueryDecodeError" error.rowIndex // 0 -error.message // "Compiled query row 0 did not match its declared output schema" -error.cause // the underlying Schema parse error +error.message // 'Compiled query row 0 did not match its declared output schema: at ["name"] expected string' +error.dialect // "clickhouse" +error.collection // "events", the table the query reads +error.column // "name", the column alias of the first issue +error.issues // [{ path: ["name"], kind: "InvalidType", expected: "string" }] +error.schemaError // the underlying Schema error ``` It is an Effect `Schema.TaggedError`, so `Effect.catchTag` works on it directly. Decoding @@ -163,6 +167,32 @@ stops at the first bad row rather than accumulating. _(Backed by `docs/decoding-results.md > A bad row fails with CompiledQueryDecodeError`.)_ +### Logging a decode failure + +A row is user data. The error holds only what the schema says: the path of each issue, its +kind, and the expected type from the schema AST. Its `message`, `toString()`, `JSON.stringify` +output, `util.inspect` output and `Cause.pretty` output contain no row values. + +The Schema error can contain row values: a codec message such as `` `` is not a +ClickHouse DateTime ``, or a filter that builds its message from its input. So it is not the +error's `cause`. It is the non-enumerable `schemaError` property, which logging does not print. + +`formatDecodeError` gives a one-line summary for a log. Set `verbose: true` to append the raw +Schema error message, for local debugging only: + +```ts +Effect.tapErrorTag("@maple-dev/effect-orm/CompiledQueryDecodeError", (error) => + Effect.logError(CH.formatDecodeError(error)), +) +// CompiledQueryDecodeError (clickhouse, events): row 0: at ["name"] expected string + +CH.formatDecodeError(error, { verbose: true }) // + the Schema message, which can hold values +``` + +Column aliases, field names and table names are schema identifiers. They are not redacted. + +_(Backed by `docs/decoding-results.md > formatDecodeError leaves row values out`.)_ + ## Choosing schema types Only relevant when you declare one by hand; the column types already handle these. diff --git a/docs/reference.md b/docs/reference.md index abc2d46..8b51bba 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -236,7 +236,23 @@ when a decoded row cannot be written back to its wire shape. Fields: `message`, #### `CompiledQueryDecodeError` Tag `"@maple-dev/effect-orm/CompiledQueryDecodeError"`. Fails the `decodeRows` / -`decodeFirstRow` Effect. Fields: `message`, `rowIndex`, `cause`. +`decodeFirstRow` Effect and `Db.query` with a row schema. Fields: `message`, `rowIndex`, +`dialect?`, `collection?`, `column?` (the column alias of the first issue) and `issues` +(`DecodeIssue[]`). None of them holds a row value, so the error is safe to log. The Schema +error is on the non-enumerable `schemaError` property, not on `cause`, because it can hold row +values. See [Logging a decode failure](./decoding-results.md#logging-a-decode-failure). + +#### `DecodeIssue` + +`{ path, kind, expected? }`: one issue of a `CompiledQueryDecodeError`. `path` is the Schema +path from the row (its first segment is the column alias), `kind` the Schema issue tag +(`InvalidType`, `MissingKey`, `Filter`, `Encoding`, …), `expected` what the schema AST expects. + +#### `formatDecodeError(error, options?)` + +A one-line summary of a `CompiledQueryDecodeError` for a log: dialect, table, row index, and +the path and expected type of each issue. `FormatDecodeErrorOptions.verbose: true` appends the +raw Schema error message, which can hold row values. #### `SchemaDefinitionError` diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 8600382..359f409 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -53,8 +53,11 @@ assuming a typed query is optimized. See [Recipes](./recipes.md#time-buckets-ove ## Decode failures and lost information -`CompiledQueryDecodeError` reports `rowIndex`. Inspect that row's field names and wire values -alongside `compiled.rowSchemaSource`, `compiled.untypedColumns`, and `compiled.rowSchemaMismatch`. +`CompiledQueryDecodeError` reports `rowIndex`, the `column` and the `issues` (path and expected +type). It does not hold the wire value. Inspect that row's field names and wire values alongside +`compiled.rowSchemaSource`, `compiled.untypedColumns`, and `compiled.rowSchemaMismatch`. For the +raw Schema message, read `error.schemaError` or `formatDecodeError(error, { verbose: true })` +locally. Do not send it to shared logs: it can hold row values. | Symptom | Check | | ------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------- | diff --git a/src/docs-examples.test.ts b/src/docs-examples.test.ts index ef673be..ab8b216 100644 --- a/src/docs-examples.test.ts +++ b/src/docs-examples.test.ts @@ -633,6 +633,25 @@ describe("docs/decoding-results.md", () => { expect(result._tag).toBe("@maple-dev/effect-orm/CompiledQueryDecodeError") expect(result.rowIndex).toBe(0) + expect(result.message).toBe( + 'Compiled query row 0 did not match its declared output schema: at ["name"] expected string', + ) + expect(result.dialect).toBe("clickhouse") + expect(result.collection).toBe("events") + expect(result.column).toBe("name") + expect(result.issues).toEqual([{ path: ["name"], kind: "InvalidType", expected: "string" }]) + expect(result.schemaError?._tag).toBe("SchemaError") + }), + ) + + it.effect("formatDecodeError leaves row values out", () => + Effect.gen(function* () { + const result = yield* Effect.flip(compiled().decodeRows([{ name: 42, count: 1 }])) + + expect(CHE.formatDecodeError(result)).toBe( + 'CompiledQueryDecodeError (clickhouse, events): row 0: at ["name"] expected string', + ) + expect(CHE.formatDecodeError(result, { verbose: true })).toContain("Expected string") }), ) From 1b2ccc2c2b115174d11f6077859b4fb9258abd93 Mon Sep 17 00:00:00 2001 From: Makisuo Date: Mon, 2 Nov 2026 16:34:39 +0100 Subject: [PATCH 3/3] test: decode errors on SQLite and D1 name dialect, table and column, not the value Co-Authored-By: Claude Opus 5.5 --- src/sqlite/d1.test.ts | 6 +++++- src/sqlite/sqlite.test.ts | 4 +++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/sqlite/d1.test.ts b/src/sqlite/d1.test.ts index c35df5a..f19c07e 100644 --- a/src/sqlite/d1.test.ts +++ b/src/sqlite/d1.test.ts @@ -96,5 +96,9 @@ it("refuses a D1 integer past 2^53 instead of decoding a different number", asyn return yield* Effect.exit(db.run(SQLite.from(Big).select().crossTenant())) })) if (!Exit.isFailure(exit)) throw new Error(`an unsafe integer decoded as ${JSON.stringify(exit.value)}`) - expect(Cause.findError(exit.cause)).toMatchObject({ success: { _tag: "@maple-dev/effect-orm/CompiledQueryDecodeError" } }) + expect(Cause.findError(exit.cause)).toMatchObject({ + success: { _tag: "@maple-dev/effect-orm/CompiledQueryDecodeError", dialect: "d1", collection: "big", column: "id" }, + }) + // The value is row data: the printed failure names the column, not the number. + expect(Cause.pretty(exit.cause)).not.toContain("9007199254740992") }, 30_000) diff --git a/src/sqlite/sqlite.test.ts b/src/sqlite/sqlite.test.ts index fe5ff92..eeb0b36 100644 --- a/src/sqlite/sqlite.test.ts +++ b/src/sqlite/sqlite.test.ts @@ -169,7 +169,9 @@ it("rejects a stored value that does not match the derived row schema", async () return yield* Effect.exit(db.run(SQLite.from(Items).select())) })) if (!Exit.isFailure(exit)) throw new Error("invalid database row was accepted") - expect(Cause.findError(exit.cause)).toMatchObject({ success: { _tag: "@maple-dev/effect-orm/CompiledQueryDecodeError" } }) + expect(Cause.findError(exit.cause)).toMatchObject({ + success: { _tag: "@maple-dev/effect-orm/CompiledQueryDecodeError", dialect: "sqlite", collection: "items", column: "enabled" }, + }) }) it("rolls back failed SQLite transactions and nested savepoints", async () => {