@prisma-next/sql-runtime 0.5.0-dev.1 → 0.5.0-dev.11

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 (41) hide show
  1. package/README.md +29 -21
  2. package/dist/{exports-TJ70Qw3r.mjs → exports-BOHa3Emo.mjs} +502 -121
  3. package/dist/exports-BOHa3Emo.mjs.map +1 -0
  4. package/dist/{index-DyDQ4fyK.d.mts → index-CZmC2kD3.d.mts} +87 -23
  5. package/dist/index-CZmC2kD3.d.mts.map +1 -0
  6. package/dist/index.d.mts +2 -2
  7. package/dist/index.mjs +1 -1
  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 +7 -2
  11. package/dist/test/utils.mjs.map +1 -1
  12. package/package.json +10 -12
  13. package/src/codecs/decoding.ts +172 -116
  14. package/src/codecs/encoding.ts +59 -21
  15. package/src/exports/index.ts +10 -7
  16. package/src/fingerprint.ts +22 -0
  17. package/src/guardrails/raw.ts +214 -0
  18. package/src/lower-sql-plan.ts +3 -3
  19. package/src/marker.ts +82 -0
  20. package/src/middleware/before-compile-chain.ts +59 -0
  21. package/src/middleware/budgets.ts +25 -33
  22. package/src/middleware/lints.ts +5 -5
  23. package/src/middleware/sql-middleware.ts +36 -6
  24. package/src/runtime-spi.ts +43 -0
  25. package/src/sql-family-adapter.ts +3 -2
  26. package/src/sql-marker.ts +1 -1
  27. package/src/sql-runtime.ts +279 -101
  28. package/dist/exports-TJ70Qw3r.mjs.map +0 -1
  29. package/dist/index-DyDQ4fyK.d.mts.map +0 -1
  30. package/test/async-iterable-result.test.ts +0 -141
  31. package/test/budgets.test.ts +0 -431
  32. package/test/context.types.test-d.ts +0 -68
  33. package/test/execution-stack.test.ts +0 -161
  34. package/test/json-schema-validation.test.ts +0 -571
  35. package/test/lints.test.ts +0 -159
  36. package/test/mutation-default-generators.test.ts +0 -254
  37. package/test/parameterized-types.test.ts +0 -529
  38. package/test/sql-context.test.ts +0 -384
  39. package/test/sql-family-adapter.test.ts +0 -103
  40. package/test/sql-runtime.test.ts +0 -634
  41. package/test/utils.ts +0 -297
@@ -1,8 +1,9 @@
1
- import type { Contract, ExecutionPlan } from '@prisma-next/contract/types';
1
+ import type { Contract } from '@prisma-next/contract/types';
2
+ import type { ExecutionPlan } from '@prisma-next/framework-components/runtime';
2
3
  import { runtimeError } from '@prisma-next/framework-components/runtime';
3
- import type { MarkerReader, RuntimeFamilyAdapter } from '@prisma-next/runtime-executor';
4
4
  import type { SqlStorage } from '@prisma-next/sql-contract/types';
5
5
  import type { AdapterProfile } from '@prisma-next/sql-relational-core/ast';
6
+ import type { MarkerReader, RuntimeFamilyAdapter } from './runtime-spi';
6
7
 
7
8
  export class SqlFamilyAdapter<TContract extends Contract<SqlStorage>>
8
9
  implements RuntimeFamilyAdapter<TContract>
package/src/sql-marker.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { MarkerStatement } from '@prisma-next/runtime-executor';
1
+ import type { MarkerStatement } from '@prisma-next/sql-relational-core/ast';
2
2
 
3
3
  export interface SqlStatement {
4
4
  readonly sql: string;
@@ -1,23 +1,16 @@
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
+ checkMiddlewareCompatibility,
9
+ RuntimeCore,
10
+ type RuntimeLog,
19
11
  runtimeError,
20
- } from '@prisma-next/runtime-executor';
12
+ runWithMiddleware,
13
+ } from '@prisma-next/framework-components/runtime';
21
14
  import type { SqlStorage } from '@prisma-next/sql-contract/types';
22
15
  import type {
23
16
  Adapter,
@@ -25,14 +18,27 @@ import type {
25
18
  CodecRegistry,
26
19
  LoweredStatement,
27
20
  SqlDriver,
21
+ SqlQueryable,
22
+ SqlTransaction,
28
23
  } from '@prisma-next/sql-relational-core/ast';
29
- import type { SqlQueryPlan } from '@prisma-next/sql-relational-core/plan';
24
+ import type { SqlExecutionPlan, SqlQueryPlan } from '@prisma-next/sql-relational-core/plan';
30
25
  import type { JsonSchemaValidatorRegistry } from '@prisma-next/sql-relational-core/query-lane-context';
26
+ import type { RuntimeScope } from '@prisma-next/sql-relational-core/types';
31
27
  import { ifDefined } from '@prisma-next/utils/defined';
32
28
  import { decodeRow } from './codecs/decoding';
33
29
  import { encodeParams } from './codecs/encoding';
34
30
  import { validateCodecRegistryCompleteness } from './codecs/validation';
31
+ import { computeSqlFingerprint } from './fingerprint';
35
32
  import { lowerSqlPlan } from './lower-sql-plan';
33
+ import { parseContractMarkerRow } from './marker';
34
+ import { runBeforeCompileChain } from './middleware/before-compile-chain';
35
+ import type { SqlMiddleware, SqlMiddlewareContext } from './middleware/sql-middleware';
36
+ import type {
37
+ RuntimeFamilyAdapter,
38
+ RuntimeTelemetryEvent,
39
+ RuntimeVerifyOptions,
40
+ TelemetryOutcome,
41
+ } from './runtime-spi';
36
42
  import type {
37
43
  ExecutionContext,
38
44
  SqlRuntimeAdapterInstance,
@@ -40,12 +46,14 @@ import type {
40
46
  } from './sql-context';
41
47
  import { SqlFamilyAdapter } from './sql-family-adapter';
42
48
 
49
+ export type Log = RuntimeLog;
50
+
43
51
  export interface RuntimeOptions<TContract extends Contract<SqlStorage> = Contract<SqlStorage>> {
44
52
  readonly context: ExecutionContext<TContract>;
45
53
  readonly adapter: Adapter<AnyQueryAst, Contract<SqlStorage>, LoweredStatement>;
46
54
  readonly driver: SqlDriver<unknown>;
47
55
  readonly verify: RuntimeVerifyOptions;
48
- readonly middleware?: readonly Middleware<TContract>[];
56
+ readonly middleware?: readonly SqlMiddleware[];
49
57
  readonly mode?: 'strict' | 'permissive';
50
58
  readonly log?: Log;
51
59
  }
@@ -64,7 +72,7 @@ export interface CreateRuntimeOptions<
64
72
  readonly context: ExecutionContext<TContract>;
65
73
  readonly driver: SqlDriver<unknown>;
66
74
  readonly verify: RuntimeVerifyOptions;
67
- readonly middleware?: readonly Middleware<TContract>[];
75
+ readonly middleware?: readonly SqlMiddleware[];
68
76
  readonly mode?: 'strict' | 'permissive';
69
77
  readonly log?: Log;
70
78
  }
@@ -106,39 +114,37 @@ export interface RuntimeTransaction extends RuntimeQueryable {
106
114
  rollback(): Promise<void>;
107
115
  }
108
116
 
109
- export interface RuntimeQueryable {
110
- execute<Row = Record<string, unknown>>(
111
- plan: ExecutionPlan<Row> | SqlQueryPlan<Row>,
112
- ): AsyncIterableResult<Row>;
113
- }
117
+ export interface RuntimeQueryable extends RuntimeScope {}
114
118
 
115
119
  export interface TransactionContext extends RuntimeQueryable {
116
120
  readonly invalidated: boolean;
117
121
  }
118
122
 
119
- interface CoreQueryable {
120
- execute<Row = Record<string, unknown>>(plan: ExecutionPlan<Row>): AsyncIterableResult<Row>;
121
- }
122
-
123
123
  export type { RuntimeTelemetryEvent, RuntimeVerifyOptions, TelemetryOutcome };
124
124
 
125
+ function isExecutionPlan(plan: SqlExecutionPlan | SqlQueryPlan): plan is SqlExecutionPlan {
126
+ return 'sql' in plan;
127
+ }
128
+
125
129
  class SqlRuntimeImpl<TContract extends Contract<SqlStorage> = Contract<SqlStorage>>
130
+ extends RuntimeCore<SqlQueryPlan, SqlExecutionPlan, SqlMiddleware>
126
131
  implements Runtime
127
132
  {
128
- private readonly core: RuntimeCore<TContract, SqlDriver<unknown>>;
129
133
  private readonly contract: TContract;
130
134
  private readonly adapter: Adapter<AnyQueryAst, Contract<SqlStorage>, LoweredStatement>;
135
+ private readonly driver: SqlDriver<unknown>;
136
+ private readonly familyAdapter: RuntimeFamilyAdapter<Contract<SqlStorage>>;
131
137
  private readonly codecRegistry: CodecRegistry;
132
138
  private readonly jsonSchemaValidators: JsonSchemaValidatorRegistry | undefined;
139
+ private readonly sqlCtx: SqlMiddlewareContext;
140
+ private readonly verify: RuntimeVerifyOptions;
133
141
  private codecRegistryValidated: boolean;
142
+ private verified: boolean;
143
+ private startupVerified: boolean;
144
+ private _telemetry: RuntimeTelemetryEvent | null;
134
145
 
135
146
  constructor(options: RuntimeOptions<TContract>) {
136
147
  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
148
 
143
149
  if (middleware) {
144
150
  for (const mw of middleware) {
@@ -146,18 +152,31 @@ class SqlRuntimeImpl<TContract extends Contract<SqlStorage> = Contract<SqlStorag
146
152
  }
147
153
  }
148
154
 
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),
155
+ const sqlCtx: SqlMiddlewareContext = {
156
+ contract: context.contract,
157
+ mode: mode ?? 'strict',
158
+ now: () => Date.now(),
159
+ log: log ?? {
160
+ info: () => {},
161
+ warn: () => {},
162
+ error: () => {},
163
+ },
158
164
  };
159
165
 
160
- this.core = createRuntimeCore(coreOptions);
166
+ super({ middleware: middleware ?? [], ctx: sqlCtx });
167
+
168
+ this.contract = context.contract;
169
+ this.adapter = adapter;
170
+ this.driver = driver;
171
+ this.familyAdapter = new SqlFamilyAdapter(context.contract, adapter.profile);
172
+ this.codecRegistry = context.codecs;
173
+ this.jsonSchemaValidators = context.jsonSchemaValidators;
174
+ this.sqlCtx = sqlCtx;
175
+ this.verify = verify;
176
+ this.codecRegistryValidated = false;
177
+ this.verified = verify.mode === 'startup' ? false : verify.mode === 'always';
178
+ this.startupVerified = false;
179
+ this._telemetry = null;
161
180
 
162
181
  if (verify.mode === 'startup') {
163
182
  validateCodecRegistryCompleteness(this.codecRegistry, context.contract);
@@ -165,92 +184,251 @@ class SqlRuntimeImpl<TContract extends Contract<SqlStorage> = Contract<SqlStorag
165
184
  }
166
185
  }
167
186
 
168
- private ensureCodecRegistryValidated(contract: Contract<SqlStorage>): void {
169
- if (!this.codecRegistryValidated) {
170
- validateCodecRegistryCompleteness(this.codecRegistry, contract);
171
- this.codecRegistryValidated = true;
172
- }
187
+ /**
188
+ * Lower a `SqlQueryPlan` (AST + meta) into a `SqlExecutionPlan` with
189
+ * encoded parameters ready for the driver. This is the single point at
190
+ * which params transition from app-layer values to driver wire-format.
191
+ */
192
+ protected override async lower(plan: SqlQueryPlan): Promise<SqlExecutionPlan> {
193
+ const lowered = lowerSqlPlan(this.adapter, this.contract, plan);
194
+ return Object.freeze({
195
+ ...lowered,
196
+ params: await encodeParams(lowered, this.codecRegistry),
197
+ });
173
198
  }
174
199
 
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
- };
200
+ /**
201
+ * Default driver invocation. Production execution paths override the
202
+ * queryable target (e.g. transaction or connection) by going through
203
+ * `executeAgainstQueryable`; this implementation supports any caller of
204
+ * `super.execute(plan)` and the abstract-base contract.
205
+ */
206
+ protected override runDriver(exec: SqlExecutionPlan): AsyncIterable<Record<string, unknown>> {
207
+ return this.driver.execute<Record<string, unknown>>({
208
+ sql: exec.sql,
209
+ params: exec.params,
210
+ });
211
+ }
179
212
 
180
- return isSqlQueryPlan(plan) ? lowerSqlPlan(this.adapter, this.contract, plan) : plan;
213
+ /**
214
+ * SQL pre-compile hook. Runs the registered middleware `beforeCompile`
215
+ * chain over the plan's draft (AST + meta) and returns a `SqlQueryPlan`
216
+ * with the rewritten AST and meta when the chain mutates them. The chain
217
+ * re-derives `meta.paramDescriptors` from the rewritten AST so descriptors
218
+ * stay in lockstep with the params the adapter will emit during lowering.
219
+ */
220
+ protected override async runBeforeCompile(plan: SqlQueryPlan): Promise<SqlQueryPlan> {
221
+ const rewrittenDraft = await runBeforeCompileChain(
222
+ this.middleware,
223
+ { ast: plan.ast, meta: plan.meta },
224
+ this.sqlCtx,
225
+ );
226
+ return rewrittenDraft.ast === plan.ast
227
+ ? plan
228
+ : { ...plan, ast: rewrittenDraft.ast, meta: rewrittenDraft.meta };
181
229
  }
182
230
 
183
- private executeAgainstQueryable<Row = Record<string, unknown>>(
184
- plan: ExecutionPlan<Row> | SqlQueryPlan<Row>,
185
- queryable: CoreQueryable,
231
+ override execute<Row>(
232
+ plan: (SqlExecutionPlan<unknown> | SqlQueryPlan<unknown>) & { readonly _row?: Row },
186
233
  ): 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
- };
234
+ return this.executeAgainstQueryable<Row>(plan, this.driver);
235
+ }
236
+
237
+ private executeAgainstQueryable<Row>(
238
+ plan: SqlExecutionPlan<unknown> | SqlQueryPlan<unknown>,
239
+ queryable: SqlQueryable,
240
+ ): AsyncIterableResult<Row> {
241
+ this.ensureCodecRegistryValidated();
242
+
243
+ const self = this;
244
+ const generator = async function* (): AsyncGenerator<Row, void, unknown> {
245
+ const exec: SqlExecutionPlan = isExecutionPlan(plan)
246
+ ? Object.freeze({
247
+ ...plan,
248
+ params: await encodeParams(plan, self.codecRegistry),
249
+ })
250
+ : await self.lower(await self.runBeforeCompile(plan));
251
+
252
+ self.familyAdapter.validatePlan(exec, self.contract);
253
+ self._telemetry = null;
254
+
255
+ if (!self.startupVerified && self.verify.mode === 'startup') {
256
+ await self.verifyMarker();
257
+ }
258
+
259
+ if (!self.verified && self.verify.mode === 'onFirstUse') {
260
+ await self.verifyMarker();
261
+ }
198
262
 
199
- const coreIterator = queryable.execute(planWithEncodedParams);
263
+ const startedAt = Date.now();
264
+ let outcome: TelemetryOutcome | null = null;
200
265
 
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,
266
+ try {
267
+ if (self.verify.mode === 'always') {
268
+ await self.verifyMarker();
269
+ }
270
+
271
+ const stream = runWithMiddleware<SqlExecutionPlan, Record<string, unknown>>(
272
+ exec,
273
+ self.middleware,
274
+ self.ctx,
275
+ () =>
276
+ queryable.execute<Record<string, unknown>>({
277
+ sql: exec.sql,
278
+ params: exec.params,
279
+ }),
207
280
  );
208
- yield decodedRow as Row;
281
+
282
+ for await (const rawRow of stream) {
283
+ const decodedRow = await decodeRow(
284
+ rawRow,
285
+ exec,
286
+ self.codecRegistry,
287
+ self.jsonSchemaValidators,
288
+ );
289
+ yield decodedRow as Row;
290
+ }
291
+
292
+ outcome = 'success';
293
+ } catch (error) {
294
+ outcome = 'runtime-error';
295
+ throw error;
296
+ } finally {
297
+ if (outcome !== null) {
298
+ self.recordTelemetry(exec, outcome, Date.now() - startedAt);
299
+ }
209
300
  }
210
301
  };
211
302
 
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);
303
+ return new AsyncIterableResult(generator());
219
304
  }
220
305
 
221
306
  async connection(): Promise<RuntimeConnection> {
222
- const coreConn = await this.core.connection();
307
+ const driverConn = await this.driver.acquireConnection();
223
308
  const self = this;
309
+
224
310
  const wrappedConnection: RuntimeConnection = {
225
311
  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
- };
312
+ const driverTx = await driverConn.beginTransaction();
313
+ return self.wrapTransaction(driverTx);
314
+ },
315
+ async release(): Promise<void> {
316
+ await driverConn.release();
317
+ },
318
+ async destroy(reason?: unknown): Promise<void> {
319
+ await driverConn.destroy(reason);
236
320
  },
237
- release: coreConn.release.bind(coreConn),
238
- destroy: coreConn.destroy.bind(coreConn),
239
- execute<Row = Record<string, unknown>>(
240
- plan: ExecutionPlan<Row> | SqlQueryPlan<Row>,
321
+ execute<Row>(
322
+ plan: (SqlExecutionPlan<unknown> | SqlQueryPlan<unknown>) & { readonly _row?: Row },
241
323
  ): AsyncIterableResult<Row> {
242
- return self.executeAgainstQueryable(plan, coreConn);
324
+ return self.executeAgainstQueryable<Row>(plan, driverConn);
243
325
  },
244
326
  };
327
+
245
328
  return wrappedConnection;
246
329
  }
247
330
 
331
+ private wrapTransaction(driverTx: SqlTransaction): RuntimeTransaction {
332
+ const self = this;
333
+ return {
334
+ async commit(): Promise<void> {
335
+ await driverTx.commit();
336
+ },
337
+ async rollback(): Promise<void> {
338
+ await driverTx.rollback();
339
+ },
340
+ execute<Row>(
341
+ plan: (SqlExecutionPlan<unknown> | SqlQueryPlan<unknown>) & { readonly _row?: Row },
342
+ ): AsyncIterableResult<Row> {
343
+ return self.executeAgainstQueryable<Row>(plan, driverTx);
344
+ },
345
+ };
346
+ }
347
+
248
348
  telemetry(): RuntimeTelemetryEvent | null {
249
- return this.core.telemetry();
349
+ return this._telemetry;
350
+ }
351
+
352
+ async close(): Promise<void> {
353
+ await this.driver.close();
354
+ }
355
+
356
+ private ensureCodecRegistryValidated(): void {
357
+ if (!this.codecRegistryValidated) {
358
+ validateCodecRegistryCompleteness(this.codecRegistry, this.contract);
359
+ this.codecRegistryValidated = true;
360
+ }
361
+ }
362
+
363
+ private async verifyMarker(): Promise<void> {
364
+ if (this.verify.mode === 'always') {
365
+ this.verified = false;
366
+ }
367
+
368
+ if (this.verified) {
369
+ return;
370
+ }
371
+
372
+ const readStatement = this.familyAdapter.markerReader.readMarkerStatement();
373
+ const result = await this.driver.query(readStatement.sql, readStatement.params);
374
+
375
+ if (result.rows.length === 0) {
376
+ if (this.verify.requireMarker) {
377
+ throw runtimeError('CONTRACT.MARKER_MISSING', 'Contract marker not found in database');
378
+ }
379
+
380
+ this.verified = true;
381
+ return;
382
+ }
383
+
384
+ const marker = parseContractMarkerRow(result.rows[0]);
385
+
386
+ const contract = this.contract as {
387
+ storage: { storageHash: string };
388
+ execution?: { executionHash?: string | null };
389
+ profileHash?: string | null;
390
+ };
391
+
392
+ if (marker.storageHash !== contract.storage.storageHash) {
393
+ throw runtimeError(
394
+ 'CONTRACT.MARKER_MISMATCH',
395
+ 'Database storage hash does not match contract',
396
+ {
397
+ expected: contract.storage.storageHash,
398
+ actual: marker.storageHash,
399
+ },
400
+ );
401
+ }
402
+
403
+ const expectedProfile = contract.profileHash ?? null;
404
+ if (expectedProfile !== null && marker.profileHash !== expectedProfile) {
405
+ throw runtimeError(
406
+ 'CONTRACT.MARKER_MISMATCH',
407
+ 'Database profile hash does not match contract',
408
+ {
409
+ expectedProfile,
410
+ actualProfile: marker.profileHash,
411
+ },
412
+ );
413
+ }
414
+
415
+ this.verified = true;
416
+ this.startupVerified = true;
250
417
  }
251
418
 
252
- close(): Promise<void> {
253
- return this.core.close();
419
+ private recordTelemetry(
420
+ plan: SqlExecutionPlan,
421
+ outcome: TelemetryOutcome,
422
+ durationMs?: number,
423
+ ): void {
424
+ const contract = this.contract as { target: string };
425
+ this._telemetry = Object.freeze({
426
+ lane: plan.meta.lane,
427
+ target: contract.target,
428
+ fingerprint: computeSqlFingerprint(plan.sql),
429
+ outcome,
430
+ ...(durationMs !== undefined ? { durationMs } : {}),
431
+ });
254
432
  }
255
433
  }
256
434
 
@@ -274,8 +452,8 @@ export async function withTransaction<R>(
274
452
  get invalidated() {
275
453
  return invalidated;
276
454
  },
277
- execute<Row = Record<string, unknown>>(
278
- plan: ExecutionPlan<Row> | SqlQueryPlan<Row>,
455
+ execute<Row>(
456
+ plan: (SqlExecutionPlan<unknown> | SqlQueryPlan<unknown>) & { readonly _row?: Row },
279
457
  ): AsyncIterableResult<Row> {
280
458
  if (invalidated) {
281
459
  throw transactionClosedError();