@prisma-next/sql-runtime 0.5.0-dev.4 → 0.5.0-dev.40
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -21
- package/dist/exports-CrHMfIKo.mjs +1564 -0
- package/dist/exports-CrHMfIKo.mjs.map +1 -0
- package/dist/{index-yb51L_1h.d.mts → index-_dXSGeho.d.mts} +78 -25
- package/dist/index-_dXSGeho.d.mts.map +1 -0
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +2 -2
- package/dist/test/utils.d.mts +6 -5
- package/dist/test/utils.d.mts.map +1 -1
- package/dist/test/utils.mjs +11 -5
- package/dist/test/utils.mjs.map +1 -1
- package/package.json +10 -12
- package/src/codecs/decoding.ts +294 -173
- package/src/codecs/encoding.ts +162 -37
- package/src/codecs/validation.ts +22 -3
- package/src/exports/index.ts +11 -7
- package/src/fingerprint.ts +22 -0
- package/src/guardrails/raw.ts +165 -0
- package/src/lower-sql-plan.ts +3 -3
- package/src/marker.ts +75 -0
- package/src/middleware/before-compile-chain.ts +1 -0
- package/src/middleware/budgets.ts +26 -96
- package/src/middleware/lints.ts +3 -3
- package/src/middleware/sql-middleware.ts +6 -5
- package/src/runtime-spi.ts +44 -0
- package/src/sql-context.ts +332 -78
- package/src/sql-family-adapter.ts +3 -2
- package/src/sql-marker.ts +62 -47
- package/src/sql-runtime.ts +332 -113
- package/dist/exports-BQZSVXXt.mjs +0 -981
- package/dist/exports-BQZSVXXt.mjs.map +0 -1
- package/dist/index-yb51L_1h.d.mts.map +0 -1
- package/test/async-iterable-result.test.ts +0 -141
- package/test/before-compile-chain.test.ts +0 -223
- package/test/budgets.test.ts +0 -431
- package/test/context.types.test-d.ts +0 -68
- package/test/execution-stack.test.ts +0 -161
- package/test/json-schema-validation.test.ts +0 -571
- package/test/lints.test.ts +0 -160
- package/test/mutation-default-generators.test.ts +0 -254
- package/test/parameterized-types.test.ts +0 -529
- package/test/sql-context.test.ts +0 -384
- package/test/sql-family-adapter.test.ts +0 -103
- package/test/sql-runtime.test.ts +0 -792
- package/test/utils.ts +0 -297
package/src/sql-context.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { Contract, ExecutionMutationDefaultValue } from '@prisma-next/contract/types';
|
|
2
|
+
import type { CodecDescriptor } from '@prisma-next/framework-components/codec';
|
|
3
|
+
import { synthesizeNonParameterizedDescriptor } from '@prisma-next/framework-components/codec';
|
|
2
4
|
import type { ComponentDescriptor } from '@prisma-next/framework-components/components';
|
|
3
5
|
import { checkContractComponentRequirements } from '@prisma-next/framework-components/components';
|
|
4
6
|
import {
|
|
@@ -14,7 +16,7 @@ import {
|
|
|
14
16
|
type RuntimeTargetInstance,
|
|
15
17
|
} from '@prisma-next/framework-components/execution';
|
|
16
18
|
import { runtimeError } from '@prisma-next/framework-components/runtime';
|
|
17
|
-
import type { SqlStorage
|
|
19
|
+
import type { SqlStorage } from '@prisma-next/sql-contract/types';
|
|
18
20
|
import {
|
|
19
21
|
createSqlOperationRegistry,
|
|
20
22
|
type SqlOperationDescriptor,
|
|
@@ -22,39 +24,41 @@ import {
|
|
|
22
24
|
import type {
|
|
23
25
|
Adapter,
|
|
24
26
|
AnyQueryAst,
|
|
25
|
-
|
|
27
|
+
Codec,
|
|
26
28
|
CodecRegistry,
|
|
29
|
+
ContractCodecRegistry,
|
|
27
30
|
LoweredStatement,
|
|
31
|
+
SqlCodecInstanceContext,
|
|
28
32
|
SqlDriver,
|
|
29
33
|
} from '@prisma-next/sql-relational-core/ast';
|
|
30
34
|
import { createCodecRegistry } from '@prisma-next/sql-relational-core/ast';
|
|
31
35
|
import type {
|
|
32
36
|
AppliedMutationDefault,
|
|
37
|
+
CodecDescriptorRegistry,
|
|
33
38
|
ExecutionContext,
|
|
34
39
|
JsonSchemaValidateFn,
|
|
35
40
|
JsonSchemaValidatorRegistry,
|
|
36
41
|
MutationDefaultsOptions,
|
|
37
42
|
TypeHelperRegistry,
|
|
38
43
|
} from '@prisma-next/sql-relational-core/query-lane-context';
|
|
39
|
-
import { type as arktype } from 'arktype';
|
|
40
44
|
|
|
41
45
|
/**
|
|
42
46
|
* Runtime parameterized codec descriptor.
|
|
43
|
-
* Provides validation schema and optional init hook for codecs that support type parameters.
|
|
44
|
-
* Used at runtime to validate typeParams and create type helpers.
|
|
45
47
|
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
+
* The unified `CodecDescriptor<P>` shape applied to parameterized codecs
|
|
49
|
+
* — `paramsSchema: StandardSchemaV1<P>` for JSON-boundary validation,
|
|
50
|
+
* `factory: (P) => (CodecInstanceContext) => Codec` for the curried higher-order codec.
|
|
51
|
+
* The factory is called once per `storage.types` instance (or once per
|
|
52
|
+
* inline-`typeParams` column); per-instance state lives in the closure.
|
|
53
|
+
*
|
|
54
|
+
* Codec-registry-unification spec § Decision.
|
|
48
55
|
*/
|
|
49
|
-
export type RuntimeParameterizedCodecDescriptor<
|
|
50
|
-
TParams = Record<string, unknown>,
|
|
51
|
-
THelper = unknown,
|
|
52
|
-
> = CodecParamsDescriptor<TParams, THelper>;
|
|
56
|
+
export type RuntimeParameterizedCodecDescriptor<P = Record<string, unknown>> = CodecDescriptor<P>;
|
|
53
57
|
|
|
54
58
|
export interface SqlStaticContributions {
|
|
55
59
|
readonly codecs: () => CodecRegistry;
|
|
56
60
|
// biome-ignore lint/suspicious/noExplicitAny: needed for covariance with concrete descriptor types
|
|
57
|
-
readonly parameterizedCodecs: () => ReadonlyArray<RuntimeParameterizedCodecDescriptor<any
|
|
61
|
+
readonly parameterizedCodecs: () => ReadonlyArray<RuntimeParameterizedCodecDescriptor<any>>;
|
|
58
62
|
readonly queryOperations?: () => ReadonlyArray<SqlOperationDescriptor>;
|
|
59
63
|
readonly mutationDefaultGenerators?: () => ReadonlyArray<RuntimeMutationDefaultGenerator>;
|
|
60
64
|
}
|
|
@@ -206,9 +210,16 @@ function validateTypeParams(
|
|
|
206
210
|
codecDescriptor: RuntimeParameterizedCodecDescriptor,
|
|
207
211
|
context: { typeName?: string; tableName?: string; columnName?: string },
|
|
208
212
|
): Record<string, unknown> {
|
|
209
|
-
const result = codecDescriptor.paramsSchema(typeParams);
|
|
210
|
-
if (result instanceof
|
|
211
|
-
|
|
213
|
+
const result = codecDescriptor.paramsSchema['~standard'].validate(typeParams);
|
|
214
|
+
if (result instanceof Promise) {
|
|
215
|
+
throw runtimeError(
|
|
216
|
+
'RUNTIME.TYPE_PARAMS_INVALID',
|
|
217
|
+
`paramsSchema for codec '${codecDescriptor.codecId}' returned a Promise; runtime validation requires a synchronous Standard Schema validator.`,
|
|
218
|
+
{ ...context, codecId: codecDescriptor.codecId, typeParams },
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
if (result.issues) {
|
|
222
|
+
const messages = result.issues.map((issue) => issue.message).join('; ');
|
|
212
223
|
const locationInfo = context.typeName
|
|
213
224
|
? `type '${context.typeName}'`
|
|
214
225
|
: `column '${context.tableName}.${context.columnName}'`;
|
|
@@ -218,7 +229,7 @@ function validateTypeParams(
|
|
|
218
229
|
{ ...context, codecId: codecDescriptor.codecId, typeParams },
|
|
219
230
|
);
|
|
220
231
|
}
|
|
221
|
-
return result as Record<string, unknown>;
|
|
232
|
+
return result.value as Record<string, unknown>;
|
|
222
233
|
}
|
|
223
234
|
|
|
224
235
|
function collectParameterizedCodecDescriptors(
|
|
@@ -242,32 +253,116 @@ function collectParameterizedCodecDescriptors(
|
|
|
242
253
|
return descriptors;
|
|
243
254
|
}
|
|
244
255
|
|
|
256
|
+
/**
|
|
257
|
+
* Build the unified descriptor map. Combines parameterized descriptors
|
|
258
|
+
* (which already ship as `CodecDescriptor`s) with synthesized descriptors
|
|
259
|
+
* for non-parameterized codecs registered through the legacy `codecs:`
|
|
260
|
+
* slot. Codec ids that ship a parameterized descriptor take precedence —
|
|
261
|
+
* even when the legacy registry registers a representative codec under
|
|
262
|
+
* the same id, the parameterized descriptor is the authoritative source.
|
|
263
|
+
*
|
|
264
|
+
* Codec-registry-unification spec § Decision: every codec resolves
|
|
265
|
+
* through one descriptor map; reads are non-branching.
|
|
266
|
+
*/
|
|
267
|
+
function buildCodecDescriptorRegistry(
|
|
268
|
+
codecRegistry: CodecRegistry,
|
|
269
|
+
parameterizedDescriptors: Map<string, RuntimeParameterizedCodecDescriptor>,
|
|
270
|
+
): CodecDescriptorRegistry {
|
|
271
|
+
type AnyDescriptor = CodecDescriptor<unknown>;
|
|
272
|
+
const byId = new Map<string, AnyDescriptor>();
|
|
273
|
+
const byTargetType = new Map<string, Array<AnyDescriptor>>();
|
|
274
|
+
|
|
275
|
+
function registerInIndices(descriptor: AnyDescriptor): void {
|
|
276
|
+
byId.set(descriptor.codecId, descriptor);
|
|
277
|
+
for (const targetType of descriptor.targetTypes) {
|
|
278
|
+
const list = byTargetType.get(targetType);
|
|
279
|
+
if (list) {
|
|
280
|
+
list.push(descriptor);
|
|
281
|
+
} else {
|
|
282
|
+
byTargetType.set(targetType, [descriptor]);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// The descriptor map is heterogeneous in `P` — each codec id has its own
|
|
288
|
+
// params shape. The public `CodecDescriptorRegistry` interface widens to
|
|
289
|
+
// `CodecDescriptor<unknown>` and consumers narrow per codec id at the
|
|
290
|
+
// call site (the descriptor's `paramsSchema` validates JSON-sourced
|
|
291
|
+
// params before the factory ever sees them, so the runtime narrow is
|
|
292
|
+
// safe). The cast at registration goes through `unknown` because
|
|
293
|
+
// `CodecDescriptor<P>` is invariant in `P` (the `factory` and
|
|
294
|
+
// `renderOutputType` slots use `P` contravariantly).
|
|
295
|
+
for (const descriptor of parameterizedDescriptors.values()) {
|
|
296
|
+
registerInIndices(descriptor as unknown as AnyDescriptor);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
for (const codec of codecRegistry.values()) {
|
|
300
|
+
if (byId.has(codec.id)) continue;
|
|
301
|
+
registerInIndices(synthesizeNonParameterizedDescriptor(codec) as unknown as AnyDescriptor);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return {
|
|
305
|
+
descriptorFor(codecId: string): AnyDescriptor | undefined {
|
|
306
|
+
return byId.get(codecId);
|
|
307
|
+
},
|
|
308
|
+
*values(): IterableIterator<AnyDescriptor> {
|
|
309
|
+
yield* byId.values();
|
|
310
|
+
},
|
|
311
|
+
byTargetType(targetType: string): readonly AnyDescriptor[] {
|
|
312
|
+
return byTargetType.get(targetType) ?? Object.freeze([]);
|
|
313
|
+
},
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function collectTypeRefSites(
|
|
318
|
+
storage: SqlStorage,
|
|
319
|
+
): Map<string, Array<{ readonly table: string; readonly column: string }>> {
|
|
320
|
+
const sites = new Map<string, Array<{ readonly table: string; readonly column: string }>>();
|
|
321
|
+
for (const [tableName, table] of Object.entries(storage.tables)) {
|
|
322
|
+
for (const [columnName, column] of Object.entries(table.columns)) {
|
|
323
|
+
if (typeof column.typeRef !== 'string') continue;
|
|
324
|
+
const list = sites.get(column.typeRef);
|
|
325
|
+
const entry = { table: tableName, column: columnName };
|
|
326
|
+
if (list) {
|
|
327
|
+
list.push(entry);
|
|
328
|
+
} else {
|
|
329
|
+
sites.set(column.typeRef, [entry]);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return sites;
|
|
334
|
+
}
|
|
335
|
+
|
|
245
336
|
function initializeTypeHelpers(
|
|
246
|
-
|
|
337
|
+
storage: SqlStorage,
|
|
247
338
|
codecDescriptors: Map<string, RuntimeParameterizedCodecDescriptor>,
|
|
248
339
|
): TypeHelperRegistry {
|
|
249
340
|
const helpers: TypeHelperRegistry = {};
|
|
341
|
+
const storageTypes = storage.types;
|
|
250
342
|
|
|
251
343
|
if (!storageTypes) {
|
|
252
344
|
return helpers;
|
|
253
345
|
}
|
|
254
346
|
|
|
347
|
+
const typeRefSites = collectTypeRefSites(storage);
|
|
348
|
+
|
|
255
349
|
for (const [typeName, typeInstance] of Object.entries(storageTypes)) {
|
|
256
350
|
const descriptor = codecDescriptors.get(typeInstance.codecId);
|
|
257
351
|
|
|
258
|
-
if (descriptor) {
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
if (descriptor.init) {
|
|
264
|
-
helpers[typeName] = descriptor.init(validatedParams);
|
|
265
|
-
} else {
|
|
266
|
-
helpers[typeName] = typeInstance;
|
|
267
|
-
}
|
|
268
|
-
} else {
|
|
352
|
+
if (!descriptor) {
|
|
353
|
+
// No parameterized descriptor for this codec id — store the raw
|
|
354
|
+
// type instance for callers that need typeParams metadata.
|
|
269
355
|
helpers[typeName] = typeInstance;
|
|
356
|
+
continue;
|
|
270
357
|
}
|
|
358
|
+
|
|
359
|
+
const validatedParams = validateTypeParams(typeInstance.typeParams, descriptor, {
|
|
360
|
+
typeName,
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
const usedAt = typeRefSites.get(typeName) ?? [];
|
|
364
|
+
const ctx: SqlCodecInstanceContext = { name: typeName, usedAt };
|
|
365
|
+
helpers[typeName] = descriptor.factory(validatedParams)(ctx);
|
|
271
366
|
}
|
|
272
367
|
|
|
273
368
|
return helpers;
|
|
@@ -290,67 +385,217 @@ function validateColumnTypeParams(
|
|
|
290
385
|
}
|
|
291
386
|
|
|
292
387
|
/**
|
|
293
|
-
*
|
|
294
|
-
*
|
|
388
|
+
* View of a codec that exposes a per-instance JSON-schema `validate`
|
|
389
|
+
* function. Codecs declare this contract by including the
|
|
390
|
+
* `'json-validator'` `CodecTrait` in their `traits` array; the trait is
|
|
391
|
+
* the gate that lets `extractValidator` resolve from structurally-typed
|
|
392
|
+
* `unknown` to this typed view.
|
|
393
|
+
*/
|
|
394
|
+
type JsonValidatorCodec = {
|
|
395
|
+
readonly traits?: ReadonlyArray<unknown>;
|
|
396
|
+
readonly validate: JsonSchemaValidateFn;
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
function hasJsonValidatorTrait(candidate: unknown): candidate is JsonValidatorCodec {
|
|
400
|
+
if (candidate === null || typeof candidate !== 'object') return false;
|
|
401
|
+
const traits = (candidate as { readonly traits?: unknown }).traits;
|
|
402
|
+
if (!Array.isArray(traits)) return false;
|
|
403
|
+
if (!traits.includes('json-validator')) return false;
|
|
404
|
+
const validate = (candidate as { readonly validate?: unknown }).validate;
|
|
405
|
+
return typeof validate === 'function';
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function extractValidator(candidate: unknown): JsonSchemaValidateFn | undefined {
|
|
409
|
+
return hasJsonValidatorTrait(candidate) ? candidate.validate : undefined;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function isResolvedCodec(candidate: unknown): candidate is Codec {
|
|
413
|
+
return (
|
|
414
|
+
candidate !== null &&
|
|
415
|
+
typeof candidate === 'object' &&
|
|
416
|
+
'id' in candidate &&
|
|
417
|
+
'decode' in candidate
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Walk the contract's `storage.tables[].columns[]` and resolve each
|
|
423
|
+
* column to a `Codec` through the unified descriptor map. Per-instance
|
|
424
|
+
* behavior:
|
|
425
|
+
*
|
|
426
|
+
* - **typeRef columns**: reuse the resolved codec materialized once by
|
|
427
|
+
* `initializeTypeHelpers` for the `storage.types` entry. Multiple
|
|
428
|
+
* columns sharing one typeRef share one codec instance.
|
|
429
|
+
* - **inline-typeParams columns**: call `descriptor.factory(typeParams)
|
|
430
|
+
* (ctx)` once per column (per-column anonymous instance).
|
|
431
|
+
* - **non-parameterized columns**: call `descriptor.factory()(ctx)`
|
|
432
|
+
* once. The synthesized descriptor's factory is constant — every call
|
|
433
|
+
* returns the same shared codec instance — so columns sharing a non-
|
|
434
|
+
* parameterized codec id share one resolved codec without explicit
|
|
435
|
+
* caching.
|
|
295
436
|
*
|
|
296
|
-
*
|
|
297
|
-
*
|
|
298
|
-
*
|
|
437
|
+
* Combines what `initializeTypeHelpers` (named-instance walk) and the
|
|
438
|
+
* old `buildJsonSchemaValidatorRegistry` (per-column walk) used to do
|
|
439
|
+
* separately: one walk over all columns, one resolved codec per column,
|
|
440
|
+
* one trait-gated validator extraction per column. The result drives
|
|
441
|
+
* both the dispatch registry (`ContractCodecRegistry.forColumn`) and the
|
|
442
|
+
* validator registry.
|
|
443
|
+
*
|
|
444
|
+
* Codec-registry-unification spec § AC-4: every column resolves through
|
|
445
|
+
* one descriptor map without branching on parameterization.
|
|
299
446
|
*/
|
|
300
|
-
function
|
|
447
|
+
function buildContractCodecRegistry(
|
|
301
448
|
contract: Contract<SqlStorage>,
|
|
449
|
+
codecDescriptors: CodecDescriptorRegistry,
|
|
450
|
+
legacyCodecRegistry: CodecRegistry,
|
|
302
451
|
types: TypeHelperRegistry,
|
|
303
|
-
|
|
304
|
-
):
|
|
452
|
+
parameterizedDescriptors: Map<string, RuntimeParameterizedCodecDescriptor>,
|
|
453
|
+
): {
|
|
454
|
+
readonly registry: ContractCodecRegistry;
|
|
455
|
+
readonly jsonValidators: JsonSchemaValidatorRegistry | undefined;
|
|
456
|
+
} {
|
|
457
|
+
const byColumn = new Map<string, Codec>();
|
|
458
|
+
const byCodecId = new Map<string, Codec>();
|
|
459
|
+
// Codec ids whose `byCodecId` entry is ambiguous — multiple distinct
|
|
460
|
+
// resolved instances landed under the same parameterized codec id (e.g.
|
|
461
|
+
// `Vector<1024>` and `Vector<1536>` both registering under
|
|
462
|
+
// `pg/vector@1`). The encode-side `forCodecId` fallback rejects these
|
|
463
|
+
// ids so a DSL-param without a column ref cannot silently bind to the
|
|
464
|
+
// wrong instance. Retires when AC-5's `ParamRef.refs` plumbing lands
|
|
465
|
+
// (TML-2357).
|
|
466
|
+
const ambiguousCodecIds = new Set<string>();
|
|
305
467
|
const validators = new Map<string, JsonSchemaValidateFn>();
|
|
306
468
|
|
|
307
|
-
// Collect codec IDs that have init hooks (these produce { validate } helpers)
|
|
308
|
-
const codecIdsWithInit = new Set<string>();
|
|
309
|
-
for (const [codecId, descriptor] of codecDescriptors) {
|
|
310
|
-
if (descriptor.init) {
|
|
311
|
-
codecIdsWithInit.add(codecId);
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
if (codecIdsWithInit.size === 0) {
|
|
316
|
-
return undefined;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
469
|
for (const [tableName, table] of Object.entries(contract.storage.tables)) {
|
|
320
470
|
for (const [columnName, column] of Object.entries(table.columns)) {
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
if (
|
|
327
|
-
const
|
|
328
|
-
|
|
329
|
-
|
|
471
|
+
const columnKey = `${tableName}.${columnName}`;
|
|
472
|
+
const descriptor = codecDescriptors.descriptorFor(column.codecId);
|
|
473
|
+
|
|
474
|
+
let resolvedCodec: Codec | undefined;
|
|
475
|
+
|
|
476
|
+
if (descriptor) {
|
|
477
|
+
const isParameterized = parameterizedDescriptors.has(column.codecId);
|
|
478
|
+
|
|
479
|
+
if (column.typeRef) {
|
|
480
|
+
// The named instance was already materialized once by
|
|
481
|
+
// `initializeTypeHelpers`; reuse it so multiple columns sharing
|
|
482
|
+
// the same typeRef share one codec instance (and any per-
|
|
483
|
+
// instance helper state on it).
|
|
484
|
+
const helper = types[column.typeRef];
|
|
485
|
+
if (isResolvedCodec(helper)) {
|
|
486
|
+
resolvedCodec = helper;
|
|
487
|
+
}
|
|
488
|
+
} else if (column.typeParams && isParameterized) {
|
|
489
|
+
const parameterizedDescriptor = parameterizedDescriptors.get(column.codecId);
|
|
490
|
+
if (parameterizedDescriptor) {
|
|
491
|
+
const validatedParams = validateTypeParams(column.typeParams, parameterizedDescriptor, {
|
|
492
|
+
tableName,
|
|
493
|
+
columnName,
|
|
494
|
+
});
|
|
495
|
+
const ctx: SqlCodecInstanceContext = {
|
|
496
|
+
name: `<anon:${tableName}.${columnName}>`,
|
|
497
|
+
usedAt: [{ table: tableName, column: columnName }],
|
|
498
|
+
};
|
|
499
|
+
resolvedCodec = parameterizedDescriptor.factory(validatedParams)(ctx);
|
|
500
|
+
}
|
|
501
|
+
} else if (!isParameterized) {
|
|
502
|
+
// Non-parameterized column. Cache the resolved codec by codec
|
|
503
|
+
// id — the synthesized descriptor's factory is constant for
|
|
504
|
+
// non-parameterized codecs, so columns sharing this codec id
|
|
505
|
+
// share one resolved instance.
|
|
506
|
+
let cached = byCodecId.get(column.codecId);
|
|
507
|
+
if (!cached) {
|
|
508
|
+
const ctx: SqlCodecInstanceContext = {
|
|
509
|
+
name: `<shared:${column.codecId}>`,
|
|
510
|
+
usedAt: [{ table: tableName, column: columnName }],
|
|
511
|
+
};
|
|
512
|
+
// `synthesizeNonParameterizedDescriptor` produces a
|
|
513
|
+
// `CodecDescriptor<void>` whose factory ignores its params
|
|
514
|
+
// and ctx; the runtime's `void` value is `undefined`. The
|
|
515
|
+
// structural cast goes through `unknown` to satisfy the
|
|
516
|
+
// heterogeneous-`P` registry boundary (the factory's
|
|
517
|
+
// declared `P` is `any` here; the consumer narrows per
|
|
518
|
+
// codec id). The cast narrows the descriptor's
|
|
519
|
+
// family-agnostic `CodecInstanceContext` slot to the SQL
|
|
520
|
+
// `SqlCodecInstanceContext` we pass at this call site —
|
|
521
|
+
// function-argument contravariance makes the narrow safe
|
|
522
|
+
// (a callee that accepts the base will also accept the
|
|
523
|
+
// SQL extension). Per spec § Non-functional constraints.
|
|
524
|
+
const voidFactory = descriptor.factory as unknown as (
|
|
525
|
+
params: undefined,
|
|
526
|
+
) => (ctx: SqlCodecInstanceContext) => Codec;
|
|
527
|
+
cached = voidFactory(undefined)(ctx);
|
|
528
|
+
byCodecId.set(column.codecId, cached);
|
|
529
|
+
}
|
|
530
|
+
resolvedCodec = cached;
|
|
330
531
|
}
|
|
331
|
-
|
|
532
|
+
// else: parameterized codec id with no typeRef and no typeParams
|
|
533
|
+
// — this is the legitimate "undimensioned" form for codecs that
|
|
534
|
+
// ship a no-params column variant alongside a parameterized one
|
|
535
|
+
// (e.g. pgvector's `vectorColumn` vs. `vector(N)`). Leave
|
|
536
|
+
// `resolvedCodec` undefined; encode/decode for this column flows
|
|
537
|
+
// through `forCodecId` (the AC-5-deferred carve-out documented
|
|
538
|
+
// in `relational-core/src/ast/codec-types.ts`). The fallback
|
|
539
|
+
// works for these cases because their wire format is
|
|
540
|
+
// params-independent (vector formats `[v1,v2,...]` regardless
|
|
541
|
+
// of declared length).
|
|
332
542
|
}
|
|
333
543
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
const
|
|
337
|
-
if (
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
544
|
+
if (resolvedCodec) {
|
|
545
|
+
byColumn.set(columnKey, resolvedCodec);
|
|
546
|
+
const validate = extractValidator(resolvedCodec);
|
|
547
|
+
if (validate) {
|
|
548
|
+
validators.set(columnKey, validate);
|
|
549
|
+
}
|
|
550
|
+
const existing = byCodecId.get(column.codecId);
|
|
551
|
+
if (existing === undefined) {
|
|
552
|
+
byCodecId.set(column.codecId, resolvedCodec);
|
|
553
|
+
} else if (existing !== resolvedCodec && parameterizedDescriptors.has(column.codecId)) {
|
|
554
|
+
ambiguousCodecIds.add(column.codecId);
|
|
344
555
|
}
|
|
345
556
|
}
|
|
346
557
|
}
|
|
347
558
|
}
|
|
348
559
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
560
|
+
const registry: ContractCodecRegistry = {
|
|
561
|
+
forColumn(table, column) {
|
|
562
|
+
return byColumn.get(`${table}.${column}`);
|
|
563
|
+
},
|
|
564
|
+
forCodecId(codecId) {
|
|
565
|
+
// Codec-id-only fallback for sites without a column ref (encode-
|
|
566
|
+
// side DSL params whose `ParamRef.refs` isn't populated). Prefer
|
|
567
|
+
// the contract-walk-derived shared codec; fall back to the legacy
|
|
568
|
+
// `codecRegistry.get` for parameterized codec ids whose contracts
|
|
569
|
+
// don't have a typeRef/typeParams column the walk could resolve
|
|
570
|
+
// through. The legacy fallback retires once `ParamRef.refs` is
|
|
571
|
+
// threaded everywhere (TML-2357).
|
|
572
|
+
//
|
|
573
|
+
// Reject ambiguous parameterized fallbacks: if the contract walk
|
|
574
|
+
// resolved more than one distinct codec instance under this id
|
|
575
|
+
// (e.g. multiple vector dimensions, multiple arktype-json
|
|
576
|
+
// schemas), the codec-id-keyed lookup cannot honor the call site
|
|
577
|
+
// — fail fast rather than bind to whichever instance happened to
|
|
578
|
+
// land first.
|
|
579
|
+
if (ambiguousCodecIds.has(codecId)) {
|
|
580
|
+
throw runtimeError(
|
|
581
|
+
'RUNTIME.TYPE_PARAMS_INVALID',
|
|
582
|
+
`Codec '${codecId}' resolves to multiple parameterized instances; column-aware dispatch is required.`,
|
|
583
|
+
{ codecId },
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
return byCodecId.get(codecId) ?? legacyCodecRegistry.get(codecId);
|
|
587
|
+
},
|
|
353
588
|
};
|
|
589
|
+
|
|
590
|
+
const jsonValidators: JsonSchemaValidatorRegistry | undefined =
|
|
591
|
+
validators.size > 0
|
|
592
|
+
? {
|
|
593
|
+
get: (key: string) => validators.get(key),
|
|
594
|
+
size: validators.size,
|
|
595
|
+
}
|
|
596
|
+
: undefined;
|
|
597
|
+
|
|
598
|
+
return { registry, jsonValidators };
|
|
354
599
|
}
|
|
355
600
|
|
|
356
601
|
function collectMutationDefaultGenerators(
|
|
@@ -476,23 +721,32 @@ export function createExecutionContext<
|
|
|
476
721
|
}
|
|
477
722
|
|
|
478
723
|
const parameterizedCodecDescriptors = collectParameterizedCodecDescriptors(contributors);
|
|
724
|
+
const codecDescriptors = buildCodecDescriptorRegistry(
|
|
725
|
+
codecRegistry,
|
|
726
|
+
parameterizedCodecDescriptors,
|
|
727
|
+
);
|
|
479
728
|
const mutationDefaultGeneratorRegistry = collectMutationDefaultGenerators(contributors);
|
|
480
729
|
|
|
481
730
|
if (parameterizedCodecDescriptors.size > 0) {
|
|
482
731
|
validateColumnTypeParams(contract.storage, parameterizedCodecDescriptors);
|
|
483
732
|
}
|
|
484
733
|
|
|
485
|
-
const types = initializeTypeHelpers(contract.storage
|
|
734
|
+
const types = initializeTypeHelpers(contract.storage, parameterizedCodecDescriptors);
|
|
486
735
|
|
|
487
|
-
const jsonSchemaValidators =
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
736
|
+
const { registry: contractCodecs, jsonValidators: jsonSchemaValidators } =
|
|
737
|
+
buildContractCodecRegistry(
|
|
738
|
+
contract,
|
|
739
|
+
codecDescriptors,
|
|
740
|
+
codecRegistry,
|
|
741
|
+
types,
|
|
742
|
+
parameterizedCodecDescriptors,
|
|
743
|
+
);
|
|
492
744
|
|
|
493
745
|
return {
|
|
494
746
|
contract,
|
|
495
747
|
codecs: codecRegistry,
|
|
748
|
+
contractCodecs,
|
|
749
|
+
codecDescriptors,
|
|
496
750
|
queryOperations: queryOperationRegistry,
|
|
497
751
|
types,
|
|
498
752
|
...(jsonSchemaValidators ? { jsonSchemaValidators } : {}),
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import type { Contract
|
|
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/
|
|
1
|
+
import type { MarkerStatement } from '@prisma-next/sql-relational-core/ast';
|
|
2
2
|
|
|
3
3
|
export interface SqlStatement {
|
|
4
4
|
readonly sql: string;
|
|
@@ -12,6 +12,15 @@ export interface WriteMarkerInput {
|
|
|
12
12
|
readonly canonicalVersion?: number;
|
|
13
13
|
readonly appTag?: string;
|
|
14
14
|
readonly meta?: Record<string, unknown>;
|
|
15
|
+
/**
|
|
16
|
+
* Applied-invariants set on the marker.
|
|
17
|
+
*
|
|
18
|
+
* - `undefined` → existing column left untouched. Sign and
|
|
19
|
+
* verify-database paths use this; they don't accumulate invariants.
|
|
20
|
+
* - explicit value (including `[]`) → column overwritten with
|
|
21
|
+
* exactly that value.
|
|
22
|
+
*/
|
|
23
|
+
readonly invariants?: readonly string[];
|
|
15
24
|
}
|
|
16
25
|
|
|
17
26
|
export const ensureSchemaStatement: SqlStatement = {
|
|
@@ -28,7 +37,8 @@ export const ensureTableStatement: SqlStatement = {
|
|
|
28
37
|
canonical_version int,
|
|
29
38
|
updated_at timestamptz not null default now(),
|
|
30
39
|
app_tag text,
|
|
31
|
-
meta jsonb not null default '{}'
|
|
40
|
+
meta jsonb not null default '{}',
|
|
41
|
+
invariants text[] not null default '{}'
|
|
32
42
|
)`,
|
|
33
43
|
params: [],
|
|
34
44
|
};
|
|
@@ -42,7 +52,8 @@ export function readContractMarker(): MarkerStatement {
|
|
|
42
52
|
canonical_version,
|
|
43
53
|
updated_at,
|
|
44
54
|
app_tag,
|
|
45
|
-
meta
|
|
55
|
+
meta,
|
|
56
|
+
invariants
|
|
46
57
|
from prisma_contract.marker
|
|
47
58
|
where id = $1`,
|
|
48
59
|
params: [1],
|
|
@@ -54,52 +65,56 @@ export interface WriteContractMarkerStatements {
|
|
|
54
65
|
readonly update: SqlStatement;
|
|
55
66
|
}
|
|
56
67
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
68
|
+
/**
|
|
69
|
+
* Variable columns that participate in INSERT/UPDATE alongside the
|
|
70
|
+
* always-on `id = $1` and `updated_at = now()`. Each column declares
|
|
71
|
+
* its name, optional cast type, and parameter value; the placeholder
|
|
72
|
+
* (`$N`) is computed positionally below — adding or reordering a
|
|
73
|
+
* column doesn't desync indices. `invariants` only appears when the
|
|
74
|
+
* caller supplies it — see `WriteMarkerInput.invariants`.
|
|
75
|
+
*/
|
|
76
|
+
function markerColumns(
|
|
77
|
+
input: WriteMarkerInput,
|
|
78
|
+
): ReadonlyArray<{ readonly name: string; readonly type?: string; readonly param: unknown }> {
|
|
79
|
+
return [
|
|
80
|
+
{ name: 'core_hash', param: input.storageHash },
|
|
81
|
+
{ name: 'profile_hash', param: input.profileHash },
|
|
82
|
+
{ name: 'contract_json', type: 'jsonb', param: input.contractJson ?? null },
|
|
83
|
+
{ name: 'canonical_version', param: input.canonicalVersion ?? null },
|
|
84
|
+
{ name: 'app_tag', param: input.appTag ?? null },
|
|
85
|
+
{ name: 'meta', type: 'jsonb', param: JSON.stringify(input.meta ?? {}) },
|
|
86
|
+
...(input.invariants !== undefined
|
|
87
|
+
? [{ name: 'invariants' as const, type: 'text[]' as const, param: input.invariants }]
|
|
88
|
+
: []),
|
|
66
89
|
];
|
|
90
|
+
}
|
|
67
91
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
meta
|
|
78
|
-
) values (
|
|
79
|
-
$1,
|
|
80
|
-
$2,
|
|
81
|
-
$3,
|
|
82
|
-
$4::jsonb,
|
|
83
|
-
$5,
|
|
84
|
-
now(),
|
|
85
|
-
$6,
|
|
86
|
-
$7::jsonb
|
|
87
|
-
)`,
|
|
88
|
-
params: baseParams,
|
|
89
|
-
};
|
|
92
|
+
export function writeContractMarker(input: WriteMarkerInput): WriteContractMarkerStatements {
|
|
93
|
+
const cols = markerColumns(input);
|
|
94
|
+
// $1 is reserved for `id`; subsequent positions follow the order of cols.
|
|
95
|
+
const placed = cols.map((c, i) => ({
|
|
96
|
+
name: c.name,
|
|
97
|
+
expr: c.type ? `$${i + 2}::${c.type}` : `$${i + 2}`,
|
|
98
|
+
param: c.param,
|
|
99
|
+
}));
|
|
100
|
+
const params: readonly unknown[] = [1, ...placed.map((c) => c.param)];
|
|
90
101
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
app_tag = $6,
|
|
99
|
-
meta = $7::jsonb
|
|
100
|
-
where id = $1`,
|
|
101
|
-
params: baseParams,
|
|
102
|
-
};
|
|
102
|
+
// `updated_at = now()` is a SQL literal with no parameter slot, so it
|
|
103
|
+
// sits outside `placed` and is appended directly to each statement.
|
|
104
|
+
const insertColumns = ['id', ...placed.map((c) => c.name), 'updated_at'].join(', ');
|
|
105
|
+
const insertValues = ['$1', ...placed.map((c) => c.expr), 'now()'].join(', ');
|
|
106
|
+
const setClauses = [...placed.map((c) => `${c.name} = ${c.expr}`), 'updated_at = now()'].join(
|
|
107
|
+
', ',
|
|
108
|
+
);
|
|
103
109
|
|
|
104
|
-
return {
|
|
110
|
+
return {
|
|
111
|
+
insert: {
|
|
112
|
+
sql: `insert into prisma_contract.marker (${insertColumns}) values (${insertValues})`,
|
|
113
|
+
params,
|
|
114
|
+
},
|
|
115
|
+
update: {
|
|
116
|
+
sql: `update prisma_contract.marker set ${setClauses} where id = $1`,
|
|
117
|
+
params,
|
|
118
|
+
},
|
|
119
|
+
};
|
|
105
120
|
}
|