@prisma-next/sql-runtime 0.5.0-dev.4 → 0.5.0-dev.40

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 (45) 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-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/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 +3 -3
  20. package/src/marker.ts +75 -0
  21. package/src/middleware/before-compile-chain.ts +1 -0
  22. package/src/middleware/budgets.ts +26 -96
  23. package/src/middleware/lints.ts +3 -3
  24. package/src/middleware/sql-middleware.ts +6 -5
  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 +332 -113
  30. package/dist/exports-BQZSVXXt.mjs +0 -981
  31. package/dist/exports-BQZSVXXt.mjs.map +0 -1
  32. package/dist/index-yb51L_1h.d.mts.map +0 -1
  33. package/test/async-iterable-result.test.ts +0 -141
  34. package/test/before-compile-chain.test.ts +0 -223
  35. package/test/budgets.test.ts +0 -431
  36. package/test/context.types.test-d.ts +0 -68
  37. package/test/execution-stack.test.ts +0 -161
  38. package/test/json-schema-validation.test.ts +0 -571
  39. package/test/lints.test.ts +0 -160
  40. package/test/mutation-default-generators.test.ts +0 -254
  41. package/test/parameterized-types.test.ts +0 -529
  42. package/test/sql-context.test.ts +0 -384
  43. package/test/sql-family-adapter.test.ts +0 -103
  44. package/test/sql-runtime.test.ts +0 -792
  45. package/test/utils.ts +0 -297
@@ -1,39 +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
- 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 { computeSqlFingerprint } from './fingerprint';
34
39
  import { lowerSqlPlan } from './lower-sql-plan';
35
40
  import { runBeforeCompileChain } from './middleware/before-compile-chain';
36
- import type { SqlMiddleware } from './middleware/sql-middleware';
41
+ import type { SqlMiddleware, SqlMiddlewareContext } from './middleware/sql-middleware';
42
+ import type {
43
+ RuntimeFamilyAdapter,
44
+ RuntimeTelemetryEvent,
45
+ RuntimeVerifyOptions,
46
+ TelemetryOutcome,
47
+ } from './runtime-spi';
37
48
  import type {
38
49
  ExecutionContext,
39
50
  SqlRuntimeAdapterInstance,
@@ -41,6 +52,8 @@ import type {
41
52
  } from './sql-context';
42
53
  import { SqlFamilyAdapter } from './sql-family-adapter';
43
54
 
55
+ export type Log = RuntimeLog;
56
+
44
57
  export interface RuntimeOptions<TContract extends Contract<SqlStorage> = Contract<SqlStorage>> {
45
58
  readonly context: ExecutionContext<TContract>;
46
59
  readonly adapter: Adapter<AnyQueryAst, Contract<SqlStorage>, LoweredStatement>;
@@ -107,39 +120,39 @@ export interface RuntimeTransaction extends RuntimeQueryable {
107
120
  rollback(): Promise<void>;
108
121
  }
109
122
 
110
- export interface RuntimeQueryable {
111
- execute<Row = Record<string, unknown>>(
112
- plan: ExecutionPlan<Row> | SqlQueryPlan<Row>,
113
- ): AsyncIterableResult<Row>;
114
- }
123
+ export interface RuntimeQueryable extends RuntimeScope {}
115
124
 
116
125
  export interface TransactionContext extends RuntimeQueryable {
117
126
  readonly invalidated: boolean;
118
127
  }
119
128
 
120
- interface CoreQueryable {
121
- execute<Row = Record<string, unknown>>(plan: ExecutionPlan<Row>): AsyncIterableResult<Row>;
122
- }
123
-
124
129
  export type { RuntimeTelemetryEvent, RuntimeVerifyOptions, TelemetryOutcome };
125
130
 
131
+ function isExecutionPlan(plan: SqlExecutionPlan | SqlQueryPlan): plan is SqlExecutionPlan {
132
+ return 'sql' in plan;
133
+ }
134
+
126
135
  class SqlRuntimeImpl<TContract extends Contract<SqlStorage> = Contract<SqlStorage>>
136
+ extends RuntimeCore<SqlQueryPlan, SqlExecutionPlan, SqlMiddleware>
127
137
  implements Runtime
128
138
  {
129
- private readonly core: RuntimeCore<TContract, SqlDriver<unknown>, SqlMiddleware>;
130
139
  private readonly contract: TContract;
131
140
  private readonly adapter: Adapter<AnyQueryAst, Contract<SqlStorage>, LoweredStatement>;
141
+ private readonly driver: SqlDriver<unknown>;
142
+ private readonly familyAdapter: RuntimeFamilyAdapter<Contract<SqlStorage>>;
132
143
  private readonly codecRegistry: CodecRegistry;
144
+ private readonly contractCodecs: ContractCodecRegistry;
145
+ private readonly codecDescriptors: CodecDescriptorRegistry;
133
146
  private readonly jsonSchemaValidators: JsonSchemaValidatorRegistry | undefined;
147
+ private readonly sqlCtx: SqlMiddlewareContext;
148
+ private readonly verify: RuntimeVerifyOptions;
134
149
  private codecRegistryValidated: boolean;
150
+ private verified: boolean;
151
+ private startupVerified: boolean;
152
+ private _telemetry: RuntimeTelemetryEvent | null;
135
153
 
136
154
  constructor(options: RuntimeOptions<TContract>) {
137
155
  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
156
 
144
157
  if (middleware) {
145
158
  for (const mw of middleware) {
@@ -147,126 +160,331 @@ class SqlRuntimeImpl<TContract extends Contract<SqlStorage> = Contract<SqlStorag
147
160
  }
148
161
  }
149
162
 
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),
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
+ },
159
172
  };
160
173
 
161
- 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;
162
190
 
163
191
  if (verify.mode === 'startup') {
164
- validateCodecRegistryCompleteness(this.codecRegistry, context.contract);
192
+ validateCodecRegistryCompleteness(this.codecDescriptors, context.contract);
165
193
  this.codecRegistryValidated = true;
166
194
  }
167
195
  }
168
196
 
169
- private ensureCodecRegistryValidated(contract: Contract<SqlStorage>): void {
170
- if (!this.codecRegistryValidated) {
171
- validateCodecRegistryCompleteness(this.codecRegistry, contract);
172
- this.codecRegistryValidated = true;
173
- }
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
+ });
174
217
  }
175
218
 
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
- }
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
+ }
186
231
 
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> {
187
242
  const rewrittenDraft = await runBeforeCompileChain(
188
- this.core.middleware,
243
+ this.middleware,
189
244
  { ast: plan.ast, meta: plan.meta },
190
- this.core.middlewareContext,
245
+ this.sqlCtx,
191
246
  );
247
+ return rewrittenDraft.ast === plan.ast
248
+ ? plan
249
+ : { ...plan, ast: rewrittenDraft.ast, meta: rewrittenDraft.meta };
250
+ }
192
251
 
193
- const planToLower: SqlQueryPlan<Row> =
194
- rewrittenDraft.ast === plan.ast ? plan : { ...plan, ast: rewrittenDraft.ast };
195
-
196
- return lowerSqlPlan(this.adapter, this.contract, planToLower);
252
+ override execute<Row>(
253
+ plan: (SqlExecutionPlan<unknown> | SqlQueryPlan<unknown>) & { readonly _row?: Row },
254
+ options?: RuntimeExecuteOptions,
255
+ ): AsyncIterableResult<Row> {
256
+ return this.executeAgainstQueryable<Row>(plan, this.driver, options);
197
257
  }
198
258
 
199
- private executeAgainstQueryable<Row = Record<string, unknown>>(
200
- plan: ExecutionPlan<Row> | SqlQueryPlan<Row>,
201
- queryable: CoreQueryable,
259
+ private executeAgainstQueryable<Row>(
260
+ plan: SqlExecutionPlan<unknown> | SqlQueryPlan<unknown>,
261
+ queryable: SqlQueryable,
262
+ options?: RuntimeExecuteOptions,
202
263
  ): 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
- };
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
+ }
214
292
 
215
- const coreIterator = queryable.execute(planWithEncodedParams);
293
+ if (!self.verified && self.verify.mode === 'onFirstUse') {
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 startedAt = Date.now();
298
+ let outcome: TelemetryOutcome | null = null;
299
+
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
+ }),
223
314
  );
224
- 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
+ }
225
354
  }
226
355
  };
227
356
 
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);
357
+ return new AsyncIterableResult(generator());
235
358
  }
236
359
 
237
360
  async connection(): Promise<RuntimeConnection> {
238
- const coreConn = await this.core.connection();
361
+ const driverConn = await this.driver.acquireConnection();
239
362
  const self = this;
363
+
240
364
  const wrappedConnection: RuntimeConnection = {
241
365
  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
- };
366
+ const driverTx = await driverConn.beginTransaction();
367
+ return self.wrapTransaction(driverTx);
252
368
  },
253
- release: coreConn.release.bind(coreConn),
254
- destroy: coreConn.destroy.bind(coreConn),
255
- execute<Row = Record<string, unknown>>(
256
- plan: ExecutionPlan<Row> | SqlQueryPlan<Row>,
369
+ async release(): Promise<void> {
370
+ await driverConn.release();
371
+ },
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,
257
378
  ): AsyncIterableResult<Row> {
258
- return self.executeAgainstQueryable(plan, coreConn);
379
+ return self.executeAgainstQueryable<Row>(plan, driverConn, options);
259
380
  },
260
381
  };
382
+
261
383
  return wrappedConnection;
262
384
  }
263
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
+
264
404
  telemetry(): RuntimeTelemetryEvent | null {
265
- 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;
266
473
  }
267
474
 
268
- close(): Promise<void> {
269
- 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
+ });
270
488
  }
271
489
  }
272
490
 
@@ -290,13 +508,14 @@ export async function withTransaction<R>(
290
508
  get invalidated() {
291
509
  return invalidated;
292
510
  },
293
- execute<Row = Record<string, unknown>>(
294
- plan: ExecutionPlan<Row> | SqlQueryPlan<Row>,
511
+ execute<Row>(
512
+ plan: (SqlExecutionPlan<unknown> | SqlQueryPlan<unknown>) & { readonly _row?: Row },
513
+ options?: RuntimeExecuteOptions,
295
514
  ): AsyncIterableResult<Row> {
296
515
  if (invalidated) {
297
516
  throw transactionClosedError();
298
517
  }
299
- const inner = transaction.execute(plan);
518
+ const inner = transaction.execute(plan, options);
300
519
  const guarded = async function* (): AsyncGenerator<Row, void, unknown> {
301
520
  for await (const row of inner) {
302
521
  if (invalidated) {