@prisma-next/sql-runtime 0.5.0-dev.5 → 0.5.0-dev.51

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