@prisma-next/sql-runtime 0.5.0-dev.9 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/sql-marker.ts CHANGED
@@ -1,17 +1,38 @@
1
+ import { APP_SPACE_ID } from '@prisma-next/framework-components/control';
1
2
  import type { MarkerStatement } from '@prisma-next/sql-relational-core/ast';
2
3
 
4
+ export { APP_SPACE_ID };
5
+
3
6
  export interface SqlStatement {
4
7
  readonly sql: string;
5
8
  readonly params: readonly unknown[];
6
9
  }
7
10
 
8
11
  export interface WriteMarkerInput {
12
+ /**
13
+ * Logical space identifier for this marker row. Required at every
14
+ * call site so the type system surfaces every place that needs to
15
+ * thread the value (rather than letting an `?? APP_SPACE_ID`
16
+ * fall-through silently collapse multi-space markers onto the
17
+ * `'app'` row). App-plan callers pass {@link APP_SPACE_ID}
18
+ * (`'app'`); per-extension callers pass the extension's space id.
19
+ */
20
+ readonly space: string;
9
21
  readonly storageHash: string;
10
22
  readonly profileHash: string;
11
23
  readonly contractJson?: unknown;
12
24
  readonly canonicalVersion?: number;
13
25
  readonly appTag?: string;
14
26
  readonly meta?: Record<string, unknown>;
27
+ /**
28
+ * Applied-invariants set on the marker.
29
+ *
30
+ * - `undefined` → existing column left untouched. Sign and
31
+ * verify-database paths use this; they don't accumulate invariants.
32
+ * - explicit value (including `[]`) → column overwritten with
33
+ * exactly that value.
34
+ */
35
+ readonly invariants?: readonly string[];
15
36
  }
16
37
 
17
38
  export const ensureSchemaStatement: SqlStatement = {
@@ -19,21 +40,33 @@ export const ensureSchemaStatement: SqlStatement = {
19
40
  params: [],
20
41
  };
21
42
 
43
+ /**
44
+ * Schema for `prisma_contract.marker`. The `space text` primary key
45
+ * supports one row per loaded contract space (`'app'`,
46
+ * `'<extension-id>'`, …); brand-new databases create this shape
47
+ * directly. Pre-1.0 single-row markers (no `space` column) are not
48
+ * auto-migrated — the target-specific migration runner detects the
49
+ * legacy shape at boot and surfaces a structured `LEGACY_MARKER_SHAPE`
50
+ * failure pointing the operator at re-running `dbInit`.
51
+ *
52
+ * @see specs/framework-mechanism.spec.md § 2.
53
+ */
22
54
  export const ensureTableStatement: SqlStatement = {
23
55
  sql: `create table if not exists prisma_contract.marker (
24
- id smallint primary key default 1,
56
+ space text not null primary key default '${APP_SPACE_ID}',
25
57
  core_hash text not null,
26
58
  profile_hash text not null,
27
59
  contract_json jsonb,
28
60
  canonical_version int,
29
61
  updated_at timestamptz not null default now(),
30
62
  app_tag text,
31
- meta jsonb not null default '{}'
63
+ meta jsonb not null default '{}',
64
+ invariants text[] not null default '{}'
32
65
  )`,
33
66
  params: [],
34
67
  };
35
68
 
36
- export function readContractMarker(): MarkerStatement {
69
+ export function readContractMarker(space: string): MarkerStatement {
37
70
  return {
38
71
  sql: `select
39
72
  core_hash,
@@ -42,10 +75,11 @@ export function readContractMarker(): MarkerStatement {
42
75
  canonical_version,
43
76
  updated_at,
44
77
  app_tag,
45
- meta
78
+ meta,
79
+ invariants
46
80
  from prisma_contract.marker
47
- where id = $1`,
48
- params: [1],
81
+ where space = $1`,
82
+ params: [space],
49
83
  };
50
84
  }
51
85
 
@@ -54,52 +88,56 @@ export interface WriteContractMarkerStatements {
54
88
  readonly update: SqlStatement;
55
89
  }
56
90
 
57
- export function writeContractMarker(input: WriteMarkerInput): WriteContractMarkerStatements {
58
- const baseParams: readonly unknown[] = [
59
- 1,
60
- input.storageHash,
61
- input.profileHash,
62
- input.contractJson ?? null,
63
- input.canonicalVersion ?? null,
64
- input.appTag ?? null,
65
- JSON.stringify(input.meta ?? {}),
91
+ /**
92
+ * Variable columns that participate in INSERT/UPDATE alongside the
93
+ * always-on `space = $1` and `updated_at = now()`. Each column declares
94
+ * its name, optional cast type, and parameter value; the placeholder
95
+ * (`$N`) is computed positionally below — adding or reordering a
96
+ * column doesn't desync indices. `invariants` only appears when the
97
+ * caller supplies it — see `WriteMarkerInput.invariants`.
98
+ */
99
+ function markerColumns(
100
+ input: WriteMarkerInput,
101
+ ): ReadonlyArray<{ readonly name: string; readonly type?: string; readonly param: unknown }> {
102
+ return [
103
+ { name: 'core_hash', param: input.storageHash },
104
+ { name: 'profile_hash', param: input.profileHash },
105
+ { name: 'contract_json', type: 'jsonb', param: input.contractJson ?? null },
106
+ { name: 'canonical_version', param: input.canonicalVersion ?? null },
107
+ { name: 'app_tag', param: input.appTag ?? null },
108
+ { name: 'meta', type: 'jsonb', param: JSON.stringify(input.meta ?? {}) },
109
+ ...(input.invariants !== undefined
110
+ ? [{ name: 'invariants' as const, type: 'text[]' as const, param: input.invariants }]
111
+ : []),
66
112
  ];
113
+ }
67
114
 
68
- const insert: SqlStatement = {
69
- sql: `insert into prisma_contract.marker (
70
- id,
71
- core_hash,
72
- profile_hash,
73
- contract_json,
74
- canonical_version,
75
- updated_at,
76
- app_tag,
77
- meta
78
- ) values (
79
- $1,
80
- $2,
81
- $3,
82
- $4::jsonb,
83
- $5,
84
- now(),
85
- $6,
86
- $7::jsonb
87
- )`,
88
- params: baseParams,
89
- };
115
+ export function writeContractMarker(input: WriteMarkerInput): WriteContractMarkerStatements {
116
+ const cols = markerColumns(input);
117
+ // $1 is reserved for `space`; subsequent positions follow the order of cols.
118
+ const placed = cols.map((c, i) => ({
119
+ name: c.name,
120
+ expr: c.type ? `$${i + 2}::${c.type}` : `$${i + 2}`,
121
+ param: c.param,
122
+ }));
123
+ const params: readonly unknown[] = [input.space, ...placed.map((c) => c.param)];
90
124
 
91
- const update: SqlStatement = {
92
- sql: `update prisma_contract.marker set
93
- core_hash = $2,
94
- profile_hash = $3,
95
- contract_json = $4::jsonb,
96
- canonical_version = $5,
97
- updated_at = now(),
98
- app_tag = $6,
99
- meta = $7::jsonb
100
- where id = $1`,
101
- params: baseParams,
102
- };
125
+ // `updated_at = now()` is a SQL literal with no parameter slot, so it
126
+ // sits outside `placed` and is appended directly to each statement.
127
+ const insertColumns = ['space', ...placed.map((c) => c.name), 'updated_at'].join(', ');
128
+ const insertValues = ['$1', ...placed.map((c) => c.expr), 'now()'].join(', ');
129
+ const setClauses = [...placed.map((c) => `${c.name} = ${c.expr}`), 'updated_at = now()'].join(
130
+ ', ',
131
+ );
103
132
 
104
- return { insert, update };
133
+ return {
134
+ insert: {
135
+ sql: `insert into prisma_contract.marker (${insertColumns}) values (${insertValues})`,
136
+ params,
137
+ },
138
+ update: {
139
+ sql: `update prisma_contract.marker set ${setClauses} where space = $1`,
140
+ params,
141
+ },
142
+ };
105
143
  }
@@ -5,8 +5,10 @@ import type {
5
5
  } from '@prisma-next/framework-components/execution';
6
6
  import {
7
7
  AsyncIterableResult,
8
+ checkAborted,
8
9
  checkMiddlewareCompatibility,
9
10
  RuntimeCore,
11
+ type RuntimeExecuteOptions,
10
12
  type RuntimeLog,
11
13
  runtimeError,
12
14
  runWithMiddleware,
@@ -15,22 +17,29 @@ import type { SqlStorage } from '@prisma-next/sql-contract/types';
15
17
  import type {
16
18
  Adapter,
17
19
  AnyQueryAst,
18
- CodecRegistry,
20
+ ContractCodecRegistry,
19
21
  LoweredStatement,
22
+ SqlCodecCallContext,
20
23
  SqlDriver,
21
24
  SqlQueryable,
22
25
  SqlTransaction,
23
26
  } from '@prisma-next/sql-relational-core/ast';
27
+ import { validateParamRefRefs } from '@prisma-next/sql-relational-core/ast';
28
+ import {
29
+ createSqlParamRefMutator,
30
+ type SqlParamRefMutator,
31
+ type SqlParamRefMutatorInternal,
32
+ } from '@prisma-next/sql-relational-core/middleware';
24
33
  import type { SqlExecutionPlan, SqlQueryPlan } from '@prisma-next/sql-relational-core/plan';
25
- import type { JsonSchemaValidatorRegistry } from '@prisma-next/sql-relational-core/query-lane-context';
34
+ import type { CodecDescriptorRegistry } from '@prisma-next/sql-relational-core/query-lane-context';
26
35
  import type { RuntimeScope } from '@prisma-next/sql-relational-core/types';
27
36
  import { ifDefined } from '@prisma-next/utils/defined';
28
37
  import { decodeRow } from './codecs/decoding';
29
38
  import { encodeParams } from './codecs/encoding';
30
39
  import { validateCodecRegistryCompleteness } from './codecs/validation';
40
+ import { computeSqlContentHash } from './content-hash';
31
41
  import { computeSqlFingerprint } from './fingerprint';
32
42
  import { lowerSqlPlan } from './lower-sql-plan';
33
- import { parseContractMarkerRow } from './marker';
34
43
  import { runBeforeCompileChain } from './middleware/before-compile-chain';
35
44
  import type { SqlMiddleware, SqlMiddlewareContext } from './middleware/sql-middleware';
36
45
  import type {
@@ -86,25 +95,15 @@ export interface Runtime extends RuntimeQueryable {
86
95
  export interface RuntimeConnection extends RuntimeQueryable {
87
96
  transaction(): Promise<RuntimeTransaction>;
88
97
  /**
89
- * Returns the connection to the pool for reuse. Only call this when the
90
- * connection is known to be in a clean state. If a transaction
91
- * commit/rollback failed or the connection is otherwise suspect, call
92
- * `destroy(reason)` instead.
98
+ * Returns the connection to the pool for reuse. Only call this when the connection is known to be in a clean state. If a transaction commit/rollback failed or the connection is otherwise suspect, call `destroy(reason)` instead.
93
99
  */
94
100
  release(): Promise<void>;
95
101
  /**
96
- * Evicts the connection so it is never reused. Call this when the
97
- * connection may be in an indeterminate state (e.g. a failed rollback
98
- * leaving an open transaction, or a broken socket).
102
+ * Evicts the connection so it is never reused. Call this when the connection may be in an indeterminate state (e.g. a failed rollback leaving an open transaction, or a broken socket).
99
103
  *
100
- * If teardown fails the error is propagated and the connection remains
101
- * retryable, so the caller can decide whether to swallow the failure or
102
- * retry cleanup. Calling destroy() or release() more than once after a
103
- * successful teardown is caller error.
104
+ * If teardown fails the error is propagated and the connection remains retryable, so the caller can decide whether to swallow the failure or retry cleanup. Calling destroy() or release() more than once after a successful teardown is caller error.
104
105
  *
105
- * `reason` is advisory context only. It may be surfaced to driver-level
106
- * observability hooks (e.g. pg-pool's `'release'` event) but does not
107
- * influence eviction behavior and is not rethrown.
106
+ * `reason` is advisory context only. It may be surfaced to driver-level observability hooks (e.g. pg-pool's `'release'` event) but does not influence eviction behavior and is not rethrown.
108
107
  */
109
108
  destroy(reason?: unknown): Promise<void>;
110
109
  }
@@ -126,6 +125,10 @@ function isExecutionPlan(plan: SqlExecutionPlan | SqlQueryPlan): plan is SqlExec
126
125
  return 'sql' in plan;
127
126
  }
128
127
 
128
+ // v8 ignore next 2
129
+ const noopLogSink = (): void => {};
130
+ const noopLog: Log = { info: noopLogSink, warn: noopLogSink, error: noopLogSink };
131
+
129
132
  class SqlRuntimeImpl<TContract extends Contract<SqlStorage> = Contract<SqlStorage>>
130
133
  extends RuntimeCore<SqlQueryPlan, SqlExecutionPlan, SqlMiddleware>
131
134
  implements Runtime
@@ -134,8 +137,8 @@ class SqlRuntimeImpl<TContract extends Contract<SqlStorage> = Contract<SqlStorag
134
137
  private readonly adapter: Adapter<AnyQueryAst, Contract<SqlStorage>, LoweredStatement>;
135
138
  private readonly driver: SqlDriver<unknown>;
136
139
  private readonly familyAdapter: RuntimeFamilyAdapter<Contract<SqlStorage>>;
137
- private readonly codecRegistry: CodecRegistry;
138
- private readonly jsonSchemaValidators: JsonSchemaValidatorRegistry | undefined;
140
+ private readonly contractCodecs: ContractCodecRegistry;
141
+ private readonly codecDescriptors: CodecDescriptorRegistry;
139
142
  private readonly sqlCtx: SqlMiddlewareContext;
140
143
  private readonly verify: RuntimeVerifyOptions;
141
144
  private codecRegistryValidated: boolean;
@@ -156,11 +159,9 @@ class SqlRuntimeImpl<TContract extends Contract<SqlStorage> = Contract<SqlStorag
156
159
  contract: context.contract,
157
160
  mode: mode ?? 'strict',
158
161
  now: () => Date.now(),
159
- log: log ?? {
160
- info: () => {},
161
- warn: () => {},
162
- error: () => {},
163
- },
162
+ log: log ?? noopLog,
163
+ // ctx is only invoked by runWithMiddleware with execs this runtime lowered; the framework parameter type is the cross-family base.
164
+ contentHash: (exec) => computeSqlContentHash(exec as SqlExecutionPlan),
164
165
  };
165
166
 
166
167
  super({ middleware: middleware ?? [], ctx: sqlCtx });
@@ -169,8 +170,8 @@ class SqlRuntimeImpl<TContract extends Contract<SqlStorage> = Contract<SqlStorag
169
170
  this.adapter = adapter;
170
171
  this.driver = driver;
171
172
  this.familyAdapter = new SqlFamilyAdapter(context.contract, adapter.profile);
172
- this.codecRegistry = context.codecs;
173
- this.jsonSchemaValidators = context.jsonSchemaValidators;
173
+ this.contractCodecs = context.contractCodecs;
174
+ this.codecDescriptors = context.codecDescriptors;
174
175
  this.sqlCtx = sqlCtx;
175
176
  this.verify = verify;
176
177
  this.codecRegistryValidated = false;
@@ -179,30 +180,32 @@ class SqlRuntimeImpl<TContract extends Contract<SqlStorage> = Contract<SqlStorag
179
180
  this._telemetry = null;
180
181
 
181
182
  if (verify.mode === 'startup') {
182
- validateCodecRegistryCompleteness(this.codecRegistry, context.contract);
183
+ validateCodecRegistryCompleteness(this.codecDescriptors, context.contract);
183
184
  this.codecRegistryValidated = true;
184
185
  }
185
186
  }
186
187
 
187
188
  /**
188
- * Lower a `SqlQueryPlan` (AST + meta) into a `SqlExecutionPlan` with
189
- * encoded parameters ready for the driver. This is the single point at
190
- * which params transition from app-layer values to driver wire-format.
189
+ * Lower a `SqlQueryPlan` (AST + meta) into a `SqlExecutionPlan` with encoded parameters ready for the driver. This is the single point at which params transition from app-layer values to driver wire-format.
190
+ *
191
+ * `ctx: SqlCodecCallContext` is forwarded to `encodeParams` so per-query cancellation reaches every codec body during parameter encoding. The framework abstract typed this as `CodecCallContext`; the SQL family narrows it to the SQL-specific extension. SQL params do not populate `ctx.column` — encode-side column metadata is the middleware's domain.
191
192
  */
192
- protected override async lower(plan: SqlQueryPlan): Promise<SqlExecutionPlan> {
193
+ protected override async lower(
194
+ plan: SqlQueryPlan,
195
+ ctx: SqlCodecCallContext,
196
+ ): Promise<SqlExecutionPlan> {
197
+ validateParamRefRefs(plan.ast, this.codecDescriptors);
193
198
  const lowered = lowerSqlPlan(this.adapter, this.contract, plan);
194
199
  return Object.freeze({
195
200
  ...lowered,
196
- params: await encodeParams(lowered, this.codecRegistry),
201
+ params: await encodeParams(lowered, ctx, this.contractCodecs),
197
202
  });
198
203
  }
199
204
 
200
205
  /**
201
- * Default driver invocation. Production execution paths override the
202
- * queryable target (e.g. transaction or connection) by going through
203
- * `executeAgainstQueryable`; this implementation supports any caller of
204
- * `super.execute(plan)` and the abstract-base contract.
206
+ * Default driver invocation required by the abstract `RuntimeCore` contract. Every production path overrides `execute()` and routes through `executeAgainstQueryable`, so this hook is defensive only — subclasses that delegate back to `super.execute()` would land here.
205
207
  */
208
+ // v8 ignore next 6
206
209
  protected override runDriver(exec: SqlExecutionPlan): AsyncIterable<Record<string, unknown>> {
207
210
  return this.driver.execute<Record<string, unknown>>({
208
211
  sql: exec.sql,
@@ -211,11 +214,8 @@ class SqlRuntimeImpl<TContract extends Contract<SqlStorage> = Contract<SqlStorag
211
214
  }
212
215
 
213
216
  /**
214
- * SQL pre-compile hook. Runs the registered middleware `beforeCompile`
215
- * chain over the plan's draft (AST + meta) and returns a `SqlQueryPlan`
216
- * with the rewritten AST and meta when the chain mutates them. The chain
217
- * re-derives `meta.paramDescriptors` from the rewritten AST so descriptors
218
- * stay in lockstep with the params the adapter will emit during lowering.
217
+ * SQL pre-compile hook. Runs the registered middleware `beforeCompile` chain over the plan's draft (AST + meta). Returns the original plan unchanged when no middleware rewrote the AST; otherwise returns a new plan carrying the rewritten AST and meta. The AST is the authoritative source of execution metadata, so a rewrite needs no sidecar reconciliation here — the lowering adapter and the encoder both walk the rewritten
218
+ * AST directly.
219
219
  */
220
220
  protected override async runBeforeCompile(plan: SqlQueryPlan): Promise<SqlQueryPlan> {
221
221
  const rewrittenDraft = await runBeforeCompileChain(
@@ -230,24 +230,43 @@ class SqlRuntimeImpl<TContract extends Contract<SqlStorage> = Contract<SqlStorag
230
230
 
231
231
  override execute<Row>(
232
232
  plan: (SqlExecutionPlan<unknown> | SqlQueryPlan<unknown>) & { readonly _row?: Row },
233
+ options?: RuntimeExecuteOptions,
233
234
  ): AsyncIterableResult<Row> {
234
- return this.executeAgainstQueryable<Row>(plan, this.driver);
235
+ return this.executeAgainstQueryable<Row>(plan, this.driver, options);
235
236
  }
236
237
 
237
238
  private executeAgainstQueryable<Row>(
238
239
  plan: SqlExecutionPlan<unknown> | SqlQueryPlan<unknown>,
239
240
  queryable: SqlQueryable,
241
+ options?: RuntimeExecuteOptions,
240
242
  ): AsyncIterableResult<Row> {
241
243
  this.ensureCodecRegistryValidated();
242
244
 
243
245
  const self = this;
246
+ const signal = options?.signal;
247
+ // One ctx per execute() call — the same reference is shared by encodeParams (lower), decodeRow (per-row), and the stream loop's between-row checks. Per-cell ctx allocations inside decodeField add `column` for resolvable cells without re-wrapping the signal. The ctx object is always allocated; the `signal` field is only included when a signal was supplied (exactOptionalPropertyTypes).
248
+ const codecCtx: SqlCodecCallContext = signal === undefined ? {} : { signal };
249
+ // Per-execute view of the middleware ctx that carries the per-query
250
+ // signal. `self.ctx` is allocated once at construction (no signal); we
251
+ // shallow-clone it here so middleware sees the same `AbortSignal`
252
+ // reference threaded into `codecCtx.signal` (ADR 207 identity).
253
+ const execMiddlewareCtx = signal === undefined ? self.ctx : { ...self.ctx, signal };
254
+
244
255
  const generator = async function* (): AsyncGenerator<Row, void, unknown> {
245
- const exec: SqlExecutionPlan = isExecutionPlan(plan)
246
- ? Object.freeze({
247
- ...plan,
248
- params: await encodeParams(plan, self.codecRegistry),
249
- })
250
- : await self.lower(await self.runBeforeCompile(plan));
256
+ checkAborted(codecCtx, 'stream');
257
+
258
+ let exec: SqlExecutionPlan;
259
+ if (isExecutionPlan(plan)) {
260
+ if (plan.ast) {
261
+ validateParamRefRefs(plan.ast, self.codecDescriptors);
262
+ }
263
+ exec = Object.freeze({
264
+ ...plan,
265
+ params: await encodeParams(plan, codecCtx, self.contractCodecs),
266
+ });
267
+ } else {
268
+ exec = await self.lower(await self.runBeforeCompile(plan), codecCtx);
269
+ }
251
270
 
252
271
  self.familyAdapter.validatePlan(exec, self.contract);
253
272
  self._telemetry = null;
@@ -268,25 +287,42 @@ class SqlRuntimeImpl<TContract extends Contract<SqlStorage> = Contract<SqlStorag
268
287
  await self.verifyMarker();
269
288
  }
270
289
 
271
- const stream = runWithMiddleware<SqlExecutionPlan, Record<string, unknown>>(
290
+ const paramsMutator: SqlParamRefMutatorInternal = createSqlParamRefMutator(exec);
291
+ const stream = runWithMiddleware<
292
+ SqlExecutionPlan,
293
+ Record<string, unknown>,
294
+ SqlParamRefMutator
295
+ >(
272
296
  exec,
273
297
  self.middleware,
274
- self.ctx,
298
+ execMiddlewareCtx,
275
299
  () =>
276
300
  queryable.execute<Record<string, unknown>>({
277
301
  sql: exec.sql,
278
- params: exec.params,
302
+ // Read params after the `beforeExecute` middleware chain has
303
+ // run so that any `mutator.replaceValue(...)` calls land in
304
+ // the driver-bound params array. When no middleware mutated,
305
+ // `currentParams()` returns `exec.params` by reference identity.
306
+ params: paramsMutator.currentParams(),
279
307
  }),
308
+ paramsMutator,
280
309
  );
281
310
 
282
- for await (const rawRow of stream) {
283
- const decodedRow = await decodeRow(
284
- rawRow,
285
- exec,
286
- self.codecRegistry,
287
- self.jsonSchemaValidators,
288
- );
289
- yield decodedRow as Row;
311
+ // Manually drive the driver's async iterator so the between-row abort check fires *before* requesting the next row. With a `for await...of` loop the runtime would await `iterator.next()` first, leaving a window where one extra row is pulled through the driver after the signal aborted.
312
+ const iterator = stream[Symbol.asyncIterator]();
313
+ try {
314
+ while (true) {
315
+ checkAborted(codecCtx, 'stream');
316
+ const next = await iterator.next();
317
+ if (next.done) {
318
+ break;
319
+ }
320
+ const decodedRow = await decodeRow(next.value, exec, codecCtx, self.contractCodecs);
321
+ yield decodedRow as Row;
322
+ }
323
+ } finally {
324
+ // Best-effort iterator cleanup so the driver can release its resources whether the stream finished normally, threw, or was abandoned by the consumer.
325
+ await iterator.return?.();
290
326
  }
291
327
 
292
328
  outcome = 'success';
@@ -320,8 +356,9 @@ class SqlRuntimeImpl<TContract extends Contract<SqlStorage> = Contract<SqlStorag
320
356
  },
321
357
  execute<Row>(
322
358
  plan: (SqlExecutionPlan<unknown> | SqlQueryPlan<unknown>) & { readonly _row?: Row },
359
+ options?: RuntimeExecuteOptions,
323
360
  ): AsyncIterableResult<Row> {
324
- return self.executeAgainstQueryable<Row>(plan, driverConn);
361
+ return self.executeAgainstQueryable<Row>(plan, driverConn, options);
325
362
  },
326
363
  };
327
364
 
@@ -339,8 +376,9 @@ class SqlRuntimeImpl<TContract extends Contract<SqlStorage> = Contract<SqlStorag
339
376
  },
340
377
  execute<Row>(
341
378
  plan: (SqlExecutionPlan<unknown> | SqlQueryPlan<unknown>) & { readonly _row?: Row },
379
+ options?: RuntimeExecuteOptions,
342
380
  ): AsyncIterableResult<Row> {
343
- return self.executeAgainstQueryable<Row>(plan, driverTx);
381
+ return self.executeAgainstQueryable<Row>(plan, driverTx, options);
344
382
  },
345
383
  };
346
384
  }
@@ -355,24 +393,15 @@ class SqlRuntimeImpl<TContract extends Contract<SqlStorage> = Contract<SqlStorag
355
393
 
356
394
  private ensureCodecRegistryValidated(): void {
357
395
  if (!this.codecRegistryValidated) {
358
- validateCodecRegistryCompleteness(this.codecRegistry, this.contract);
396
+ validateCodecRegistryCompleteness(this.codecDescriptors, this.contract);
359
397
  this.codecRegistryValidated = true;
360
398
  }
361
399
  }
362
400
 
363
401
  private async verifyMarker(): Promise<void> {
364
- if (this.verify.mode === 'always') {
365
- this.verified = false;
366
- }
367
-
368
- if (this.verified) {
369
- return;
370
- }
371
-
372
- const readStatement = this.familyAdapter.markerReader.readMarkerStatement();
373
- const result = await this.driver.query(readStatement.sql, readStatement.params);
402
+ const readResult = await this.familyAdapter.markerReader.readMarker(this.driver);
374
403
 
375
- if (result.rows.length === 0) {
404
+ if (readResult.kind !== 'present') {
376
405
  if (this.verify.requireMarker) {
377
406
  throw runtimeError('CONTRACT.MARKER_MISSING', 'Contract marker not found in database');
378
407
  }
@@ -381,7 +410,7 @@ class SqlRuntimeImpl<TContract extends Contract<SqlStorage> = Contract<SqlStorag
381
410
  return;
382
411
  }
383
412
 
384
- const marker = parseContractMarkerRow(result.rows[0]);
413
+ const marker = readResult.record;
385
414
 
386
415
  const contract = this.contract as {
387
416
  storage: { storageHash: string };
@@ -454,11 +483,12 @@ export async function withTransaction<R>(
454
483
  },
455
484
  execute<Row>(
456
485
  plan: (SqlExecutionPlan<unknown> | SqlQueryPlan<unknown>) & { readonly _row?: Row },
486
+ options?: RuntimeExecuteOptions,
457
487
  ): AsyncIterableResult<Row> {
458
488
  if (invalidated) {
459
489
  throw transactionClosedError();
460
490
  }
461
- const inner = transaction.execute(plan);
491
+ const inner = transaction.execute(plan, options);
462
492
  const guarded = async function* (): AsyncGenerator<Row, void, unknown> {
463
493
  for await (const row of inner) {
464
494
  if (invalidated) {
@@ -475,11 +505,7 @@ export async function withTransaction<R>(
475
505
  const destroyConnection = async (reason: unknown): Promise<void> => {
476
506
  if (connectionDisposed) return;
477
507
  connectionDisposed = true;
478
- // SqlConnection.destroy() propagates teardown errors so callers can
479
- // decide what to do with them. Here, we're already about to throw a
480
- // more informative error describing why we're evicting the connection
481
- // (rollback/commit failure), so swallowing the teardown error is the
482
- // right call — surfacing it would mask the original cause.
508
+ // SqlConnection.destroy() propagates teardown errors so callers can decide what to do with them. Here, we're already about to throw a more informative error describing why we're evicting the connection (rollback/commit failure), so swallowing the teardown error is the right call — surfacing it would mask the original cause.
483
509
  await connection.destroy(reason).catch(() => undefined);
484
510
  };
485
511
 
@@ -508,17 +534,9 @@ export async function withTransaction<R>(
508
534
  try {
509
535
  await transaction.commit();
510
536
  } catch (commitError) {
511
- // After a failed COMMIT the server-side transaction may be: (a) already
512
- // committed (error on response path), (b) already rolled back (deferred
513
- // constraint / serialization failure), or (c) still open (COMMIT never
514
- // reached the server). Attempt a best-effort rollback to cover (c) and
515
- // confirm the protocol is healthy.
537
+ // After a failed COMMIT the server-side transaction may be: (a) already committed (error on response path), (b) already rolled back (deferred constraint / serialization failure), or (c) still open (COMMIT never reached the server). Attempt a best-effort rollback to cover (c) and confirm the protocol is healthy.
516
538
  //
517
- // If rollback succeeds, the server is definitely no longer in a
518
- // transaction (no-op in (a)/(b), real cleanup in (c)) and we've just
519
- // proved the connection round-trips correctly — it's safe to return
520
- // to the pool. If rollback fails, the connection state is ambiguous
521
- // (broken socket, protocol desync, etc.) and we must destroy it.
539
+ // If rollback succeeds, the server is definitely no longer in a transaction (no-op in (a)/(b), real cleanup in (c)) and we've just proved the connection round-trips correctly — it's safe to return to the pool. If rollback fails, the connection state is ambiguous (broken socket, protocol desync, etc.) and we must destroy it.
522
540
  try {
523
541
  await transaction.rollback();
524
542
  } catch {