@prisma-next/sql-runtime 0.9.0-dev.1 → 0.9.0-dev.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.
@@ -0,0 +1,65 @@
1
+ import type { JsonValue, PlanMeta } from '@prisma-next/contract/types';
2
+ import type {
3
+ AsyncIterableResult,
4
+ RuntimeExecuteOptions,
5
+ } from '@prisma-next/framework-components/runtime';
6
+ import type { AnyQueryAst, LoweredParam } from '@prisma-next/sql-relational-core/ast';
7
+ import type {
8
+ CodecTypesBase,
9
+ CodecValue,
10
+ Expression,
11
+ } from '@prisma-next/sql-relational-core/expression';
12
+ import type { SqlQueryPlan } from '@prisma-next/sql-relational-core/plan';
13
+ import type { RuntimeQueryable } from '../sql-runtime';
14
+
15
+ export type ParamSpec<CT extends CodecTypesBase = CodecTypesBase> =
16
+ | (keyof CT & string)
17
+ | {
18
+ readonly codecId: keyof CT & string;
19
+ readonly typeParams?: JsonValue;
20
+ readonly nullable?: boolean;
21
+ };
22
+
23
+ export type Declaration<CT extends CodecTypesBase = CodecTypesBase> = Readonly<
24
+ Record<string, ParamSpec<CT>>
25
+ >;
26
+
27
+ export type DeclaredCodecId<S> = S extends string
28
+ ? S
29
+ : S extends { readonly codecId: infer C extends string }
30
+ ? C
31
+ : never;
32
+
33
+ export type DeclaredNullable<S> = S extends { readonly nullable: true } ? true : false;
34
+
35
+ export type BindSiteParams<D> = {
36
+ readonly [K in keyof D]: Expression<{
37
+ codecId: DeclaredCodecId<D[K]>;
38
+ nullable: DeclaredNullable<D[K]>;
39
+ }>;
40
+ };
41
+
42
+ export type ParamsFromDeclaration<D, CT extends CodecTypesBase> = {
43
+ readonly [K in keyof D]: CodecValue<DeclaredCodecId<D[K]>, DeclaredNullable<D[K]>, CT>;
44
+ };
45
+
46
+ export type PrepareCallback<D, Row> = (params: BindSiteParams<D>) => SqlQueryPlan<Row>;
47
+
48
+ export interface PreparedStatement<Params, Row> {
49
+ readonly sql: string;
50
+ readonly ast: AnyQueryAst;
51
+ readonly meta: PlanMeta;
52
+ readonly slots: readonly LoweredParam[];
53
+
54
+ /**
55
+ * Run this prepared statement against the given target. The target carries
56
+ * the execution scope (top-level runtime, an explicit connection, an active
57
+ * transaction). It is required and explicit — there is no implicit binding
58
+ * back to the runtime that produced this statement.
59
+ */
60
+ execute(
61
+ target: RuntimeQueryable,
62
+ params: Params,
63
+ options?: RuntimeExecuteOptions,
64
+ ): AsyncIterableResult<Row>;
65
+ }
@@ -21,11 +21,14 @@ import type {
21
21
  AnyQueryAst,
22
22
  ContractCodecRegistry,
23
23
  LoweredStatement,
24
+ PreparedExecuteRequest,
24
25
  SqlCodecCallContext,
25
26
  SqlDriver,
26
27
  SqlQueryable,
27
28
  SqlTransaction,
28
29
  } from '@prisma-next/sql-relational-core/ast';
30
+ import { collectOrderedParamRefs } from '@prisma-next/sql-relational-core/ast';
31
+ import type { CodecTypesBase } from '@prisma-next/sql-relational-core/expression';
29
32
  import {
30
33
  createSqlParamRefMutator,
31
34
  type SqlParamRefMutator,
@@ -35,14 +38,26 @@ import type { SqlExecutionPlan, SqlQueryPlan } from '@prisma-next/sql-relational
35
38
  import type { CodecDescriptorRegistry } from '@prisma-next/sql-relational-core/query-lane-context';
36
39
  import type { RuntimeScope } from '@prisma-next/sql-relational-core/types';
37
40
  import { ifDefined } from '@prisma-next/utils/defined';
38
- import { decodeRow } from './codecs/decoding';
39
- import { encodeParams } from './codecs/encoding';
41
+ import { buildDecodeContext, type DecodeContext, decodeRow } from './codecs/decoding';
42
+ import { deriveParamMetadata, encodeParams, encodeParamsWithMetadata } from './codecs/encoding';
40
43
  import { validateCodecRegistryCompleteness } from './codecs/validation';
41
44
  import { computeSqlContentHash } from './content-hash';
42
45
  import { computeSqlFingerprint } from './fingerprint';
43
46
  import { lowerSqlPlan } from './lower-sql-plan';
44
47
  import { runBeforeCompileChain } from './middleware/before-compile-chain';
45
48
  import type { SqlMiddleware, SqlMiddlewareContext } from './middleware/sql-middleware';
49
+ import { buildBindSiteParams } from './prepared/bind-site-params';
50
+ import { resolvePreparedSlotValues } from './prepared/encode-prepared';
51
+ import {
52
+ PreparedStatementImpl,
53
+ type PreparedStatementInternals,
54
+ } from './prepared/prepared-statement';
55
+ import type {
56
+ Declaration,
57
+ ParamsFromDeclaration,
58
+ PrepareCallback,
59
+ PreparedStatement,
60
+ } from './prepared/types';
46
61
  import type {
47
62
  RuntimeFamilyAdapter,
48
63
  RuntimeTelemetryEvent,
@@ -91,6 +106,16 @@ export interface Runtime extends RuntimeQueryable {
91
106
  connection(): Promise<RuntimeConnection>;
92
107
  telemetry(): RuntimeTelemetryEvent | null;
93
108
  close(): Promise<void>;
109
+
110
+ /**
111
+ * Build a reusable {@link PreparedStatement}. Throws
112
+ * `RUNTIME.PREPARE_UNUSED_PARAM` if any declared name is unreferenced
113
+ * by the callback's plan.
114
+ */
115
+ prepare<D extends Declaration<CT>, Row, CT extends CodecTypesBase = CodecTypesBase>(
116
+ declaration: D,
117
+ callback: PrepareCallback<D, Row>,
118
+ ): Promise<PreparedStatement<ParamsFromDeclaration<D, CT>, Row>>;
94
119
  }
95
120
 
96
121
  export interface RuntimeConnection extends RuntimeQueryable {
@@ -114,7 +139,19 @@ export interface RuntimeTransaction extends RuntimeQueryable {
114
139
  rollback(): Promise<void>;
115
140
  }
116
141
 
117
- export interface RuntimeQueryable extends RuntimeScope {}
142
+ export interface RuntimeQueryable extends RuntimeScope {
143
+ /**
144
+ * Run a prepared statement against this scope. Required for the explicit
145
+ * `PreparedStatement.execute(target, params)` API — every scope (top-level
146
+ * runtime, connection, transaction) routes prepared executions through the
147
+ * `SqlQueryable` it is backed by.
148
+ */
149
+ executePrepared<Params, Row>(
150
+ ps: PreparedStatement<Params, Row>,
151
+ params: Params,
152
+ options?: RuntimeExecuteOptions,
153
+ ): AsyncIterableResult<Row>;
154
+ }
118
155
 
119
156
  export interface TransactionContext extends RuntimeQueryable {
120
157
  readonly invalidated: boolean;
@@ -142,6 +179,7 @@ class SqlRuntimeImpl<TContract extends Contract<SqlStorage> = Contract<SqlStorag
142
179
  private readonly codecDescriptors: CodecDescriptorRegistry;
143
180
  private readonly sqlCtx: SqlMiddlewareContext;
144
181
  private readonly verify: RuntimeVerifyOptions;
182
+ readonly #preparedStatementHandles = new WeakMap<object, unknown>();
145
183
  private codecRegistryValidated: boolean;
146
184
  private verified: boolean;
147
185
  private startupVerified: boolean;
@@ -275,6 +313,86 @@ class SqlRuntimeImpl<TContract extends Contract<SqlStorage> = Contract<SqlStorag
275
313
  return this.executeAgainstQueryable<Row>(plan, this.driver, options);
276
314
  }
277
315
 
316
+ executePrepared<Params, Row>(
317
+ ps: PreparedStatement<Params, Row>,
318
+ params: Params,
319
+ options?: RuntimeExecuteOptions,
320
+ ): AsyncIterableResult<Row> {
321
+ return this.executePreparedAgainstQueryable<Params, Row>(
322
+ ps as PreparedStatementImpl<Params, Row>,
323
+ params as Record<string, unknown>,
324
+ this.driver,
325
+ options,
326
+ );
327
+ }
328
+
329
+ private async *streamRows<Row>(
330
+ exec: SqlExecutionPlan,
331
+ decodeContext: DecodeContext,
332
+ driverCall: () => AsyncIterable<Record<string, unknown>>,
333
+ codecCtx: SqlCodecCallContext,
334
+ execMiddlewareCtx: RuntimeMiddlewareContext,
335
+ ): AsyncGenerator<Row, void, unknown> {
336
+ this.familyAdapter.validatePlan(exec, this.contract);
337
+ this._telemetry = null;
338
+
339
+ if (!this.startupVerified && this.verify.mode === 'startup') {
340
+ await this.verifyMarker();
341
+ }
342
+
343
+ if (!this.verified && this.verify.mode === 'onFirstUse') {
344
+ await this.verifyMarker();
345
+ }
346
+
347
+ const startedAt = Date.now();
348
+ let outcome: TelemetryOutcome | null = null;
349
+
350
+ try {
351
+ if (this.verify.mode === 'always') {
352
+ await this.verifyMarker();
353
+ }
354
+
355
+ const stream = runWithMiddleware<SqlExecutionPlan, Record<string, unknown>>(
356
+ exec,
357
+ this.middleware,
358
+ execMiddlewareCtx,
359
+ driverCall,
360
+ );
361
+
362
+ // Manually drive the driver's async iterator so the between-row
363
+ // abort check fires *before* requesting the next row. With a
364
+ // `for await...of` loop the runtime would await `iterator.next()`
365
+ // first, leaving a window where one extra row is pulled through
366
+ // the driver after the signal aborted.
367
+ const iterator = stream[Symbol.asyncIterator]();
368
+ try {
369
+ while (true) {
370
+ checkAborted(codecCtx, 'stream');
371
+ const next = await iterator.next();
372
+ if (next.done) {
373
+ break;
374
+ }
375
+ const decodedRow = await decodeRow(next.value, decodeContext, codecCtx);
376
+ yield decodedRow as Row;
377
+ }
378
+ } finally {
379
+ // Best-effort iterator cleanup so the driver can release its
380
+ // resources whether the stream finished normally, threw, or was
381
+ // abandoned by the consumer.
382
+ await iterator.return?.();
383
+ }
384
+
385
+ outcome = 'success';
386
+ } catch (error) {
387
+ outcome = 'runtime-error';
388
+ throw error;
389
+ } finally {
390
+ if (outcome !== null) {
391
+ this.recordTelemetry(exec, outcome, Date.now() - startedAt);
392
+ }
393
+ }
394
+ }
395
+
278
396
  private executeAgainstQueryable<Row>(
279
397
  plan: SqlExecutionPlan<unknown> | SqlQueryPlan<unknown>,
280
398
  queryable: SqlQueryable,
@@ -350,65 +468,138 @@ class SqlRuntimeImpl<TContract extends Contract<SqlStorage> = Contract<SqlStorag
350
468
  exec = await self.encodeDraftParams(draftWithMutations, codecCtx);
351
469
  }
352
470
 
353
- self.familyAdapter.validatePlan(exec, self.contract);
354
- self._telemetry = null;
471
+ const decodeContext = buildDecodeContext(exec.ast, self.contractCodecs);
355
472
 
356
- if (!self.startupVerified && self.verify.mode === 'startup') {
357
- await self.verifyMarker();
358
- }
473
+ yield* self.streamRows<Row>(
474
+ exec,
475
+ decodeContext,
476
+ () => queryable.execute<Record<string, unknown>>({ sql: exec.sql, params: exec.params }),
477
+ codecCtx,
478
+ execMiddlewareCtx,
479
+ );
480
+ };
359
481
 
360
- if (!self.verified && self.verify.mode === 'onFirstUse') {
361
- await self.verifyMarker();
362
- }
482
+ return new AsyncIterableResult(generator());
483
+ }
363
484
 
364
- const startedAt = Date.now();
365
- let outcome: TelemetryOutcome | null = null;
485
+ async prepare<D extends Declaration<CT>, Row, CT extends CodecTypesBase = CodecTypesBase>(
486
+ declaration: D,
487
+ callback: PrepareCallback<D, Row>,
488
+ ): Promise<PreparedStatement<ParamsFromDeclaration<D, CT>, Row>> {
489
+ this.ensureCodecRegistryValidated();
366
490
 
367
- try {
368
- if (self.verify.mode === 'always') {
369
- await self.verifyMarker();
370
- }
491
+ const bindSiteParams = buildBindSiteParams(declaration);
371
492
 
372
- const stream = runWithMiddleware<SqlExecutionPlan, Record<string, unknown>>(
373
- exec,
374
- self.middleware,
375
- execMiddlewareCtx,
376
- () =>
377
- queryable.execute<Record<string, unknown>>({
378
- sql: exec.sql,
379
- // `beforeExecute` ran on the pre-encode draft (see
380
- // generator setup above); `exec.params` already carries
381
- // any mutator-driven replacements through `encodeParams`.
382
- params: exec.params,
383
- }),
384
- );
493
+ const userPlan = callback(bindSiteParams);
494
+ const finalPlan = await this.runBeforeCompile(userPlan);
495
+ const orderedRefs = collectOrderedParamRefs(finalPlan.ast);
385
496
 
386
- // 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.
387
- const iterator = stream[Symbol.asyncIterator]();
388
- try {
389
- while (true) {
390
- checkAborted(codecCtx, 'stream');
391
- const next = await iterator.next();
392
- if (next.done) {
393
- break;
394
- }
395
- const decodedRow = await decodeRow(next.value, exec, codecCtx, self.contractCodecs);
396
- yield decodedRow as Row;
397
- }
398
- } finally {
399
- // Best-effort iterator cleanup so the driver can release its resources whether the stream finished normally, threw, or was abandoned by the consumer.
400
- await iterator.return?.();
401
- }
497
+ // Type-level detection isn't achievable across chained-builder generics.
498
+ const referencedNames = new Set<string>();
499
+ for (const ref of orderedRefs) {
500
+ if (ref.kind === 'prepared-param-ref') referencedNames.add(ref.name);
501
+ }
502
+ const missing = Object.keys(declaration).filter((name) => !referencedNames.has(name));
503
+ if (missing.length > 0) {
504
+ throw runtimeError(
505
+ 'RUNTIME.PREPARE_UNUSED_PARAM',
506
+ `Prepared statement declaration includes parameter${missing.length === 1 ? '' : 's'} not referenced by the callback's plan: ${missing.join(', ')}`,
507
+ { unused: missing },
508
+ );
509
+ }
402
510
 
403
- outcome = 'success';
404
- } catch (error) {
405
- outcome = 'runtime-error';
406
- throw error;
407
- } finally {
408
- if (outcome !== null) {
409
- self.recordTelemetry(exec, outcome, Date.now() - startedAt);
410
- }
411
- }
511
+ const lowered = this.adapter.lower(finalPlan.ast, {
512
+ contract: this.contract,
513
+ params: orderedRefs.map((r) => (r.kind === 'param-ref' ? r.value : undefined)),
514
+ });
515
+
516
+ const decodeContext = buildDecodeContext(finalPlan.ast, this.contractCodecs);
517
+ const paramMetadata = deriveParamMetadata(finalPlan.ast);
518
+
519
+ const internals: PreparedStatementInternals = Object.freeze({
520
+ sql: lowered.sql,
521
+ ast: finalPlan.ast,
522
+ meta: finalPlan.meta,
523
+ slots: lowered.params,
524
+ decodeContext,
525
+ paramMetadata,
526
+ });
527
+
528
+ return new PreparedStatementImpl<ParamsFromDeclaration<D, CT>, Row>(internals);
529
+ }
530
+
531
+ private executePreparedAgainstQueryable<P, Row>(
532
+ ps: PreparedStatementImpl<P, Row>,
533
+ userParams: Record<string, unknown>,
534
+ queryable: SqlQueryable,
535
+ options?: RuntimeExecuteOptions,
536
+ ): AsyncIterableResult<Row> {
537
+ this.ensureCodecRegistryValidated();
538
+
539
+ const self = this;
540
+ const signal = options?.signal;
541
+ const scope = options?.scope ?? 'runtime';
542
+ const codecCtx: SqlCodecCallContext = signal === undefined ? {} : { signal };
543
+ const execMiddlewareCtx: RuntimeMiddlewareContext = {
544
+ ...self.ctx,
545
+ ...ifDefined('signal', signal),
546
+ ...(scope !== 'runtime' ? { scope } : {}),
547
+ };
548
+
549
+ const generator = async function* (): AsyncGenerator<Row, void, unknown> {
550
+ checkAborted(codecCtx, 'stream');
551
+
552
+ // Resolve slot order to unencoded values so `beforeExecute`'s
553
+ // mutator sees pre-encode user values for prepared-param slots
554
+ // and can override them before encode runs.
555
+ const preEncodeValues = resolvePreparedSlotValues(ps, userParams);
556
+ const preEncodeExec: SqlExecutionPlan = {
557
+ sql: ps.sql,
558
+ params: preEncodeValues,
559
+ ast: ps.ast,
560
+ meta: ps.meta,
561
+ };
562
+
563
+ const mutator: SqlParamRefMutatorInternal = createSqlParamRefMutator(preEncodeExec);
564
+ await runBeforeExecuteChain<SqlExecutionPlan, SqlParamRefMutator>(
565
+ preEncodeExec,
566
+ self.middleware,
567
+ execMiddlewareCtx,
568
+ mutator,
569
+ );
570
+
571
+ const encodedParams = await encodeParamsWithMetadata(
572
+ mutator.currentParams(),
573
+ ps.paramMetadata,
574
+ codecCtx,
575
+ self.contractCodecs,
576
+ );
577
+ const exec: SqlExecutionPlan = {
578
+ sql: ps.sql,
579
+ params: encodedParams,
580
+ ast: ps.ast,
581
+ meta: ps.meta,
582
+ };
583
+
584
+ const handles = self.#preparedStatementHandles;
585
+ const request: PreparedExecuteRequest = {
586
+ sql: exec.sql,
587
+ params: exec.params,
588
+ handle: {
589
+ get: () => handles.get(ps),
590
+ set: (value) => {
591
+ handles.set(ps, value);
592
+ },
593
+ },
594
+ };
595
+
596
+ yield* self.streamRows<Row>(
597
+ exec,
598
+ ps.decodeContext,
599
+ () => queryable.executePrepared<Record<string, unknown>>(request),
600
+ codecCtx,
601
+ execMiddlewareCtx,
602
+ );
412
603
  };
413
604
 
414
605
  return new AsyncIterableResult(generator());
@@ -438,6 +629,18 @@ class SqlRuntimeImpl<TContract extends Contract<SqlStorage> = Contract<SqlStorag
438
629
  scope: 'connection',
439
630
  });
440
631
  },
632
+ executePrepared<Params, Row>(
633
+ ps: PreparedStatement<Params, Row>,
634
+ params: Params,
635
+ options?: RuntimeExecuteOptions,
636
+ ): AsyncIterableResult<Row> {
637
+ return self.executePreparedAgainstQueryable<Params, Row>(
638
+ ps as PreparedStatementImpl<Params, Row>,
639
+ params as Record<string, unknown>,
640
+ driverConn,
641
+ { ...options, scope: 'connection' },
642
+ );
643
+ },
441
644
  };
442
645
 
443
646
  return wrappedConnection;
@@ -461,6 +664,18 @@ class SqlRuntimeImpl<TContract extends Contract<SqlStorage> = Contract<SqlStorag
461
664
  scope: 'transaction',
462
665
  });
463
666
  },
667
+ executePrepared<Params, Row>(
668
+ ps: PreparedStatement<Params, Row>,
669
+ params: Params,
670
+ options?: RuntimeExecuteOptions,
671
+ ): AsyncIterableResult<Row> {
672
+ return self.executePreparedAgainstQueryable<Params, Row>(
673
+ ps as PreparedStatementImpl<Params, Row>,
674
+ params as Record<string, unknown>,
675
+ driverTx,
676
+ { ...options, scope: 'transaction' },
677
+ );
678
+ },
464
679
  };
465
680
  }
466
681
 
@@ -580,6 +795,25 @@ export async function withTransaction<R>(
580
795
  };
581
796
  return new AsyncIterableResult(guarded());
582
797
  },
798
+ executePrepared<Params, Row>(
799
+ ps: PreparedStatement<Params, Row>,
800
+ params: Params,
801
+ options?: RuntimeExecuteOptions,
802
+ ): AsyncIterableResult<Row> {
803
+ if (invalidated) {
804
+ throw transactionClosedError();
805
+ }
806
+ const inner = transaction.executePrepared(ps, params, options);
807
+ const guarded = async function* (): AsyncGenerator<Row, void, unknown> {
808
+ for await (const row of inner) {
809
+ if (invalidated) {
810
+ throw transactionClosedError();
811
+ }
812
+ yield row;
813
+ }
814
+ };
815
+ return new AsyncIterableResult(guarded());
816
+ },
583
817
  };
584
818
 
585
819
  let connectionDisposed = false;