@prisma-next/sql-runtime 0.5.0-dev.3 → 0.5.0-dev.30

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.
Files changed (42) hide show
  1. package/README.md +29 -21
  2. package/dist/{exports-BQZSVXXt.mjs → exports-Cq_9ZrU4.mjs} +649 -275
  3. package/dist/exports-Cq_9ZrU4.mjs.map +1 -0
  4. package/dist/{index-yb51L_1h.d.mts → index-Df2GsLSH.d.mts} +65 -16
  5. package/dist/index-Df2GsLSH.d.mts.map +1 -0
  6. package/dist/index.d.mts +2 -2
  7. package/dist/index.mjs +2 -2
  8. package/dist/test/utils.d.mts +6 -5
  9. package/dist/test/utils.d.mts.map +1 -1
  10. package/dist/test/utils.mjs +11 -5
  11. package/dist/test/utils.mjs.map +1 -1
  12. package/package.json +10 -12
  13. package/src/codecs/decoding.ts +256 -173
  14. package/src/codecs/encoding.ts +123 -39
  15. package/src/exports/index.ts +11 -7
  16. package/src/fingerprint.ts +22 -0
  17. package/src/guardrails/raw.ts +165 -0
  18. package/src/lower-sql-plan.ts +3 -3
  19. package/src/marker.ts +75 -0
  20. package/src/middleware/before-compile-chain.ts +1 -0
  21. package/src/middleware/budgets.ts +26 -96
  22. package/src/middleware/lints.ts +3 -3
  23. package/src/middleware/sql-middleware.ts +6 -5
  24. package/src/runtime-spi.ts +44 -0
  25. package/src/sql-family-adapter.ts +3 -2
  26. package/src/sql-marker.ts +62 -47
  27. package/src/sql-runtime.ts +321 -111
  28. package/dist/exports-BQZSVXXt.mjs.map +0 -1
  29. package/dist/index-yb51L_1h.d.mts.map +0 -1
  30. package/test/async-iterable-result.test.ts +0 -141
  31. package/test/before-compile-chain.test.ts +0 -223
  32. package/test/budgets.test.ts +0 -431
  33. package/test/context.types.test-d.ts +0 -68
  34. package/test/execution-stack.test.ts +0 -161
  35. package/test/json-schema-validation.test.ts +0 -571
  36. package/test/lints.test.ts +0 -160
  37. package/test/mutation-default-generators.test.ts +0 -254
  38. package/test/parameterized-types.test.ts +0 -529
  39. package/test/sql-context.test.ts +0 -384
  40. package/test/sql-family-adapter.test.ts +0 -103
  41. package/test/sql-runtime.test.ts +0 -792
  42. package/test/utils.ts +0 -297
@@ -1,39 +1,46 @@
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
20
  CodecRegistry,
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';
27
+ import type { SqlExecutionPlan, SqlQueryPlan } from '@prisma-next/sql-relational-core/plan';
29
28
  import type { JsonSchemaValidatorRegistry } from '@prisma-next/sql-relational-core/query-lane-context';
29
+ import type { RuntimeScope } from '@prisma-next/sql-relational-core/types';
30
30
  import { ifDefined } from '@prisma-next/utils/defined';
31
31
  import { decodeRow } from './codecs/decoding';
32
32
  import { encodeParams } from './codecs/encoding';
33
33
  import { validateCodecRegistryCompleteness } from './codecs/validation';
34
+ import { computeSqlFingerprint } from './fingerprint';
34
35
  import { lowerSqlPlan } from './lower-sql-plan';
35
36
  import { runBeforeCompileChain } from './middleware/before-compile-chain';
36
- import type { SqlMiddleware } from './middleware/sql-middleware';
37
+ import type { SqlMiddleware, SqlMiddlewareContext } from './middleware/sql-middleware';
38
+ import type {
39
+ RuntimeFamilyAdapter,
40
+ RuntimeTelemetryEvent,
41
+ RuntimeVerifyOptions,
42
+ TelemetryOutcome,
43
+ } from './runtime-spi';
37
44
  import type {
38
45
  ExecutionContext,
39
46
  SqlRuntimeAdapterInstance,
@@ -41,6 +48,8 @@ import type {
41
48
  } from './sql-context';
42
49
  import { SqlFamilyAdapter } from './sql-family-adapter';
43
50
 
51
+ export type Log = RuntimeLog;
52
+
44
53
  export interface RuntimeOptions<TContract extends Contract<SqlStorage> = Contract<SqlStorage>> {
45
54
  readonly context: ExecutionContext<TContract>;
46
55
  readonly adapter: Adapter<AnyQueryAst, Contract<SqlStorage>, LoweredStatement>;
@@ -107,39 +116,37 @@ export interface RuntimeTransaction extends RuntimeQueryable {
107
116
  rollback(): Promise<void>;
108
117
  }
109
118
 
110
- export interface RuntimeQueryable {
111
- execute<Row = Record<string, unknown>>(
112
- plan: ExecutionPlan<Row> | SqlQueryPlan<Row>,
113
- ): AsyncIterableResult<Row>;
114
- }
119
+ export interface RuntimeQueryable extends RuntimeScope {}
115
120
 
116
121
  export interface TransactionContext extends RuntimeQueryable {
117
122
  readonly invalidated: boolean;
118
123
  }
119
124
 
120
- interface CoreQueryable {
121
- execute<Row = Record<string, unknown>>(plan: ExecutionPlan<Row>): AsyncIterableResult<Row>;
122
- }
123
-
124
125
  export type { RuntimeTelemetryEvent, RuntimeVerifyOptions, TelemetryOutcome };
125
126
 
127
+ function isExecutionPlan(plan: SqlExecutionPlan | SqlQueryPlan): plan is SqlExecutionPlan {
128
+ return 'sql' in plan;
129
+ }
130
+
126
131
  class SqlRuntimeImpl<TContract extends Contract<SqlStorage> = Contract<SqlStorage>>
132
+ extends RuntimeCore<SqlQueryPlan, SqlExecutionPlan, SqlMiddleware>
127
133
  implements Runtime
128
134
  {
129
- private readonly core: RuntimeCore<TContract, SqlDriver<unknown>, SqlMiddleware>;
130
135
  private readonly contract: TContract;
131
136
  private readonly adapter: Adapter<AnyQueryAst, Contract<SqlStorage>, LoweredStatement>;
137
+ private readonly driver: SqlDriver<unknown>;
138
+ private readonly familyAdapter: RuntimeFamilyAdapter<Contract<SqlStorage>>;
132
139
  private readonly codecRegistry: CodecRegistry;
133
140
  private readonly jsonSchemaValidators: JsonSchemaValidatorRegistry | undefined;
141
+ private readonly sqlCtx: SqlMiddlewareContext;
142
+ private readonly verify: RuntimeVerifyOptions;
134
143
  private codecRegistryValidated: boolean;
144
+ private verified: boolean;
145
+ private startupVerified: boolean;
146
+ private _telemetry: RuntimeTelemetryEvent | null;
135
147
 
136
148
  constructor(options: RuntimeOptions<TContract>) {
137
149
  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
150
 
144
151
  if (middleware) {
145
152
  for (const mw of middleware) {
@@ -147,18 +154,31 @@ class SqlRuntimeImpl<TContract extends Contract<SqlStorage> = Contract<SqlStorag
147
154
  }
148
155
  }
149
156
 
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),
157
+ const sqlCtx: SqlMiddlewareContext = {
158
+ contract: context.contract,
159
+ mode: mode ?? 'strict',
160
+ now: () => Date.now(),
161
+ log: log ?? {
162
+ info: () => {},
163
+ warn: () => {},
164
+ error: () => {},
165
+ },
159
166
  };
160
167
 
161
- this.core = createRuntimeCore(coreOptions);
168
+ super({ middleware: middleware ?? [], ctx: sqlCtx });
169
+
170
+ this.contract = context.contract;
171
+ this.adapter = adapter;
172
+ this.driver = driver;
173
+ this.familyAdapter = new SqlFamilyAdapter(context.contract, adapter.profile);
174
+ this.codecRegistry = context.codecs;
175
+ this.jsonSchemaValidators = context.jsonSchemaValidators;
176
+ this.sqlCtx = sqlCtx;
177
+ this.verify = verify;
178
+ this.codecRegistryValidated = false;
179
+ this.verified = verify.mode === 'startup' ? false : verify.mode === 'always';
180
+ this.startupVerified = false;
181
+ this._telemetry = null;
162
182
 
163
183
  if (verify.mode === 'startup') {
164
184
  validateCodecRegistryCompleteness(this.codecRegistry, context.contract);
@@ -166,107 +186,296 @@ class SqlRuntimeImpl<TContract extends Contract<SqlStorage> = Contract<SqlStorag
166
186
  }
167
187
  }
168
188
 
169
- private ensureCodecRegistryValidated(contract: Contract<SqlStorage>): void {
170
- if (!this.codecRegistryValidated) {
171
- validateCodecRegistryCompleteness(this.codecRegistry, contract);
172
- this.codecRegistryValidated = true;
173
- }
189
+ /**
190
+ * Lower a `SqlQueryPlan` (AST + meta) into a `SqlExecutionPlan` with
191
+ * encoded parameters ready for the driver. This is the single point at
192
+ * which params transition from app-layer values to driver wire-format.
193
+ *
194
+ * `ctx: SqlCodecCallContext` is forwarded to `encodeParams` so per-query
195
+ * cancellation reaches every codec body during parameter encoding. The
196
+ * framework abstract typed this as `CodecCallContext`; the SQL family
197
+ * narrows it to the SQL-specific extension. SQL params do not populate
198
+ * `ctx.column` — encode-side column metadata is the middleware's domain.
199
+ */
200
+ protected override async lower(
201
+ plan: SqlQueryPlan,
202
+ ctx: SqlCodecCallContext,
203
+ ): Promise<SqlExecutionPlan> {
204
+ const lowered = lowerSqlPlan(this.adapter, this.contract, plan);
205
+ return Object.freeze({
206
+ ...lowered,
207
+ params: await encodeParams(lowered, this.codecRegistry, ctx),
208
+ });
174
209
  }
175
210
 
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
- }
211
+ /**
212
+ * Default driver invocation. Production execution paths override the
213
+ * queryable target (e.g. transaction or connection) by going through
214
+ * `executeAgainstQueryable`; this implementation supports any caller of
215
+ * `super.execute(plan)` and the abstract-base contract.
216
+ */
217
+ protected override runDriver(exec: SqlExecutionPlan): AsyncIterable<Record<string, unknown>> {
218
+ return this.driver.execute<Record<string, unknown>>({
219
+ sql: exec.sql,
220
+ params: exec.params,
221
+ });
222
+ }
186
223
 
224
+ /**
225
+ * SQL pre-compile hook. Runs the registered middleware `beforeCompile`
226
+ * chain over the plan's draft (AST + meta). Returns the original plan
227
+ * unchanged when no middleware rewrote the AST; otherwise returns a new
228
+ * plan carrying the rewritten AST and meta. The AST is the authoritative
229
+ * source of execution metadata, so a rewrite needs no sidecar
230
+ * reconciliation here — the lowering adapter and the encoder both walk
231
+ * the rewritten AST directly.
232
+ */
233
+ protected override async runBeforeCompile(plan: SqlQueryPlan): Promise<SqlQueryPlan> {
187
234
  const rewrittenDraft = await runBeforeCompileChain(
188
- this.core.middleware,
235
+ this.middleware,
189
236
  { ast: plan.ast, meta: plan.meta },
190
- this.core.middlewareContext,
237
+ this.sqlCtx,
191
238
  );
239
+ return rewrittenDraft.ast === plan.ast
240
+ ? plan
241
+ : { ...plan, ast: rewrittenDraft.ast, meta: rewrittenDraft.meta };
242
+ }
192
243
 
193
- const planToLower: SqlQueryPlan<Row> =
194
- rewrittenDraft.ast === plan.ast ? plan : { ...plan, ast: rewrittenDraft.ast };
195
-
196
- return lowerSqlPlan(this.adapter, this.contract, planToLower);
244
+ override execute<Row>(
245
+ plan: (SqlExecutionPlan<unknown> | SqlQueryPlan<unknown>) & { readonly _row?: Row },
246
+ options?: RuntimeExecuteOptions,
247
+ ): AsyncIterableResult<Row> {
248
+ return this.executeAgainstQueryable<Row>(plan, this.driver, options);
197
249
  }
198
250
 
199
- private executeAgainstQueryable<Row = Record<string, unknown>>(
200
- plan: ExecutionPlan<Row> | SqlQueryPlan<Row>,
201
- queryable: CoreQueryable,
251
+ private executeAgainstQueryable<Row>(
252
+ plan: SqlExecutionPlan<unknown> | SqlQueryPlan<unknown>,
253
+ queryable: SqlQueryable,
254
+ options?: RuntimeExecuteOptions,
202
255
  ): AsyncIterableResult<Row> {
203
- this.ensureCodecRegistryValidated(this.contract);
204
-
205
- const iterator = async function* (
206
- self: SqlRuntimeImpl<TContract>,
207
- ): AsyncGenerator<Row, void, unknown> {
208
- const executablePlan = await self.toExecutionPlan(plan);
209
- const encodedParams = encodeParams(executablePlan, self.codecRegistry);
210
- const planWithEncodedParams: ExecutionPlan<Row> = {
211
- ...executablePlan,
212
- params: encodedParams,
213
- };
256
+ this.ensureCodecRegistryValidated();
257
+
258
+ const self = this;
259
+ const signal = options?.signal;
260
+ // One ctx per execute() call the same reference is shared by
261
+ // encodeParams (lower), decodeRow (per-row), and the stream loop's
262
+ // between-row checks. Per-cell ctx allocations inside decodeField add
263
+ // `column` for resolvable cells without re-wrapping the signal. The
264
+ // ctx object is always allocated; the `signal` field is only included
265
+ // when a signal was supplied (exactOptionalPropertyTypes).
266
+ const codecCtx: SqlCodecCallContext = signal === undefined ? {} : { signal };
267
+
268
+ const generator = async function* (): AsyncGenerator<Row, void, unknown> {
269
+ checkAborted(codecCtx, 'stream');
270
+
271
+ const exec: SqlExecutionPlan = isExecutionPlan(plan)
272
+ ? Object.freeze({
273
+ ...plan,
274
+ params: await encodeParams(plan, self.codecRegistry, codecCtx),
275
+ })
276
+ : await self.lower(await self.runBeforeCompile(plan), codecCtx);
277
+
278
+ self.familyAdapter.validatePlan(exec, self.contract);
279
+ self._telemetry = null;
280
+
281
+ if (!self.startupVerified && self.verify.mode === 'startup') {
282
+ await self.verifyMarker();
283
+ }
284
+
285
+ if (!self.verified && self.verify.mode === 'onFirstUse') {
286
+ await self.verifyMarker();
287
+ }
288
+
289
+ const startedAt = Date.now();
290
+ let outcome: TelemetryOutcome | null = null;
214
291
 
215
- const coreIterator = queryable.execute(planWithEncodedParams);
292
+ try {
293
+ if (self.verify.mode === 'always') {
294
+ await self.verifyMarker();
295
+ }
216
296
 
217
- for await (const rawRow of coreIterator) {
218
- const decodedRow = decodeRow(
219
- rawRow as Record<string, unknown>,
220
- executablePlan,
221
- self.codecRegistry,
222
- self.jsonSchemaValidators,
297
+ const stream = runWithMiddleware<SqlExecutionPlan, Record<string, unknown>>(
298
+ exec,
299
+ self.middleware,
300
+ self.ctx,
301
+ () =>
302
+ queryable.execute<Record<string, unknown>>({
303
+ sql: exec.sql,
304
+ params: exec.params,
305
+ }),
223
306
  );
224
- yield decodedRow as Row;
307
+
308
+ // Manually drive the driver's async iterator so the between-row
309
+ // abort check fires *before* requesting the next row. With a
310
+ // `for await...of` loop the runtime would await `iterator.next()`
311
+ // first, leaving a window where one extra row is pulled through
312
+ // the driver after the signal aborted.
313
+ const iterator = stream[Symbol.asyncIterator]();
314
+ try {
315
+ while (true) {
316
+ checkAborted(codecCtx, 'stream');
317
+ const next = await iterator.next();
318
+ if (next.done) {
319
+ break;
320
+ }
321
+ const decodedRow = await decodeRow(
322
+ next.value,
323
+ exec,
324
+ self.codecRegistry,
325
+ self.jsonSchemaValidators,
326
+ codecCtx,
327
+ );
328
+ yield decodedRow as Row;
329
+ }
330
+ } finally {
331
+ // Best-effort iterator cleanup so the driver can release its
332
+ // resources whether the stream finished normally, threw, or was
333
+ // abandoned by the consumer.
334
+ await iterator.return?.();
335
+ }
336
+
337
+ outcome = 'success';
338
+ } catch (error) {
339
+ outcome = 'runtime-error';
340
+ throw error;
341
+ } finally {
342
+ if (outcome !== null) {
343
+ self.recordTelemetry(exec, outcome, Date.now() - startedAt);
344
+ }
225
345
  }
226
346
  };
227
347
 
228
- return new AsyncIterableResult(iterator(this));
229
- }
230
-
231
- execute<Row = Record<string, unknown>>(
232
- plan: ExecutionPlan<Row> | SqlQueryPlan<Row>,
233
- ): AsyncIterableResult<Row> {
234
- return this.executeAgainstQueryable(plan, this.core);
348
+ return new AsyncIterableResult(generator());
235
349
  }
236
350
 
237
351
  async connection(): Promise<RuntimeConnection> {
238
- const coreConn = await this.core.connection();
352
+ const driverConn = await this.driver.acquireConnection();
239
353
  const self = this;
354
+
240
355
  const wrappedConnection: RuntimeConnection = {
241
356
  async transaction(): Promise<RuntimeTransaction> {
242
- const coreTx = await coreConn.transaction();
243
- return {
244
- commit: coreTx.commit.bind(coreTx),
245
- rollback: coreTx.rollback.bind(coreTx),
246
- execute<Row = Record<string, unknown>>(
247
- plan: ExecutionPlan<Row> | SqlQueryPlan<Row>,
248
- ): AsyncIterableResult<Row> {
249
- return self.executeAgainstQueryable(plan, coreTx);
250
- },
251
- };
357
+ const driverTx = await driverConn.beginTransaction();
358
+ return self.wrapTransaction(driverTx);
252
359
  },
253
- release: coreConn.release.bind(coreConn),
254
- destroy: coreConn.destroy.bind(coreConn),
255
- execute<Row = Record<string, unknown>>(
256
- plan: ExecutionPlan<Row> | SqlQueryPlan<Row>,
360
+ async release(): Promise<void> {
361
+ await driverConn.release();
362
+ },
363
+ async destroy(reason?: unknown): Promise<void> {
364
+ await driverConn.destroy(reason);
365
+ },
366
+ execute<Row>(
367
+ plan: (SqlExecutionPlan<unknown> | SqlQueryPlan<unknown>) & { readonly _row?: Row },
368
+ options?: RuntimeExecuteOptions,
257
369
  ): AsyncIterableResult<Row> {
258
- return self.executeAgainstQueryable(plan, coreConn);
370
+ return self.executeAgainstQueryable<Row>(plan, driverConn, options);
259
371
  },
260
372
  };
373
+
261
374
  return wrappedConnection;
262
375
  }
263
376
 
377
+ private wrapTransaction(driverTx: SqlTransaction): RuntimeTransaction {
378
+ const self = this;
379
+ return {
380
+ async commit(): Promise<void> {
381
+ await driverTx.commit();
382
+ },
383
+ async rollback(): Promise<void> {
384
+ await driverTx.rollback();
385
+ },
386
+ execute<Row>(
387
+ plan: (SqlExecutionPlan<unknown> | SqlQueryPlan<unknown>) & { readonly _row?: Row },
388
+ options?: RuntimeExecuteOptions,
389
+ ): AsyncIterableResult<Row> {
390
+ return self.executeAgainstQueryable<Row>(plan, driverTx, options);
391
+ },
392
+ };
393
+ }
394
+
264
395
  telemetry(): RuntimeTelemetryEvent | null {
265
- return this.core.telemetry();
396
+ return this._telemetry;
397
+ }
398
+
399
+ async close(): Promise<void> {
400
+ await this.driver.close();
401
+ }
402
+
403
+ private ensureCodecRegistryValidated(): void {
404
+ if (!this.codecRegistryValidated) {
405
+ validateCodecRegistryCompleteness(this.codecRegistry, this.contract);
406
+ this.codecRegistryValidated = true;
407
+ }
408
+ }
409
+
410
+ private async verifyMarker(): Promise<void> {
411
+ if (this.verify.mode === 'always') {
412
+ this.verified = false;
413
+ }
414
+
415
+ if (this.verified) {
416
+ return;
417
+ }
418
+
419
+ const readStatement = this.familyAdapter.markerReader.readMarkerStatement();
420
+ const result = await this.driver.query(readStatement.sql, readStatement.params);
421
+
422
+ if (result.rows.length === 0) {
423
+ if (this.verify.requireMarker) {
424
+ throw runtimeError('CONTRACT.MARKER_MISSING', 'Contract marker not found in database');
425
+ }
426
+
427
+ this.verified = true;
428
+ return;
429
+ }
430
+
431
+ const marker = this.familyAdapter.markerReader.parseMarkerRow(result.rows[0]);
432
+
433
+ const contract = this.contract as {
434
+ storage: { storageHash: string };
435
+ execution?: { executionHash?: string | null };
436
+ profileHash?: string | null;
437
+ };
438
+
439
+ if (marker.storageHash !== contract.storage.storageHash) {
440
+ throw runtimeError(
441
+ 'CONTRACT.MARKER_MISMATCH',
442
+ 'Database storage hash does not match contract',
443
+ {
444
+ expected: contract.storage.storageHash,
445
+ actual: marker.storageHash,
446
+ },
447
+ );
448
+ }
449
+
450
+ const expectedProfile = contract.profileHash ?? null;
451
+ if (expectedProfile !== null && marker.profileHash !== expectedProfile) {
452
+ throw runtimeError(
453
+ 'CONTRACT.MARKER_MISMATCH',
454
+ 'Database profile hash does not match contract',
455
+ {
456
+ expectedProfile,
457
+ actualProfile: marker.profileHash,
458
+ },
459
+ );
460
+ }
461
+
462
+ this.verified = true;
463
+ this.startupVerified = true;
266
464
  }
267
465
 
268
- close(): Promise<void> {
269
- return this.core.close();
466
+ private recordTelemetry(
467
+ plan: SqlExecutionPlan,
468
+ outcome: TelemetryOutcome,
469
+ durationMs?: number,
470
+ ): void {
471
+ const contract = this.contract as { target: string };
472
+ this._telemetry = Object.freeze({
473
+ lane: plan.meta.lane,
474
+ target: contract.target,
475
+ fingerprint: computeSqlFingerprint(plan.sql),
476
+ outcome,
477
+ ...(durationMs !== undefined ? { durationMs } : {}),
478
+ });
270
479
  }
271
480
  }
272
481
 
@@ -290,13 +499,14 @@ export async function withTransaction<R>(
290
499
  get invalidated() {
291
500
  return invalidated;
292
501
  },
293
- execute<Row = Record<string, unknown>>(
294
- plan: ExecutionPlan<Row> | SqlQueryPlan<Row>,
502
+ execute<Row>(
503
+ plan: (SqlExecutionPlan<unknown> | SqlQueryPlan<unknown>) & { readonly _row?: Row },
504
+ options?: RuntimeExecuteOptions,
295
505
  ): AsyncIterableResult<Row> {
296
506
  if (invalidated) {
297
507
  throw transactionClosedError();
298
508
  }
299
- const inner = transaction.execute(plan);
509
+ const inner = transaction.execute(plan, options);
300
510
  const guarded = async function* (): AsyncGenerator<Row, void, unknown> {
301
511
  for await (const row of inner) {
302
512
  if (invalidated) {