@prisma-next/sql-runtime 0.5.0-dev.7 → 0.5.0-dev.70

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