From d8fd690641321c3d8d932cae6487536e1e37bbbc Mon Sep 17 00:00:00 2001 From: Steven McClankerton Date: Tue, 15 Sep 2026 13:15:02 +0000 Subject: [PATCH 1/3] experiment: prototype synchronous codec decoding (do not merge) Signed-off-by: Steven McClankerton --- .../framework-components/src/shared/codec.ts | 8 +-- .../1-foundation/mongo-codec/src/codecs.ts | 12 +--- .../7-runtime/src/codecs/decoding.ts | 24 +++----- .../7-runtime/src/mongo-runtime.ts | 5 +- .../7-runtime/test/decode-sync.test.ts | 27 +++++++++ .../relational-core/src/ast/codec-types.ts | 2 +- .../relational-core/src/ast/sql-codecs.ts | 10 ++-- .../2-sql/5-runtime/src/codecs/decoding.ts | 59 ++++--------------- packages/2-sql/5-runtime/src/sql-runtime.ts | 2 +- .../5-runtime/test/raw-query-decode.test.ts | 26 ++++---- packages/2-sql/5-runtime/test/test-codec.ts | 12 +--- .../src/core/arktype-json-codec.ts | 2 +- .../3-extensions/pgvector/src/core/codecs.ts | 2 +- .../3-extensions/postgis/src/core/codecs.ts | 2 +- .../3-targets/postgres/src/core/codecs.ts | 45 +++++++------- .../postgres/src/core/date-codecs.ts | 2 +- .../postgres/src/core/temporal-codecs.ts | 8 +-- .../src/core/temporal-string-codecs.ts | 8 +-- .../3-targets/sqlite/src/core/codecs.ts | 16 ++--- 19 files changed, 123 insertions(+), 149 deletions(-) create mode 100644 packages/2-mongo-family/7-runtime/test/decode-sync.test.ts diff --git a/packages/1-framework/1-core/framework-components/src/shared/codec.ts b/packages/1-framework/1-core/framework-components/src/shared/codec.ts index 75f8a8cec974..d74b696a17c6 100644 --- a/packages/1-framework/1-core/framework-components/src/shared/codec.ts +++ b/packages/1-framework/1-core/framework-components/src/shared/codec.ts @@ -26,7 +26,7 @@ import type { CodecCallContext, CodecTrait } from './codec-types'; * * Codec methods split into two groups: * - * - **Query-time** methods (`encode`, `decode`) run per row/parameter at the IO boundary; they are required and Promise-returning. The per-family codec factory accepts sync or async author functions and lifts sync ones to Promise-shaped methods automatically. + * - **Query-time** methods (`encode`, `decode`) run per row/parameter at the IO boundary; encoding is Promise-returning, while decoding is strictly synchronous in this performance prototype. * - **JSON** methods (`encodeJson`, `decodeJson`) run when the contract is serialized or loaded. Runtimes may also use `decodeJson` for values embedded in database-produced JSON results. They stay synchronous so contract validation and client construction are synchronous. * * Target-family codec interfaces extend this base; family-specific concerns (e.g. the SQL `column?` per-call context) layer on through the `CodecCallContext` extension pattern. @@ -43,8 +43,8 @@ export interface Codec< readonly __codecTraits?: TTraits; /** Converts a JS value to the wire format expected by the database driver. Always Promise-returning at the boundary. The {@link CodecCallContext} is supplied by the runtime on every call (allocated once per runtime operation, including `query()`, `PreparedStatement.query()`, and `execute()`); family layers may narrow the ctx to extend it (e.g. SQL adds `column`). Author-side single-arg `(value) => …` functions remain legal via TypeScript's bivariance for trailing parameters. */ encode(value: TInput, ctx: CodecCallContext): Promise; - /** Converts a wire value from the database driver into the JS application type. Always Promise-returning at the boundary. The {@link CodecCallContext} is supplied by the runtime on every call (allocated once per runtime operation, including `query()`, `PreparedStatement.query()`, and `execute()`); family layers may narrow the ctx to extend it (e.g. SQL adds `column`). Author-side single-arg `(wire) => …` functions remain legal via TypeScript's bivariance for trailing parameters. */ - decode(wire: TWire, ctx: CodecCallContext): Promise; + /** Converts a wire value from the database driver into the JS application type synchronously. The {@link CodecCallContext} is supplied by the runtime on every call (allocated once per runtime operation, including `query()`, `PreparedStatement.query()`, and `execute()`); family layers may narrow the ctx to extend it (e.g. SQL adds `column`). Author-side single-arg `(wire) => …` functions remain legal via TypeScript's bivariance for trailing parameters. */ + decode(wire: TWire, ctx: CodecCallContext): TInput; /** Converts a JS value to the target-defined JSON representation used for contract serialization. This must match the scalar shape produced by the target inside JSON values. Synchronous; called during contract emission. */ encodeJson(value: TInput): JsonValue; /** Converts the target-defined JSON representation back to the JS input type. Synchronous; called during contract loading via `family.deserializeContract` and may be called by runtimes for embedded JSON values. */ @@ -74,7 +74,7 @@ export abstract class CodecImpl< } abstract encode(value: TInput, ctx: CodecCallContext): Promise; - abstract decode(wire: TWire, ctx: CodecCallContext): Promise; + abstract decode(wire: TWire, ctx: CodecCallContext): TInput; abstract encodeJson(value: TInput): JsonValue; abstract decodeJson(json: JsonValue): TInput; } diff --git a/packages/2-mongo-family/1-foundation/mongo-codec/src/codecs.ts b/packages/2-mongo-family/1-foundation/mongo-codec/src/codecs.ts index 857069872d58..c141b80d515d 100644 --- a/packages/2-mongo-family/1-foundation/mongo-codec/src/codecs.ts +++ b/packages/2-mongo-family/1-foundation/mongo-codec/src/codecs.ts @@ -35,7 +35,7 @@ type JsonRoundTripConfig = [TInput] extends [JsonValue] /** * Construct a Mongo codec from author functions. * - * Author `encode` and `decode` as sync or async functions; the factory produces a {@link MongoCodec} whose query-time methods follow the boundary contract documented on the framework {@link BaseCodec}. Authors receive a second `ctx` options argument carrying the per-call context; ignore it if you don't need it. + * Author `encode` as sync or async and `decode` as synchronous; the factory produces a {@link MongoCodec} whose query-time methods follow the boundary contract documented on the framework {@link BaseCodec}. Authors receive a second `ctx` options argument carrying the per-call context; ignore it if you don't need it. * * Both `encode` and `decode` are required so `TInput` and `TWire` are always covered by an explicit author function — the factory installs no identity fallback. `encodeJson` and `decodeJson` default to identity **only when `TInput` is assignable to `JsonValue`**; otherwise both are required so the contract artifact stays JSON-safe. * @@ -50,7 +50,7 @@ export function mongoCodec< config: { typeId: Id; encode: (value: TInput, ctx: CodecCallContext) => TWire | Promise; - decode: (wire: TWire, ctx: CodecCallContext) => TInput | Promise; + decode: (wire: TWire, ctx: CodecCallContext) => TInput; } & JsonRoundTripConfig, ): MongoCodec { const identity = (v: unknown) => v; @@ -70,13 +70,7 @@ export function mongoCodec< return Promise.reject(error); } }, - decode: (wire, ctx) => { - try { - return Promise.resolve(userDecode(wire, ctx)); - } catch (error) { - return Promise.reject(error); - } - }, + decode: userDecode, encodeJson: (widenedConfig.encodeJson ?? identity) as (value: TInput) => JsonValue, decodeJson: (widenedConfig.decodeJson ?? identity) as (json: JsonValue) => TInput, }; diff --git a/packages/2-mongo-family/7-runtime/src/codecs/decoding.ts b/packages/2-mongo-family/7-runtime/src/codecs/decoding.ts index ae4524a22d34..ed75436d9547 100644 --- a/packages/2-mongo-family/7-runtime/src/codecs/decoding.ts +++ b/packages/2-mongo-family/7-runtime/src/codecs/decoding.ts @@ -45,13 +45,13 @@ function wrapDecodeFailure( throw wrapped; } -export async function decodeMongoRow( +export function decodeMongoRow( row: unknown, shape: MongoResultShape, registry: MongoCodecLookup, collection: string, ctx: CodecCallContext = {}, -): Promise { +): unknown { if (shape.kind === 'unknown') { return row; } @@ -60,9 +60,8 @@ export async function decodeMongoRow( } const rowObj = row as Record; const out: Record = {}; - const tasks: Array> = []; - function scheduleLeaf( + function decodeLeaf( path: string, codecId: string, wire: unknown, @@ -73,15 +72,11 @@ export async function decodeMongoRow( assign(wire); return; } - tasks.push( - (async () => { - try { - assign(await codec.decode(wire, ctx)); - } catch (error) { - wrapDecodeFailure(error, collection, path, codecId, wire); - } - })(), - ); + try { + assign(codec.decode(wire, ctx)); + } catch (error) { + wrapDecodeFailure(error, collection, path, codecId, wire); + } } function walkField( @@ -102,7 +97,7 @@ export async function decodeMongoRow( assign(value); return; } - scheduleLeaf(path, fieldShape.codecId, value, assign); + decodeLeaf(path, fieldShape.codecId, value, assign); return; case 'document': { if (value === null || value === undefined) { @@ -174,6 +169,5 @@ export async function decodeMongoRow( } } - await Promise.all(tasks); return out; } diff --git a/packages/2-mongo-family/7-runtime/src/mongo-runtime.ts b/packages/2-mongo-family/7-runtime/src/mongo-runtime.ts index 42d62e2c769b..a229fc9fc7b4 100644 --- a/packages/2-mongo-family/7-runtime/src/mongo-runtime.ts +++ b/packages/2-mongo-family/7-runtime/src/mongo-runtime.ts @@ -92,8 +92,7 @@ export interface MongoRuntime { * * Mongo's read path decodes rows via `resultShape` (per ADR 209). The * same `CodecCallContext` is forwarded into each `codec.decode(wire, ctx)` - * call, so async decoders that respect the signal get cancellation; the - * runtime itself does not currently emit a `phase: 'decode'` envelope. + * call synchronously; the runtime does not emit a `phase: 'decode'` envelope. */ query(plan: MongoQueryPlan, options?: RuntimeExecuteOptions): AsyncIterableResult; execute(plan: MongoQueryPlan, options?: RuntimeExecuteOptions): Promise; @@ -253,7 +252,7 @@ class MongoRuntimeImpl rawRow, ); } else { - const decoded = await decodeMongoRow( + const decoded = decodeMongoRow( rawRow, exec.resultShape, self.#codecs, diff --git a/packages/2-mongo-family/7-runtime/test/decode-sync.test.ts b/packages/2-mongo-family/7-runtime/test/decode-sync.test.ts new file mode 100644 index 000000000000..9510f38bbf67 --- /dev/null +++ b/packages/2-mongo-family/7-runtime/test/decode-sync.test.ts @@ -0,0 +1,27 @@ +import { mongoCodec } from '@internal/mongo-codec'; +import { expect, it } from 'vitest'; +import { decodeMongoRow } from '../src/codecs/decoding'; + +it('decodes nested array leaves synchronously', () => { + const codec = mongoCodec({ + typeId: 'test/sync@1', + encode: (value: number) => value, + decode: (value: number) => value * 10, + }); + const result = decodeMongoRow( + { values: [1, null, 2], untouched: true }, + { + kind: 'document', + fields: { + values: { + kind: 'array', + nullable: false, + element: { kind: 'leaf', codecId: codec.id, nullable: true }, + }, + }, + }, + new Map([[codec.id, codec]]), + 'items', + ); + expect(result).toEqual({ values: [10, null, 20], untouched: true }); +}); diff --git a/packages/2-sql/4-lanes/relational-core/src/ast/codec-types.ts b/packages/2-sql/4-lanes/relational-core/src/ast/codec-types.ts index e9c28476caa0..f3074d1e38b1 100644 --- a/packages/2-sql/4-lanes/relational-core/src/ast/codec-types.ts +++ b/packages/2-sql/4-lanes/relational-core/src/ast/codec-types.ts @@ -97,7 +97,7 @@ export interface Codec< TInput = unknown, > extends BaseCodec { encode(value: TInput, ctx: SqlCodecCallContext): Promise; - decode(wire: TWire, ctx: SqlCodecCallContext): Promise; + decode(wire: TWire, ctx: SqlCodecCallContext): TInput; } /** diff --git a/packages/2-sql/4-lanes/relational-core/src/ast/sql-codecs.ts b/packages/2-sql/4-lanes/relational-core/src/ast/sql-codecs.ts index 14d81e7e387e..00c44f97c17e 100644 --- a/packages/2-sql/4-lanes/relational-core/src/ast/sql-codecs.ts +++ b/packages/2-sql/4-lanes/relational-core/src/ast/sql-codecs.ts @@ -59,7 +59,7 @@ export class SqlTextCodec extends CodecImpl< async encode(value: string, _ctx: CodecCallContext): Promise { return sqlTextEncode(value); } - async decode(wire: string, _ctx: CodecCallContext): Promise { + decode(wire: string, _ctx: CodecCallContext): string { return sqlTextDecode(wire); } encodeJson(value: string): JsonValue { @@ -97,7 +97,7 @@ export class SqlIntCodec extends CodecImpl< async encode(value: number, _ctx: CodecCallContext): Promise { return sqlIntEncode(value); } - async decode(wire: number, _ctx: CodecCallContext): Promise { + decode(wire: number, _ctx: CodecCallContext): number { return sqlIntDecode(wire); } encodeJson(value: number): JsonValue { @@ -135,7 +135,7 @@ export class SqlFloatCodec extends CodecImpl< async encode(value: number, _ctx: CodecCallContext): Promise { return sqlFloatEncode(value); } - async decode(wire: number, _ctx: CodecCallContext): Promise { + decode(wire: number, _ctx: CodecCallContext): number { return sqlFloatDecode(wire); } encodeJson(value: number): JsonValue { @@ -173,7 +173,7 @@ export class SqlCharCodec extends CodecImpl< async encode(value: string, _ctx: CodecCallContext): Promise { return sqlCharEncode(value); } - async decode(wire: string, _ctx: CodecCallContext): Promise { + decode(wire: string, _ctx: CodecCallContext): string { return sqlCharDecode(wire); } encodeJson(value: string): JsonValue { @@ -214,7 +214,7 @@ export class SqlVarcharCodec extends CodecImpl< async encode(value: string, _ctx: CodecCallContext): Promise { return sqlVarcharEncode(value); } - async decode(wire: string, _ctx: CodecCallContext): Promise { + decode(wire: string, _ctx: CodecCallContext): string { return sqlVarcharDecode(wire); } encodeJson(value: string): JsonValue { diff --git a/packages/2-sql/5-runtime/src/codecs/decoding.ts b/packages/2-sql/5-runtime/src/codecs/decoding.ts index 2daec9663e7a..9a30637b0613 100644 --- a/packages/2-sql/5-runtime/src/codecs/decoding.ts +++ b/packages/2-sql/5-runtime/src/codecs/decoding.ts @@ -1,8 +1,4 @@ -import { - checkAborted, - raceAgainstAbort, - runtimeError, -} from '@internal/framework-components/runtime'; +import { checkAborted, runtimeError } from '@internal/framework-components/runtime'; import type { AnyQueryAst, Codec, @@ -214,7 +210,7 @@ function decodeIncludeAggregate(alias: string, wireValue: unknown): unknown { } /** - * Decodes a single field. Single-armed: every cell takes the same path — `codec.decode → await → return plain value` — so sync- and async-authored codecs are indistinguishable to callers. JSON-Schema validation, when required, lives inside the resolved codec's `decode` body (e.g. `arktype-json` validates against its rehydrated schema and throws `RUNTIME.JSON_SCHEMA_VALIDATION_FAILED` from `decode` directly); there is + * Decodes a single field synchronously. JSON-Schema validation, when required, lives inside the resolved codec's `decode` body (e.g. `arktype-json` validates against its rehydrated schema and throws `RUNTIME.JSON_SCHEMA_VALIDATION_FAILED` from `decode` directly); there is * no separate validator-registry pass. * * The row-level `rowCtx` is repackaged into a per-cell `SqlCodecCallContext` whose `column = { table, name }` is a structural projection of the per-cell `ColumnRef = { table, column }` resolved from the AST-backed `DecodeContext` (the same resolution `wrapDecodeFailure` uses for envelope construction — one resolution per cell, two consumers). Cells the runtime cannot resolve to a single underlying column (aggregate @@ -222,12 +218,12 @@ function decodeIncludeAggregate(alias: string, wireValue: unknown): unknown { * * For `many`-flagged aliases the driver has already parsed the wire form into a JS array; this function maps the element codec over that array element-by-element, passing `null` elements through unchanged. Element-level failures surface through the existing `RUNTIME.DECODE_FAILED` envelope with the column/codec context from the parent cell. */ -async function decodeField( +function decodeField( alias: string, wireValue: unknown, decodeCtx: DecodeContext, rowCtx: SqlCodecCallContext, -): Promise { +): unknown { if (wireValue === null) { return null; } @@ -266,7 +262,7 @@ async function decodeField( continue; } try { - decoded.push(await codec.decode(elem, cellCtx)); + decoded.push(codec.decode(elem, cellCtx)); } catch (error) { if (isStructuredError(error)) throw error; wrapDecodeFailure(error, alias, ref, codec, elem); @@ -276,7 +272,7 @@ async function decodeField( } try { - return await codec.decode(wireValue, cellCtx); + return codec.decode(wireValue, cellCtx); } catch (error) { // Any structured envelope (dotted `code` per `isStructuredError`) is // stable by construction — let it pass through unchanged. This covers @@ -291,22 +287,12 @@ async function decodeField( } } -/** - * Decodes a row by dispatching all per-cell codec calls concurrently via `Promise.all`. Each cell follows the single-armed `decodeField` path. Structured envelopes thrown by codec bodies (anything passing `isStructuredError`) pass through unchanged; all other failures are wrapped in `RUNTIME.DECODE_FAILED` with `{ table, column, codec }` (or `{ alias, codec }` when no column ref is resolvable) and the original error attached on `cause`. - * - * When `rowCtx.signal` is provided: - * - * - **Already-aborted at entry** short-circuits with `RUNTIME.ABORTED` (`{ phase: 'decode' }`) before any `codec.decode` call is made. - * - **Mid-flight aborts** race the per-cell `Promise.all` against the signal so the runtime returns promptly even when codec bodies ignore it. In-flight bodies that ignore the signal complete in the background (cooperative cancellation). - * - Existing structured envelopes (any dotted-code error passing `isStructuredError`, e.g. `RUNTIME.DECODE_FAILED`) from codec bodies pass through unchanged (no double wrap). - */ -export async function decodeRow( +export function decodeRow( row: Record, decodeCtx: DecodeContext, rowCtx: SqlCodecCallContext, -): Promise> { +): Record { checkAborted(rowCtx, 'decode'); - const signal = rowCtx.signal; const aliases = decodeCtx.aliases ?? Object.keys(row); @@ -332,31 +318,12 @@ export async function decodeRow( } } - const tasks: Promise[] = []; - const includeIndices: { index: number; alias: string; value: unknown }[] = []; - - for (let i = 0; i < aliases.length; i++) { - const alias = aliases[i] as string; - const wireValue = row[alias]; - - if (decodeCtx.includeAliases.has(alias)) { - includeIndices.push({ index: i, alias, value: wireValue }); - tasks.push(Promise.resolve(undefined)); - continue; - } - - tasks.push(decodeField(alias, wireValue, decodeCtx, rowCtx)); - } - - const settled = await raceAgainstAbort(Promise.all(tasks), signal, 'decode'); - - for (const entry of includeIndices) { - settled[entry.index] = decodeIncludeAggregate(entry.alias, entry.value); - } - const decoded: Record = {}; - for (let i = 0; i < aliases.length; i++) { - decoded[aliases[i] as string] = settled[i]; + for (const alias of aliases) { + const wireValue = row[alias]; + decoded[alias] = decodeCtx.includeAliases.has(alias) + ? decodeIncludeAggregate(alias, wireValue) + : decodeField(alias, wireValue, decodeCtx, rowCtx); } return decoded; } diff --git a/packages/2-sql/5-runtime/src/sql-runtime.ts b/packages/2-sql/5-runtime/src/sql-runtime.ts index 89447ce8bc4b..79ff466dee3c 100644 --- a/packages/2-sql/5-runtime/src/sql-runtime.ts +++ b/packages/2-sql/5-runtime/src/sql-runtime.ts @@ -404,7 +404,7 @@ export abstract class SqlRuntimeBase = Co if (next.done) { break; } - const decodedRow = await decodeRow(next.value, decodeContext, codecCtx); + const decodedRow = decodeRow(next.value, decodeContext, codecCtx); yield blindCast(decodedRow); } } finally { diff --git a/packages/2-sql/5-runtime/test/raw-query-decode.test.ts b/packages/2-sql/5-runtime/test/raw-query-decode.test.ts index 55acd5eb390d..1901e747c49e 100644 --- a/packages/2-sql/5-runtime/test/raw-query-decode.test.ts +++ b/packages/2-sql/5-runtime/test/raw-query-decode.test.ts @@ -44,8 +44,8 @@ describe('raw-query decode context', () => { }); describe('raw-query row decoding', () => { - it('decodes each column through the codec its spec declares', async () => { - const decoded = await decodeRow( + it('decodes each column synchronously through the codec its spec declares', () => { + const decoded = decodeRow( { id: 4, email: 'a@b.example' }, buildDecodeContext(rowsAst, contractCodecs), {}, @@ -74,17 +74,19 @@ describe('raw-query row decoding', () => { expect(decoded).toEqual({ id: 40, email: 'decoded:a@b.example' }); }); - it('raises RUNTIME.RAW_ROW_COLUMN_MISSING when the result omits a declared column', async () => { - await expect( + it('raises RUNTIME.RAW_ROW_COLUMN_MISSING when the result omits a declared column', () => { + expect(() => decodeRow({ id: 4 }, buildDecodeContext(rowsAst, contractCodecs), {}), - ).rejects.toMatchObject({ - code: 'RUNTIME.RAW_ROW_COLUMN_MISSING', - details: { - column: 'email', - declaredColumns: ['id', 'email'], - resultColumns: ['id'], - }, - }); + ).toThrowError( + expect.objectContaining({ + code: 'RUNTIME.RAW_ROW_COLUMN_MISSING', + details: { + column: 'email', + declaredColumns: ['id', 'email'], + resultColumns: ['id'], + }, + }), + ); }); it('leaves an affected-count row untouched', async () => { diff --git a/packages/2-sql/5-runtime/test/test-codec.ts b/packages/2-sql/5-runtime/test/test-codec.ts index 931966916b6a..ee3bc2bfd74a 100644 --- a/packages/2-sql/5-runtime/test/test-codec.ts +++ b/packages/2-sql/5-runtime/test/test-codec.ts @@ -1,7 +1,7 @@ /** * Test-only helper that constructs a SQL-family `Codec` instance from author-side encode/decode functions. Replaces the legacy public `mkCodec()` factory (deleted under TML-2357); tests that need a stub codec for behavioural assertions instantiate one through this helper rather than going through `descriptor.factory(...)`. * - * The body is identical in spirit to the retired `mkCodec`: promise-lift sync author functions onto the framework-required `Promise<…>` boundary, default `encodeJson`/`decodeJson` to identity when `TInput` is JSON-safe, fail loudly otherwise. + * The body is identical in spirit to the retired `mkCodec`: promise-lift encoding and preserve synchronous decoding, default `encodeJson`/`decodeJson` to identity when `TInput` is JSON-safe, fail loudly otherwise. */ import type { JsonValue } from '@internal/contract/types'; import type { CodecTrait } from '@internal/framework-components/codec'; @@ -27,7 +27,7 @@ export function defineTestCodec< typeId: Id; targetTypes?: readonly string[]; encode: (value: TInput, ctx: SqlCodecCallContext) => TWire | Promise; - decode: (wire: TWire, ctx: SqlCodecCallContext) => TInput | Promise; + decode: (wire: TWire, ctx: SqlCodecCallContext) => TInput; traits?: TTraits; } & JsonRoundTripConfig, ): Codec { @@ -47,13 +47,7 @@ export function defineTestCodec< return Promise.reject(error); } }, - decode: (wire, ctx) => { - try { - return Promise.resolve(userDecode(wire, ctx)); - } catch (error) { - return Promise.reject(error); - } - }, + decode: userDecode, encodeJson: (widenedConfig.encodeJson ?? identity) as (value: TInput) => JsonValue, decodeJson: (widenedConfig.decodeJson ?? identity) as (json: JsonValue) => TInput, } as Codec; diff --git a/packages/3-extensions/arktype-json/src/core/arktype-json-codec.ts b/packages/3-extensions/arktype-json/src/core/arktype-json-codec.ts index ae36f015d0f0..b3d33a445662 100644 --- a/packages/3-extensions/arktype-json/src/core/arktype-json-codec.ts +++ b/packages/3-extensions/arktype-json/src/core/arktype-json-codec.ts @@ -191,7 +191,7 @@ export class ArktypeJsonCodecClass extends CodecImpl< return serializeWire(value); } - async decode(wire: string | JsonValue, _ctx: CodecCallContext): Promise { + decode(wire: string | JsonValue, _ctx: CodecCallContext): TInferred { return decodeWireValue(this.schema, wire); } diff --git a/packages/3-extensions/pgvector/src/core/codecs.ts b/packages/3-extensions/pgvector/src/core/codecs.ts index 0a43e3e58d07..631fb16da3f4 100644 --- a/packages/3-extensions/pgvector/src/core/codecs.ts +++ b/packages/3-extensions/pgvector/src/core/codecs.ts @@ -123,7 +123,7 @@ export class PgVectorCodec extends CodecImpl< return `[${value.join(',')}]`; } - async decode(wire: string, _ctx: CodecCallContext): Promise { + decode(wire: string, _ctx: CodecCallContext): number[] { if (typeof wire !== 'string') { throw pgVectorError('RUNTIME.DECODE_FAILED', 'Vector wire value must be a string', { meta: { codecId: VECTOR_CODEC_ID }, diff --git a/packages/3-extensions/postgis/src/core/codecs.ts b/packages/3-extensions/postgis/src/core/codecs.ts index 5dd23059972a..bf29fe4ca1e5 100644 --- a/packages/3-extensions/postgis/src/core/codecs.ts +++ b/packages/3-extensions/postgis/src/core/codecs.ts @@ -118,7 +118,7 @@ export class PostgisGeometryCodec extends CodecImpl< return encodeEWKT(value); } - async decode(wire: string, _ctx: CodecCallContext): Promise { + decode(wire: string, _ctx: CodecCallContext): Geometry { if (typeof wire !== 'string') { throw postgisError('RUNTIME.DECODE_FAILED', 'Geometry wire value must be a string', { meta: { codecId: POSTGIS_GEOMETRY_CODEC_ID }, diff --git a/packages/3-targets/3-targets/postgres/src/core/codecs.ts b/packages/3-targets/3-targets/postgres/src/core/codecs.ts index 8146655ad5ad..1446a558eaa7 100644 --- a/packages/3-targets/3-targets/postgres/src/core/codecs.ts +++ b/packages/3-targets/3-targets/postgres/src/core/codecs.ts @@ -314,7 +314,7 @@ export class PgTextCodec extends CodecImpl< async encode(value: string, _ctx: CodecCallContext): Promise { return value; } - async decode(wire: string, _ctx: CodecCallContext): Promise { + decode(wire: string, _ctx: CodecCallContext): string { return wire; } encodeJson(value: string): JsonValue { @@ -379,7 +379,7 @@ export class PgEnumCodec extends CodecImpl< async encode(value: string, _ctx: CodecCallContext): Promise { return value; } - async decode(wire: string, _ctx: CodecCallContext): Promise { + decode(wire: string, _ctx: CodecCallContext): string { return wire; } encodeJson(value: string): JsonValue { @@ -516,7 +516,7 @@ export class PgTextArrayCodec extends CodecImpl< async encode(value: readonly string[], _ctx: CodecCallContext): Promise { return value; } - async decode(wire: readonly string[], _ctx: CodecCallContext): Promise { + decode(wire: readonly string[], _ctx: CodecCallContext): readonly string[] { return wire; } encodeJson(value: readonly string[]): JsonValue { @@ -554,7 +554,7 @@ export class PgInt4Codec extends CodecImpl< async encode(value: number, _ctx: CodecCallContext): Promise { return value; } - async decode(wire: number, _ctx: CodecCallContext): Promise { + decode(wire: number, _ctx: CodecCallContext): number { return wire; } encodeJson(value: number): JsonValue { @@ -603,7 +603,7 @@ export class PgInt2Codec extends CodecImpl< async encode(value: number, _ctx: CodecCallContext): Promise { return value; } - async decode(wire: number, _ctx: CodecCallContext): Promise { + decode(wire: number, _ctx: CodecCallContext): number { return wire; } encodeJson(value: number): JsonValue { @@ -658,7 +658,7 @@ export class PgInt8Codec extends CodecImpl< async encode(value: bigint, _ctx: CodecCallContext): Promise { return pgBigintEncode(PG_INT8_CODEC_ID, value); } - async decode(wire: string | number | bigint, _ctx: CodecCallContext): Promise { + decode(wire: string | number | bigint, _ctx: CodecCallContext): bigint { return pgInt8Decode(wire); } encodeJson(value: bigint): JsonValue { @@ -721,7 +721,7 @@ export class PgInt8NumberCodec extends CodecImpl< async encode(value: number, _ctx: CodecCallContext): Promise { return pgInt8NumberEncode(value); } - async decode(wire: string | number | bigint, _ctx: CodecCallContext): Promise { + decode(wire: string | number | bigint, _ctx: CodecCallContext): number { return pgInt8NumberDecode(wire); } encodeJson(value: number): JsonValue { @@ -768,7 +768,7 @@ export class PgFloat4Codec extends CodecImpl< async encode(value: number, _ctx: CodecCallContext): Promise { return value; } - async decode(wire: number, _ctx: CodecCallContext): Promise { + decode(wire: number, _ctx: CodecCallContext): number { return wire; } encodeJson(value: number): JsonValue { @@ -817,7 +817,7 @@ export class PgFloat8Codec extends CodecImpl< async encode(value: number, _ctx: CodecCallContext): Promise { return value; } - async decode(wire: number, _ctx: CodecCallContext): Promise { + decode(wire: number, _ctx: CodecCallContext): number { return wire; } encodeJson(value: number): JsonValue { @@ -866,7 +866,7 @@ export class PgBoolCodec extends CodecImpl< async encode(value: boolean, _ctx: CodecCallContext): Promise { return value; } - async decode(wire: boolean, _ctx: CodecCallContext): Promise { + decode(wire: boolean, _ctx: CodecCallContext): boolean { return wire; } encodeJson(value: boolean): JsonValue { @@ -913,7 +913,7 @@ export class PgNumericCodec extends CodecImpl< async encode(value: string, _ctx: CodecCallContext): Promise { return value; } - async decode(wire: string | number, _ctx: CodecCallContext): Promise { + decode(wire: string | number, _ctx: CodecCallContext): string { return pgNumericDecode(wire); } encodeJson(value: string): JsonValue { @@ -980,7 +980,7 @@ export class PgUnboundedIntCodec extends CodecImpl< async encode(value: bigint, _ctx: CodecCallContext): Promise { return pgBigintEncode(PG_UNBOUNDED_INT_CODEC_ID, value); } - async decode(wire: string | number | bigint, _ctx: CodecCallContext): Promise { + decode(wire: string | number | bigint, _ctx: CodecCallContext): bigint { return pgUnboundedIntDecode(wire); } encodeJson(value: bigint): JsonValue { @@ -1039,7 +1039,7 @@ export class PgTimetzCodec extends CodecImpl< async encode(value: string, _ctx: CodecCallContext): Promise { return value; } - async decode(wire: string, _ctx: CodecCallContext): Promise { + decode(wire: string, _ctx: CodecCallContext): string { return wire; } encodeJson(value: string): JsonValue { @@ -1089,7 +1089,7 @@ export class PgBitCodec extends CodecImpl< async encode(value: string, _ctx: CodecCallContext): Promise { return value; } - async decode(wire: string, _ctx: CodecCallContext): Promise { + decode(wire: string, _ctx: CodecCallContext): string { return wire; } encodeJson(value: string): JsonValue { @@ -1138,7 +1138,7 @@ export class PgVarbitCodec extends CodecImpl< async encode(value: string, _ctx: CodecCallContext): Promise { return value; } - async decode(wire: string, _ctx: CodecCallContext): Promise { + decode(wire: string, _ctx: CodecCallContext): string { return wire; } encodeJson(value: string): JsonValue { @@ -1187,7 +1187,7 @@ export class PgByteaCodec extends CodecImpl< async encode(value: Uint8Array, _ctx: CodecCallContext): Promise { return value; } - async decode(wire: Uint8Array, _ctx: CodecCallContext): Promise { + decode(wire: Uint8Array, _ctx: CodecCallContext): Uint8Array { // Postgres node drivers commonly return Buffer instances (which extend Uint8Array) — normalize to a plain Uint8Array view so engine-agnostic consumers don't accidentally observe Buffer-specific APIs. return wire instanceof Uint8Array && wire.constructor === Uint8Array ? wire @@ -1236,7 +1236,7 @@ export class PgUuidCodec extends CodecImpl< async encode(value: string, _ctx: CodecCallContext): Promise { return value; } - async decode(wire: string, _ctx: CodecCallContext): Promise { + decode(wire: string, _ctx: CodecCallContext): string { return wire; } encodeJson(value: string): JsonValue { @@ -1282,7 +1282,7 @@ export class PgInetCodec extends CodecImpl< async encode(value: string, _ctx: CodecCallContext): Promise { return value; } - async decode(wire: string, _ctx: CodecCallContext): Promise { + decode(wire: string, _ctx: CodecCallContext): string { return wire; } encodeJson(value: string): JsonValue { @@ -1345,10 +1345,7 @@ export class PgIntervalCodec extends CodecImpl< // canonical rendering doubles as the wire form. return pgIntervalToIso(value); } - async decode( - wire: string | Record, - _ctx: CodecCallContext, - ): Promise { + decode(wire: string | Record, _ctx: CodecCallContext): PgInterval { return pgIntervalDecode(wire); } encodeJson(value: PgInterval): JsonValue { @@ -1396,7 +1393,7 @@ export class PgJsonCodec extends CodecImpl< async encode(value: JsonValue, _ctx: CodecCallContext): Promise { return pgJsonEncode(value); } - async decode(wire: string | JsonValue, _ctx: CodecCallContext): Promise { + decode(wire: string | JsonValue, _ctx: CodecCallContext): JsonValue { return pgJsonDecode(wire); } encodeJson(value: JsonValue): JsonValue { @@ -1440,7 +1437,7 @@ export class PgJsonbCodec extends CodecImpl< async encode(value: JsonValue, _ctx: CodecCallContext): Promise { return pgJsonbEncode(value); } - async decode(wire: string | JsonValue, _ctx: CodecCallContext): Promise { + decode(wire: string | JsonValue, _ctx: CodecCallContext): JsonValue { return pgJsonbDecode(wire); } encodeJson(value: JsonValue): JsonValue { diff --git a/packages/3-targets/3-targets/postgres/src/core/date-codecs.ts b/packages/3-targets/3-targets/postgres/src/core/date-codecs.ts index a981fa4a851c..b1e10df50605 100644 --- a/packages/3-targets/3-targets/postgres/src/core/date-codecs.ts +++ b/packages/3-targets/3-targets/postgres/src/core/date-codecs.ts @@ -105,7 +105,7 @@ export class PgTimestamptzDateCodec extends CodecImpl< async encode(value: Date, _ctx: CodecCallContext): Promise { return encodeDate(value); } - async decode(wire: string, _ctx: CodecCallContext): Promise { + decode(wire: string, _ctx: CodecCallContext): Date { return decodeDate(wire); } encodeJson(value: Date): JsonValue { diff --git a/packages/3-targets/3-targets/postgres/src/core/temporal-codecs.ts b/packages/3-targets/3-targets/postgres/src/core/temporal-codecs.ts index 42b8bf307102..78875be926f5 100644 --- a/packages/3-targets/3-targets/postgres/src/core/temporal-codecs.ts +++ b/packages/3-targets/3-targets/postgres/src/core/temporal-codecs.ts @@ -43,7 +43,7 @@ export class PgDateTemporalCodec extends CodecImpl< async encode(value: Temporal.PlainDate, _ctx: CodecCallContext): Promise { return pgDateTemporalEncode(value); } - async decode(wire: string, _ctx: CodecCallContext): Promise { + decode(wire: string, _ctx: CodecCallContext): Temporal.PlainDate { return pgDateTemporalDecode(wire); } encodeJson(value: Temporal.PlainDate): JsonValue { @@ -89,7 +89,7 @@ export class PgTimestampTemporalCodec extends CodecImpl< async encode(value: Temporal.PlainDateTime, _ctx: CodecCallContext): Promise { return pgTimestampTemporalEncode(value); } - async decode(wire: string, _ctx: CodecCallContext): Promise { + decode(wire: string, _ctx: CodecCallContext): Temporal.PlainDateTime { return pgTimestampTemporalDecode(wire); } encodeJson(value: Temporal.PlainDateTime): JsonValue { @@ -145,7 +145,7 @@ export class PgTimestamptzTemporalCodec extends CodecImpl< async encode(value: Temporal.Instant, _ctx: CodecCallContext): Promise { return pgTimestamptzTemporalEncode(value); } - async decode(wire: string, _ctx: CodecCallContext): Promise { + decode(wire: string, _ctx: CodecCallContext): Temporal.Instant { return pgTimestamptzTemporalDecode(wire); } encodeJson(value: Temporal.Instant): JsonValue { @@ -201,7 +201,7 @@ export class PgTimeTemporalCodec extends CodecImpl< async encode(value: Temporal.PlainTime, _ctx: CodecCallContext): Promise { return pgTimeTemporalEncode(value); } - async decode(wire: string, _ctx: CodecCallContext): Promise { + decode(wire: string, _ctx: CodecCallContext): Temporal.PlainTime { return pgTimeTemporalDecode(wire); } encodeJson(value: Temporal.PlainTime): JsonValue { diff --git a/packages/3-targets/3-targets/postgres/src/core/temporal-string-codecs.ts b/packages/3-targets/3-targets/postgres/src/core/temporal-string-codecs.ts index 243d6ff5e68e..17af628db80b 100644 --- a/packages/3-targets/3-targets/postgres/src/core/temporal-string-codecs.ts +++ b/packages/3-targets/3-targets/postgres/src/core/temporal-string-codecs.ts @@ -35,7 +35,7 @@ export class PgDateStringCodec extends CodecImpl< async encode(value: string, _ctx: CodecCallContext): Promise { return value; } - async decode(wire: string, _ctx: CodecCallContext): Promise { + decode(wire: string, _ctx: CodecCallContext): string { return wire; } encodeJson(value: string): JsonValue { @@ -81,7 +81,7 @@ export class PgTimestampStringCodec extends CodecImpl< async encode(value: string, _ctx: CodecCallContext): Promise { return value; } - async decode(wire: string, _ctx: CodecCallContext): Promise { + decode(wire: string, _ctx: CodecCallContext): string { return wire; } encodeJson(value: string): JsonValue { @@ -139,7 +139,7 @@ export class PgTimestamptzStringCodec extends CodecImpl< async encode(value: string, _ctx: CodecCallContext): Promise { return value; } - async decode(wire: string, _ctx: CodecCallContext): Promise { + decode(wire: string, _ctx: CodecCallContext): string { return wire; } encodeJson(value: string): JsonValue { @@ -197,7 +197,7 @@ export class PgTimeStringCodec extends CodecImpl< async encode(value: string, _ctx: CodecCallContext): Promise { return value; } - async decode(wire: string, _ctx: CodecCallContext): Promise { + decode(wire: string, _ctx: CodecCallContext): string { return wire; } encodeJson(value: string): JsonValue { diff --git a/packages/3-targets/3-targets/sqlite/src/core/codecs.ts b/packages/3-targets/3-targets/sqlite/src/core/codecs.ts index 4ac2d0303760..4d1a40a7caa3 100644 --- a/packages/3-targets/3-targets/sqlite/src/core/codecs.ts +++ b/packages/3-targets/3-targets/sqlite/src/core/codecs.ts @@ -264,7 +264,7 @@ export class SqliteTextCodec extends CodecImpl< async encode(value: string, _ctx: CodecCallContext): Promise { return value; } - async decode(wire: string, _ctx: CodecCallContext): Promise { + decode(wire: string, _ctx: CodecCallContext): string { return wire; } encodeJson(value: string): JsonValue { @@ -305,7 +305,7 @@ export class SqliteIntegerCodec extends CodecImpl< async encode(value: number, _ctx: CodecCallContext): Promise { return value; } - async decode(wire: number, _ctx: CodecCallContext): Promise { + decode(wire: number, _ctx: CodecCallContext): number { return wire; } encodeJson(value: number): JsonValue { @@ -346,7 +346,7 @@ export class SqliteRealCodec extends CodecImpl< async encode(value: number, _ctx: CodecCallContext): Promise { return value; } - async decode(wire: number, _ctx: CodecCallContext): Promise { + decode(wire: number, _ctx: CodecCallContext): number { return wire; } encodeJson(value: number): JsonValue { @@ -396,7 +396,7 @@ export class SqliteBlobCodec extends CodecImpl< async encode(value: Uint8Array, _ctx: CodecCallContext): Promise { return value; } - async decode(wire: Uint8Array, _ctx: CodecCallContext): Promise { + decode(wire: Uint8Array, _ctx: CodecCallContext): Uint8Array { return wire; } encodeJson(value: Uint8Array): JsonValue { @@ -456,7 +456,7 @@ export class SqliteDatetimeCodec extends CodecImpl< async encode(value: Date, _ctx: CodecCallContext): Promise { return value.toISOString(); } - async decode(wire: string, _ctx: CodecCallContext): Promise { + decode(wire: string, _ctx: CodecCallContext): Date { return this.parseDate(wire); } encodeJson(value: Date): JsonValue { @@ -504,7 +504,7 @@ export class SqliteJsonCodec extends CodecImpl< async encode(value: JsonValue, _ctx: CodecCallContext): Promise { return JSON.stringify(value); } - async decode(wire: string | JsonValue, _ctx: CodecCallContext): Promise { + decode(wire: string | JsonValue, _ctx: CodecCallContext): JsonValue { return typeof wire === 'string' ? (JSON.parse(wire) as JsonValue) : wire; } encodeJson(value: JsonValue): JsonValue { @@ -554,7 +554,7 @@ export class SqliteBigintCodec extends CodecImpl< * integer — past ±(2^53 − 1) it has already rounded, and converting it would * mint a spuriously-exact `bigint` that need not equal the stored value. */ - async decode(wire: number | bigint | string, _ctx: CodecCallContext): Promise { + decode(wire: number | bigint | string, _ctx: CodecCallContext): bigint { if (typeof wire === 'number' && !Number.isSafeInteger(wire)) { throw sqliteError( 'RUNTIME.DECODE_FAILED', @@ -630,7 +630,7 @@ export class SqliteBigintNumberCodec extends CodecImpl< * `bigint`; a bigint (or decimal text) is range-checked exactly before any * conversion to `number`, so an out-of-range value throws rather than rounds. */ - async decode(wire: number | bigint | string, _ctx: CodecCallContext): Promise { + decode(wire: number | bigint | string, _ctx: CodecCallContext): number { if (typeof wire === 'number') return safeIntegerNumber(wire, 'RUNTIME.DECODE_FAILED'); if (typeof wire === 'string' && !DECIMAL_INTEGER.test(wire)) { throw sqliteError( From 26f8bc7c7313c62e7f1f8d0b4b1c85ec5d2e33cb Mon Sep 17 00:00:00 2001 From: Steven McClankerton Date: Tue, 15 Sep 2026 14:30:05 +0000 Subject: [PATCH 2/3] test: migrate codec coverage to synchronous decoding Signed-off-by: Steven McClankerton --- .../test/codec-call-context.types.test-d.ts | 10 +- .../test/codec-types.types.test-d.ts | 8 +- .../framework-components/test/codec.test.ts | 4 +- .../test/codec.types.test-d.ts | 4 +- .../test/control-stack.test.ts | 4 +- .../test/materialize-codec.test.ts | 4 +- .../psl-extension-block-validator.test.ts | 2 +- ...clarative-policy-select.round-trip.test.ts | 2 +- .../generic-extension-block-printer.test.ts | 4 +- .../mongo-codec/test/codecs.test-d.ts | 2 +- .../mongo-codec/test/codecs.test.ts | 14 +- .../test/interpreter.polymorphism.test.ts | 2 +- .../contract-psl/test/interpreter.test.ts | 2 +- .../test/enum-type.authoring.test.ts | 4 +- .../7-runtime/test/codecs/decoding.test.ts | 53 ++---- .../test/interpreter.enum.test.ts | 4 +- .../test/interpreter.no-check.test.ts | 2 +- ...ntract-builder.contract-definition.test.ts | 2 +- .../contract-builder.value-objects.test.ts | 2 +- .../test/enum-type.codec-encoding.test.ts | 4 +- .../emitter-hook.typeref-resolver.test.ts | 2 +- .../test/ast/codec-factory-ctx.test.ts | 8 +- .../test/ast/codec-factory.test.ts | 36 ++-- .../test/ast/codec-factory.types.test-d.ts | 27 +-- .../test/ast/sql-codecs.test.ts | 14 +- .../relational-core/test/ast/test-codec.ts | 10 +- .../test/typed-codec-flow.test-d.ts | 2 +- .../2-sql/5-runtime/test/codec-async.test.ts | 156 +++++++++--------- .../5-runtime/test/codec-decode-ctx.test.ts | 100 +++-------- .../test/decode-error-passthrough.test.ts | 36 ++-- .../5-runtime/test/scalar-list-codec.test.ts | 16 +- .../5-runtime/test/seeded-secret-codec.ts | 28 +--- .../5-runtime/test/sql-runtime-abort.test.ts | 81 +-------- .../2-sql/5-runtime/test/sql-runtime.test.ts | 2 +- .../test/authoring-entity-types.enum.test.ts | 4 +- .../test/arktype-json-codec.test.ts | 12 +- .../3-extensions/pgvector/test/codecs.test.ts | 15 +- .../pgvector/test/structured-errors.test.ts | 8 +- .../3-extensions/postgis/test/codecs.test.ts | 12 +- .../3-targets/postgres/test/codecs.test.ts | 26 +-- .../postgres/test/date-codec-wiring.test-d.ts | 2 +- .../postgres/test/date-codecs.test.ts | 4 +- .../integer-representation-codecs.test.ts | 72 ++++---- .../test/postgres-codec-descriptor.test-d.ts | 2 +- .../test/postgres-codec-descriptor.test.ts | 2 +- .../infer-psl-contract.enum-adoption.test.ts | 4 +- .../print-psl.top-level-blocks.test.ts | 2 +- .../test/psl-native-enum-authoring.test.ts | 2 +- .../postgres/test/psl-pg-enum-column.test.ts | 4 +- .../postgres/test/temporal-codecs.test.ts | 16 +- .../3-targets/sqlite/test/codecs.test.ts | 12 +- .../integer-representation-codecs.test.ts | 64 +++---- .../test/sqlite-codec-descriptor.test-d.ts | 2 +- .../test/sqlite-codec-descriptor.test.ts | 2 +- .../aggregate-defaults.integration.test.ts | 14 +- .../test/lower-to-execute-request.test.ts | 6 +- .../test/migrations/data-transform.test.ts | 2 +- ...r-list-codec-roundtrip.integration.test.ts | 18 +- ...mporal-codec-roundtrip.integration.test.ts | 14 +- .../6-adapters/postgres/test/test-codec.ts | 10 +- .../aggregate-defaults.integration.test.ts | 14 +- .../test/lower-to-execute-request.test.ts | 6 +- .../sqlite-codec-registry-composition.test.ts | 2 +- .../sqlite/test/structured-errors.test.ts | 2 +- .../instructions.md | 6 + .../sql-orm-client/connection-release.test.ts | 39 ++--- .../sql-orm-client/include-codecs.test.ts | 2 +- 67 files changed, 443 insertions(+), 610 deletions(-) diff --git a/packages/1-framework/1-core/framework-components/test/codec-call-context.types.test-d.ts b/packages/1-framework/1-core/framework-components/test/codec-call-context.types.test-d.ts index c2786b6db8ef..0a7b36d1c903 100644 --- a/packages/1-framework/1-core/framework-components/test/codec-call-context.types.test-d.ts +++ b/packages/1-framework/1-core/framework-components/test/codec-call-context.types.test-d.ts @@ -35,11 +35,11 @@ test('encode/decode call sites accept an explicit ctx (signal optional inside th type StringCodec = Codec<'demo/text@1', readonly [], string, string>; const encodeWithCtx = (c: StringCodec, v: string, ctx: CodecCallContext): Promise => c.encode(v, ctx); - const decodeWithCtx = (c: StringCodec, w: string, ctx: CodecCallContext): Promise => + const decodeWithCtx = (c: StringCodec, w: string, ctx: CodecCallContext): string => c.decode(w, ctx); // An empty ctx is legal — `signal` is the only field today and is optional inside the context shape. const encodeWithEmptyCtx = (c: StringCodec, v: string): Promise => c.encode(v, {}); - const decodeWithEmptyCtx = (c: StringCodec, w: string): Promise => c.decode(w, {}); + const decodeWithEmptyCtx = (c: StringCodec, w: string): string => c.decode(w, {}); void encodeWithCtx; void decodeWithCtx; void encodeWithEmptyCtx; @@ -68,9 +68,9 @@ test('encode return type is unconditionally Promise (no conditional types expectTypeOf>().toEqualTypeOf>(); }); -test('decode return type is unconditionally Promise (no conditional types)', () => { +test('decode return type is unconditionally TInput (no conditional types)', () => { type CodecA = Codec<'demo/a@1', readonly [], string, string>; type CodecB = Codec<'demo/b@1', readonly [], number, number>; - expectTypeOf>().toEqualTypeOf>(); - expectTypeOf>().toEqualTypeOf>(); + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); }); diff --git a/packages/1-framework/1-core/framework-components/test/codec-types.types.test-d.ts b/packages/1-framework/1-core/framework-components/test/codec-types.types.test-d.ts index cfe1e8a5ceae..5b92dfb4f591 100644 --- a/packages/1-framework/1-core/framework-components/test/codec-types.types.test-d.ts +++ b/packages/1-framework/1-core/framework-components/test/codec-types.types.test-d.ts @@ -10,11 +10,11 @@ test('encode is required and Promise-returning', () => { expectTypeOf().toExtend>(); }); -test('decode is required and Promise-returning', () => { +test('decode is required and synchronous', () => { expectTypeOf().toHaveProperty('decode'); expectTypeOf().toBeFunction(); type DecodeReturn = ReturnType; - expectTypeOf().toExtend>(); + expectTypeOf().toEqualTypeOf(); }); test('encodeJson is required and synchronous', () => { @@ -60,7 +60,7 @@ test('Codec carries four generics: encode TInput → TWire, decode TWire → TIn expectTypeOf[0]>().toEqualTypeOf(); expectTypeOf>().toExtend>(); expectTypeOf[0]>().toEqualTypeOf(); - expectTypeOf>().toExtend>(); + expectTypeOf>().toEqualTypeOf(); expectTypeOf[0]>().toEqualTypeOf(); expectTypeOf>().toEqualTypeOf(); }); @@ -70,5 +70,5 @@ test('TInput drives both write input and read output (no asymmetric output)', () expectTypeOf[0]>().toEqualTypeOf(); expectTypeOf>().toExtend>(); expectTypeOf[0]>().toEqualTypeOf(); - expectTypeOf>().toExtend>(); + expectTypeOf>().toEqualTypeOf(); }); diff --git a/packages/1-framework/1-core/framework-components/test/codec.test.ts b/packages/1-framework/1-core/framework-components/test/codec.test.ts index 98d9f0fddf21..b2eb82a4fd0b 100644 --- a/packages/1-framework/1-core/framework-components/test/codec.test.ts +++ b/packages/1-framework/1-core/framework-components/test/codec.test.ts @@ -23,7 +23,7 @@ class Int4FixtureCodec extends CodecImpl<'demo/int4@1', readonly ['equality'], n async encode(value: number, _ctx: CodecCallContext): Promise { return value; } - async decode(wire: number, _ctx: CodecCallContext): Promise { + decode(wire: number, _ctx: CodecCallContext): number { return wire; } encodeJson(value: number): JsonValue { @@ -70,7 +70,7 @@ class VectorFixtureCodec extends CodecImpl< async encode(value: number[], _ctx: CodecCallContext): Promise { return `[${value.join(',')}]`; } - async decode(wire: string, _ctx: CodecCallContext): Promise { + decode(wire: string, _ctx: CodecCallContext): number[] { return wire.slice(1, -1).split(',').map(Number); } encodeJson(value: number[]): JsonValue { diff --git a/packages/1-framework/1-core/framework-components/test/codec.types.test-d.ts b/packages/1-framework/1-core/framework-components/test/codec.types.test-d.ts index 2effb56a5b3f..a0086bc5ad7a 100644 --- a/packages/1-framework/1-core/framework-components/test/codec.types.test-d.ts +++ b/packages/1-framework/1-core/framework-components/test/codec.types.test-d.ts @@ -29,7 +29,7 @@ class Int4FixtureCodec extends CodecImpl<'demo/int4@1', readonly ['equality'], n async encode(value: number, _ctx: CodecCallContext): Promise { return value; } - async decode(wire: number, _ctx: CodecCallContext): Promise { + decode(wire: number, _ctx: CodecCallContext): number { return wire; } encodeJson(value: number): JsonValue { @@ -82,7 +82,7 @@ class VectorFixtureCodec extends CodecImpl< async encode(value: number[], _ctx: CodecCallContext): Promise { return `[${value.join(',')}]`; } - async decode(wire: string, _ctx: CodecCallContext): Promise { + decode(wire: string, _ctx: CodecCallContext): number[] { return wire.slice(1, -1).split(',').map(Number); } encodeJson(value: number[]): JsonValue { diff --git a/packages/1-framework/1-core/framework-components/test/control-stack.test.ts b/packages/1-framework/1-core/framework-components/test/control-stack.test.ts index b3a2df664ba6..2c03a421c677 100644 --- a/packages/1-framework/1-core/framework-components/test/control-stack.test.ts +++ b/packages/1-framework/1-core/framework-components/test/control-stack.test.ts @@ -1141,7 +1141,7 @@ describe('extractCodecLookup', () => { ({ id, encode: async (v: unknown) => v, - decode: async (v: unknown) => v, + decode: (v: unknown) => v, encodeJson: (v: unknown) => v, decodeJson: (j: unknown) => j, }) as unknown as Codec; @@ -1487,7 +1487,7 @@ describe('validateScalarTypeCodecIds', () => { ? { id, encode: async (v: unknown) => v, - decode: async (v: unknown) => v, + decode: (v: unknown) => v, encodeJson: (v: unknown) => v as JsonValue, decodeJson: (v: JsonValue) => v, } diff --git a/packages/1-framework/1-core/framework-components/test/materialize-codec.test.ts b/packages/1-framework/1-core/framework-components/test/materialize-codec.test.ts index f57fefef9e06..a646ea07c988 100644 --- a/packages/1-framework/1-core/framework-components/test/materialize-codec.test.ts +++ b/packages/1-framework/1-core/framework-components/test/materialize-codec.test.ts @@ -18,7 +18,7 @@ class Int4FixtureCodec extends CodecImpl<'demo/int4@1', readonly ['equality'], n async encode(value: number, _ctx: CodecCallContext): Promise { return value; } - async decode(wire: number, _ctx: CodecCallContext): Promise { + decode(wire: number, _ctx: CodecCallContext): number { return wire; } encodeJson(value: number): JsonValue { @@ -65,7 +65,7 @@ class VectorFixtureCodec extends CodecImpl< async encode(value: number[], _ctx: CodecCallContext): Promise { return `[${value.join(',')}]`; } - async decode(wire: string, _ctx: CodecCallContext): Promise { + decode(wire: string, _ctx: CodecCallContext): number[] { return wire.slice(1, -1).split(',').map(Number); } encodeJson(value: number[]): JsonValue { diff --git a/packages/1-framework/1-core/framework-components/test/psl-extension-block-validator.test.ts b/packages/1-framework/1-core/framework-components/test/psl-extension-block-validator.test.ts index 79588b7c42e7..2239d6364fab 100644 --- a/packages/1-framework/1-core/framework-components/test/psl-extension-block-validator.test.ts +++ b/packages/1-framework/1-core/framework-components/test/psl-extension-block-validator.test.ts @@ -63,7 +63,7 @@ class StubStringCodec extends CodecImpl<'stub/string@1', readonly ['textual'], s async encode(value: string, _ctx: CodecCallContext): Promise { return value; } - async decode(wire: string, _ctx: CodecCallContext): Promise { + decode(wire: string, _ctx: CodecCallContext): string { return wire; } encodeJson(value: string): JsonValue { diff --git a/packages/1-framework/2-authoring/psl-printer/test/declarative-policy-select.round-trip.test.ts b/packages/1-framework/2-authoring/psl-printer/test/declarative-policy-select.round-trip.test.ts index 929da3e37e3d..fa6b6ab54956 100644 --- a/packages/1-framework/2-authoring/psl-printer/test/declarative-policy-select.round-trip.test.ts +++ b/packages/1-framework/2-authoring/psl-printer/test/declarative-policy-select.round-trip.test.ts @@ -71,7 +71,7 @@ class FixturePolicyTextCodec extends CodecImpl< async encode(value: string, _ctx: CodecCallContext): Promise { return value; } - async decode(wire: string, _ctx: CodecCallContext): Promise { + decode(wire: string, _ctx: CodecCallContext): string { return wire; } encodeJson(value: string): JsonValue { diff --git a/packages/1-framework/2-authoring/psl-printer/test/generic-extension-block-printer.test.ts b/packages/1-framework/2-authoring/psl-printer/test/generic-extension-block-printer.test.ts index 3b42ce7e9b28..337e6ef5609c 100644 --- a/packages/1-framework/2-authoring/psl-printer/test/generic-extension-block-printer.test.ts +++ b/packages/1-framework/2-authoring/psl-printer/test/generic-extension-block-printer.test.ts @@ -60,7 +60,7 @@ class StubPolicyTextCodec extends CodecImpl< async encode(value: string, _ctx: CodecCallContext): Promise { return value; } - async decode(wire: string, _ctx: CodecCallContext): Promise { + decode(wire: string, _ctx: CodecCallContext): string { return wire; } encodeJson(value: string): JsonValue { @@ -222,7 +222,7 @@ describe('generic extension-block printer (P2)', () => { async encode(value: number, _ctx: CodecCallContext): Promise { return value; } - async decode(wire: number, _ctx: CodecCallContext): Promise { + decode(wire: number, _ctx: CodecCallContext): number { return wire; } // A quoted "42" or a bare 42 both decode to the number 42 … diff --git a/packages/2-mongo-family/1-foundation/mongo-codec/test/codecs.test-d.ts b/packages/2-mongo-family/1-foundation/mongo-codec/test/codecs.test-d.ts index c866433fedcc..e0374e5b1f37 100644 --- a/packages/2-mongo-family/1-foundation/mongo-codec/test/codecs.test-d.ts +++ b/packages/2-mongo-family/1-foundation/mongo-codec/test/codecs.test-d.ts @@ -20,5 +20,5 @@ test('MongoCodecInput extracts the JS application type used for both write input expectTypeOf>().toEqualTypeOf(); expectTypeOf[0]>().toEqualTypeOf(); - expectTypeOf>().toExtend>(); + expectTypeOf>().toEqualTypeOf(); }); diff --git a/packages/2-mongo-family/1-foundation/mongo-codec/test/codecs.test.ts b/packages/2-mongo-family/1-foundation/mongo-codec/test/codecs.test.ts index dce071c1b3ad..e7531e7b6e49 100644 --- a/packages/2-mongo-family/1-foundation/mongo-codec/test/codecs.test.ts +++ b/packages/2-mongo-family/1-foundation/mongo-codec/test/codecs.test.ts @@ -13,7 +13,7 @@ describe('mongoCodec()', () => { }); expect(codec.id).toBe('test/string@1'); - expect(await codec.decode('hello', {})).toBe('hello'); + expect(codec.decode('hello', {})).toBe('hello'); expect(await codec.encode('hello', {})).toBe('hello'); }); @@ -24,11 +24,11 @@ describe('mongoCodec()', () => { encode: (value: string) => value.toLowerCase(), }); - expect(await codec.decode('hello', {})).toBe('HELLO'); + expect(codec.decode('hello', {})).toBe('HELLO'); expect(await codec.encode('HELLO', {})).toBe('hello'); }); - it('lifts sync author functions to Promise-returning methods', () => { + it('keeps decode synchronous while lifting encode to a Promise', () => { const codec = mongoCodec({ typeId: 'test/sync@1', decode: (wire: string) => wire, @@ -37,18 +37,18 @@ describe('mongoCodec()', () => { const decoded = codec.decode('x', {}); const encoded = codec.encode('y', {}); - expect(typeof (decoded as { then?: unknown }).then).toBe('function'); + expect(decoded).toBe('x'); expect(typeof (encoded as { then?: unknown }).then).toBe('function'); }); - it('accepts async author functions and uses them directly', async () => { + it('accepts async encode alongside synchronous decode', async () => { const codec = mongoCodec({ typeId: 'test/async@1', - decode: async (wire: string) => `decoded:${wire}`, + decode: (wire: string) => `decoded:${wire}`, encode: async (value: string) => `encoded:${value}`, }); - expect(await codec.decode('a', {})).toBe('decoded:a'); + expect(codec.decode('a', {})).toBe('decoded:a'); expect(await codec.encode('b', {})).toBe('encoded:b'); }); }); diff --git a/packages/2-mongo-family/2-authoring/contract-psl/test/interpreter.polymorphism.test.ts b/packages/2-mongo-family/2-authoring/contract-psl/test/interpreter.polymorphism.test.ts index 642ec2902416..57ef8f7ec029 100644 --- a/packages/2-mongo-family/2-authoring/contract-psl/test/interpreter.polymorphism.test.ts +++ b/packages/2-mongo-family/2-authoring/contract-psl/test/interpreter.polymorphism.test.ts @@ -39,7 +39,7 @@ const mongoCodecLookup: CodecLookup = { return { id, encode: async (v: unknown) => v, - decode: async (w: unknown) => w, + decode: (w: unknown) => w, encodeJson: (v: unknown) => v, decodeJson: (j: unknown) => j, } as ReturnType; diff --git a/packages/2-mongo-family/2-authoring/contract-psl/test/interpreter.test.ts b/packages/2-mongo-family/2-authoring/contract-psl/test/interpreter.test.ts index e15c83e74323..09432874c973 100644 --- a/packages/2-mongo-family/2-authoring/contract-psl/test/interpreter.test.ts +++ b/packages/2-mongo-family/2-authoring/contract-psl/test/interpreter.test.ts @@ -63,7 +63,7 @@ const mongoCodecLookup: CodecLookup = { return { id, encode: async (v: unknown) => v, - decode: async (w: unknown) => w, + decode: (w: unknown) => w, encodeJson: (v: unknown) => v, decodeJson: (j: unknown) => j, } as ReturnType; diff --git a/packages/2-mongo-family/2-authoring/contract-ts/test/enum-type.authoring.test.ts b/packages/2-mongo-family/2-authoring/contract-ts/test/enum-type.authoring.test.ts index 8cfac545a001..5874b3f6ee91 100644 --- a/packages/2-mongo-family/2-authoring/contract-ts/test/enum-type.authoring.test.ts +++ b/packages/2-mongo-family/2-authoring/contract-ts/test/enum-type.authoring.test.ts @@ -25,7 +25,7 @@ const identityDescriptor = (id: string): AnyCodecDescriptor => ({ ({ id, encode: async (v: unknown) => v, - decode: async (v: unknown) => v, + decode: (v: unknown) => v, encodeJson: (v: unknown) => v, decodeJson: (j: unknown) => j, }) as unknown as Codec, @@ -273,7 +273,7 @@ describe('defineContract() — codec-encoded value set', () => { ({ id: 'test/upper@1', encode: async (v: unknown) => v, - decode: async (v: unknown) => v, + decode: (v: unknown) => v, encodeJson: (v: unknown) => (v as string).toUpperCase(), decodeJson: (j: unknown) => j, }) as unknown as Codec, diff --git a/packages/2-mongo-family/7-runtime/test/codecs/decoding.test.ts b/packages/2-mongo-family/7-runtime/test/codecs/decoding.test.ts index 93e0998b1d97..163356e78914 100644 --- a/packages/2-mongo-family/7-runtime/test/codecs/decoding.test.ts +++ b/packages/2-mongo-family/7-runtime/test/codecs/decoding.test.ts @@ -6,20 +6,6 @@ import { ObjectId } from 'mongodb'; import { describe, expect, it, vi } from 'vitest'; import { decodeMongoRow } from '../../src/codecs/decoding'; -function deferred(): { - promise: Promise; - resolve: (v: T) => void; - reject: (e: unknown) => void; -} { - let resolve!: (v: T) => void; - let reject!: (e: unknown) => void; - const promise = new Promise((res, rej) => { - resolve = res; - reject = rej; - }); - return { promise, resolve, reject }; -} - function registryWithDefaults(): MongoCodecRegistry { const registry = newMongoCodecRegistry(); registry.register( @@ -139,12 +125,12 @@ describe('decodeMongoRow', () => { }, }, }; - await expect( - decodeMongoRow({ tags: ['ok', 'bad'] }, shapeThrow, registry, 'col'), - ).rejects.toMatchObject({ - code: 'RUNTIME.DECODE_FAILED', - details: expect.objectContaining({ path: 'tags.1', collection: 'col' }), - }); + expect(() => decodeMongoRow({ tags: ['ok', 'bad'] }, shapeThrow, registry, 'col')).toThrowError( + expect.objectContaining({ + code: 'RUNTIME.DECODE_FAILED', + details: expect.objectContaining({ path: 'tags.1', collection: 'col' }), + }), + ); }); it('top-level non-object rows pass through unchanged', async () => { @@ -488,43 +474,38 @@ describe('decodeMongoRow', () => { } }); - it('dispatches all leaf decodes for one row via a single Promise.all', async () => { - const dA = deferred(); - const dB = deferred(); + it('traverses every leaf synchronously before returning the row', () => { const callOrder: string[] = []; const registry = newMongoCodecRegistry(); registry.register( mongoCodec({ - typeId: 'slow-a@1', + typeId: 'sync-a@1', encode: (v: string) => v, decode: (w: string) => { - callOrder.push('a-start'); - return dA.promise.then((s) => `${w}:${s}`); + callOrder.push('a'); + return `${w}:A2`; }, }), ); registry.register( mongoCodec({ - typeId: 'slow-b@1', + typeId: 'sync-b@1', encode: (v: string) => v, decode: (w: string) => { - callOrder.push('b-start'); - return dB.promise.then((s) => `${w}:${s}`); + callOrder.push('b'); + return `${w}:B2`; }, }), ); const shape: MongoResultShape = { kind: 'document', fields: { - a: { kind: 'leaf', codecId: 'slow-a@1', nullable: false }, - b: { kind: 'leaf', codecId: 'slow-b@1', nullable: false }, + a: { kind: 'leaf', codecId: 'sync-a@1', nullable: false }, + b: { kind: 'leaf', codecId: 'sync-b@1', nullable: false }, }, }; - const p = decodeMongoRow({ a: 'A', b: 'B' }, shape, registry, 'c'); - expect(callOrder).toEqual(['a-start', 'b-start']); - dB.resolve('B2'); - dA.resolve('A2'); - const out = await p; + const out = decodeMongoRow({ a: 'A', b: 'B' }, shape, registry, 'c'); + expect(callOrder).toEqual(['a', 'b']); expect(out).toEqual({ a: 'A:A2', b: 'B:B2' }); }); }); diff --git a/packages/2-sql/2-authoring/contract-psl/test/interpreter.enum.test.ts b/packages/2-sql/2-authoring/contract-psl/test/interpreter.enum.test.ts index 78da506ad6e2..ff0b350c7506 100644 --- a/packages/2-sql/2-authoring/contract-psl/test/interpreter.enum.test.ts +++ b/packages/2-sql/2-authoring/contract-psl/test/interpreter.enum.test.ts @@ -36,7 +36,7 @@ import { const textCodec: Codec = { id: 'pg/text@1', encode: async (v: unknown) => v, - decode: async (w: unknown) => w, + decode: (w: unknown) => w, encodeJson: (value) => value as never, decodeJson(json) { if (typeof json !== 'string') throw new Error(`expected string, got ${typeof json}`); @@ -47,7 +47,7 @@ const textCodec: Codec = { const int4Codec: Codec = { id: 'pg/int4@1', encode: async (v: unknown) => v, - decode: async (w: unknown) => w, + decode: (w: unknown) => w, encodeJson: (value) => value as never, decodeJson(json) { if (typeof json !== 'number') throw new Error(`expected number, got ${typeof json}`); diff --git a/packages/2-sql/2-authoring/contract-psl/test/interpreter.no-check.test.ts b/packages/2-sql/2-authoring/contract-psl/test/interpreter.no-check.test.ts index 423296b063ca..40317c75de96 100644 --- a/packages/2-sql/2-authoring/contract-psl/test/interpreter.no-check.test.ts +++ b/packages/2-sql/2-authoring/contract-psl/test/interpreter.no-check.test.ts @@ -24,7 +24,7 @@ import { const textCodec: Codec = { id: 'pg/text@1', encode: async (v: unknown) => v, - decode: async (w: unknown) => w, + decode: (w: unknown) => w, encodeJson: (value) => value as never, decodeJson(json) { if (typeof json !== 'string') throw new Error(`expected string, got ${typeof json}`); diff --git a/packages/2-sql/2-authoring/contract-ts/test/contract-builder.contract-definition.test.ts b/packages/2-sql/2-authoring/contract-ts/test/contract-builder.contract-definition.test.ts index ffeb0c4140cd..73da929245da 100644 --- a/packages/2-sql/2-authoring/contract-ts/test/contract-builder.contract-definition.test.ts +++ b/packages/2-sql/2-authoring/contract-ts/test/contract-builder.contract-definition.test.ts @@ -196,7 +196,7 @@ describe('shared contract definition lowering', () => { return { id, encode: async (value: unknown) => value, - decode: async (wire: unknown) => wire, + decode: (wire: unknown) => wire, encodeJson: (value: unknown) => value instanceof Date ? value.toISOString() : (value as string), decodeJson: (json: unknown) => new Date(json as string), diff --git a/packages/2-sql/2-authoring/contract-ts/test/contract-builder.value-objects.test.ts b/packages/2-sql/2-authoring/contract-ts/test/contract-builder.value-objects.test.ts index 8f28f92b4473..8424e13cb7bd 100644 --- a/packages/2-sql/2-authoring/contract-ts/test/contract-builder.value-objects.test.ts +++ b/packages/2-sql/2-authoring/contract-ts/test/contract-builder.value-objects.test.ts @@ -35,7 +35,7 @@ describe('value objects in contract definition builder', () => { return { id, encode: async (value: unknown) => value, - decode: async (wire: unknown) => wire, + decode: (wire: unknown) => wire, encodeJson: (value: unknown) => { if (!isMoneyValue(value)) { throw new Error('Expected a Money value'); diff --git a/packages/2-sql/2-authoring/contract-ts/test/enum-type.codec-encoding.test.ts b/packages/2-sql/2-authoring/contract-ts/test/enum-type.codec-encoding.test.ts index 682cdc052c88..696e8a8600b7 100644 --- a/packages/2-sql/2-authoring/contract-ts/test/enum-type.codec-encoding.test.ts +++ b/packages/2-sql/2-authoring/contract-ts/test/enum-type.codec-encoding.test.ts @@ -30,7 +30,9 @@ function stubCodec(id: string, encodeJson: (value: unknown) => JsonValue): Codec encodeJson: encodeJson as Codec['encodeJson'], decodeJson: ((json: JsonValue) => json) as Codec['decodeJson'], encode: (() => Promise.reject(new Error('unused'))) as Codec['encode'], - decode: (() => Promise.reject(new Error('unused'))) as Codec['decode'], + decode: () => { + throw new Error('unused'); + }, }; } diff --git a/packages/2-sql/3-tooling/emitter/test/emitter-hook.typeref-resolver.test.ts b/packages/2-sql/3-tooling/emitter/test/emitter-hook.typeref-resolver.test.ts index bfc4967388ca..7bbfe94abd38 100644 --- a/packages/2-sql/3-tooling/emitter/test/emitter-hook.typeref-resolver.test.ts +++ b/packages/2-sql/3-tooling/emitter/test/emitter-hook.typeref-resolver.test.ts @@ -12,7 +12,7 @@ function vectorCodecLookup(): CodecLookup { const vectorCodec = { id: 'pg/vector@1', encode: async (v: unknown) => v, - decode: async (w: unknown) => w, + decode: (w: unknown) => w, encodeJson: (v: unknown) => v as never, decodeJson: (j: unknown) => j as never, } as ReturnType; diff --git a/packages/2-sql/4-lanes/relational-core/test/ast/codec-factory-ctx.test.ts b/packages/2-sql/4-lanes/relational-core/test/ast/codec-factory-ctx.test.ts index eff7a87a9c74..67df5f6e14df 100644 --- a/packages/2-sql/4-lanes/relational-core/test/ast/codec-factory-ctx.test.ts +++ b/packages/2-sql/4-lanes/relational-core/test/ast/codec-factory-ctx.test.ts @@ -33,7 +33,7 @@ describe('defineTestCodec() factory — SqlCodecCallContext arity', () => { expect(observed?.column).toEqual({ table: 'users', name: 'email' }); }); - it('forwards ctx (signal + column) to a `(value, ctx)` decode author', async () => { + it('forwards ctx (signal + column) to a `(value, ctx)` decode author', () => { let observed: SqlCodecCallContext | undefined; const c = defineTestCodec({ typeId: 'demo/ctx-decode@1', @@ -48,7 +48,7 @@ describe('defineTestCodec() factory — SqlCodecCallContext arity', () => { signal: controller.signal, column: { table: 'orders', name: 'total' }, }; - await c.decode('x', ctx); + expect(c.decode('x', ctx)).toBe('x'); expect(observed).toBe(ctx); expect(observed?.signal).toBe(controller.signal); expect(observed?.column).toEqual({ table: 'orders', name: 'total' }); @@ -93,7 +93,7 @@ describe('defineTestCodec() factory — SqlCodecCallContext arity', () => { expect(await c.encode('x', { signal: new AbortController().signal })).toBe('enc:x'); }); - it('a column-aware decode author observes ctx.column shape `{ table, name }`', async () => { + it('a column-aware decode author observes ctx.column shape `{ table, name }`', () => { let observedColumn: SqlCodecCallContext['column']; const c = defineTestCodec({ typeId: 'demo/column-aware@1', @@ -103,7 +103,7 @@ describe('defineTestCodec() factory — SqlCodecCallContext arity', () => { return wire; }, }); - await c.decode('x', { column: { table: 'users', name: 'email' } }); + expect(c.decode('x', { column: { table: 'users', name: 'email' } })).toBe('x'); expect(observedColumn).toEqual({ table: 'users', name: 'email' }); }); }); diff --git a/packages/2-sql/4-lanes/relational-core/test/ast/codec-factory.test.ts b/packages/2-sql/4-lanes/relational-core/test/ast/codec-factory.test.ts index 5e7881cac914..134f3728bf85 100644 --- a/packages/2-sql/4-lanes/relational-core/test/ast/codec-factory.test.ts +++ b/packages/2-sql/4-lanes/relational-core/test/ast/codec-factory.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from 'vitest'; import { defineTestCodec } from './test-codec'; -describe('defineTestCodec — query-time methods are Promise-returning', () => { +describe('defineTestCodec — async encoding and synchronous decoding', () => { it('lifts a sync encode into a Promise-returning method', async () => { const c = defineTestCodec({ typeId: 'demo/sync-encode@1', @@ -14,7 +14,7 @@ describe('defineTestCodec — query-time methods are Promise-returning', () => { expect(await encoded).toBe('HELLO'); }); - it('lifts a sync decode into a Promise-returning method', async () => { + it('preserves synchronous decoding', () => { const c = defineTestCodec({ typeId: 'demo/sync-decode@1', encode: (value: string) => value, @@ -22,8 +22,7 @@ describe('defineTestCodec — query-time methods are Promise-returning', () => { }); const decoded = c.decode('WORLD', {}); - expect(decoded).toBeInstanceOf(Promise); - expect(await decoded).toBe('world'); + expect(decoded).toBe('world'); }); it('accepts an async encode and produces a Promise-returning method', async () => { @@ -38,29 +37,17 @@ describe('defineTestCodec — query-time methods are Promise-returning', () => { expect(await encoded).toBe('HELLO'); }); - it('accepts an async decode and produces a Promise-returning method', async () => { + it('propagates decode errors synchronously', () => { + const error = new Error('invalid wire value'); const c = defineTestCodec({ - typeId: 'demo/async-decode@1', + typeId: 'demo/throwing-decode@1', encode: (value: string) => value, - decode: async (wire: string) => wire.toLowerCase(), + decode: (_wire: string): string => { + throw error; + }, }); - const decoded = c.decode('WORLD', {}); - expect(decoded).toBeInstanceOf(Promise); - expect(await decoded).toBe('world'); - }); - - it('accepts a mix of sync encode + async decode', async () => { - const c = defineTestCodec({ - typeId: 'demo/mixed-a@1', - encode: (value: string) => value, - decode: async (wire: string) => wire.toUpperCase(), - }); - - expect(c.encode!('a', {})).toBeInstanceOf(Promise); - expect(c.decode('a', {})).toBeInstanceOf(Promise); - expect(await c.encode!('a', {})).toBe('a'); - expect(await c.decode('a', {})).toBe('A'); + expect(() => c.decode('invalid', {})).toThrow(error); }); it('accepts a mix of async encode + sync decode', async () => { @@ -71,9 +58,8 @@ describe('defineTestCodec — query-time methods are Promise-returning', () => { }); expect(c.encode!('a', {})).toBeInstanceOf(Promise); - expect(c.decode('a', {})).toBeInstanceOf(Promise); expect(await c.encode!('a', {})).toBe('A'); - expect(await c.decode('a', {})).toBe('a'); + expect(c.decode('a', {})).toBe('a'); }); it('passes encodeJson and decodeJson through as synchronous methods', () => { diff --git a/packages/2-sql/4-lanes/relational-core/test/ast/codec-factory.types.test-d.ts b/packages/2-sql/4-lanes/relational-core/test/ast/codec-factory.types.test-d.ts index dc19ce2c45b7..db88a04ba3c9 100644 --- a/packages/2-sql/4-lanes/relational-core/test/ast/codec-factory.types.test-d.ts +++ b/packages/2-sql/4-lanes/relational-core/test/ast/codec-factory.types.test-d.ts @@ -1,7 +1,7 @@ import { expectTypeOf, test } from 'vitest'; import { defineTestCodec } from './test-codec'; -test('factory accepts sync encode and decode and produces Promise-returning methods', () => { +test('factory lifts sync encode and preserves sync decode', () => { const c = defineTestCodec({ typeId: 'demo/sync@1', encode: (value: string) => value, @@ -11,29 +11,16 @@ test('factory accepts sync encode and decode and produces Promise-returning meth expectTypeOf(c.encode).toBeFunction(); expectTypeOf(c.decode).toBeFunction(); expectTypeOf>>().toExtend>(); - expectTypeOf>().toExtend>(); + expectTypeOf>().toEqualTypeOf(); }); -test('factory accepts async encode and decode', () => { - const c = defineTestCodec({ - typeId: 'demo/async@1', - encode: async (value: string) => value, - decode: async (wire: string) => wire, - }); - - expectTypeOf>>().toExtend>(); - expectTypeOf>().toExtend>(); -}); - -test('factory accepts mixed sync encode + async decode', () => { - const c = defineTestCodec({ - typeId: 'demo/mixed-a@1', +test('factory rejects async decode', () => { + defineTestCodec<'demo/async-decode@1', readonly [], string, string>({ + typeId: 'demo/async-decode@1', encode: (value: string) => value, + // @ts-expect-error decode must return the input value synchronously. decode: async (wire: string) => wire, }); - - expectTypeOf>>().toExtend>(); - expectTypeOf>().toExtend>(); }); test('factory accepts mixed async encode + sync decode', () => { @@ -44,7 +31,7 @@ test('factory accepts mixed async encode + sync decode', () => { }); expectTypeOf>>().toExtend>(); - expectTypeOf>().toExtend>(); + expectTypeOf>().toEqualTypeOf(); }); test('factory rejects an omitted encode — the property is required', () => { diff --git a/packages/2-sql/4-lanes/relational-core/test/ast/sql-codecs.test.ts b/packages/2-sql/4-lanes/relational-core/test/ast/sql-codecs.test.ts index 0294d0a3f382..ca5a5884e6ee 100644 --- a/packages/2-sql/4-lanes/relational-core/test/ast/sql-codecs.test.ts +++ b/packages/2-sql/4-lanes/relational-core/test/ast/sql-codecs.test.ts @@ -32,7 +32,7 @@ describe('sql-codecs', () => { it('encodes and decodes string values', async () => { expect(await codec.encode('hello', callCtx)).toBe('hello'); - expect(await codec.decode('hello', callCtx)).toBe('hello'); + expect(codec.decode('hello', callCtx)).toBe('hello'); }); it('round-trips through JSON identity', () => { @@ -50,7 +50,7 @@ describe('sql-codecs', () => { it('encodes and decodes number values', async () => { expect(await codec.encode(42, callCtx)).toBe(42); - expect(await codec.decode(42, callCtx)).toBe(42); + expect(codec.decode(42, callCtx)).toBe(42); }); it('round-trips through JSON identity', () => { @@ -68,7 +68,7 @@ describe('sql-codecs', () => { it('encodes and decodes number values', async () => { expect(await codec.encode(3.14, callCtx)).toBe(3.14); - expect(await codec.decode(3.14, callCtx)).toBe(3.14); + expect(codec.decode(3.14, callCtx)).toBe(3.14); }); it('round-trips through JSON identity', () => { @@ -107,9 +107,9 @@ describe('sql-codecs', () => { expect(await codec.encode('user_001', callCtx)).toBe('user_001'); }); - it('trims trailing spaces on decode', async () => { - expect(await codec.decode('user_001 ', callCtx)).toBe('user_001'); - expect(await codec.decode('user_001', callCtx)).toBe('user_001'); + it('trims trailing spaces on decode', () => { + expect(codec.decode('user_001 ', callCtx)).toBe('user_001'); + expect(codec.decode('user_001', callCtx)).toBe('user_001'); }); it('round-trips through JSON identity', () => { @@ -135,7 +135,7 @@ describe('sql-codecs', () => { it('encodes and decodes string values verbatim', async () => { expect(await codec.encode('hello', callCtx)).toBe('hello'); - expect(await codec.decode('hello', callCtx)).toBe('hello'); + expect(codec.decode('hello', callCtx)).toBe('hello'); }); it('round-trips through JSON identity', () => { diff --git a/packages/2-sql/4-lanes/relational-core/test/ast/test-codec.ts b/packages/2-sql/4-lanes/relational-core/test/ast/test-codec.ts index 96488cabe24f..f074a4a58fdc 100644 --- a/packages/2-sql/4-lanes/relational-core/test/ast/test-codec.ts +++ b/packages/2-sql/4-lanes/relational-core/test/ast/test-codec.ts @@ -25,7 +25,7 @@ export function defineTestCodec< typeId: Id; targetTypes?: readonly string[]; encode: (value: TInput, ctx: SqlCodecCallContext) => TWire | Promise; - decode: (wire: TWire, ctx: SqlCodecCallContext) => TInput | Promise; + decode: (wire: TWire, ctx: SqlCodecCallContext) => TInput; traits?: TTraits; } & JsonRoundTripConfig, ): Codec { @@ -45,13 +45,7 @@ export function defineTestCodec< return Promise.reject(error); } }, - decode: (wire, ctx) => { - try { - return Promise.resolve(userDecode(wire, ctx)); - } catch (error) { - return Promise.reject(error); - } - }, + decode: (wire, ctx) => userDecode(wire, ctx), encodeJson: (widenedConfig.encodeJson ?? identity) as (value: TInput) => JsonValue, decodeJson: (widenedConfig.decodeJson ?? identity) as (json: JsonValue) => TInput, } as Codec; diff --git a/packages/2-sql/4-lanes/relational-core/test/typed-codec-flow.test-d.ts b/packages/2-sql/4-lanes/relational-core/test/typed-codec-flow.test-d.ts index da58e828835c..5f2d3ead6d7f 100644 --- a/packages/2-sql/4-lanes/relational-core/test/typed-codec-flow.test-d.ts +++ b/packages/2-sql/4-lanes/relational-core/test/typed-codec-flow.test-d.ts @@ -56,7 +56,7 @@ class TestVectorCodec extends CodecImpl<'test/vector@1', readonly ['equality'], async encode(value: number[], _ctx: CodecCallContext): Promise { return `[${value.join(',')}]`; } - async decode(_wire: string, _ctx: CodecCallContext): Promise { + decode(_wire: string, _ctx: CodecCallContext): number[] { return []; } encodeJson(value: number[]): JsonValue { diff --git a/packages/2-sql/5-runtime/test/codec-async.test.ts b/packages/2-sql/5-runtime/test/codec-async.test.ts index 287d190f6707..d84dd5e57d2b 100644 --- a/packages/2-sql/5-runtime/test/codec-async.test.ts +++ b/packages/2-sql/5-runtime/test/codec-async.test.ts @@ -173,7 +173,7 @@ describe('encodeParams — async, concurrent dispatch', () => { typeId: 'test/async@1', targetTypes: ['text'], encode: async (value: string) => `wire:${value}`, - decode: async (wire: string) => wire, + decode: (wire: string) => wire, }), ]; @@ -313,10 +313,8 @@ describe('encodeParams — async, concurrent dispatch', () => { // ============================================================================= decodeRow / decodeField: concurrent per-cell + envelope + JSON validation ============================================================================= -describe('decodeRow — async, concurrent per-cell dispatch', () => { - it('dispatches per-cell decoders concurrently via Promise.all', async () => { - const dA = deferred(); - const dB = deferred(); +describe('decodeRow — synchronous per-cell dispatch', () => { + it('decodes every cell synchronously in projection order', () => { const callOrder: string[] = []; const registry = [ @@ -326,7 +324,7 @@ describe('decodeRow — async, concurrent per-cell dispatch', () => { encode: (v: string) => v, decode: (w: string) => { callOrder.push('decode-a-start'); - return dA.promise.then((suffix) => `${w}:${suffix}`); + return `${w}:A-DEC`; }, }), defineTestCodec({ @@ -335,7 +333,7 @@ describe('decodeRow — async, concurrent per-cell dispatch', () => { encode: (v: string) => v, decode: (w: string) => { callOrder.push('decode-b-start'); - return dB.promise.then((suffix) => `${w}:${suffix}`); + return `${w}:B-DEC`; }, }), defineTestCodec({ @@ -358,7 +356,7 @@ describe('decodeRow — async, concurrent per-cell dispatch', () => { }); const row = { a: 'A', b: 'B', n: 21 }; - const promise = decodeRow( + const result = decodeRow( row, buildDecodeContext(plan.ast, buildTestContractCodecs(registry)), {}, @@ -366,20 +364,16 @@ describe('decodeRow — async, concurrent per-cell dispatch', () => { expect(callOrder).toEqual(['decode-a-start', 'decode-b-start', 'decode-sync']); - dB.resolve('B-DEC'); - dA.resolve('A-DEC'); - - const result = await promise; expect(result).toEqual({ a: 'A:A-DEC', b: 'B:B-DEC', n: 42 }); }); - it('always awaits codec.decode and yields plain values (no Promise leaks)', async () => { + it('returns decoded plain values synchronously', () => { const registry = [ defineTestCodec({ typeId: 'test/async@1', targetTypes: ['text'], encode: (v: string) => v, - decode: async (w: string) => `decoded:${w}`, + decode: (w: string) => `decoded:${w}`, }), ]; @@ -387,7 +381,7 @@ describe('decodeRow — async, concurrent per-cell dispatch', () => { projections: [{ alias: 'name', codecId: 'test/async@1' }], }); - const result = await decodeRow( + const result = decodeRow( { name: 'alice' }, buildDecodeContext(plan.ast, buildTestContractCodecs(registry)), {}, @@ -403,7 +397,7 @@ describe('decodeRow — async, concurrent per-cell dispatch', () => { typeId: 'pg/inline-validating-json@1', targetTypes: ['jsonb'], encode: (v: JsonValue) => JSON.stringify(v), - decode: async (w: string) => { + decode: (w: string) => { const parsed = JSON.parse(w) as Record; if (!('name' in parsed)) { throw runtimeError( @@ -427,23 +421,25 @@ describe('decodeRow — async, concurrent per-cell dispatch', () => { ], }); - const ok = await decodeRow( + const ok = decodeRow( { metadata: '{"name":"alice"}' }, buildDecodeContext(plan.ast, buildTestContractCodecs(registry)), {}, ); expect(ok['metadata']).toEqual({ name: 'alice' }); - const rejection = (await decodeRow( - { metadata: '{"age":30}' }, - buildDecodeContext(plan.ast, buildTestContractCodecs(registry)), - {}, - ).catch((e: unknown) => e)) as Error & { - code?: string; - details?: { issues?: unknown }; - }; - expect(rejection.code).toBe('RUNTIME.JSON_SCHEMA_VALIDATION_FAILED'); - expect(rejection.details?.issues).toBe("must have required property 'name'"); + expect(() => + decodeRow( + { metadata: '{"age":30}' }, + buildDecodeContext(plan.ast, buildTestContractCodecs(registry)), + {}, + ), + ).toThrowError( + expect.objectContaining({ + code: 'RUNTIME.JSON_SCHEMA_VALIDATION_FAILED', + details: { issues: "must have required property 'name'" }, + }), + ); }); it('wraps decode failures in RUNTIME.DECODE_FAILED with { table, column, codec } and cause', async () => { @@ -469,23 +465,26 @@ describe('decodeRow — async, concurrent per-cell dispatch', () => { ], }); - await expect( + expect(() => decodeRow( { explody: 'wire' }, buildDecodeContext(plan.ast, buildTestContractCodecs(registry)), {}, ), - ).rejects.toMatchObject({ - code: 'RUNTIME.DECODE_FAILED', - category: 'RUNTIME', - severity: 'error', - details: { - table: 'user', - column: 'payload', - codec: 'test/explody@1', - }, - cause, - }); + ).toThrowError( + expect.objectContaining({ + code: 'RUNTIME.DECODE_FAILED', + category: 'RUNTIME', + severity: 'error', + details: { + table: 'user', + column: 'payload', + codec: 'test/explody@1', + wirePreview: 'wire', + }, + cause, + }), + ); }); it('passes wire values through for raw plans (no AST, no codec decoding)', async () => { @@ -501,7 +500,7 @@ describe('decodeRow — async, concurrent per-cell dispatch', () => { ]; const plan = buildRawPlan(); - const result = await decodeRow( + const result = decodeRow( { id: 1, email: 'a@b.com' }, buildDecodeContext(plan.ast, buildTestContractCodecs(registry)), {}, @@ -515,16 +514,18 @@ describe('decodeRow — async, concurrent per-cell dispatch', () => { projections: [{ alias: 'id' }, { alias: 'email' }], }); - await expect( + expect(() => decodeRow({ id: 1 }, buildDecodeContext(plan.ast, buildTestContractCodecs(registry)), {}), - ).rejects.toMatchObject({ - code: 'RUNTIME.DECODE_FAILED', - details: { - alias: 'email', - expectedAliases: ['id', 'email'], - presentKeys: ['id'], - }, - }); + ).toThrowError( + expect.objectContaining({ + code: 'RUNTIME.DECODE_FAILED', + details: { + alias: 'email', + expectedAliases: ['id', 'email'], + presentKeys: ['id'], + }, + }), + ); }); it('preserves wire null for AST-backed plans (distinct from missing alias)', async () => { @@ -542,7 +543,7 @@ describe('decodeRow — async, concurrent per-cell dispatch', () => { projections: [{ alias: 'id', codecId: 'test/should-not-run@1' }], }); - const result = await decodeRow( + const result = decodeRow( { id: null }, buildDecodeContext(plan.ast, buildTestContractCodecs(registry)), {}, @@ -550,11 +551,11 @@ describe('decodeRow — async, concurrent per-cell dispatch', () => { expect(result).toEqual({ id: null }); }); - it('decodeField is single-armed: same path for sync and async codec authors', async () => { + it('decodes different column codecs through the same synchronous path', () => { const buildCodec = ( id: string, encode: (value: string) => string, - decode: (wire: string) => string | Promise, + decode: (wire: string) => string, ): Codec => defineTestCodec({ typeId: id, @@ -572,7 +573,7 @@ describe('decodeRow — async, concurrent per-cell dispatch', () => { buildCodec( 'async@1', (v) => v, - async (w) => `async:${String(w)}`, + (w) => `other:${String(w)}`, ), ]; @@ -583,12 +584,12 @@ describe('decodeRow — async, concurrent per-cell dispatch', () => { ], }); - const result = await decodeRow( + const result = decodeRow( { syncCol: 'a', asyncCol: 'b' }, buildDecodeContext(plan.ast, buildTestContractCodecs(registry)), {}, ); - expect(result).toEqual({ syncCol: 'sync:a', asyncCol: 'async:b' }); + expect(result).toEqual({ syncCol: 'sync:a', asyncCol: 'other:b' }); }); }); @@ -610,10 +611,10 @@ describe('seeded-secret-codec — realistic crypto path against the runtime', () const wire = result[0]; expect(typeof wire).toBe('string'); expect(wire).not.toBe('Alice'); - await expect(decryptSecret(wire as string, seed)).resolves.toBe('Alice'); + expect(decryptSecret(wire as string, seed)).toBe('Alice'); }); - it('decodeRow decrypts ciphertext via async codec.decode and yields plain values', { + it('decodeRow decrypts ciphertext synchronously and yields plain values', { timeout: timeouts.databaseOperation, }, async () => { const registry = [createAsyncSecretCodec({ typeId: 'pg/secret@1', seed })]; @@ -629,7 +630,7 @@ describe('seeded-secret-codec — realistic crypto path against the runtime', () ], }); - const result = await decodeRow( + const result = decodeRow( { secret: wire }, buildDecodeContext(plan.ast, buildTestContractCodecs(registry)), {}, @@ -637,7 +638,7 @@ describe('seeded-secret-codec — realistic crypto path against the runtime', () expect(result['secret']).toBe('top-secret'); }); - it('decode failures from async crypto are wrapped in RUNTIME.DECODE_FAILED with cause', async () => { + it('decode failures from synchronous crypto are wrapped in RUNTIME.DECODE_FAILED with cause', async () => { const registry = [createAsyncSecretCodec({ typeId: 'pg/secret@1', seed })]; const plan = buildAstPlan({ @@ -650,24 +651,23 @@ describe('seeded-secret-codec — realistic crypto path against the runtime', () ], }); - const rejection = await decodeRow( - { secret: 'bad-payload' }, - buildDecodeContext(plan.ast, buildTestContractCodecs(registry)), - {}, - ).catch((e: unknown) => e); - expect(rejection).toBeInstanceOf(Error); - const err = rejection as Error & { - code?: string; - details?: { table?: string; column?: string; codec?: string; wirePreview?: string }; - cause?: unknown; - }; - expect(err.code).toBe('RUNTIME.DECODE_FAILED'); - expect(err.details).toMatchObject({ - table: 'user', - column: 'secret', - codec: 'pg/secret@1', - wirePreview: 'bad-payload', - }); - expect((err.cause as Error | undefined)?.message).toBe('invalid secret payload'); + expect(() => + decodeRow( + { secret: 'bad-payload' }, + buildDecodeContext(plan.ast, buildTestContractCodecs(registry)), + {}, + ), + ).toThrowError( + expect.objectContaining({ + code: 'RUNTIME.DECODE_FAILED', + details: expect.objectContaining({ + table: 'user', + column: 'secret', + codec: 'pg/secret@1', + wirePreview: 'bad-payload', + }), + cause: expect.objectContaining({ message: 'invalid secret payload' }), + }), + ); }); }); diff --git a/packages/2-sql/5-runtime/test/codec-decode-ctx.test.ts b/packages/2-sql/5-runtime/test/codec-decode-ctx.test.ts index 177dc98722e5..b459ac6cb981 100644 --- a/packages/2-sql/5-runtime/test/codec-decode-ctx.test.ts +++ b/packages/2-sql/5-runtime/test/codec-decode-ctx.test.ts @@ -39,20 +39,6 @@ function columnProjection( return ProjectionItem.of(alias, ColumnRef.of(table, column), { codecId }); } -function deferred(): { - promise: Promise; - resolve: (v: T) => void; - reject: (e: unknown) => void; -} { - let resolve!: (v: T) => void; - let reject!: (e: unknown) => void; - const promise = new Promise((res, rej) => { - resolve = res; - reject = rej; - }); - return { promise, resolve, reject }; -} - describe('decodeRow — SqlCodecCallContext threading', () => { it('forwards a per-cell ctx whose signal is the same instance as the row-level ctx (signal identity preserved)', async () => { const observed: AbortSignal[] = []; @@ -75,7 +61,7 @@ describe('decodeRow — SqlCodecCallContext threading', () => { const controller = new AbortController(); const rowCtx: SqlCodecCallContext = { signal: controller.signal }; - await decodeRow( + decodeRow( { a: 'A', b: 'B' }, buildDecodeContext(p.ast, buildTestContractCodecs(registry)), rowCtx, @@ -105,7 +91,7 @@ describe('decodeRow — SqlCodecCallContext threading', () => { columnProjection('total', 'orders', 'total', 'test/observe-col@1'), ]); - await decodeRow( + decodeRow( { email: 'email', total: 'total' }, buildDecodeContext(p.ast, buildTestContractCodecs(registry)), { signal: new AbortController().signal }, @@ -135,11 +121,9 @@ describe('decodeRow — SqlCodecCallContext threading', () => { columnProjection('secret', 'user', 'secret', 'test/observe-projection@1'), ]); - await decodeRow( - { secret: 'wire' }, - buildDecodeContext(p.ast, buildTestContractCodecs(registry)), - { signal: new AbortController().signal }, - ); + decodeRow({ secret: 'wire' }, buildDecodeContext(p.ast, buildTestContractCodecs(registry)), { + signal: new AbortController().signal, + }); expect(observed?.column).toEqual({ table: 'user', name: 'secret' }); }); @@ -169,11 +153,7 @@ describe('decodeRow — SqlCodecCallContext threading', () => { column: { table: 'stale', name: 'stale' }, }; - await decodeRow( - { agg: '1' }, - buildDecodeContext(p.ast, buildTestContractCodecs(registry)), - rowCtx, - ); + decodeRow({ agg: '1' }, buildDecodeContext(p.ast, buildTestContractCodecs(registry)), rowCtx); expect(observed).toBeDefined(); expect(observed?.column).toBeUndefined(); @@ -202,7 +182,7 @@ describe('decodeRow — SqlCodecCallContext threading', () => { column: { table: 'stale', name: 'stale' }, }; - await decodeRow( + decodeRow( { computed: 'wire' }, buildDecodeContext(p.ast, buildTestContractCodecs(registry)), rowCtx, @@ -230,7 +210,7 @@ describe('decodeRow — SqlCodecCallContext threading', () => { const p = buildPlan([columnProjection('x', 'users', 'x', 'test/single-arg-author@1')]); - const result = await decodeRow( + const result = decodeRow( { x: 'wire' }, buildDecodeContext(p.ast, buildTestContractCodecs(registry)), {}, @@ -263,48 +243,18 @@ describe('decodeRow — SqlCodecCallContext threading', () => { const reason = new Error('decode short-circuit'); controller.abort(reason); - await expect( + expect(() => decodeRow({ a: '1', b: '2' }, buildDecodeContext(p.ast, buildTestContractCodecs(registry)), { signal: controller.signal, }), - ).rejects.toMatchObject({ - code: 'RUNTIME.ABORTED', - details: { phase: 'decode' }, - cause: reason, - }); - expect(callCount).toBe(0); - }); - - it('mid-decode abort surfaces RUNTIME.ABORTED { phase: decode } via abortable race', async () => { - const release = deferred(); - const registry = [ - defineTestCodec({ - typeId: 'test/blocking@1', - targetTypes: ['text'], - encode: (v: string) => v, - decode: (w: string) => release.promise.then((suffix) => `${w}:${suffix}`), + ).toThrowError( + expect.objectContaining({ + code: 'RUNTIME.ABORTED', + details: { phase: 'decode' }, + cause: reason, }), - ]; - - const p = buildPlan([columnProjection('x', 'users', 'x', 'test/blocking@1')]); - - const controller = new AbortController(); - const reason = new Error('mid-decode abort'); - const promise = decodeRow( - { x: 'wire' }, - buildDecodeContext(p.ast, buildTestContractCodecs(registry)), - { signal: controller.signal }, ); - - queueMicrotask(() => controller.abort(reason)); - - await expect(promise).rejects.toMatchObject({ - code: 'RUNTIME.ABORTED', - details: { phase: 'decode' }, - cause: reason, - }); - - release.resolve('done'); + expect(callCount).toBe(0); }); it('passes through RUNTIME.DECODE_FAILED unchanged when the codec body throws (no double-wrap)', async () => { @@ -322,14 +272,16 @@ describe('decodeRow — SqlCodecCallContext threading', () => { const p = buildPlan([columnProjection('x', 'users', 'x', 'test/explody@1')]); - await expect( + expect(() => decodeRow({ x: 'wire' }, buildDecodeContext(p.ast, buildTestContractCodecs(registry)), { signal: new AbortController().signal, }), - ).rejects.toMatchObject({ - code: 'RUNTIME.DECODE_FAILED', - cause, - }); + ).toThrowError( + expect.objectContaining({ + code: 'RUNTIME.DECODE_FAILED', + cause, + }), + ); }); it('reuses the existing per-cell ColumnRef resolution: the column passed to the codec matches the table/name used by RUNTIME.DECODE_FAILED for the same cell', async () => { @@ -349,11 +301,9 @@ describe('decodeRow — SqlCodecCallContext threading', () => { const p = buildPlan([columnProjection('email', 'users', 'email', 'test/recorder@1')]); - await decodeRow( - { email: 'wire' }, - buildDecodeContext(p.ast, buildTestContractCodecs(registry)), - { signal: new AbortController().signal }, - ); + decodeRow({ email: 'wire' }, buildDecodeContext(p.ast, buildTestContractCodecs(registry)), { + signal: new AbortController().signal, + }); // SqlColumnRef shape `{ table, name }` projected from the ColumnRef shape `{ table, column }` the resolver returns — same source, one resolution per cell. expect(observedColumns).toEqual([{ table: 'users', name: 'email' }]); diff --git a/packages/2-sql/5-runtime/test/decode-error-passthrough.test.ts b/packages/2-sql/5-runtime/test/decode-error-passthrough.test.ts index ebea74758939..6a1d37250869 100644 --- a/packages/2-sql/5-runtime/test/decode-error-passthrough.test.ts +++ b/packages/2-sql/5-runtime/test/decode-error-passthrough.test.ts @@ -50,13 +50,13 @@ describe('decodeRow — runtime-envelope passthrough', () => { }), ]; - await expect( + expect(() => decodeRow( { value: 'wire' }, buildDecodeContext(buildPlan().ast, buildTestContractCodecs(registry)), {}, ), - ).rejects.toBe(original); + ).toThrowError(expect.toSatisfy((error: unknown) => error === original)); }); it('rethrows codec-authored RUNTIME.ABORTED without wrapping', async () => { @@ -72,13 +72,13 @@ describe('decodeRow — runtime-envelope passthrough', () => { }), ]; - await expect( + expect(() => decodeRow( { value: 'wire' }, buildDecodeContext(buildPlan().ast, buildTestContractCodecs(registry)), {}, ), - ).rejects.toBe(original); + ).toThrowError(expect.toSatisfy((error: unknown) => error === original)); }); it('rethrows a plain structuredError DECODE_FAILED envelope without wrapping', async () => { @@ -96,13 +96,13 @@ describe('decodeRow — runtime-envelope passthrough', () => { }), ]; - await expect( + expect(() => decodeRow( { value: 'wire' }, buildDecodeContext(buildPlan().ast, buildTestContractCodecs(registry)), {}, ), - ).rejects.toBe(original); + ).toThrowError(expect.toSatisfy((error: unknown) => error === original)); }); it('rethrows a plain structuredError envelope from a many-element decode without wrapping', async () => { @@ -124,13 +124,13 @@ describe('decodeRow — runtime-envelope passthrough', () => { }), ]; - await expect( + expect(() => decodeRow( { value: ['wire'] }, buildDecodeContext(ast, buildTestContractCodecs(registry)), {}, ), - ).rejects.toBe(original); + ).toThrowError(expect.toSatisfy((error: unknown) => error === original)); }); it('wraps a foreign Error into RUNTIME.DECODE_FAILED with the original on cause', async () => { @@ -146,20 +146,22 @@ describe('decodeRow — runtime-envelope passthrough', () => { }), ]; - await expect( + expect(() => decodeRow( { value: 'wire' }, buildDecodeContext(buildPlan().ast, buildTestContractCodecs(registry)), {}, ), - ).rejects.toMatchObject({ - code: 'RUNTIME.DECODE_FAILED', - cause: original, - details: expect.objectContaining({ - table: 'users', - column: 'value', - codec: 'test/passthrough@1', + ).toThrowError( + expect.objectContaining({ + code: 'RUNTIME.DECODE_FAILED', + cause: original, + details: expect.objectContaining({ + table: 'users', + column: 'value', + codec: 'test/passthrough@1', + }), }), - }); + ); }); }); diff --git a/packages/2-sql/5-runtime/test/scalar-list-codec.test.ts b/packages/2-sql/5-runtime/test/scalar-list-codec.test.ts index ee5e1416128e..1e9699be5979 100644 --- a/packages/2-sql/5-runtime/test/scalar-list-codec.test.ts +++ b/packages/2-sql/5-runtime/test/scalar-list-codec.test.ts @@ -189,7 +189,7 @@ describe('decodeRow — many CodecRef via ProjectionItem', () => { const ast = buildPlan(true); const ctx = buildDecodeContext(ast, registry); - const result = await decodeRow({ vals: ['a', 'b', 'c'] }, ctx, CTX); + const result = decodeRow({ vals: ['a', 'b', 'c'] }, ctx, CTX); expect(result['vals']).toEqual(['DEC:a', 'DEC:b', 'DEC:c']); expect(calls).toEqual(['a', 'b', 'c']); @@ -209,7 +209,7 @@ describe('decodeRow — many CodecRef via ProjectionItem', () => { const ast = buildPlan(true); const ctx = buildDecodeContext(ast, registry); - const result = await decodeRow({ vals: ['x', null, 'z'] }, ctx, CTX); + const result = decodeRow({ vals: ['x', null, 'z'] }, ctx, CTX); expect(result['vals']).toEqual(['DEC:x', null, 'DEC:z']); expect(calls).toEqual(['x', 'z']); @@ -235,9 +235,11 @@ describe('decodeRow — many CodecRef via ProjectionItem', () => { ]); const ctx = buildDecodeContext(ast, registry); - await expect(decodeRow({ vals: [1, 2, 3] }, ctx, CTX)).rejects.toMatchObject({ - code: 'RUNTIME.DECODE_FAILED', - }); + expect(() => decodeRow({ vals: [1, 2, 3] }, ctx, CTX)).toThrowError( + expect.objectContaining({ + code: 'RUNTIME.DECODE_FAILED', + }), + ); }); it('returns null for the whole column when the wire value is null (not an array)', async () => { @@ -253,7 +255,7 @@ describe('decodeRow — many CodecRef via ProjectionItem', () => { const ast = buildPlan(true); const ctx = buildDecodeContext(ast, registry); - const result = await decodeRow({ vals: null }, ctx, CTX); + const result = decodeRow({ vals: null }, ctx, CTX); expect(result['vals']).toBeNull(); }); @@ -268,7 +270,7 @@ describe('decodeRow — many CodecRef via ProjectionItem', () => { const ast = buildPlan(false); const ctx = buildDecodeContext(ast, registry); - const result = await decodeRow({ vals: 'hello' }, ctx, CTX); + const result = decodeRow({ vals: 'hello' }, ctx, CTX); expect(result['vals']).toBe('DEC:hello'); }); diff --git a/packages/2-sql/5-runtime/test/seeded-secret-codec.ts b/packages/2-sql/5-runtime/test/seeded-secret-codec.ts index 2170ce5a5383..c1b16bbbab82 100644 --- a/packages/2-sql/5-runtime/test/seeded-secret-codec.ts +++ b/packages/2-sql/5-runtime/test/seeded-secret-codec.ts @@ -5,6 +5,7 @@ // // Guard against accidental production use. ============================================================================ +import { createDecipheriv, createHash } from 'node:crypto'; import { defineTestCodec } from './test-codec'; if (typeof process !== 'undefined' && process.env?.['NODE_ENV'] === 'production') { @@ -12,22 +13,11 @@ if (typeof process !== 'undefined' && process.env?.['NODE_ENV'] === 'production' } const textEncoder = new TextEncoder(); -const textDecoder = new TextDecoder(); function toBase64(bytes: Uint8Array): string { return Buffer.from(bytes).toString('base64'); } -// Return a `Uint8Array` (not `Uint8Array`) so the value -// satisfies WebCrypto's `BufferSource` parameters, which require an `ArrayBuffer`- -// backed view in newer DOM lib typings. -function fromBase64(value: string): Uint8Array { - const decoded = Buffer.from(value, 'base64'); - const out = new Uint8Array(decoded.byteLength); - out.set(decoded); - return out; -} - async function digestBytes(value: string): Promise> { const encoded = textEncoder.encode(value); const input = new Uint8Array(encoded.byteLength); @@ -54,7 +44,7 @@ export async function encryptSecret(value: string, seed: string): Promise { +export function decryptSecret(wire: string, seed: string): string { const [ivEncoded, ciphertextEncoded, extra] = wire.split(':'); if ( ivEncoded === undefined || @@ -66,17 +56,17 @@ export async function decryptSecret(wire: string, seed: string): Promise throw new Error('invalid secret payload'); } - const key = await importSeedKey(seed); - const plaintext = await globalThis.crypto.subtle.decrypt( - { name: 'AES-GCM', iv: fromBase64(ivEncoded) }, - key, - fromBase64(ciphertextEncoded), + const key = createHash('sha256').update(`${seed}:key`).digest(); + const ciphertext = Buffer.from(ciphertextEncoded, 'base64'); + const decipher = createDecipheriv('aes-256-gcm', key, Buffer.from(ivEncoded, 'base64')); + decipher.setAuthTag(ciphertext.subarray(-16)); + return Buffer.concat([decipher.update(ciphertext.subarray(0, -16)), decipher.final()]).toString( + 'utf8', ); - return textDecoder.decode(plaintext); } /** - * Build a `Codec` whose query-time `encode` / `decode` are async crypto operations. Authors pass the underlying async functions directly to `defineTestCodec({...})`; the single-path runtime always awaits them, so the codec needs no async marker. + * Build a test codec with WebCrypto async encoding and synchronous Node crypto decoding. */ export function createAsyncSecretCodec({ seed, diff --git a/packages/2-sql/5-runtime/test/sql-runtime-abort.test.ts b/packages/2-sql/5-runtime/test/sql-runtime-abort.test.ts index 48f2cd57b133..58c525e78a9e 100644 --- a/packages/2-sql/5-runtime/test/sql-runtime-abort.test.ts +++ b/packages/2-sql/5-runtime/test/sql-runtime-abort.test.ts @@ -320,29 +320,15 @@ describe('SqlRuntime operations with signals — abort semantics', () => { expect(collected).toEqual([{ id: 1 }]); }); - it('codec forwarding ctx.signal observes downstream abort (HTTPS-style cancellation)', async () => { - let abortObservedByCodec = false; - const blockingDecodeStarted = deferred(); - const codecAbortObserved = deferred(); + it('forwards the query signal into synchronous decoding', async () => { + let observedSignal: AbortSignal | undefined; const observingCodec = defineTestCodec({ typeId: 'test/observe-signal@1', targetTypes: ['text'], encode: (v: string) => v, - decode: async (w: string, ctx?: SqlCodecCallContext) => { - // Mimic an SDK that registers an abort listener on the supplied signal. The runtime threads the same AbortSignal into every codec call; codec authors who forward it observe true cancellation. - await new Promise((_resolve, reject) => { - if (ctx?.signal) { - ctx.signal.addEventListener('abort', () => { - abortObservedByCodec = true; - codecAbortObserved.resolve(); - reject(ctx.signal?.reason); - }); - } - // Hold the decode open so the abort fires while we're inside it. - blockingDecodeStarted.resolve(); - // Never resolves; the abort listener will reject this promise. - }); + decode: (w: string, ctx?: SqlCodecCallContext) => { + observedSignal = ctx?.signal; return w; }, }); @@ -361,62 +347,9 @@ describe('SqlRuntime operations with signals — abort semantics', () => { }); const controller = new AbortController(); - const collector = runtime.query(plan, { signal: controller.signal }).toArray(); + const rows = await runtime.query(plan, { signal: controller.signal }).toArray(); - await blockingDecodeStarted.promise; - controller.abort(new Error('forwarded')); - await codecAbortObserved.promise; - - await expect(collector).rejects.toMatchObject({ - code: 'RUNTIME.ABORTED', - details: { phase: 'decode' }, - }); - expect(abortObservedByCodec).toBe(true); - }); - - it('codec ignoring ctx.signal does not block runtime — RUNTIME.ABORTED still surfaces (cooperative cancellation)', async () => { - const decodeStarted = deferred(); - const release = deferred(); - const ignoringCodec = defineTestCodec({ - typeId: 'test/ignore-signal@1', - targetTypes: ['text'], - encode: (v: string) => v, - decode: async (w: string) => { - // Signal we're inside the decode body and deliberately ignore ctx.signal. - decodeStarted.resolve(); - const suffix = await release.promise; - return `${w}:${suffix}`; - }, - }); - - const { stackInstance, context, driver } = createTestSetup([ignoringCodec]); - const runtime = createRuntime({ - stackInstance, - context, - driver, - verifyMarker: false, - }); - - const plan = projectingExecutionPlan('name', 'users', 'name', 'test/ignore-signal@1'); - driver.__executeMock.mockImplementationOnce(async function* () { - yield { name: 'alice' }; - }); - - const controller = new AbortController(); - const reason = new Error('runtime aborted while codec body still running'); - const collector = runtime.query(plan, { signal: controller.signal }).toArray(); - - // Wait until the decode body has actually started (we're now mid-decode); then abort. The race in raceAgainstAbort surfaces RUNTIME.ABORTED with phase: 'decode', even though the codec body is still running and does not honour the signal. - await decodeStarted.promise; - controller.abort(reason); - - await expect(collector).rejects.toMatchObject({ - code: 'RUNTIME.ABORTED', - details: { phase: 'decode' }, - cause: reason, - }); - - // The codec body completes in the background; cleanup so the test exits. - release.resolve('done'); + expect(rows).toEqual([{ name: 'alice' }]); + expect(observedSignal).toBe(controller.signal); }); }); diff --git a/packages/2-sql/5-runtime/test/sql-runtime.test.ts b/packages/2-sql/5-runtime/test/sql-runtime.test.ts index 8f9e860e5888..253036af529e 100644 --- a/packages/2-sql/5-runtime/test/sql-runtime.test.ts +++ b/packages/2-sql/5-runtime/test/sql-runtime.test.ts @@ -789,7 +789,7 @@ describe('SqlRuntime', () => { const sentSecret = sentRequest?.params?.[0]; expect(typeof sentSecret).toBe('string'); expect(sentSecret).not.toBe('Alice'); - await expect(decryptSecret(sentSecret as string, runtimeSecretSeed)).resolves.toBe('Alice'); + expect(decryptSecret(sentSecret as string, runtimeSecretSeed)).toBe('Alice'); }); it('wraps async parameter encoding failures before the driver runs', async () => { diff --git a/packages/2-sql/9-family/test/authoring-entity-types.enum.test.ts b/packages/2-sql/9-family/test/authoring-entity-types.enum.test.ts index 368f1818e226..de704328004d 100644 --- a/packages/2-sql/9-family/test/authoring-entity-types.enum.test.ts +++ b/packages/2-sql/9-family/test/authoring-entity-types.enum.test.ts @@ -54,7 +54,7 @@ const PG_INT_CODEC_ID = 'pg/int@1'; const pgTextCodec: Codec = { id: PG_TEXT_CODEC_ID, encode: async (v: unknown) => v, - decode: async (w: unknown) => w, + decode: (w: unknown) => w, encodeJson: (value) => value as never, decodeJson(json) { if (typeof json !== 'string') throw new Error(`expected string, got ${typeof json}`); @@ -65,7 +65,7 @@ const pgTextCodec: Codec = { const pgIntCodec: Codec = { id: PG_INT_CODEC_ID, encode: async (v: unknown) => v, - decode: async (w: unknown) => w, + decode: (w: unknown) => w, encodeJson: (value) => value as never, decodeJson(json) { if (typeof json !== 'number') throw new Error(`expected number, got ${typeof json}`); diff --git a/packages/3-extensions/arktype-json/test/arktype-json-codec.test.ts b/packages/3-extensions/arktype-json/test/arktype-json-codec.test.ts index 516f97b194db..de173a1858c8 100644 --- a/packages/3-extensions/arktype-json/test/arktype-json-codec.test.ts +++ b/packages/3-extensions/arktype-json/test/arktype-json-codec.test.ts @@ -55,7 +55,7 @@ describe('arktypeJsonColumn(schema)', () => { const col = arktypeJsonColumn(productSchema); const codec = col.codecFactory(SYNTH_CTX); const wire = JSON.stringify({ name: 'Widget' }); - await expect(codec.decode(wire, CALL_CTX)).rejects.toThrow(/schema validation failed/); + expect(() => codec.decode(wire, CALL_CTX)).toThrow(/schema validation failed/); }); it('decode accepts already-parsed jsonb values from the driver', async () => { @@ -66,7 +66,7 @@ describe('arktypeJsonColumn(schema)', () => { it('decode validates pre-parsed payloads against the schema', async () => { const codec = arktypeJsonColumn(productSchema).codecFactory(SYNTH_CTX); - await expect(codec.decode({ name: 'Widget' }, CALL_CTX)).rejects.toThrow(/price/); + expect(() => codec.decode({ name: 'Widget' }, CALL_CTX)).toThrow(/price/); }); it('encodeJson / decodeJson round-trip through schema', () => { @@ -144,12 +144,12 @@ describe('arktypeJsonColumn encode/encodeJson agreement', () => { it('decode rejects payloads with type-mismatched fields', async () => { const codec = arktypeJsonColumn(productSchema).codecFactory(SYNTH_CTX); const wire = JSON.stringify({ name: 'Widget', price: 'not-a-number' }); - await expect(codec.decode(wire, CALL_CTX)).rejects.toThrow(/price/); + expect(() => codec.decode(wire, CALL_CTX)).toThrow(/price/); }); it('decode preserves the original validation error when fallback JSON parsing fails', async () => { const codec = arktypeJsonColumn(productSchema).codecFactory(SYNTH_CTX); - await expect(codec.decode('{not json', CALL_CTX)).rejects.toThrow(/schema validation failed/); + expect(() => codec.decode('{not json', CALL_CTX)).toThrow(/schema validation failed/); }); it('decode rethrows non-runtime schema errors from the raw string pass', async () => { @@ -161,7 +161,7 @@ describe('arktypeJsonColumn encode/encodeJson agreement', () => { ); const codec = arktypeJsonColumn(throwingSchema as never).codecFactory(SYNTH_CTX); - await expect(codec.decode('raw wire', CALL_CTX)).rejects.toThrow('schema exploded'); + expect(() => codec.decode('raw wire', CALL_CTX)).toThrow('schema exploded'); }); it('decode accepts pre-parsed JSON string primitives for string-schema columns', async () => { @@ -194,7 +194,7 @@ describe('arktypeJsonColumn encode/encodeJson agreement', () => { it('decode rejects pre-parsed primitives that violate the schema', async () => { const stringSchema = type('string'); const codec = arktypeJsonColumn(stringSchema).codecFactory(SYNTH_CTX); - await expect(codec.decode(42, CALL_CTX)).rejects.toThrow(/string/); + expect(() => codec.decode(42, CALL_CTX)).toThrow(/string/); }); }); diff --git a/packages/3-extensions/pgvector/test/codecs.test.ts b/packages/3-extensions/pgvector/test/codecs.test.ts index b56974f67543..c6842657e1f3 100644 --- a/packages/3-extensions/pgvector/test/codecs.test.ts +++ b/packages/3-extensions/pgvector/test/codecs.test.ts @@ -4,10 +4,9 @@ import { describe, expect, it } from 'vitest'; import { pgVectorColumn, pgVectorDescriptor } from '../src/core/codecs'; import { VECTOR_CODEC_ID, VECTOR_MAX_DIM } from '../src/core/constants'; -// The pgvector codec authors `encode`/`decode` synchronously; codecs route through `Promise`-returning methods at the boundary. The tests below cast through the Promise-returning shape and `await` every call so unit-level coverage stays aligned with the codec contract: `Codec` — encode/decode return Promise. type AsyncVectorCodec = { readonly encode: (value: number[]) => Promise; - readonly decode: (wire: string) => Promise; + readonly decode: (wire: string) => number[]; readonly encodeJson: (value: number[]) => JsonValue; readonly decodeJson: (json: JsonValue) => number[]; }; @@ -87,7 +86,7 @@ describe('pgvector codecs', () => { await expect(vectorCodec.encode(value)).rejects.toThrow( 'Vector value must contain only finite numbers', ); - await expect(vectorCodec.decode(wire)).rejects.toThrow( + expect(() => vectorCodec.decode(wire)).toThrow( 'Vector value must contain only finite numbers', ); expect(() => vectorCodec.encodeJson(value)).toThrow( @@ -101,14 +100,14 @@ describe('pgvector codecs', () => { it('rejects when decoding invalid string format', async () => { const vectorCodec = asAsyncCodec(4); - await expect(vectorCodec.decode('not a vector format')).rejects.toThrow( + expect(() => vectorCodec.decode('not a vector format')).toThrow( 'Invalid vector format: expected "[...]", got "not a vector format"', ); }); it('rejects when decoding non-string', async () => { const vectorCodec = asAsyncCodec(4); - await expect(vectorCodec.decode(123 as unknown as string)).rejects.toThrow( + expect(() => vectorCodec.decode(123 as unknown as string)).toThrow( 'Vector wire value must be a string', ); }); @@ -125,14 +124,12 @@ describe('pgvector codecs', () => { it('rejects decoding when wire length mismatches declared dimension', async () => { const vectorCodec = asAsyncCodec(3); - await expect(vectorCodec.decode('[1,2]')).rejects.toThrow( - 'Vector length mismatch: expected 3, got 2', - ); + expect(() => vectorCodec.decode('[1,2]')).toThrow('Vector length mismatch: expected 3, got 2'); }); it('rejects decoding when the wire payload contains a non-number token', async () => { const vectorCodec = asAsyncCodec(3); - await expect(vectorCodec.decode('[1,foo,3]')).rejects.toThrow( + expect(() => vectorCodec.decode('[1,foo,3]')).toThrow( /Invalid vector value: "foo" is not a number/, ); }); diff --git a/packages/3-extensions/pgvector/test/structured-errors.test.ts b/packages/3-extensions/pgvector/test/structured-errors.test.ts index 4ed99d3ade77..0969138a2127 100644 --- a/packages/3-extensions/pgvector/test/structured-errors.test.ts +++ b/packages/3-extensions/pgvector/test/structured-errors.test.ts @@ -46,9 +46,9 @@ describe('pgvector structured error codes', () => { }); }); - it('RUNTIME.DECODE_FAILED on decode of a malformed wire string', async () => { + it('RUNTIME.DECODE_FAILED on decode of a malformed wire string', () => { const codec = new PgVectorCodec(pgVectorDescriptor, 3); - const err = await catchAsyncError(() => codec.decode('not a vector', codecCtx)); + const err = catchError(() => codec.decode('not a vector', codecCtx)); expect(isStructuredError(err)).toBe(true); expect(err).toMatchObject({ code: 'RUNTIME.DECODE_FAILED', @@ -57,9 +57,9 @@ describe('pgvector structured error codes', () => { }); }); - it('RUNTIME.DECODE_FAILED (not ENCODE_FAILED) on decode with a length mismatch', async () => { + it('RUNTIME.DECODE_FAILED (not ENCODE_FAILED) on decode with a length mismatch', () => { const codec = new PgVectorCodec(pgVectorDescriptor, 3); - const err = await catchAsyncError(() => codec.decode('[1,2]', codecCtx)); + const err = catchError(() => codec.decode('[1,2]', codecCtx)); expect(isStructuredError(err)).toBe(true); expect(err).toMatchObject({ code: 'RUNTIME.DECODE_FAILED', diff --git a/packages/3-extensions/postgis/test/codecs.test.ts b/packages/3-extensions/postgis/test/codecs.test.ts index 4142986eb0e9..528a81a709c8 100644 --- a/packages/3-extensions/postgis/test/codecs.test.ts +++ b/packages/3-extensions/postgis/test/codecs.test.ts @@ -4,13 +4,9 @@ import { describe, expect, it } from 'vitest'; import { pgGeometryColumn, postgisGeometryDescriptor } from '../src/core/codecs'; import type { Geometry } from '../src/core/geojson'; -// The postgis codec authors `encode`/`decode` synchronously; codecs -// route through `Promise`-returning methods at the boundary. The tests -// below cast through the Promise-returning shape and `await` every -// call so unit-level coverage stays aligned with the codec contract. type AsyncGeometryCodec = { readonly encode: (value: Geometry) => Promise; - readonly decode: (wire: string) => Promise; + readonly decode: (wire: string) => Geometry; readonly encodeJson: (value: Geometry) => JsonValue; readonly decodeJson: (json: JsonValue) => Geometry; }; @@ -139,19 +135,19 @@ describe('postgis codecs', () => { it('rejects non-string wire input', async () => { const c = asAsyncCodec(); - await expect(c.decode(123 as unknown as string)).rejects.toThrow( + expect(() => c.decode(123 as unknown as string)).toThrow( 'Geometry wire value must be a string', ); }); it('rejects an odd-length hex string', async () => { const c = asAsyncCodec(); - await expect(c.decode('0')).rejects.toThrow('odd-length hex string'); + expect(() => c.decode('0')).toThrow('odd-length hex string'); }); it('rejects malformed hex bytes', async () => { const c = asAsyncCodec(); - await expect(c.decode('ZZ')).rejects.toThrow('invalid hex byte'); + expect(() => c.decode('ZZ')).toThrow('invalid hex byte'); }); }); diff --git a/packages/3-targets/3-targets/postgres/test/codecs.test.ts b/packages/3-targets/3-targets/postgres/test/codecs.test.ts index aae57e1bbaa6..02deb0c7679e 100644 --- a/packages/3-targets/3-targets/postgres/test/codecs.test.ts +++ b/packages/3-targets/3-targets/postgres/test/codecs.test.ts @@ -79,7 +79,7 @@ describe('adapter-postgres codecs', () => { describe('json codec', () => { const jsonCodec = codecForScalar('json') as { encode: (value: unknown, ctx: SqlCodecCallContext) => Promise; - decode: (wire: string | unknown, ctx: SqlCodecCallContext) => Promise; + decode: (wire: string | unknown, ctx: SqlCodecCallContext) => unknown; }; it('encodes object to JSON string', async () => { @@ -100,7 +100,7 @@ describe('adapter-postgres codecs', () => { describe('jsonb codec', () => { const jsonbCodec = codecForScalar('jsonb') as { encode: (value: unknown, ctx: SqlCodecCallContext) => Promise; - decode: (wire: string | unknown, ctx: SqlCodecCallContext) => Promise; + decode: (wire: string | unknown, ctx: SqlCodecCallContext) => unknown; }; it('encodes arrays and null values', async () => { @@ -127,7 +127,7 @@ describe('adapter-postgres codecs', () => { ] as const)('keeps $scalar values unchanged', async ({ scalar, value }) => { const codec = codecForScalar(scalar) as { encode: (input: string, ctx: SqlCodecCallContext) => Promise; - decode: (input: string, ctx: SqlCodecCallContext) => Promise; + decode: (input: string, ctx: SqlCodecCallContext) => string; }; expect(await codec.encode(value, {})).toBe(value); expect(await codec.decode(value, {})).toBe(value); @@ -141,7 +141,7 @@ describe('adapter-postgres codecs', () => { ] as const)('keeps $scalar values unchanged', async ({ scalar, value }) => { const codec = codecForScalar(scalar) as { encode: (input: number, ctx: SqlCodecCallContext) => Promise; - decode: (input: number, ctx: SqlCodecCallContext) => Promise; + decode: (input: number, ctx: SqlCodecCallContext) => number; }; expect(await codec.encode(value, {})).toBe(value); expect(await codec.decode(value, {})).toBe(value); @@ -150,7 +150,7 @@ describe('adapter-postgres codecs', () => { it('keeps boolean values unchanged', async () => { const boolCodec = codecForScalar('bool') as { encode: (input: boolean, ctx: SqlCodecCallContext) => Promise; - decode: (input: boolean, ctx: SqlCodecCallContext) => Promise; + decode: (input: boolean, ctx: SqlCodecCallContext) => boolean; }; expect(await boolCodec.encode(true, {})).toBe(true); expect(await boolCodec.decode(false, {})).toBe(false); @@ -160,7 +160,7 @@ describe('adapter-postgres codecs', () => { describe('character codec', () => { const charCodec = codecForScalar('character') as { encode: (value: string, ctx: SqlCodecCallContext) => Promise; - decode: (wire: string, ctx: SqlCodecCallContext) => Promise; + decode: (wire: string, ctx: SqlCodecCallContext) => string; }; it('encodes string as-is', async () => { @@ -175,7 +175,7 @@ describe('adapter-postgres codecs', () => { describe('character varying codec', () => { const varcharCodec = codecForScalar('character varying') as { encode: (value: string, ctx: SqlCodecCallContext) => Promise; - decode: (wire: string, ctx: SqlCodecCallContext) => Promise; + decode: (wire: string, ctx: SqlCodecCallContext) => string; }; it('encodes string as-is', async () => { @@ -190,7 +190,7 @@ describe('adapter-postgres codecs', () => { describe('numeric codec', () => { const numericCodec = codecForScalar('numeric') as { encode: (value: string, ctx: SqlCodecCallContext) => Promise; - decode: (wire: string | number, ctx: SqlCodecCallContext) => Promise; + decode: (wire: string | number, ctx: SqlCodecCallContext) => string; }; it('encodes string as-is', async () => { @@ -205,7 +205,7 @@ describe('adapter-postgres codecs', () => { describe('timetz codec', () => { const timetzCodec = codecForScalar('timetz') as { encode: (value: string, ctx: SqlCodecCallContext) => Promise; - decode: (wire: string, ctx: SqlCodecCallContext) => Promise; + decode: (wire: string, ctx: SqlCodecCallContext) => string; }; it('encodes string as-is', async () => { @@ -220,7 +220,7 @@ describe('adapter-postgres codecs', () => { describe('bit codec', () => { const bitCodec = codecForScalar('bit') as { encode: (value: string, ctx: SqlCodecCallContext) => Promise; - decode: (wire: string, ctx: SqlCodecCallContext) => Promise; + decode: (wire: string, ctx: SqlCodecCallContext) => string; }; it('encodes string as-is', async () => { @@ -235,7 +235,7 @@ describe('adapter-postgres codecs', () => { describe('bit varying codec', () => { const varbitCodec = codecForScalar('bit varying') as { encode: (value: string, ctx: SqlCodecCallContext) => Promise; - decode: (wire: string, ctx: SqlCodecCallContext) => Promise; + decode: (wire: string, ctx: SqlCodecCallContext) => string; }; it('encodes string as-is', async () => { @@ -250,7 +250,7 @@ describe('adapter-postgres codecs', () => { describe('bytea codec', () => { const byteaCodec = codecForScalar('bytea') as { encode: (value: Uint8Array, ctx: SqlCodecCallContext) => Promise; - decode: (wire: Uint8Array, ctx: SqlCodecCallContext) => Promise; + decode: (wire: Uint8Array, ctx: SqlCodecCallContext) => Uint8Array; encodeJson: (value: Uint8Array) => unknown; decodeJson: (json: unknown) => Uint8Array; }; @@ -333,7 +333,7 @@ describe('adapter-postgres codecs', () => { }); it('rejects a text wire value that is not an ISO-8601 duration', async () => { - await expect(codec.decode('1 day', {})).rejects.toThrow( + expect(() => codec.decode('1 day', {})).toThrow( 'pg/interval@1 value must be an ISO-8601 duration, got 1 day', ); }); diff --git a/packages/3-targets/3-targets/postgres/test/date-codec-wiring.test-d.ts b/packages/3-targets/3-targets/postgres/test/date-codec-wiring.test-d.ts index 7503c4d2f849..a045a2797de8 100644 --- a/packages/3-targets/3-targets/postgres/test/date-codec-wiring.test-d.ts +++ b/packages/3-targets/3-targets/postgres/test/date-codec-wiring.test-d.ts @@ -13,5 +13,5 @@ test('Date codec participates in the public codec type map', () => { expectTypeOf(codec).not.toBeAny(); expectTypeOf(codec.id).toEqualTypeOf<'pg/timestamptz-date@1'>(); expectTypeOf(codec.encode).parameter(0).toEqualTypeOf(); - expectTypeOf(codec.decode).returns.resolves.toEqualTypeOf(); + expectTypeOf(codec.decode).returns.toEqualTypeOf(); }); diff --git a/packages/3-targets/3-targets/postgres/test/date-codecs.test.ts b/packages/3-targets/3-targets/postgres/test/date-codecs.test.ts index 61a79bc198c7..e44d7671f1f5 100644 --- a/packages/3-targets/3-targets/postgres/test/date-codecs.test.ts +++ b/packages/3-targets/3-targets/postgres/test/date-codecs.test.ts @@ -50,7 +50,7 @@ describe('pg/timestamptz-date@1', () => { '2026-01-01 00:00:00+25', '2026-01-01 00:00:00+00:60', ])('rejects unrepresentable or unsupported text: %s', async (wire) => { - await expect(codec.decode(wire, {})).rejects.toThrow('pg/timestamptz-date@1'); + expect(() => codec.decode(wire, {})).toThrow('pg/timestamptz-date@1'); expect(() => codec.decodeJson(wire)).toThrow('pg/timestamptz-date@1'); }); @@ -63,7 +63,7 @@ describe('pg/timestamptz-date@1', () => { '4715-01-01 00:00:00+00 BC', ])('rejects text before the UTC boundary: %s', async (wire) => { if (format === 'wire') { - await expect(codec.decode(wire, {})).rejects.toThrow(RangeError); + expect(() => codec.decode(wire, {})).toThrow(RangeError); } else { expect(() => codec.decodeJson(wire)).toThrow(RangeError); } diff --git a/packages/3-targets/3-targets/postgres/test/integer-representation-codecs.test.ts b/packages/3-targets/3-targets/postgres/test/integer-representation-codecs.test.ts index ddb789d70885..b0ecc2daf3df 100644 --- a/packages/3-targets/3-targets/postgres/test/integer-representation-codecs.test.ts +++ b/packages/3-targets/3-targets/postgres/test/integer-representation-codecs.test.ts @@ -30,37 +30,37 @@ describe('pg/int8number@1', () => { }); it('throws at 2^53 on every wire form', async () => { - await expect(codec.decode(9007199254740992, {})).rejects.toThrow( + expect(() => codec.decode(9007199254740992, {})).toThrow( 'pg/int8number@1 value must be an integer within the safe integer range', ); - await expect(codec.decode('9007199254740992', {})).rejects.toThrow( + expect(() => codec.decode('9007199254740992', {})).toThrow( 'pg/int8number@1 value must be an integer within the safe integer range', ); - await expect(codec.decode(9007199254740992n, {})).rejects.toThrow( + expect(() => codec.decode(9007199254740992n, {})).toThrow( 'pg/int8number@1 value must be an integer within the safe integer range', ); }); it('throws at -(2^53)', async () => { - await expect(codec.decode(-9007199254740992n, {})).rejects.toThrow( + expect(() => codec.decode(-9007199254740992n, {})).toThrow( 'pg/int8number@1 value must be an integer within the safe integer range', ); - await expect(codec.decode('-9007199254740992', {})).rejects.toThrow( + expect(() => codec.decode('-9007199254740992', {})).toThrow( 'pg/int8number@1 value must be an integer within the safe integer range', ); }); it('throws on decimal text a Number() coercion would silently round', async () => { - await expect(codec.decode('9007199254740993', {})).rejects.toThrow( + expect(() => codec.decode('9007199254740993', {})).toThrow( 'pg/int8number@1 value must be an integer within the safe integer range', ); }); it('throws on non-integral wire values', async () => { - await expect(codec.decode(1.5, {})).rejects.toThrow( + expect(() => codec.decode(1.5, {})).toThrow( 'pg/int8number@1 value must be an integer within the safe integer range', ); - await expect(codec.decode('1.5', {})).rejects.toThrow( + expect(() => codec.decode('1.5', {})).toThrow( 'pg/int8number@1 value must be a decimal integer', ); }); @@ -221,16 +221,20 @@ describe('pg/int8@1 number wire values', () => { }); it('rejects a number wire value past the safe range, which has already lost precision', async () => { - await expect(codec.decode(9007199254740992, {})).rejects.toMatchObject({ - code: 'RUNTIME.DECODE_FAILED', - message: - 'pg/int8@1 wire number must be an integer within the safe integer range, got 9007199254740992', - meta: { codecId: 'pg/int8@1', received: '9007199254740992' }, - }); - await expect(codec.decode(-9007199254740992, {})).rejects.toMatchObject({ - code: 'RUNTIME.DECODE_FAILED', - meta: { codecId: 'pg/int8@1', received: '-9007199254740992' }, - }); + expect(() => codec.decode(9007199254740992, {})).toThrow( + expect.objectContaining({ + code: 'RUNTIME.DECODE_FAILED', + message: + 'pg/int8@1 wire number must be an integer within the safe integer range, got 9007199254740992', + meta: { codecId: 'pg/int8@1', received: '9007199254740992' }, + }), + ); + expect(() => codec.decode(-9007199254740992, {})).toThrow( + expect.objectContaining({ + code: 'RUNTIME.DECODE_FAILED', + meta: { codecId: 'pg/int8@1', received: '-9007199254740992' }, + }), + ); }); }); @@ -253,32 +257,36 @@ describe('pg/unboundedint@1', () => { }); it('rejects a number wire value past the safe range, which has already lost precision', async () => { - await expect(codec.decode(9007199254740992, {})).rejects.toMatchObject({ - code: 'RUNTIME.DECODE_FAILED', - message: - 'pg/unboundedint@1 wire number must be an integer within the safe integer range, got 9007199254740992', - meta: { codecId: 'pg/unboundedint@1', received: '9007199254740992' }, - }); - await expect(codec.decode(-9007199254740992, {})).rejects.toMatchObject({ - code: 'RUNTIME.DECODE_FAILED', - meta: { codecId: 'pg/unboundedint@1', received: '-9007199254740992' }, - }); + expect(() => codec.decode(9007199254740992, {})).toThrow( + expect.objectContaining({ + code: 'RUNTIME.DECODE_FAILED', + message: + 'pg/unboundedint@1 wire number must be an integer within the safe integer range, got 9007199254740992', + meta: { codecId: 'pg/unboundedint@1', received: '9007199254740992' }, + }), + ); + expect(() => codec.decode(-9007199254740992, {})).toThrow( + expect.objectContaining({ + code: 'RUNTIME.DECODE_FAILED', + meta: { codecId: 'pg/unboundedint@1', received: '-9007199254740992' }, + }), + ); }); it('rejects non-integral values', async () => { - await expect(codec.decode('1.5', {})).rejects.toThrow( + expect(() => codec.decode('1.5', {})).toThrow( 'pg/unboundedint@1 value must be a decimal integer', ); - await expect(codec.decode(1.5, {})).rejects.toThrow( + expect(() => codec.decode(1.5, {})).toThrow( 'pg/unboundedint@1 value must be a decimal integer', ); }); it('rejects the non-finite numeric values', async () => { - await expect(codec.decode('NaN', {})).rejects.toThrow( + expect(() => codec.decode('NaN', {})).toThrow( 'pg/unboundedint@1 value must be a decimal integer', ); - await expect(codec.decode('Infinity', {})).rejects.toThrow( + expect(() => codec.decode('Infinity', {})).toThrow( 'pg/unboundedint@1 value must be a decimal integer', ); }); diff --git a/packages/3-targets/3-targets/postgres/test/postgres-codec-descriptor.test-d.ts b/packages/3-targets/3-targets/postgres/test/postgres-codec-descriptor.test-d.ts index 3e06c5a15365..9e7c0949436f 100644 --- a/packages/3-targets/3-targets/postgres/test/postgres-codec-descriptor.test-d.ts +++ b/packages/3-targets/3-targets/postgres/test/postgres-codec-descriptor.test-d.ts @@ -45,7 +45,7 @@ class VectorCodec extends CodecImpl< return `[${value.join(',')}]`; } - async decode(wire: string, _ctx: CodecCallContext): Promise> { + decode(wire: string, _ctx: CodecCallContext): ReadonlyArray { return wire.slice(1, -1).split(',').map(Number); } diff --git a/packages/3-targets/3-targets/postgres/test/postgres-codec-descriptor.test.ts b/packages/3-targets/3-targets/postgres/test/postgres-codec-descriptor.test.ts index 6385d73c407b..393d3aed4972 100644 --- a/packages/3-targets/3-targets/postgres/test/postgres-codec-descriptor.test.ts +++ b/packages/3-targets/3-targets/postgres/test/postgres-codec-descriptor.test.ts @@ -70,7 +70,7 @@ class VectorCodec extends CodecImpl< return `[${value.join(',')}]`; } - async decode(wire: string, _ctx: CodecCallContext): Promise> { + decode(wire: string, _ctx: CodecCallContext): ReadonlyArray { return wire.slice(1, -1).split(',').map(Number); } diff --git a/packages/3-targets/3-targets/postgres/test/psl-infer/infer-psl-contract.enum-adoption.test.ts b/packages/3-targets/3-targets/postgres/test/psl-infer/infer-psl-contract.enum-adoption.test.ts index d3023d383875..00a8318a20ab 100644 --- a/packages/3-targets/3-targets/postgres/test/psl-infer/infer-psl-contract.enum-adoption.test.ts +++ b/packages/3-targets/3-targets/postgres/test/psl-infer/infer-psl-contract.enum-adoption.test.ts @@ -183,7 +183,9 @@ const pgEnumCodec = { id: PG_ENUM_CODEC_ID, descriptor: pgEnumDescriptor, encode: () => Promise.reject(new Error('unused')), - decode: () => Promise.reject(new Error('unused')), + decode: () => { + throw new Error('unused'); + }, encodeJson: (value) => value, decodeJson: (json) => json, } as Codec; diff --git a/packages/3-targets/3-targets/postgres/test/psl-infer/print-psl/print-psl.top-level-blocks.test.ts b/packages/3-targets/3-targets/postgres/test/psl-infer/print-psl/print-psl.top-level-blocks.test.ts index dce445a68ceb..ecca72e79767 100644 --- a/packages/3-targets/3-targets/postgres/test/psl-infer/print-psl/print-psl.top-level-blocks.test.ts +++ b/packages/3-targets/3-targets/postgres/test/psl-infer/print-psl/print-psl.top-level-blocks.test.ts @@ -78,7 +78,7 @@ const target = { const textCodec: Codec = { id: 'pg/text@1', encode: async (v: unknown) => v, - decode: async (w: unknown) => w, + decode: (w: unknown) => w, encodeJson: (value) => value as never, decodeJson(json) { if (typeof json !== 'string') throw new Error(`expected string, got ${typeof json}`); diff --git a/packages/3-targets/3-targets/postgres/test/psl-native-enum-authoring.test.ts b/packages/3-targets/3-targets/postgres/test/psl-native-enum-authoring.test.ts index 78dfd546e8eb..5df5e79ba6eb 100644 --- a/packages/3-targets/3-targets/postgres/test/psl-native-enum-authoring.test.ts +++ b/packages/3-targets/3-targets/postgres/test/psl-native-enum-authoring.test.ts @@ -404,7 +404,7 @@ describe('native_enum coexists with a PSL enum block in the same namespace', () const textCodec: Codec = { id: 'pg/text@1', encode: async (v: unknown) => v, - decode: async (w: unknown) => w, + decode: (w: unknown) => w, encodeJson: (value) => value as never, decodeJson(json) { if (typeof json !== 'string') throw new Error(`expected string, got ${typeof json}`); diff --git a/packages/3-targets/3-targets/postgres/test/psl-pg-enum-column.test.ts b/packages/3-targets/3-targets/postgres/test/psl-pg-enum-column.test.ts index 72eba7af3496..a270df58c746 100644 --- a/packages/3-targets/3-targets/postgres/test/psl-pg-enum-column.test.ts +++ b/packages/3-targets/3-targets/postgres/test/psl-pg-enum-column.test.ts @@ -40,7 +40,9 @@ const pgEnumCodec = { id: PG_ENUM_CODEC_ID, descriptor: pgEnumDescriptor, encode: () => Promise.reject(new Error('unused')), - decode: () => Promise.reject(new Error('unused')), + decode: () => { + throw new Error('unused'); + }, encodeJson: (value) => value, decodeJson: (json) => json, } as Codec; diff --git a/packages/3-targets/3-targets/postgres/test/temporal-codecs.test.ts b/packages/3-targets/3-targets/postgres/test/temporal-codecs.test.ts index 9277e05fee82..7133c18fdb07 100644 --- a/packages/3-targets/3-targets/postgres/test/temporal-codecs.test.ts +++ b/packages/3-targets/3-targets/postgres/test/temporal-codecs.test.ts @@ -147,7 +147,7 @@ describe('Temporal-backed temporal codecs', () => { describe('values Temporal cannot represent are reported, not silently coerced', () => { const unrepresentable: ReadonlyArray< - readonly [string, { decode: (w: string, c: object) => Promise }, string, string] + readonly [string, { decode: (w: string, c: object) => unknown }, string, string] > = [ ['date infinity', dateCodec, 'infinity', 'DateString'], ['date -infinity', dateCodec, '-infinity', 'DateString'], @@ -180,11 +180,13 @@ describe('Temporal-backed temporal codecs', () => { it.each(unrepresentable)( 'rejects %s and names the string type that reads it losslessly', async (_label, codec, wire, stringType) => { - await expect(codec.decode(wire, callCtx)).rejects.toMatchObject({ - code: 'RUNTIME.DECODE_FAILED', - meta: { value: wire, stringType }, - }); - await expect(codec.decode(wire, callCtx)).rejects.toThrow(stringType); + expect(() => codec.decode(wire, callCtx)).toThrow( + expect.objectContaining({ + code: 'RUNTIME.DECODE_FAILED', + meta: expect.objectContaining({ value: wire, stringType }), + }), + ); + expect(() => codec.decode(wire, callCtx)).toThrow(stringType); }, ); @@ -197,7 +199,7 @@ describe('Temporal-backed temporal codecs', () => { ] as const)( 'explains that %s %s is a timeline sentinel rather than unparseable text', async (_kind, codec, wire) => { - await expect(codec.decode(wire, callCtx)).rejects.toThrow( + expect(() => codec.decode(wire, callCtx)).toThrow( `PostgreSQL's ${wire} is a sentinel with no position on the timeline`, ); }, diff --git a/packages/3-targets/3-targets/sqlite/test/codecs.test.ts b/packages/3-targets/3-targets/sqlite/test/codecs.test.ts index 59bd8dadcbcc..243f3625755a 100644 --- a/packages/3-targets/3-targets/sqlite/test/codecs.test.ts +++ b/packages/3-targets/3-targets/sqlite/test/codecs.test.ts @@ -25,10 +25,12 @@ describe('SQLite codec JSON representations', () => { }); it('rejects a malformed string wire with a structured decode error', async () => { - await expect(bigintCodec.decode('not-a-number', {})).rejects.toMatchObject({ - code: 'RUNTIME.DECODE_FAILED', - message: 'sqlite/bigint@1 wire value must be a decimal string', - meta: { codecId: 'sqlite/bigint@1' }, - }); + expect(() => bigintCodec.decode('not-a-number', {})).toThrow( + expect.objectContaining({ + code: 'RUNTIME.DECODE_FAILED', + message: 'sqlite/bigint@1 wire value must be a decimal string', + meta: expect.objectContaining({ codecId: 'sqlite/bigint@1' }), + }), + ); }); }); diff --git a/packages/3-targets/3-targets/sqlite/test/integer-representation-codecs.test.ts b/packages/3-targets/3-targets/sqlite/test/integer-representation-codecs.test.ts index 19776ec39ce7..abf14445a21b 100644 --- a/packages/3-targets/3-targets/sqlite/test/integer-representation-codecs.test.ts +++ b/packages/3-targets/3-targets/sqlite/test/integer-representation-codecs.test.ts @@ -28,25 +28,31 @@ describe('sqlite/bigint@1 number wire values', () => { }); it('rejects a number wire value past the safe range, which has already lost precision', async () => { - await expect(codec.decode(9007199254740992, {})).rejects.toMatchObject({ - code: 'RUNTIME.DECODE_FAILED', - message: - 'sqlite/bigint@1 wire number must be an integer within the safe integer range, got 9007199254740992', - meta: { codecId: 'sqlite/bigint@1', received: '9007199254740992' }, - }); - await expect(codec.decode(-9007199254740992, {})).rejects.toMatchObject({ - code: 'RUNTIME.DECODE_FAILED', - meta: { codecId: 'sqlite/bigint@1', received: '-9007199254740992' }, - }); + expect(() => codec.decode(9007199254740992, {})).toThrow( + expect.objectContaining({ + code: 'RUNTIME.DECODE_FAILED', + message: + 'sqlite/bigint@1 wire number must be an integer within the safe integer range, got 9007199254740992', + meta: { codecId: 'sqlite/bigint@1', received: '9007199254740992' }, + }), + ); + expect(() => codec.decode(-9007199254740992, {})).toThrow( + expect.objectContaining({ + code: 'RUNTIME.DECODE_FAILED', + meta: { codecId: 'sqlite/bigint@1', received: '-9007199254740992' }, + }), + ); }); it('rejects a non-integral number wire value with a structured error', async () => { - await expect(codec.decode(1.5, {})).rejects.toMatchObject({ - code: 'RUNTIME.DECODE_FAILED', - message: - 'sqlite/bigint@1 wire number must be an integer within the safe integer range, got 1.5', - meta: { codecId: 'sqlite/bigint@1', received: '1.5' }, - }); + expect(() => codec.decode(1.5, {})).toThrow( + expect.objectContaining({ + code: 'RUNTIME.DECODE_FAILED', + message: + 'sqlite/bigint@1 wire number must be an integer within the safe integer range, got 1.5', + meta: { codecId: 'sqlite/bigint@1', received: '1.5' }, + }), + ); }); // The same guard from the other side of the pair: this codec reads a @@ -88,46 +94,48 @@ describe('sqlite/bigintnumber@1', () => { }); it('throws at 2^53 on every wire form', async () => { - await expect(codec.decode(9007199254740992, {})).rejects.toThrow( + expect(() => codec.decode(9007199254740992, {})).toThrow( 'sqlite/bigintnumber@1 value must be an integer within the safe integer range', ); - await expect(codec.decode('9007199254740992', {})).rejects.toThrow( + expect(() => codec.decode('9007199254740992', {})).toThrow( 'sqlite/bigintnumber@1 value must be an integer within the safe integer range', ); - await expect(codec.decode(9007199254740992n, {})).rejects.toThrow( + expect(() => codec.decode(9007199254740992n, {})).toThrow( 'sqlite/bigintnumber@1 value must be an integer within the safe integer range', ); }); it('throws at -(2^53)', async () => { - await expect(codec.decode(-9007199254740992n, {})).rejects.toThrow( + expect(() => codec.decode(-9007199254740992n, {})).toThrow( 'sqlite/bigintnumber@1 value must be an integer within the safe integer range', ); - await expect(codec.decode('-9007199254740992', {})).rejects.toThrow( + expect(() => codec.decode('-9007199254740992', {})).toThrow( 'sqlite/bigintnumber@1 value must be an integer within the safe integer range', ); }); it('throws on decimal text a Number() coercion would silently round', async () => { - await expect(codec.decode('9007199254740993', {})).rejects.toThrow( + expect(() => codec.decode('9007199254740993', {})).toThrow( 'sqlite/bigintnumber@1 value must be an integer within the safe integer range', ); }); it('throws on non-integral wire values', async () => { - await expect(codec.decode(1.5, {})).rejects.toThrow( + expect(() => codec.decode(1.5, {})).toThrow( 'sqlite/bigintnumber@1 value must be an integer within the safe integer range', ); - await expect(codec.decode('1.5', {})).rejects.toThrow( + expect(() => codec.decode('1.5', {})).toThrow( 'sqlite/bigintnumber@1 wire value must be a decimal string', ); }); it('raises a structured error carrying the codec id and the received value', async () => { - await expect(codec.decode(9007199254740992n, {})).rejects.toMatchObject({ - code: 'RUNTIME.DECODE_FAILED', - meta: { codecId: 'sqlite/bigintnumber@1', received: '9007199254740992' }, - }); + expect(() => codec.decode(9007199254740992n, {})).toThrow( + expect.objectContaining({ + code: 'RUNTIME.DECODE_FAILED', + meta: { codecId: 'sqlite/bigintnumber@1', received: '9007199254740992' }, + }), + ); }); }); diff --git a/packages/3-targets/3-targets/sqlite/test/sqlite-codec-descriptor.test-d.ts b/packages/3-targets/3-targets/sqlite/test/sqlite-codec-descriptor.test-d.ts index a3481cbf7ce9..9ac26b9ff079 100644 --- a/packages/3-targets/3-targets/sqlite/test/sqlite-codec-descriptor.test-d.ts +++ b/packages/3-targets/3-targets/sqlite/test/sqlite-codec-descriptor.test-d.ts @@ -45,7 +45,7 @@ class VectorCodec extends CodecImpl< return `[${value.join(',')}]`; } - async decode(wire: string, _ctx: CodecCallContext): Promise> { + decode(wire: string, _ctx: CodecCallContext): ReadonlyArray { return wire.slice(1, -1).split(',').map(Number); } diff --git a/packages/3-targets/3-targets/sqlite/test/sqlite-codec-descriptor.test.ts b/packages/3-targets/3-targets/sqlite/test/sqlite-codec-descriptor.test.ts index 175082895bb6..51ae6b6363a0 100644 --- a/packages/3-targets/3-targets/sqlite/test/sqlite-codec-descriptor.test.ts +++ b/packages/3-targets/3-targets/sqlite/test/sqlite-codec-descriptor.test.ts @@ -61,7 +61,7 @@ class VectorCodec extends CodecImpl< return `[${value.join(',')}]`; } - async decode(wire: string, _ctx: CodecCallContext): Promise> { + decode(wire: string, _ctx: CodecCallContext): ReadonlyArray { return wire.slice(1, -1).split(',').map(Number); } diff --git a/packages/3-targets/6-adapters/postgres-codec-testkit/test/aggregate-defaults.integration.test.ts b/packages/3-targets/6-adapters/postgres-codec-testkit/test/aggregate-defaults.integration.test.ts index a1bcb180f785..f2498e9de17c 100644 --- a/packages/3-targets/6-adapters/postgres-codec-testkit/test/aggregate-defaults.integration.test.ts +++ b/packages/3-targets/6-adapters/postgres-codec-testkit/test/aggregate-defaults.integration.test.ts @@ -209,12 +209,14 @@ describe('PostgreSQL aggregate defaults', { concurrent: false }, () => { const { wire, codec } = await aggregate('sum', 'pg/int8@1'); expect(wire).toBe((MAX_SAFE + 1n).toString()); - await expect(codec.decode(wire, {})).rejects.toMatchObject({ - code: 'RUNTIME.DECODE_FAILED', - message: - 'pg/int8number@1 value must be an integer within the safe integer range, got 9007199254740992', - meta: { codecId: 'pg/int8number@1', received: '9007199254740992' }, - }); + expect(() => codec.decode(wire, {})).toThrow( + expect.objectContaining({ + code: 'RUNTIME.DECODE_FAILED', + message: + 'pg/int8number@1 value must be an integer within the safe integer range, got 9007199254740992', + meta: { codecId: 'pg/int8number@1', received: '9007199254740992' }, + }), + ); }); }, timeouts.spinUpPpgDev, diff --git a/packages/3-targets/6-adapters/postgres/test/lower-to-execute-request.test.ts b/packages/3-targets/6-adapters/postgres/test/lower-to-execute-request.test.ts index d89c6fa7a852..a903e369baff 100644 --- a/packages/3-targets/6-adapters/postgres/test/lower-to-execute-request.test.ts +++ b/packages/3-targets/6-adapters/postgres/test/lower-to-execute-request.test.ts @@ -30,7 +30,7 @@ const ctx = { contract: {} as PostgresContract }; const transformingCodec = { id: 'test/transform@1', encode: async (value: unknown) => `ENC:${String(value).toUpperCase()}`, - decode: async (wire: unknown) => wire, + decode: (wire: unknown) => wire, encodeJson: (value: unknown) => value, decodeJson: (json: unknown) => json, } as unknown as Codec; @@ -214,7 +214,7 @@ const TEST_CODEC_ID = 'test/transform@1'; const queryTransformingCodec = { id: TEST_CODEC_ID, encode: async (value: unknown) => `ENC:${String(value).toUpperCase()}`, - decode: async (wire: unknown) => wire, + decode: (wire: unknown) => wire, } as unknown as Codec; const testRegistry: ContractCodecRegistry = { @@ -289,7 +289,7 @@ class ExtTransformDescriptor extends CodecDescriptorImpl { ({ id: EXT_CODEC_ID, encode: async (value: unknown) => `ENC:${String(value).toUpperCase()}`, - decode: async (wire: unknown) => wire, + decode: (wire: unknown) => wire, encodeJson: (v: unknown) => v as never, decodeJson: (v: unknown) => v as never, }) as unknown as Codec; diff --git a/packages/3-targets/6-adapters/postgres/test/migrations/data-transform.test.ts b/packages/3-targets/6-adapters/postgres/test/migrations/data-transform.test.ts index f9661ef939ac..ed497f910a38 100644 --- a/packages/3-targets/6-adapters/postgres/test/migrations/data-transform.test.ts +++ b/packages/3-targets/6-adapters/postgres/test/migrations/data-transform.test.ts @@ -219,7 +219,7 @@ const TEST_CODEC_ID = 'test/transform@1'; const transformingCodec: Codec = { id: TEST_CODEC_ID, encode: async (value: unknown) => `ENC:${String(value).toUpperCase()}`, - decode: async (wire: unknown) => wire, + decode: (wire: unknown) => wire, encodeJson: (v) => v as never, decodeJson: (v) => v as never, }; diff --git a/packages/3-targets/6-adapters/postgres/test/scalar-list-codec-roundtrip.integration.test.ts b/packages/3-targets/6-adapters/postgres/test/scalar-list-codec-roundtrip.integration.test.ts index 85cb0fb34b2b..c3a3de5e7bcc 100644 --- a/packages/3-targets/6-adapters/postgres/test/scalar-list-codec-roundtrip.integration.test.ts +++ b/packages/3-targets/6-adapters/postgres/test/scalar-list-codec-roundtrip.integration.test.ts @@ -385,7 +385,7 @@ describe('scalar-list codec round-trip (element-wise encode/decode)', { concurre // this test proves that contract without requiring a real DB fixture. describe('scalar-list decode — malformed element surfaces RUNTIME.DECODE_FAILED', () => { - it('wraps an element-level decode failure in RUNTIME.DECODE_FAILED with column/codec context', async () => { + it('wraps an element-level decode failure in RUNTIME.DECODE_FAILED with column/codec context', () => { const codec = defineTestCodec({ typeId: 'test/strict-string@1', encode: (v: string) => v, @@ -410,13 +410,15 @@ describe('scalar-list decode — malformed element surfaces RUNTIME.DECODE_FAILE const ctx = buildDecodeContext(ast, registry); // Third element is a number — should trigger the element-level decode failure path. - await expect(decodeRow({ tags: ['ok', 'also-ok', 42] }, ctx, {})).rejects.toMatchObject({ - code: 'RUNTIME.DECODE_FAILED', - details: expect.objectContaining({ - table: 'ListTest', - column: 'tags', - codec: 'test/strict-string@1', + expect(() => decodeRow({ tags: ['ok', 'also-ok', 42] }, ctx, {})).toThrowError( + expect.objectContaining({ + code: 'RUNTIME.DECODE_FAILED', + details: expect.objectContaining({ + table: 'ListTest', + column: 'tags', + codec: 'test/strict-string@1', + }), }), - }); + ); }); }); diff --git a/packages/3-targets/6-adapters/postgres/test/temporal-codec-roundtrip.integration.test.ts b/packages/3-targets/6-adapters/postgres/test/temporal-codec-roundtrip.integration.test.ts index 00ac3845ae51..b023777338e3 100644 --- a/packages/3-targets/6-adapters/postgres/test/temporal-codec-roundtrip.integration.test.ts +++ b/packages/3-targets/6-adapters/postgres/test/temporal-codec-roundtrip.integration.test.ts @@ -317,7 +317,7 @@ describe('Temporal codecs round-trip through PostgreSQL', () => { } }); - it('forwards RUNTIME.TEMPORAL_UNAVAILABLE through the generic decode path with its code intact', async () => { + it('forwards RUNTIME.TEMPORAL_UNAVAILABLE through the generic decode path with its code intact', () => { const descriptor = postgresCodecDescriptorRegistry.descriptorFor(TIMESTAMPTZ.codecId); const codec = descriptor!.factory(TIMESTAMPTZ.typeParams)({ name: '' }); const tstzOnly = SelectAst.from(TABLE).withProjection([ @@ -328,12 +328,14 @@ describe('Temporal codecs round-trip through PostgreSQL', () => { const original = Reflect.get(globalThis, 'Temporal'); Reflect.deleteProperty(globalThis, 'Temporal'); try { - await expect( + expect(() => decodeRow({ tstz: '2026-01-02 03:04:05.123456+00' }, decodeCtx, {}), - ).rejects.toMatchObject({ - code: 'RUNTIME.TEMPORAL_UNAVAILABLE', - meta: { codecId: 'pg/timestamptz-temporal@1', operation: 'decode' }, - }); + ).toThrowError( + expect.objectContaining({ + code: 'RUNTIME.TEMPORAL_UNAVAILABLE', + meta: { codecId: 'pg/timestamptz-temporal@1', operation: 'decode' }, + }), + ); } finally { Reflect.set(globalThis, 'Temporal', original); } diff --git a/packages/3-targets/6-adapters/postgres/test/test-codec.ts b/packages/3-targets/6-adapters/postgres/test/test-codec.ts index 133f67a28832..7afd933b4cfe 100644 --- a/packages/3-targets/6-adapters/postgres/test/test-codec.ts +++ b/packages/3-targets/6-adapters/postgres/test/test-codec.ts @@ -25,7 +25,7 @@ export function defineTestCodec< typeId: Id; targetTypes?: readonly string[]; encode: (value: TInput, ctx: SqlCodecCallContext) => TWire | Promise; - decode: (wire: TWire, ctx: SqlCodecCallContext) => TInput | Promise; + decode: (wire: TWire, ctx: SqlCodecCallContext) => TInput; traits?: TTraits; } & JsonRoundTripConfig, ): Codec { @@ -45,13 +45,7 @@ export function defineTestCodec< return Promise.reject(error); } }, - decode: (wire, ctx) => { - try { - return Promise.resolve(userDecode(wire, ctx)); - } catch (error) { - return Promise.reject(error); - } - }, + decode: (wire, ctx) => userDecode(wire, ctx), encodeJson: (widenedConfig.encodeJson ?? identity) as (value: TInput) => JsonValue, decodeJson: (widenedConfig.decodeJson ?? identity) as (json: JsonValue) => TInput, } as Codec; diff --git a/packages/3-targets/6-adapters/sqlite-codec-testkit/test/aggregate-defaults.integration.test.ts b/packages/3-targets/6-adapters/sqlite-codec-testkit/test/aggregate-defaults.integration.test.ts index 6c28fd45c5c0..bff44cd1cb72 100644 --- a/packages/3-targets/6-adapters/sqlite-codec-testkit/test/aggregate-defaults.integration.test.ts +++ b/packages/3-targets/6-adapters/sqlite-codec-testkit/test/aggregate-defaults.integration.test.ts @@ -172,12 +172,14 @@ describe('SQLite aggregate defaults', { concurrent: false }, () => { const pastRange = aggregate('sum', 'sqlite/integer@1'); expect(pastRange.wire).toBe((MAX_SAFE + 1n).toString()); - await expect(pastRange.codec.decode(pastRange.wire, {})).rejects.toMatchObject({ - code: 'RUNTIME.DECODE_FAILED', - message: - 'sqlite/bigintnumber@1 value must be an integer within the safe integer range, got 9007199254740992', - meta: { codecId: 'sqlite/bigintnumber@1', received: '9007199254740992' }, - }); + expect(() => pastRange.codec.decode(pastRange.wire, {})).toThrow( + expect.objectContaining({ + code: 'RUNTIME.DECODE_FAILED', + message: + 'sqlite/bigintnumber@1 value must be an integer within the safe integer range, got 9007199254740992', + meta: { codecId: 'sqlite/bigintnumber@1', received: '9007199254740992' }, + }), + ); }); it('sums the same integers past 2^53 exactly through sumBigInt', async () => { diff --git a/packages/3-targets/6-adapters/sqlite/test/lower-to-execute-request.test.ts b/packages/3-targets/6-adapters/sqlite/test/lower-to-execute-request.test.ts index 0a62b69ee6bc..43932a68e06b 100644 --- a/packages/3-targets/6-adapters/sqlite/test/lower-to-execute-request.test.ts +++ b/packages/3-targets/6-adapters/sqlite/test/lower-to-execute-request.test.ts @@ -32,7 +32,7 @@ const ctx = { contract: {} as SqliteContract }; const transformingCodec = { id: 'test/transform@1', encode: async (value: unknown) => `ENC:${String(value).toUpperCase()}`, - decode: async (wire: unknown) => wire, + decode: (wire: unknown) => wire, encodeJson: (value: unknown) => value, decodeJson: (json: unknown) => json, } as unknown as Codec; @@ -215,7 +215,7 @@ const TEST_CODEC_ID = 'test/transform@1'; const transformingQueryCodec: Codec = { id: TEST_CODEC_ID, encode: async (value: unknown) => `ENC:${String(value).toUpperCase()}`, - decode: async (wire: unknown) => wire, + decode: (wire: unknown) => wire, encodeJson: (v) => v as never, decodeJson: (v) => v as never, }; @@ -284,7 +284,7 @@ class ExtTransformDescriptor extends CodecDescriptorImpl { ({ id: EXT_CODEC_ID, encode: async (value: unknown) => `ENC:${String(value).toUpperCase()}`, - decode: async (wire: unknown) => wire, + decode: (wire: unknown) => wire, encodeJson: (v: unknown) => v as never, decodeJson: (v: unknown) => v as never, }) as unknown as Codec; diff --git a/packages/3-targets/6-adapters/sqlite/test/sqlite-codec-registry-composition.test.ts b/packages/3-targets/6-adapters/sqlite/test/sqlite-codec-registry-composition.test.ts index ee663751f6fa..7eaaffdc7a30 100644 --- a/packages/3-targets/6-adapters/sqlite/test/sqlite-codec-registry-composition.test.ts +++ b/packages/3-targets/6-adapters/sqlite/test/sqlite-codec-registry-composition.test.ts @@ -63,7 +63,7 @@ class TestCodec extends CodecImpl return this.transform(value); } - async decode(wire: string, _ctx: CodecCallContext): Promise { + decode(wire: string, _ctx: CodecCallContext): string { return wire; } diff --git a/packages/3-targets/6-adapters/sqlite/test/structured-errors.test.ts b/packages/3-targets/6-adapters/sqlite/test/structured-errors.test.ts index 0fe5db9b350f..d3ebd2b0d204 100644 --- a/packages/3-targets/6-adapters/sqlite/test/structured-errors.test.ts +++ b/packages/3-targets/6-adapters/sqlite/test/structured-errors.test.ts @@ -128,7 +128,7 @@ describe('structured error codes', () => { const symbolCodec = { id: 'test/symbol@1', encode: async () => Symbol('wire'), - decode: async (wire: unknown) => wire, + decode: (wire: unknown) => wire, encodeJson: (value: unknown) => value, decodeJson: (json: unknown) => json, } as unknown as Codec; diff --git a/skills/prisma-8/upgrading/extension/upgrades/8.0.0-rc.11-to-8.0.0-rc.12/instructions.md b/skills/prisma-8/upgrading/extension/upgrades/8.0.0-rc.11-to-8.0.0-rc.12/instructions.md index 374b066bc0d2..8b465876a915 100644 --- a/skills/prisma-8/upgrading/extension/upgrades/8.0.0-rc.11-to-8.0.0-rc.12/instructions.md +++ b/skills/prisma-8/upgrading/extension/upgrades/8.0.0-rc.11-to-8.0.0-rc.12/instructions.md @@ -4,6 +4,8 @@ to: 8.0.0-rc.12 # contract.d.ts now orders every collection the way contract.json does; a re-emit reorders, nothing else. # Prepared include decoder specialization adds no consumer migration; retain existing entries below. changes: + - id: experimental-sync-codec-decoding + summary: For the non-mergeable sync-decoding benchmark branch only, return values synchronously from codec decode methods. - id: expression-codec-on-return-type summary: Move custom expression wrapper codec metadata to returnType.codec and remove the separate ExpressionImpl codec argument. - id: shared-preparable-envelope @@ -16,6 +18,10 @@ changes: summary: Preserve expression-valued limit and offset when consuming ORM CollectionState. --- +## `experimental-sync-codec-decoding` + +Only when testing the sync-decoding benchmark branch, change codec `decode` methods from `async decode(...): Promise` to `decode(...): T` and remove promise-lifting wrappers. Keep encoding asynchronous. Async-only decoders are unsupported by this experiment; do not adapt them by casting a promise to the decoded type. Update direct decoder assertions to expect returned values or synchronous throws rather than promise resolution or rejection. This experimental entry must not be published as release guidance. + ## `expression-codec-on-return-type` For custom SQL `Expression` wrappers, move an existing top-level `codec` reference into `returnType.codec`, preserving the complete reference including `typeParams`. Read metadata through `codecOf(expression)` or `expression.returnType.codec`, not `expression.codec`. Keep the existing `returnType.codecId` and nullability; wrappers without an explicit reference continue to use the declared codec id fallback. Do not remove or relocate unrelated codec fields on AST nodes, storage declarations, runtime bindings or scope descriptors. diff --git a/test/integration/test/sql-orm-client/connection-release.test.ts b/test/integration/test/sql-orm-client/connection-release.test.ts index b33269a24bc4..5ebe6543e4ae 100644 --- a/test/integration/test/sql-orm-client/connection-release.test.ts +++ b/test/integration/test/sql-orm-client/connection-release.test.ts @@ -91,14 +91,6 @@ async function createPooledRuntime(pool: Pool) { return { runtime, pool, parents, included, codec }; } -function gate() { - let resolve = () => {}; - const promise = new Promise((done) => { - resolve = done; - }); - return { promise, resolve }; -} - const expected = [ { id: 1, name: 'Alice', children: [{ id: 10 }] }, { id: 2, name: 'Bob', children: [{ id: 20 }] }, @@ -109,24 +101,16 @@ describe('integration/ORM buffered connection release', () => { 'makes the size-one pool available while an included parent is decoding', async () => { await withPooledRuntime(async ({ pool, parents, included, codec }) => { - const entered = gate(); - const resume = gate(); const decode = codec.decode.bind(codec); - const spy = vi.spyOn(codec, 'decode').mockImplementationOnce(async (wire, context) => { - entered.resolve(); - await resume.promise; + const spy = vi.spyOn(codec, 'decode').mockImplementationOnce((wire, context) => { + expect(pool.totalCount).toBe(1); + expect(pool.idleCount).toBe(1); + expect(pool.waitingCount).toBe(0); return decode(wire, context); }); - const first = Promise.resolve(included.all()); try { - await Promise.race([ - entered.promise, - first.then(() => { - throw new Error('Query finished without entering the parent decoder'); - }), - ]); - expect(pool.totalCount).toBe(1); - expect(pool.idleCount).toBe(1); + await expect(included.all()).resolves.toEqual(expected); + expect(spy).toHaveBeenCalledTimes(2); await expect( parents .select('id') @@ -135,12 +119,7 @@ describe('integration/ORM buffered connection release', () => { ).resolves.toEqual([{ id: 1 }, { id: 2 }]); expect(pool.waitingCount).toBe(0); } finally { - resume.resolve(); - try { - await expect(first).resolves.toEqual(expected); - } finally { - spy.mockRestore(); - } + spy.mockRestore(); } }); }, @@ -178,7 +157,9 @@ describe('integration/ORM buffered connection release', () => { await withPooledRuntime(async ({ pool, parents, included, codec }) => { const released = vi.fn(); pool.on('release', released); - const spy = vi.spyOn(codec, 'decode').mockRejectedValueOnce(new Error('Decoder failed')); + const spy = vi.spyOn(codec, 'decode').mockImplementationOnce(() => { + throw new Error('Decoder failed'); + }); try { await expect(included.all()).rejects.toThrow(); expect(pool.idleCount).toBe(1); diff --git a/test/integration/test/sql-orm-client/include-codecs.test.ts b/test/integration/test/sql-orm-client/include-codecs.test.ts index 9f5d8a87008a..24c51d15830b 100644 --- a/test/integration/test/sql-orm-client/include-codecs.test.ts +++ b/test/integration/test/sql-orm-client/include-codecs.test.ts @@ -40,7 +40,7 @@ class IncludedTextCodec extends CodecImpl< return value; } - async decode(wire: string, _ctx: CodecCallContext): Promise { + decode(wire: string, _ctx: CodecCallContext): string { return wire; } From 9a942be97dbbfd4da292d8e92021b88998fcfb87 Mon Sep 17 00:00:00 2001 From: Steven McClankerton Date: Tue, 15 Sep 2026 15:14:04 +0000 Subject: [PATCH 3/3] test: assert synchronous JSON codec parse errors Signed-off-by: Steven McClankerton --- .../3-targets/postgres/test/codecs-class.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/3-targets/3-targets/postgres/test/codecs-class.test.ts b/packages/3-targets/3-targets/postgres/test/codecs-class.test.ts index 6648393db0d0..fe9f7782e27a 100644 --- a/packages/3-targets/3-targets/postgres/test/codecs-class.test.ts +++ b/packages/3-targets/3-targets/postgres/test/codecs-class.test.ts @@ -257,8 +257,8 @@ describe('codecs-class', () => { expect(await codec.decode({ key: 'value' }, callCtx)).toEqual({ key: 'value' }); }); - it('rejects invalid JSON text with SyntaxError', async () => { - await expect(codec.decode('{invalid', callCtx)).rejects.toThrow(SyntaxError); + it('rejects invalid JSON text with SyntaxError', () => { + expect(() => codec.decode('{invalid', callCtx)).toThrow(SyntaxError); }); }); @@ -281,8 +281,8 @@ describe('codecs-class', () => { expect(await codec.decode([1, 2, 3], callCtx)).toEqual([1, 2, 3]); }); - it('rejects invalid JSON text with SyntaxError', async () => { - await expect(codec.decode('{invalid', callCtx)).rejects.toThrow(SyntaxError); + it('rejects invalid JSON text with SyntaxError', () => { + expect(() => codec.decode('{invalid', callCtx)).toThrow(SyntaxError); }); });