@prisma-next/sql-runtime 0.4.1 → 0.4.3

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