@prisma-next/sql-runtime 0.5.0-dev.8 → 0.5.0-dev.80

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.
@@ -1,39 +1,53 @@
1
- import type { Contract, ExecutionPlan } from '@prisma-next/contract/types';
1
+ import type { Contract } from '@prisma-next/contract/types';
2
2
  import type {
3
3
  ExecutionStackInstance,
4
4
  RuntimeDriverInstance,
5
5
  } from '@prisma-next/framework-components/execution';
6
- import { checkMiddlewareCompatibility } from '@prisma-next/framework-components/runtime';
7
- import type {
8
- Log,
9
- RuntimeCore,
10
- RuntimeCoreOptions,
11
- RuntimeTelemetryEvent,
12
- RuntimeVerifyOptions,
13
- TelemetryOutcome,
14
- } from '@prisma-next/runtime-executor';
15
6
  import {
16
7
  AsyncIterableResult,
17
- createRuntimeCore,
8
+ checkAborted,
9
+ checkMiddlewareCompatibility,
10
+ RuntimeCore,
11
+ type RuntimeExecuteOptions,
12
+ type RuntimeLog,
18
13
  runtimeError,
19
- } from '@prisma-next/runtime-executor';
14
+ runWithMiddleware,
15
+ } from '@prisma-next/framework-components/runtime';
20
16
  import type { SqlStorage } from '@prisma-next/sql-contract/types';
21
17
  import type {
22
18
  Adapter,
23
19
  AnyQueryAst,
24
- CodecRegistry,
20
+ ContractCodecRegistry,
25
21
  LoweredStatement,
22
+ SqlCodecCallContext,
26
23
  SqlDriver,
24
+ SqlQueryable,
25
+ SqlTransaction,
27
26
  } from '@prisma-next/sql-relational-core/ast';
28
- import type { SqlQueryPlan } from '@prisma-next/sql-relational-core/plan';
29
- import type { JsonSchemaValidatorRegistry } from '@prisma-next/sql-relational-core/query-lane-context';
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';
33
+ import type { SqlExecutionPlan, SqlQueryPlan } from '@prisma-next/sql-relational-core/plan';
34
+ import type { CodecDescriptorRegistry } from '@prisma-next/sql-relational-core/query-lane-context';
35
+ import type { RuntimeScope } from '@prisma-next/sql-relational-core/types';
30
36
  import { ifDefined } from '@prisma-next/utils/defined';
31
37
  import { decodeRow } from './codecs/decoding';
32
38
  import { encodeParams } from './codecs/encoding';
33
39
  import { validateCodecRegistryCompleteness } from './codecs/validation';
40
+ import { computeSqlContentHash } from './content-hash';
41
+ import { computeSqlFingerprint } from './fingerprint';
34
42
  import { lowerSqlPlan } from './lower-sql-plan';
35
43
  import { runBeforeCompileChain } from './middleware/before-compile-chain';
36
- import type { SqlMiddleware } from './middleware/sql-middleware';
44
+ import type { SqlMiddleware, SqlMiddlewareContext } from './middleware/sql-middleware';
45
+ import type {
46
+ RuntimeFamilyAdapter,
47
+ RuntimeTelemetryEvent,
48
+ RuntimeVerifyOptions,
49
+ TelemetryOutcome,
50
+ } from './runtime-spi';
37
51
  import type {
38
52
  ExecutionContext,
39
53
  SqlRuntimeAdapterInstance,
@@ -41,6 +55,8 @@ import type {
41
55
  } from './sql-context';
42
56
  import { SqlFamilyAdapter } from './sql-family-adapter';
43
57
 
58
+ export type Log = RuntimeLog;
59
+
44
60
  export interface RuntimeOptions<TContract extends Contract<SqlStorage> = Contract<SqlStorage>> {
45
61
  readonly context: ExecutionContext<TContract>;
46
62
  readonly adapter: Adapter<AnyQueryAst, Contract<SqlStorage>, LoweredStatement>;
@@ -79,25 +95,15 @@ export interface Runtime extends RuntimeQueryable {
79
95
  export interface RuntimeConnection extends RuntimeQueryable {
80
96
  transaction(): Promise<RuntimeTransaction>;
81
97
  /**
82
- * Returns the connection to the pool for reuse. Only call this when the
83
- * connection is known to be in a clean state. If a transaction
84
- * commit/rollback failed or the connection is otherwise suspect, call
85
- * `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.
86
99
  */
87
100
  release(): Promise<void>;
88
101
  /**
89
- * Evicts the connection so it is never reused. Call this when the
90
- * connection may be in an indeterminate state (e.g. a failed rollback
91
- * 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).
92
103
  *
93
- * If teardown fails the error is propagated and the connection remains
94
- * retryable, so the caller can decide whether to swallow the failure or
95
- * retry cleanup. Calling destroy() or release() more than once after a
96
- * 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.
97
105
  *
98
- * `reason` is advisory context only. It may be surfaced to driver-level
99
- * observability hooks (e.g. pg-pool's `'release'` event) but does not
100
- * 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.
101
107
  */
102
108
  destroy(reason?: unknown): Promise<void>;
103
109
  }
@@ -107,39 +113,41 @@ export interface RuntimeTransaction extends RuntimeQueryable {
107
113
  rollback(): Promise<void>;
108
114
  }
109
115
 
110
- export interface RuntimeQueryable {
111
- execute<Row = Record<string, unknown>>(
112
- plan: ExecutionPlan<Row> | SqlQueryPlan<Row>,
113
- ): AsyncIterableResult<Row>;
114
- }
116
+ export interface RuntimeQueryable extends RuntimeScope {}
115
117
 
116
118
  export interface TransactionContext extends RuntimeQueryable {
117
119
  readonly invalidated: boolean;
118
120
  }
119
121
 
120
- interface CoreQueryable {
121
- execute<Row = Record<string, unknown>>(plan: ExecutionPlan<Row>): AsyncIterableResult<Row>;
122
+ export type { RuntimeTelemetryEvent, RuntimeVerifyOptions, TelemetryOutcome };
123
+
124
+ function isExecutionPlan(plan: SqlExecutionPlan | SqlQueryPlan): plan is SqlExecutionPlan {
125
+ return 'sql' in plan;
122
126
  }
123
127
 
124
- export type { RuntimeTelemetryEvent, RuntimeVerifyOptions, TelemetryOutcome };
128
+ // v8 ignore next 2
129
+ const noopLogSink = (): void => {};
130
+ const noopLog: Log = { info: noopLogSink, warn: noopLogSink, error: noopLogSink };
125
131
 
126
132
  class SqlRuntimeImpl<TContract extends Contract<SqlStorage> = Contract<SqlStorage>>
133
+ extends RuntimeCore<SqlQueryPlan, SqlExecutionPlan, SqlMiddleware>
127
134
  implements Runtime
128
135
  {
129
- private readonly core: RuntimeCore<TContract, SqlDriver<unknown>, SqlMiddleware>;
130
136
  private readonly contract: TContract;
131
137
  private readonly adapter: Adapter<AnyQueryAst, Contract<SqlStorage>, LoweredStatement>;
132
- private readonly codecRegistry: CodecRegistry;
133
- private readonly jsonSchemaValidators: JsonSchemaValidatorRegistry | undefined;
138
+ private readonly driver: SqlDriver<unknown>;
139
+ private readonly familyAdapter: RuntimeFamilyAdapter<Contract<SqlStorage>>;
140
+ private readonly contractCodecs: ContractCodecRegistry;
141
+ private readonly codecDescriptors: CodecDescriptorRegistry;
142
+ private readonly sqlCtx: SqlMiddlewareContext;
143
+ private readonly verify: RuntimeVerifyOptions;
134
144
  private codecRegistryValidated: boolean;
145
+ private verified: boolean;
146
+ private startupVerified: boolean;
147
+ private _telemetry: RuntimeTelemetryEvent | null;
135
148
 
136
149
  constructor(options: RuntimeOptions<TContract>) {
137
150
  const { context, adapter, driver, verify, middleware, mode, log } = options;
138
- this.contract = context.contract;
139
- this.adapter = adapter;
140
- this.codecRegistry = context.codecs;
141
- this.jsonSchemaValidators = context.jsonSchemaValidators;
142
- this.codecRegistryValidated = false;
143
151
 
144
152
  if (middleware) {
145
153
  for (const mw of middleware) {
@@ -147,128 +155,310 @@ class SqlRuntimeImpl<TContract extends Contract<SqlStorage> = Contract<SqlStorag
147
155
  }
148
156
  }
149
157
 
150
- const familyAdapter = new SqlFamilyAdapter(context.contract, adapter.profile);
151
-
152
- const coreOptions: RuntimeCoreOptions<TContract, SqlDriver<unknown>, SqlMiddleware> = {
153
- familyAdapter,
154
- driver,
155
- verify,
156
- ...ifDefined('middleware', middleware),
157
- ...ifDefined('mode', mode),
158
- ...ifDefined('log', log),
158
+ const sqlCtx: SqlMiddlewareContext = {
159
+ contract: context.contract,
160
+ mode: mode ?? 'strict',
161
+ now: () => Date.now(),
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),
159
165
  };
160
166
 
161
- this.core = createRuntimeCore(coreOptions);
167
+ super({ middleware: middleware ?? [], ctx: sqlCtx });
168
+
169
+ this.contract = context.contract;
170
+ this.adapter = adapter;
171
+ this.driver = driver;
172
+ this.familyAdapter = new SqlFamilyAdapter(context.contract, adapter.profile);
173
+ this.contractCodecs = context.contractCodecs;
174
+ this.codecDescriptors = context.codecDescriptors;
175
+ this.sqlCtx = sqlCtx;
176
+ this.verify = verify;
177
+ this.codecRegistryValidated = false;
178
+ this.verified = verify.mode === 'startup' ? false : verify.mode === 'always';
179
+ this.startupVerified = false;
180
+ this._telemetry = null;
162
181
 
163
182
  if (verify.mode === 'startup') {
164
- validateCodecRegistryCompleteness(this.codecRegistry, context.contract);
183
+ validateCodecRegistryCompleteness(this.codecDescriptors, context.contract);
165
184
  this.codecRegistryValidated = true;
166
185
  }
167
186
  }
168
187
 
169
- private ensureCodecRegistryValidated(contract: Contract<SqlStorage>): void {
170
- if (!this.codecRegistryValidated) {
171
- validateCodecRegistryCompleteness(this.codecRegistry, contract);
172
- this.codecRegistryValidated = true;
173
- }
188
+ /**
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.
192
+ */
193
+ protected override async lower(
194
+ plan: SqlQueryPlan,
195
+ ctx: SqlCodecCallContext,
196
+ ): Promise<SqlExecutionPlan> {
197
+ validateParamRefRefs(plan.ast, this.codecDescriptors);
198
+ const lowered = lowerSqlPlan(this.adapter, this.contract, plan);
199
+ return Object.freeze({
200
+ ...lowered,
201
+ params: await encodeParams(lowered, ctx, this.contractCodecs),
202
+ });
174
203
  }
175
204
 
176
- private async toExecutionPlan<Row>(
177
- plan: ExecutionPlan<Row> | SqlQueryPlan<Row>,
178
- ): Promise<ExecutionPlan<Row>> {
179
- const isSqlQueryPlan = (p: ExecutionPlan<Row> | SqlQueryPlan<Row>): p is SqlQueryPlan<Row> => {
180
- return 'ast' in p && !('sql' in p);
181
- };
182
-
183
- if (!isSqlQueryPlan(plan)) {
184
- return plan;
185
- }
205
+ /**
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.
207
+ */
208
+ // v8 ignore next 6
209
+ protected override runDriver(exec: SqlExecutionPlan): AsyncIterable<Record<string, unknown>> {
210
+ return this.driver.execute<Record<string, unknown>>({
211
+ sql: exec.sql,
212
+ params: exec.params,
213
+ });
214
+ }
186
215
 
216
+ /**
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
+ */
220
+ protected override async runBeforeCompile(plan: SqlQueryPlan): Promise<SqlQueryPlan> {
187
221
  const rewrittenDraft = await runBeforeCompileChain(
188
- this.core.middleware,
222
+ this.middleware,
189
223
  { ast: plan.ast, meta: plan.meta },
190
- this.core.middlewareContext,
224
+ this.sqlCtx,
191
225
  );
226
+ return rewrittenDraft.ast === plan.ast
227
+ ? plan
228
+ : { ...plan, ast: rewrittenDraft.ast, meta: rewrittenDraft.meta };
229
+ }
192
230
 
193
- const planToLower: SqlQueryPlan<Row> =
194
- rewrittenDraft.ast === plan.ast
195
- ? plan
196
- : { ...plan, ast: rewrittenDraft.ast, meta: rewrittenDraft.meta };
197
-
198
- return lowerSqlPlan(this.adapter, this.contract, planToLower);
231
+ override execute<Row>(
232
+ plan: (SqlExecutionPlan<unknown> | SqlQueryPlan<unknown>) & { readonly _row?: Row },
233
+ options?: RuntimeExecuteOptions,
234
+ ): AsyncIterableResult<Row> {
235
+ return this.executeAgainstQueryable<Row>(plan, this.driver, options);
199
236
  }
200
237
 
201
- private executeAgainstQueryable<Row = Record<string, unknown>>(
202
- plan: ExecutionPlan<Row> | SqlQueryPlan<Row>,
203
- queryable: CoreQueryable,
238
+ private executeAgainstQueryable<Row>(
239
+ plan: SqlExecutionPlan<unknown> | SqlQueryPlan<unknown>,
240
+ queryable: SqlQueryable,
241
+ options?: RuntimeExecuteOptions,
204
242
  ): AsyncIterableResult<Row> {
205
- this.ensureCodecRegistryValidated(this.contract);
206
-
207
- const iterator = async function* (
208
- self: SqlRuntimeImpl<TContract>,
209
- ): AsyncGenerator<Row, void, unknown> {
210
- const executablePlan = await self.toExecutionPlan(plan);
211
- const encodedParams = await encodeParams(executablePlan, self.codecRegistry);
212
- const planWithEncodedParams: ExecutionPlan<Row> = {
213
- ...executablePlan,
214
- params: encodedParams,
215
- };
243
+ this.ensureCodecRegistryValidated();
244
+
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
+
255
+ const generator = async function* (): AsyncGenerator<Row, void, unknown> {
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
+ }
216
270
 
217
- const coreIterator = queryable.execute(planWithEncodedParams);
271
+ self.familyAdapter.validatePlan(exec, self.contract);
272
+ self._telemetry = null;
218
273
 
219
- for await (const rawRow of coreIterator) {
220
- const decodedRow = await decodeRow(
221
- rawRow as Record<string, unknown>,
222
- executablePlan,
223
- self.codecRegistry,
224
- self.jsonSchemaValidators,
274
+ if (!self.startupVerified && self.verify.mode === 'startup') {
275
+ await self.verifyMarker();
276
+ }
277
+
278
+ if (!self.verified && self.verify.mode === 'onFirstUse') {
279
+ await self.verifyMarker();
280
+ }
281
+
282
+ const startedAt = Date.now();
283
+ let outcome: TelemetryOutcome | null = null;
284
+
285
+ try {
286
+ if (self.verify.mode === 'always') {
287
+ await self.verifyMarker();
288
+ }
289
+
290
+ const paramsMutator: SqlParamRefMutatorInternal = createSqlParamRefMutator(exec);
291
+ const stream = runWithMiddleware<
292
+ SqlExecutionPlan,
293
+ Record<string, unknown>,
294
+ SqlParamRefMutator
295
+ >(
296
+ exec,
297
+ self.middleware,
298
+ execMiddlewareCtx,
299
+ () =>
300
+ queryable.execute<Record<string, unknown>>({
301
+ sql: exec.sql,
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(),
307
+ }),
308
+ paramsMutator,
225
309
  );
226
- yield decodedRow as Row;
310
+
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?.();
326
+ }
327
+
328
+ outcome = 'success';
329
+ } catch (error) {
330
+ outcome = 'runtime-error';
331
+ throw error;
332
+ } finally {
333
+ if (outcome !== null) {
334
+ self.recordTelemetry(exec, outcome, Date.now() - startedAt);
335
+ }
227
336
  }
228
337
  };
229
338
 
230
- return new AsyncIterableResult(iterator(this));
231
- }
232
-
233
- execute<Row = Record<string, unknown>>(
234
- plan: ExecutionPlan<Row> | SqlQueryPlan<Row>,
235
- ): AsyncIterableResult<Row> {
236
- return this.executeAgainstQueryable(plan, this.core);
339
+ return new AsyncIterableResult(generator());
237
340
  }
238
341
 
239
342
  async connection(): Promise<RuntimeConnection> {
240
- const coreConn = await this.core.connection();
343
+ const driverConn = await this.driver.acquireConnection();
241
344
  const self = this;
345
+
242
346
  const wrappedConnection: RuntimeConnection = {
243
347
  async transaction(): Promise<RuntimeTransaction> {
244
- const coreTx = await coreConn.transaction();
245
- return {
246
- commit: coreTx.commit.bind(coreTx),
247
- rollback: coreTx.rollback.bind(coreTx),
248
- execute<Row = Record<string, unknown>>(
249
- plan: ExecutionPlan<Row> | SqlQueryPlan<Row>,
250
- ): AsyncIterableResult<Row> {
251
- return self.executeAgainstQueryable(plan, coreTx);
252
- },
253
- };
348
+ const driverTx = await driverConn.beginTransaction();
349
+ return self.wrapTransaction(driverTx);
350
+ },
351
+ async release(): Promise<void> {
352
+ await driverConn.release();
254
353
  },
255
- release: coreConn.release.bind(coreConn),
256
- destroy: coreConn.destroy.bind(coreConn),
257
- execute<Row = Record<string, unknown>>(
258
- plan: ExecutionPlan<Row> | SqlQueryPlan<Row>,
354
+ async destroy(reason?: unknown): Promise<void> {
355
+ await driverConn.destroy(reason);
356
+ },
357
+ execute<Row>(
358
+ plan: (SqlExecutionPlan<unknown> | SqlQueryPlan<unknown>) & { readonly _row?: Row },
359
+ options?: RuntimeExecuteOptions,
259
360
  ): AsyncIterableResult<Row> {
260
- return self.executeAgainstQueryable(plan, coreConn);
361
+ return self.executeAgainstQueryable<Row>(plan, driverConn, options);
261
362
  },
262
363
  };
364
+
263
365
  return wrappedConnection;
264
366
  }
265
367
 
368
+ private wrapTransaction(driverTx: SqlTransaction): RuntimeTransaction {
369
+ const self = this;
370
+ return {
371
+ async commit(): Promise<void> {
372
+ await driverTx.commit();
373
+ },
374
+ async rollback(): Promise<void> {
375
+ await driverTx.rollback();
376
+ },
377
+ execute<Row>(
378
+ plan: (SqlExecutionPlan<unknown> | SqlQueryPlan<unknown>) & { readonly _row?: Row },
379
+ options?: RuntimeExecuteOptions,
380
+ ): AsyncIterableResult<Row> {
381
+ return self.executeAgainstQueryable<Row>(plan, driverTx, options);
382
+ },
383
+ };
384
+ }
385
+
266
386
  telemetry(): RuntimeTelemetryEvent | null {
267
- return this.core.telemetry();
387
+ return this._telemetry;
388
+ }
389
+
390
+ async close(): Promise<void> {
391
+ await this.driver.close();
392
+ }
393
+
394
+ private ensureCodecRegistryValidated(): void {
395
+ if (!this.codecRegistryValidated) {
396
+ validateCodecRegistryCompleteness(this.codecDescriptors, this.contract);
397
+ this.codecRegistryValidated = true;
398
+ }
399
+ }
400
+
401
+ private async verifyMarker(): Promise<void> {
402
+ const readStatement = this.familyAdapter.markerReader.readMarkerStatement();
403
+ const result = await this.driver.query(readStatement.sql, readStatement.params);
404
+
405
+ if (result.rows.length === 0) {
406
+ if (this.verify.requireMarker) {
407
+ throw runtimeError('CONTRACT.MARKER_MISSING', 'Contract marker not found in database');
408
+ }
409
+
410
+ this.verified = true;
411
+ return;
412
+ }
413
+
414
+ const marker = this.familyAdapter.markerReader.parseMarkerRow(result.rows[0]);
415
+
416
+ const contract = this.contract as {
417
+ storage: { storageHash: string };
418
+ execution?: { executionHash?: string | null };
419
+ profileHash?: string | null;
420
+ };
421
+
422
+ if (marker.storageHash !== contract.storage.storageHash) {
423
+ throw runtimeError(
424
+ 'CONTRACT.MARKER_MISMATCH',
425
+ 'Database storage hash does not match contract',
426
+ {
427
+ expected: contract.storage.storageHash,
428
+ actual: marker.storageHash,
429
+ },
430
+ );
431
+ }
432
+
433
+ const expectedProfile = contract.profileHash ?? null;
434
+ if (expectedProfile !== null && marker.profileHash !== expectedProfile) {
435
+ throw runtimeError(
436
+ 'CONTRACT.MARKER_MISMATCH',
437
+ 'Database profile hash does not match contract',
438
+ {
439
+ expectedProfile,
440
+ actualProfile: marker.profileHash,
441
+ },
442
+ );
443
+ }
444
+
445
+ this.verified = true;
446
+ this.startupVerified = true;
268
447
  }
269
448
 
270
- close(): Promise<void> {
271
- return this.core.close();
449
+ private recordTelemetry(
450
+ plan: SqlExecutionPlan,
451
+ outcome: TelemetryOutcome,
452
+ durationMs?: number,
453
+ ): void {
454
+ const contract = this.contract as { target: string };
455
+ this._telemetry = Object.freeze({
456
+ lane: plan.meta.lane,
457
+ target: contract.target,
458
+ fingerprint: computeSqlFingerprint(plan.sql),
459
+ outcome,
460
+ ...(durationMs !== undefined ? { durationMs } : {}),
461
+ });
272
462
  }
273
463
  }
274
464
 
@@ -292,13 +482,14 @@ export async function withTransaction<R>(
292
482
  get invalidated() {
293
483
  return invalidated;
294
484
  },
295
- execute<Row = Record<string, unknown>>(
296
- plan: ExecutionPlan<Row> | SqlQueryPlan<Row>,
485
+ execute<Row>(
486
+ plan: (SqlExecutionPlan<unknown> | SqlQueryPlan<unknown>) & { readonly _row?: Row },
487
+ options?: RuntimeExecuteOptions,
297
488
  ): AsyncIterableResult<Row> {
298
489
  if (invalidated) {
299
490
  throw transactionClosedError();
300
491
  }
301
- const inner = transaction.execute(plan);
492
+ const inner = transaction.execute(plan, options);
302
493
  const guarded = async function* (): AsyncGenerator<Row, void, unknown> {
303
494
  for await (const row of inner) {
304
495
  if (invalidated) {
@@ -315,11 +506,7 @@ export async function withTransaction<R>(
315
506
  const destroyConnection = async (reason: unknown): Promise<void> => {
316
507
  if (connectionDisposed) return;
317
508
  connectionDisposed = true;
318
- // SqlConnection.destroy() propagates teardown errors so callers can
319
- // decide what to do with them. Here, we're already about to throw a
320
- // more informative error describing why we're evicting the connection
321
- // (rollback/commit failure), so swallowing the teardown error is the
322
- // right call — surfacing it would mask the original cause.
509
+ // 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.
323
510
  await connection.destroy(reason).catch(() => undefined);
324
511
  };
325
512
 
@@ -348,17 +535,9 @@ export async function withTransaction<R>(
348
535
  try {
349
536
  await transaction.commit();
350
537
  } catch (commitError) {
351
- // After a failed COMMIT the server-side transaction may be: (a) already
352
- // committed (error on response path), (b) already rolled back (deferred
353
- // constraint / serialization failure), or (c) still open (COMMIT never
354
- // reached the server). Attempt a best-effort rollback to cover (c) and
355
- // confirm the protocol is healthy.
538
+ // 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.
356
539
  //
357
- // If rollback succeeds, the server is definitely no longer in a
358
- // transaction (no-op in (a)/(b), real cleanup in (c)) and we've just
359
- // proved the connection round-trips correctly — it's safe to return
360
- // to the pool. If rollback fails, the connection state is ambiguous
361
- // (broken socket, protocol desync, etc.) and we must destroy it.
540
+ // 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.
362
541
  try {
363
542
  await transaction.rollback();
364
543
  } catch {