@prisma-next/sql-runtime 0.6.0-dev.1 → 0.6.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.
- package/dist/{exports-BSTHn_rH.mjs → exports-6tjocFbX.mjs} +225 -179
- package/dist/exports-6tjocFbX.mjs.map +1 -0
- package/dist/{index-CTCvZOWI.d.mts → index-CNy2q72g.d.mts} +24 -12
- package/dist/index-CNy2q72g.d.mts.map +1 -0
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +1 -1
- package/dist/test/utils.d.mts +2 -2
- package/dist/test/utils.d.mts.map +1 -1
- package/dist/test/utils.mjs +61 -6
- package/dist/test/utils.mjs.map +1 -1
- package/package.json +11 -11
- package/src/codecs/ast-codec-resolver.ts +99 -0
- package/src/codecs/decoding.ts +4 -39
- package/src/codecs/encoding.ts +20 -46
- package/src/middleware/sql-middleware.ts +22 -10
- package/src/sql-context.ts +157 -124
- package/src/sql-runtime.ts +84 -24
- package/dist/exports-BSTHn_rH.mjs.map +0 -1
- package/dist/index-CTCvZOWI.d.mts.map +0 -1
- package/src/codecs/alias-resolver.ts +0 -37
package/src/sql-context.ts
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
1
|
+
import type {
|
|
2
|
+
Contract,
|
|
3
|
+
ExecutionMutationDefaultValue,
|
|
4
|
+
JsonValue,
|
|
5
|
+
} from '@prisma-next/contract/types';
|
|
6
|
+
import type {
|
|
7
|
+
AnyCodecDescriptor,
|
|
8
|
+
CodecDescriptor,
|
|
9
|
+
CodecRef,
|
|
10
|
+
} from '@prisma-next/framework-components/codec';
|
|
3
11
|
import type { ComponentDescriptor } from '@prisma-next/framework-components/components';
|
|
4
12
|
import { checkContractComponentRequirements } from '@prisma-next/framework-components/components';
|
|
5
13
|
import {
|
|
@@ -15,6 +23,7 @@ import {
|
|
|
15
23
|
type RuntimeTargetInstance,
|
|
16
24
|
} from '@prisma-next/framework-components/execution';
|
|
17
25
|
import { runtimeError } from '@prisma-next/framework-components/runtime';
|
|
26
|
+
import { canonicalizeJson } from '@prisma-next/framework-components/utils';
|
|
18
27
|
import type { SqlStorage } from '@prisma-next/sql-contract/types';
|
|
19
28
|
import {
|
|
20
29
|
createSqlOperationRegistry,
|
|
@@ -23,7 +32,6 @@ import {
|
|
|
23
32
|
import type {
|
|
24
33
|
Adapter,
|
|
25
34
|
AnyQueryAst,
|
|
26
|
-
Codec,
|
|
27
35
|
ContractCodecRegistry,
|
|
28
36
|
LoweredStatement,
|
|
29
37
|
SqlCodecInstanceContext,
|
|
@@ -37,6 +45,7 @@ import type {
|
|
|
37
45
|
MutationDefaultsOptions,
|
|
38
46
|
TypeHelperRegistry,
|
|
39
47
|
} from '@prisma-next/sql-relational-core/query-lane-context';
|
|
48
|
+
import { createAstCodecResolver } from './codecs/ast-codec-resolver';
|
|
40
49
|
|
|
41
50
|
/**
|
|
42
51
|
* Runtime parameterized codec descriptor.
|
|
@@ -342,145 +351,173 @@ function validateColumnTypeParams(
|
|
|
342
351
|
}
|
|
343
352
|
}
|
|
344
353
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
354
|
+
/**
|
|
355
|
+
* Build-time contract-integrity check: every `(table, column)` resolves to a {@link CodecRef} whose `codecId` is registered and whose `typeParams` presence matches the descriptor's `isParameterized` flag.
|
|
356
|
+
*
|
|
357
|
+
* Surfaces three classes of malformed contract that AST-bound codec resolution would otherwise mask silently:
|
|
358
|
+
*
|
|
359
|
+
* - column references a codecId no contributor registered → `RUNTIME.CODEC_DESCRIPTOR_MISSING`.
|
|
360
|
+
* - parameterized codec, no `typeParams` (legacy "tolerate refs without params" shape) → `RUNTIME.CODEC_PARAMETERIZATION_MISMATCH`.
|
|
361
|
+
* - non-parameterized codec, `typeParams` supplied → `RUNTIME.CODEC_PARAMETERIZATION_MISMATCH`.
|
|
362
|
+
*
|
|
363
|
+
* Runs unconditionally from `createExecutionContext` so contract bugs fail fast at construction time instead of silently skipping affected columns in the codec registry's pre-population walk.
|
|
364
|
+
*/
|
|
365
|
+
function assertColumnCodecIntegrity(
|
|
366
|
+
storage: SqlStorage,
|
|
367
|
+
codecDescriptors: CodecDescriptorRegistry,
|
|
368
|
+
): void {
|
|
369
|
+
for (const [tableName, table] of Object.entries(storage.tables)) {
|
|
370
|
+
for (const columnName of Object.keys(table.columns)) {
|
|
371
|
+
const ref = codecDescriptors.codecRefForColumn(tableName, columnName);
|
|
372
|
+
if (!ref) continue;
|
|
373
|
+
|
|
374
|
+
const descriptor = codecDescriptors.descriptorFor(ref.codecId);
|
|
375
|
+
if (!descriptor) {
|
|
376
|
+
throw runtimeError(
|
|
377
|
+
'RUNTIME.CODEC_DESCRIPTOR_MISSING',
|
|
378
|
+
`Column '${tableName}.${columnName}' references codec '${ref.codecId}' but no contributor registered a codec descriptor for that codecId. Add the extension pack that owns the codec to the runtime stack.`,
|
|
379
|
+
{ table: tableName, column: columnName, codecId: ref.codecId },
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (descriptor.isParameterized && ref.typeParams === undefined) {
|
|
384
|
+
// Some parameterized codecs declare every paramsSchema field as optional
|
|
385
|
+
// (e.g. `pg/timestamptz@1` precision). Defer to the descriptor's own
|
|
386
|
+
// schema rather than rejecting purely on structural absence: probe the
|
|
387
|
+
// schema with an empty params object and only fail when the schema
|
|
388
|
+
// rejects it (i.e. at least one field is required).
|
|
389
|
+
const probe = descriptor.paramsSchema['~standard'].validate({});
|
|
390
|
+
if (probe instanceof Promise) {
|
|
391
|
+
// Swallow the probe Promise's rejection so Node doesn't warn about an
|
|
392
|
+
// unhandled rejection once we throw synchronously below.
|
|
393
|
+
probe.catch(() => {});
|
|
394
|
+
throw runtimeError(
|
|
395
|
+
'RUNTIME.TYPE_PARAMS_INVALID',
|
|
396
|
+
`Column '${tableName}.${columnName}' uses parameterized codec '${ref.codecId}' whose paramsSchema returned a Promise; paramsSchema must be a synchronous Standard Schema validator. Return a value/issues result directly instead of a Promise.`,
|
|
397
|
+
{ table: tableName, column: columnName, codecId: ref.codecId },
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
const rejects = 'issues' in probe && !!probe.issues;
|
|
401
|
+
if (rejects) {
|
|
402
|
+
throw runtimeError(
|
|
403
|
+
'RUNTIME.CODEC_PARAMETERIZATION_MISMATCH',
|
|
404
|
+
`Column '${tableName}.${columnName}' uses parameterized codec '${ref.codecId}' but no typeParams are supplied. Provide typeParams on the column, or use a typeRef pointing at a storage.types entry that carries them.`,
|
|
405
|
+
{
|
|
406
|
+
table: tableName,
|
|
407
|
+
column: columnName,
|
|
408
|
+
codecId: ref.codecId,
|
|
409
|
+
expected: 'parameterized',
|
|
410
|
+
actual: 'no typeParams',
|
|
411
|
+
},
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
if (!descriptor.isParameterized && ref.typeParams !== undefined) {
|
|
417
|
+
throw runtimeError(
|
|
418
|
+
'RUNTIME.CODEC_PARAMETERIZATION_MISMATCH',
|
|
419
|
+
`Column '${tableName}.${columnName}' supplies typeParams to non-parameterized codec '${ref.codecId}'. Remove the typeParams or switch to a parameterized codec id.`,
|
|
420
|
+
{
|
|
421
|
+
table: tableName,
|
|
422
|
+
column: columnName,
|
|
423
|
+
codecId: ref.codecId,
|
|
424
|
+
expected: 'non-parameterized',
|
|
425
|
+
actual: 'has typeParams',
|
|
426
|
+
},
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
352
431
|
}
|
|
353
432
|
|
|
354
433
|
/**
|
|
355
|
-
*
|
|
434
|
+
* Build a {@link ContractCodecRegistry} that resolves codecs exclusively through the `forCodecRef` content-keyed cache.
|
|
435
|
+
*
|
|
436
|
+
* One pre-population pass walks `storage.types` and `storage.tables[].columns[]` to seed the resolver's per-ref instance context with the *aggregated* `usedAt` set for each canonical `(codecId, typeParams)` key. The same codec materialised through `forColumn` or `forCodecRef` is therefore one instance with one `SqlCodecInstanceContext` — stateful codecs reading `usedAt` see the full column set regardless of which surface the caller used.
|
|
437
|
+
*
|
|
438
|
+
* Per-key instance-name policy:
|
|
356
439
|
*
|
|
357
|
-
* -
|
|
358
|
-
* -
|
|
359
|
-
* -
|
|
440
|
+
* - typeRef-shared columns use the `storage.types[name]` name.
|
|
441
|
+
* - inline-`typeParams` columns use `<col:Table.column>` (the first column observed at that key; additional columns sharing the key extend `usedAt`).
|
|
442
|
+
* - non-parameterized codec ids use `<codec:codecId>`, aggregating every column on that codec id into one `usedAt` set.
|
|
443
|
+
* - ad-hoc refs the contract walk did not pre-populate (e.g. AST-supplied refs from deserialised migration ops) fall back to the canonical cache key `${codecId}:${canonicalizeJson(typeParams)}` — the only structurally honest identity for an ad-hoc ref, distinct per `(codecId, typeParams)`.
|
|
360
444
|
*
|
|
361
|
-
*
|
|
445
|
+
* Contract integrity is enforced upstream by {@link assertColumnCodecIntegrity}: every column must reference a registered `codecId` whose `descriptor.isParameterized` flag matches the presence of `typeParams` (via `codecRefForColumn`). The pre-population walk and `forColumn` therefore make no defensive checks — malformed columns fail fast at `createExecutionContext` construction with `RUNTIME.CODEC_DESCRIPTOR_MISSING` or `RUNTIME.CODEC_PARAMETERIZATION_MISMATCH` rather than being silently skipped here.
|
|
446
|
+
*
|
|
447
|
+
* `forColumn(t, c)` is a thin delegate over `forCodecRef(codecRefForColumn(t, c))`; encode/decode hot paths read the resolver directly via `forCodecRef`. The only `undefined` `forColumn` returns is the legitimate "no such column in the contract" case.
|
|
362
448
|
*/
|
|
363
449
|
function buildContractCodecRegistry(
|
|
364
450
|
contract: Contract<SqlStorage>,
|
|
365
451
|
codecDescriptors: CodecDescriptorRegistry,
|
|
366
|
-
types: TypeHelperRegistry,
|
|
367
|
-
parameterizedDescriptors: Map<string, RuntimeParameterizedCodecDescriptor>,
|
|
368
452
|
): ContractCodecRegistry {
|
|
369
|
-
const
|
|
370
|
-
const byCodecId = new Map<string, Codec>();
|
|
371
|
-
// Codec ids whose `byCodecId` entry is ambiguous — multiple distinct resolved instances landed under the same parameterized codec id (e.g. `Vector<1024>` and `Vector<1536>` both registering under `pg/vector@1`). The refs-less `forCodecId` fallback rejects these ids so a DSL-param without a column ref cannot silently bind to the wrong instance. The validator pass enforces refs on every parameterized `ParamRef`, so this
|
|
372
|
-
// branch is reachable only as a defensive guard for non-parameterized columns whose `byCodecId` entry is unique by construction.
|
|
373
|
-
const ambiguousCodecIds = new Set<string>();
|
|
374
|
-
|
|
375
|
-
// Pre-populate `byCodecId` with non-parameterized descriptor instances. Refs-less encode/decode call sites (computed projections without a column ref, transient builder ParamRefs) resolve through `forCodecId(id)` and need a representative instance for codec ids that no contract column declares. Non-parameterized descriptors' factories are constant — every call yields the same shared codec — so a single materialization
|
|
376
|
-
// is correct.
|
|
377
|
-
for (const descriptor of codecDescriptors.values()) {
|
|
378
|
-
if (descriptor.isParameterized) continue;
|
|
379
|
-
const ctx: SqlCodecInstanceContext = {
|
|
380
|
-
name: `<shared:${descriptor.codecId}>`,
|
|
381
|
-
usedAt: [],
|
|
382
|
-
};
|
|
383
|
-
const voidFactory = descriptor.factory as unknown as (
|
|
384
|
-
params: undefined,
|
|
385
|
-
) => (ctx: SqlCodecInstanceContext) => Codec;
|
|
386
|
-
byCodecId.set(descriptor.codecId, voidFactory(undefined)(ctx));
|
|
387
|
-
}
|
|
453
|
+
const refKeyOf = (ref: CodecRef): string => `${ref.codecId}:${canonicalizeJson(ref.typeParams)}`;
|
|
388
454
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
const
|
|
395
|
-
|
|
396
|
-
|
|
455
|
+
const usedAtByKey = new Map<string, Array<{ readonly table: string; readonly column: string }>>();
|
|
456
|
+
const nameByKey = new Map<string, string>();
|
|
457
|
+
|
|
458
|
+
const typeRefSites = collectTypeRefSites(contract.storage);
|
|
459
|
+
for (const [typeName, typeInstance] of Object.entries(contract.storage.types ?? {})) {
|
|
460
|
+
const ref: CodecRef = {
|
|
461
|
+
codecId: typeInstance.codecId,
|
|
462
|
+
typeParams: typeInstance.typeParams as JsonValue,
|
|
397
463
|
};
|
|
398
|
-
|
|
399
|
-
const
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
}
|
|
405
|
-
|
|
464
|
+
const key = refKeyOf(ref);
|
|
465
|
+
const sites = typeRefSites.get(typeName) ?? [];
|
|
466
|
+
const existing = usedAtByKey.get(key);
|
|
467
|
+
// Two `storage.types` aliases that canonicalize to the same (codecId, typeParams) share a single codec instance via the resolver. Append sites instead of replacing so a stateful codec reading the aggregated site list sees every column behind every alias rather than just the last one.
|
|
468
|
+
if (existing) {
|
|
469
|
+
existing.push(...sites);
|
|
470
|
+
} else {
|
|
471
|
+
usedAtByKey.set(key, [...sites]);
|
|
472
|
+
nameByKey.set(key, typeName);
|
|
406
473
|
}
|
|
407
474
|
}
|
|
408
475
|
|
|
409
476
|
for (const [tableName, table] of Object.entries(contract.storage.tables)) {
|
|
410
477
|
for (const [columnName, column] of Object.entries(table.columns)) {
|
|
411
|
-
|
|
412
|
-
const
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
const parameterizedDescriptor = parameterizedDescriptors.get(column.codecId);
|
|
427
|
-
if (parameterizedDescriptor) {
|
|
428
|
-
const validatedParams = validateTypeParams(column.typeParams, parameterizedDescriptor, {
|
|
429
|
-
tableName,
|
|
430
|
-
columnName,
|
|
431
|
-
});
|
|
432
|
-
const ctx: SqlCodecInstanceContext = {
|
|
433
|
-
name: `<col:${tableName}.${columnName}>`,
|
|
434
|
-
usedAt: [{ table: tableName, column: columnName }],
|
|
435
|
-
};
|
|
436
|
-
resolvedCodec = parameterizedDescriptor.factory(validatedParams)(ctx);
|
|
437
|
-
}
|
|
438
|
-
} else if (!isParameterized) {
|
|
439
|
-
// Non-parameterized column: materialize a fresh codec instance per `forColumn(table, column)` entry with a column-specific `SqlCodecInstanceContext`. The pre-populated `byCodecId` representative (built with the synthetic `<shared:codecId>` context and empty `usedAt`) is reserved for `forCodecId()` refs-less fallbacks; reusing it for column-bound dispatch would erase per-column diagnostics for any descriptor whose factory reads `CodecInstanceContext`.
|
|
440
|
-
const ctx: SqlCodecInstanceContext = {
|
|
441
|
-
name: `<col:${tableName}.${columnName}>`,
|
|
442
|
-
usedAt: [{ table: tableName, column: columnName }],
|
|
443
|
-
};
|
|
444
|
-
// The descriptor's `P` is `void` for non-parameterized codecs; the runtime's `void` value is `undefined`. The cast narrows the descriptor's family-agnostic `CodecInstanceContext` slot to the SQL `SqlCodecInstanceContext` we pass at this call site — function-argument contravariance makes the narrow safe.
|
|
445
|
-
// `bind` preserves the `this`-on-descriptor invariant — several descriptors implement `factory` as a class method whose body returns an arrow that captures `this`; detaching loses the binding and produces a codec whose `descriptor` is `undefined`.
|
|
446
|
-
const voidFactory = descriptor.factory.bind(descriptor) as unknown as (
|
|
447
|
-
params: undefined,
|
|
448
|
-
) => (ctx: SqlCodecInstanceContext) => Codec;
|
|
449
|
-
resolvedCodec = voidFactory(undefined)(ctx);
|
|
450
|
-
}
|
|
451
|
-
// else: parameterized codec id with no typeRef and no typeParams — this is the legitimate "undimensioned" form for codecs that ship a no-params column variant alongside a parameterized one (e.g. pgvector's `vectorColumn` vs. `vector(N)`). Leave `resolvedCodec` undefined; encode/decode for this column flows through `forCodecId`. The fallback works for these cases because their wire format is params-independent (vector
|
|
452
|
-
// formats `[v1,v2,...]` regardless of declared length).
|
|
478
|
+
if (column.typeRef !== undefined) continue;
|
|
479
|
+
const ref = codecDescriptors.codecRefForColumn(tableName, columnName);
|
|
480
|
+
if (!ref) continue;
|
|
481
|
+
const key = refKeyOf(ref);
|
|
482
|
+
const site = { table: tableName, column: columnName };
|
|
483
|
+
const existing = usedAtByKey.get(key);
|
|
484
|
+
if (existing) {
|
|
485
|
+
existing.push(site);
|
|
486
|
+
} else {
|
|
487
|
+
usedAtByKey.set(key, [site]);
|
|
488
|
+
const name =
|
|
489
|
+
ref.typeParams !== undefined
|
|
490
|
+
? `<col:${tableName}.${columnName}>`
|
|
491
|
+
: `<codec:${ref.codecId}>`;
|
|
492
|
+
nameByKey.set(key, name);
|
|
453
493
|
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
454
496
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
497
|
+
const resolver = createAstCodecResolver(codecDescriptors, (ref) => {
|
|
498
|
+
const key = refKeyOf(ref);
|
|
499
|
+
// Fallback uses the canonical cache key as the instance name. Two ad-hoc refs with the same `codecId` but different `typeParams` resolve to distinct codecs (different cache keys) and must therefore expose distinct `name`s; a `codecId`-only fallback would collide and break stateful codecs that key per-instance state on `name`.
|
|
500
|
+
return {
|
|
501
|
+
name: nameByKey.get(key) ?? key,
|
|
502
|
+
usedAt: usedAtByKey.get(key) ?? [],
|
|
503
|
+
};
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
for (const [tableName, table] of Object.entries(contract.storage.tables)) {
|
|
507
|
+
for (const columnName of Object.keys(table.columns)) {
|
|
508
|
+
const ref = codecDescriptors.codecRefForColumn(tableName, columnName);
|
|
509
|
+
if (!ref) continue;
|
|
510
|
+
resolver.forCodecRef(ref);
|
|
464
511
|
}
|
|
465
512
|
}
|
|
466
513
|
|
|
467
514
|
const registry: ContractCodecRegistry = {
|
|
468
515
|
forColumn(table, column) {
|
|
469
|
-
|
|
516
|
+
const ref = codecDescriptors.codecRefForColumn(table, column);
|
|
517
|
+
return ref ? resolver.forCodecRef(ref) : undefined;
|
|
470
518
|
},
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
// typeRef/typeParams-bound column never reach this map.
|
|
474
|
-
//
|
|
475
|
-
// Reject ambiguous parameterized fallbacks: if the contract walk resolved more than one distinct codec instance under this id (e.g. multiple vector dimensions, multiple arktype-json schemas), the codec-id-keyed lookup cannot honor the call site — fail fast rather than bind to whichever instance happened to land first.
|
|
476
|
-
if (ambiguousCodecIds.has(codecId)) {
|
|
477
|
-
throw runtimeError(
|
|
478
|
-
'RUNTIME.TYPE_PARAMS_INVALID',
|
|
479
|
-
`Codec '${codecId}' resolves to multiple parameterized instances; column-aware dispatch is required.`,
|
|
480
|
-
{ codecId },
|
|
481
|
-
);
|
|
482
|
-
}
|
|
483
|
-
return byCodecId.get(codecId) ?? parameterizedRepresentatives.get(codecId);
|
|
519
|
+
forCodecRef(ref) {
|
|
520
|
+
return resolver.forCodecRef(ref);
|
|
484
521
|
},
|
|
485
522
|
};
|
|
486
523
|
|
|
@@ -684,7 +721,8 @@ export function createExecutionContext<
|
|
|
684
721
|
}
|
|
685
722
|
}
|
|
686
723
|
|
|
687
|
-
const codecDescriptors = buildCodecDescriptorRegistry(allCodecDescriptors);
|
|
724
|
+
const codecDescriptors = buildCodecDescriptorRegistry(allCodecDescriptors, contract.storage);
|
|
725
|
+
assertColumnCodecIntegrity(contract.storage, codecDescriptors);
|
|
688
726
|
const mutationDefaultGeneratorRegistry = collectMutationDefaultGenerators(contributors);
|
|
689
727
|
assertMutationDefaultGeneratorsAvailable(contract, mutationDefaultGeneratorRegistry);
|
|
690
728
|
|
|
@@ -694,12 +732,7 @@ export function createExecutionContext<
|
|
|
694
732
|
|
|
695
733
|
const types = initializeTypeHelpers(contract.storage, parameterizedCodecDescriptors);
|
|
696
734
|
|
|
697
|
-
const contractCodecs = buildContractCodecRegistry(
|
|
698
|
-
contract,
|
|
699
|
-
codecDescriptors,
|
|
700
|
-
types,
|
|
701
|
-
parameterizedCodecDescriptors,
|
|
702
|
-
);
|
|
735
|
+
const contractCodecs = buildContractCodecRegistry(contract, codecDescriptors);
|
|
703
736
|
|
|
704
737
|
return {
|
|
705
738
|
contract,
|
package/src/sql-runtime.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
RuntimeCore,
|
|
11
11
|
type RuntimeExecuteOptions,
|
|
12
12
|
type RuntimeLog,
|
|
13
|
+
runBeforeExecuteChain,
|
|
13
14
|
runtimeError,
|
|
14
15
|
runWithMiddleware,
|
|
15
16
|
} from '@prisma-next/framework-components/runtime';
|
|
@@ -24,7 +25,6 @@ import type {
|
|
|
24
25
|
SqlQueryable,
|
|
25
26
|
SqlTransaction,
|
|
26
27
|
} from '@prisma-next/sql-relational-core/ast';
|
|
27
|
-
import { validateParamRefRefs } from '@prisma-next/sql-relational-core/ast';
|
|
28
28
|
import {
|
|
29
29
|
createSqlParamRefMutator,
|
|
30
30
|
type SqlParamRefMutator,
|
|
@@ -186,19 +186,57 @@ class SqlRuntimeImpl<TContract extends Contract<SqlStorage> = Contract<SqlStorag
|
|
|
186
186
|
}
|
|
187
187
|
|
|
188
188
|
/**
|
|
189
|
-
* Lower a `SqlQueryPlan` (AST + meta) into a `SqlExecutionPlan`
|
|
189
|
+
* Lower a `SqlQueryPlan` (AST + meta) into a `SqlExecutionPlan`
|
|
190
|
+
* with encoded parameters ready for the driver.
|
|
190
191
|
*
|
|
191
|
-
*
|
|
192
|
+
* Implementation note: SQL splits lower-then-encode across
|
|
193
|
+
* {@link lowerToDraft} + {@link encodeDraftParams} so the runtime
|
|
194
|
+
* can fire the `beforeExecute` middleware chain between them
|
|
195
|
+
* (cipherstash bulk-encrypt, for example, mutates pre-encode
|
|
196
|
+
* `ParamRef.value` slots). This protected hook composes the two
|
|
197
|
+
* back into the cross-family `lower()` shape `RuntimeCore.execute`
|
|
198
|
+
* expects, and is called from the no-middleware fast paths /
|
|
199
|
+
* fixtures that hit `RuntimeCore`'s default template directly.
|
|
200
|
+
* `execute()` overrides the template and uses the split form so
|
|
201
|
+
* `beforeExecute` lands between the two halves.
|
|
202
|
+
*
|
|
203
|
+
* `ctx: SqlCodecCallContext` is forwarded to `encodeParams` so
|
|
204
|
+
* per-query cancellation reaches every codec body during parameter
|
|
205
|
+
* encoding. SQL params do not populate `ctx.column` — encode-side
|
|
206
|
+
* column metadata is the middleware's domain.
|
|
192
207
|
*/
|
|
193
208
|
protected override async lower(
|
|
194
209
|
plan: SqlQueryPlan,
|
|
195
210
|
ctx: SqlCodecCallContext,
|
|
196
211
|
): Promise<SqlExecutionPlan> {
|
|
197
|
-
|
|
198
|
-
|
|
212
|
+
const draft = this.lowerToDraft(plan);
|
|
213
|
+
return await this.encodeDraftParams(draft, ctx);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* AST → pre-encode draft. The returned plan has `sql` rendered and
|
|
218
|
+
* `params` populated with the user-domain values the lowering site
|
|
219
|
+
* collected from `ParamRef` nodes. No codec encode has happened
|
|
220
|
+
* yet; consumers can mutate `params` via the `SqlParamRefMutator`
|
|
221
|
+
* before {@link encodeDraftParams} runs.
|
|
222
|
+
*/
|
|
223
|
+
private lowerToDraft(plan: SqlQueryPlan): SqlExecutionPlan {
|
|
224
|
+
return lowerSqlPlan(this.adapter, this.contract, plan);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Encode a draft plan's params through the per-column codecs and
|
|
229
|
+
* freeze the result into the final `SqlExecutionPlan` the driver
|
|
230
|
+
* sees. Errors surface as `RUNTIME.ENCODE_FAILED` envelopes from
|
|
231
|
+
* {@link encodeParams}.
|
|
232
|
+
*/
|
|
233
|
+
private async encodeDraftParams(
|
|
234
|
+
draft: SqlExecutionPlan,
|
|
235
|
+
ctx: SqlCodecCallContext,
|
|
236
|
+
): Promise<SqlExecutionPlan> {
|
|
199
237
|
return Object.freeze({
|
|
200
|
-
...
|
|
201
|
-
params: await encodeParams(
|
|
238
|
+
...draft,
|
|
239
|
+
params: await encodeParams(draft, ctx, this.contractCodecs),
|
|
202
240
|
});
|
|
203
241
|
}
|
|
204
242
|
|
|
@@ -257,15 +295,44 @@ class SqlRuntimeImpl<TContract extends Contract<SqlStorage> = Contract<SqlStorag
|
|
|
257
295
|
|
|
258
296
|
let exec: SqlExecutionPlan;
|
|
259
297
|
if (isExecutionPlan(plan)) {
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
298
|
+
// Pre-lowered fixture path. The plan's params are typically
|
|
299
|
+
// already encoded; we still fire `beforeExecute` so middleware
|
|
300
|
+
// that mutates ParamRef values (e.g. cipherstash bulk-encrypt)
|
|
301
|
+
// gets a chance to run, then re-encode so any mutations land.
|
|
302
|
+
const preEncodeMutator: SqlParamRefMutatorInternal = createSqlParamRefMutator(plan);
|
|
303
|
+
await runBeforeExecuteChain<SqlExecutionPlan, SqlParamRefMutator>(
|
|
304
|
+
plan,
|
|
305
|
+
self.middleware,
|
|
306
|
+
execMiddlewareCtx,
|
|
307
|
+
preEncodeMutator,
|
|
308
|
+
);
|
|
263
309
|
exec = Object.freeze({
|
|
264
310
|
...plan,
|
|
265
|
-
params: await encodeParams(
|
|
311
|
+
params: await encodeParams(
|
|
312
|
+
{ ...plan, params: preEncodeMutator.currentParams() },
|
|
313
|
+
codecCtx,
|
|
314
|
+
self.contractCodecs,
|
|
315
|
+
),
|
|
266
316
|
});
|
|
267
317
|
} else {
|
|
268
|
-
exec
|
|
318
|
+
// Standard AST → exec path. Split lower from encode so the
|
|
319
|
+
// `beforeExecute` chain fires between them with a mutator built
|
|
320
|
+
// over the pre-encode draft params; encode then renders the
|
|
321
|
+
// (possibly mutated) values through the column codecs.
|
|
322
|
+
const compiled = await self.runBeforeCompile(plan);
|
|
323
|
+
const draft = self.lowerToDraft(compiled);
|
|
324
|
+
const preEncodeMutator: SqlParamRefMutatorInternal = createSqlParamRefMutator(draft);
|
|
325
|
+
await runBeforeExecuteChain<SqlExecutionPlan, SqlParamRefMutator>(
|
|
326
|
+
draft,
|
|
327
|
+
self.middleware,
|
|
328
|
+
execMiddlewareCtx,
|
|
329
|
+
preEncodeMutator,
|
|
330
|
+
);
|
|
331
|
+
const draftWithMutations: SqlExecutionPlan = Object.freeze({
|
|
332
|
+
...draft,
|
|
333
|
+
params: preEncodeMutator.currentParams(),
|
|
334
|
+
});
|
|
335
|
+
exec = await self.encodeDraftParams(draftWithMutations, codecCtx);
|
|
269
336
|
}
|
|
270
337
|
|
|
271
338
|
self.familyAdapter.validatePlan(exec, self.contract);
|
|
@@ -287,25 +354,18 @@ class SqlRuntimeImpl<TContract extends Contract<SqlStorage> = Contract<SqlStorag
|
|
|
287
354
|
await self.verifyMarker();
|
|
288
355
|
}
|
|
289
356
|
|
|
290
|
-
const
|
|
291
|
-
const stream = runWithMiddleware<
|
|
292
|
-
SqlExecutionPlan,
|
|
293
|
-
Record<string, unknown>,
|
|
294
|
-
SqlParamRefMutator
|
|
295
|
-
>(
|
|
357
|
+
const stream = runWithMiddleware<SqlExecutionPlan, Record<string, unknown>>(
|
|
296
358
|
exec,
|
|
297
359
|
self.middleware,
|
|
298
360
|
execMiddlewareCtx,
|
|
299
361
|
() =>
|
|
300
362
|
queryable.execute<Record<string, unknown>>({
|
|
301
363
|
sql: exec.sql,
|
|
302
|
-
//
|
|
303
|
-
//
|
|
304
|
-
//
|
|
305
|
-
|
|
306
|
-
params: paramsMutator.currentParams(),
|
|
364
|
+
// `beforeExecute` ran on the pre-encode draft (see
|
|
365
|
+
// generator setup above); `exec.params` already carries
|
|
366
|
+
// any mutator-driven replacements through `encodeParams`.
|
|
367
|
+
params: exec.params,
|
|
307
368
|
}),
|
|
308
|
-
paramsMutator,
|
|
309
369
|
);
|
|
310
370
|
|
|
311
371
|
// 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.
|