@prisma-next/family-sql 0.12.0 → 0.13.0-dev.1
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/{authoring-type-constructors-F4JpCJl7.mjs → authoring-type-constructors-D4lQ-qpj.mjs} +1 -1
- package/dist/{authoring-type-constructors-F4JpCJl7.mjs.map → authoring-type-constructors-D4lQ-qpj.mjs.map} +1 -1
- package/dist/control-adapter-CgIL9Vtx.d.mts +182 -0
- package/dist/control-adapter-CgIL9Vtx.d.mts.map +1 -0
- package/dist/control-adapter.d.mts +2 -109
- package/dist/control.d.mts +132 -4
- package/dist/control.d.mts.map +1 -1
- package/dist/control.mjs +277 -215
- package/dist/control.mjs.map +1 -1
- package/dist/ir.d.mts +4 -5
- package/dist/ir.d.mts.map +1 -1
- package/dist/ir.mjs +1 -1
- package/dist/migration.d.mts +1 -1
- package/dist/migration.d.mts.map +1 -1
- package/dist/pack.mjs +1 -1
- package/dist/runtime.d.mts +4 -2
- package/dist/runtime.d.mts.map +1 -1
- package/dist/runtime.mjs +4 -2
- package/dist/runtime.mjs.map +1 -1
- package/dist/schema-verify.d.mts +2 -1
- package/dist/schema-verify.d.mts.map +1 -1
- package/dist/schema-verify.mjs +1 -1
- package/dist/{sql-contract-serializer-8axtK4lg.mjs → sql-contract-serializer-CY7qnms7.mjs} +18 -36
- package/dist/sql-contract-serializer-CY7qnms7.mjs.map +1 -0
- package/dist/{timestamp-now-generator-r7BP5n3l.mjs → timestamp-now-generator-CloimujU.mjs} +2 -1
- package/dist/{timestamp-now-generator-r7BP5n3l.mjs.map → timestamp-now-generator-CloimujU.mjs.map} +1 -1
- package/dist/{types-CeeCStqw.d.mts → types-CbwQCzXY.d.mts} +70 -16
- package/dist/types-CbwQCzXY.d.mts.map +1 -0
- package/dist/{verify-Crewz6hG.mjs → verify-C-G0obRm.mjs} +1 -1
- package/dist/{verify-Crewz6hG.mjs.map → verify-C-G0obRm.mjs.map} +1 -1
- package/dist/{verify-sql-schema-CN7pPoTC.d.mts → verify-sql-schema-DcMaT5Zj.d.mts} +1 -1
- package/dist/{verify-sql-schema-CN7pPoTC.d.mts.map → verify-sql-schema-DcMaT5Zj.d.mts.map} +1 -1
- package/dist/{verify-sql-schema-CYLsGCFO.mjs → verify-sql-schema-DlAgBiT_.mjs} +756 -319
- package/dist/verify-sql-schema-DlAgBiT_.mjs.map +1 -0
- package/dist/verify.mjs +1 -1
- package/package.json +23 -23
- package/src/core/control-adapter.ts +116 -7
- package/src/core/control-instance.ts +269 -66
- package/src/core/default-namespace.ts +9 -0
- package/src/core/ir/sql-contract-serializer-base.ts +72 -56
- package/src/core/migrations/contract-to-schema-ir.ts +75 -9
- package/src/core/migrations/control-policy.ts +322 -0
- package/src/core/migrations/field-event-planner.ts +2 -2
- package/src/core/migrations/plan-helpers.ts +16 -0
- package/src/core/migrations/types.ts +17 -7
- package/src/core/psl-contract-infer/sql-schema-ir-to-psl-ast.ts +8 -6
- package/src/core/schema-verify/control-verify-emit.ts +46 -0
- package/src/core/schema-verify/verifier-disposition.ts +58 -0
- package/src/core/schema-verify/verify-helpers.ts +310 -111
- package/src/core/schema-verify/verify-sql-schema.ts +309 -178
- package/src/core/timestamp-now-generator.ts +1 -0
- package/src/exports/control-adapter.ts +5 -1
- package/src/exports/control.ts +7 -0
- package/src/exports/runtime.ts +7 -0
- package/dist/control-adapter.d.mts.map +0 -1
- package/dist/sql-contract-serializer-8axtK4lg.mjs.map +0 -1
- package/dist/types-CeeCStqw.d.mts.map +0 -1
- package/dist/verify-sql-schema-CYLsGCFO.mjs.map +0 -1
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
Contract,
|
|
3
|
+
ContractMarkerRecord,
|
|
4
|
+
LedgerEntryRecord,
|
|
5
|
+
} from '@prisma-next/contract/types';
|
|
2
6
|
import type {
|
|
3
7
|
TargetBoundComponentDescriptor,
|
|
4
8
|
TargetDescriptor,
|
|
5
9
|
} from '@prisma-next/framework-components/components';
|
|
6
10
|
import type {
|
|
7
|
-
ControlDriverInstance,
|
|
8
11
|
ControlFamilyInstance,
|
|
9
12
|
ControlStack,
|
|
10
13
|
CoreSchemaView,
|
|
@@ -28,17 +31,13 @@ import type { TypesImportSpec } from '@prisma-next/framework-components/emission
|
|
|
28
31
|
import type { PslDocumentAst } from '@prisma-next/framework-components/psl-ast';
|
|
29
32
|
import { assertDescriptorSelfConsistency } from '@prisma-next/migration-tools/spaces';
|
|
30
33
|
import { sqlContractCanonicalizationHooks } from '@prisma-next/sql-contract/canonicalization-hooks';
|
|
31
|
-
import type { SqlStorage } from '@prisma-next/sql-contract/types';
|
|
34
|
+
import type { SqlControlDriverInstance, SqlStorage } from '@prisma-next/sql-contract/types';
|
|
32
35
|
import type {
|
|
33
36
|
AnyQueryAst,
|
|
37
|
+
DdlNode,
|
|
34
38
|
LoweredStatement,
|
|
35
39
|
LowererContext,
|
|
36
40
|
} from '@prisma-next/sql-relational-core/ast';
|
|
37
|
-
import {
|
|
38
|
-
ensureSchemaStatement,
|
|
39
|
-
ensureTableStatement,
|
|
40
|
-
writeContractMarker,
|
|
41
|
-
} from '@prisma-next/sql-runtime';
|
|
42
41
|
import { defaultIndexName } from '@prisma-next/sql-schema-ir/naming';
|
|
43
42
|
import type { SqlSchemaIR, SqlTableIR } from '@prisma-next/sql-schema-ir/types';
|
|
44
43
|
import { ifDefined } from '@prisma-next/utils/defined';
|
|
@@ -69,10 +68,10 @@ function extractCodecTypeIdsFromContract(contract: unknown): readonly string[] {
|
|
|
69
68
|
) {
|
|
70
69
|
const namespaces = contract.storage.namespaces as Record<
|
|
71
70
|
string,
|
|
72
|
-
{ readonly
|
|
71
|
+
{ readonly entries: { readonly table: Readonly<Record<string, unknown>> } }
|
|
73
72
|
>;
|
|
74
73
|
for (const ns of Object.values(namespaces)) {
|
|
75
|
-
const tbls = ns.
|
|
74
|
+
const tbls = ns.entries.table;
|
|
76
75
|
if (typeof tbls !== 'object' || tbls === null) continue;
|
|
77
76
|
for (const table of Object.values(tbls)) {
|
|
78
77
|
if (
|
|
@@ -203,7 +202,7 @@ export interface SqlControlFamilyInstance
|
|
|
203
202
|
deserializeContract(contractJson: unknown): Contract;
|
|
204
203
|
|
|
205
204
|
verify(options: {
|
|
206
|
-
readonly driver:
|
|
205
|
+
readonly driver: SqlControlDriverInstance<string>;
|
|
207
206
|
readonly contract: unknown;
|
|
208
207
|
readonly expectedTargetId: string;
|
|
209
208
|
readonly contractPath: string;
|
|
@@ -228,43 +227,77 @@ export interface SqlControlFamilyInstance
|
|
|
228
227
|
}): VerifyDatabaseSchemaResult;
|
|
229
228
|
|
|
230
229
|
sign(options: {
|
|
231
|
-
readonly driver:
|
|
230
|
+
readonly driver: SqlControlDriverInstance<string>;
|
|
232
231
|
readonly contract: unknown;
|
|
233
232
|
readonly contractPath: string;
|
|
234
233
|
readonly configPath?: string;
|
|
235
234
|
}): Promise<SignDatabaseResult>;
|
|
236
235
|
|
|
237
236
|
introspect(options: {
|
|
238
|
-
readonly driver:
|
|
237
|
+
readonly driver: SqlControlDriverInstance<string>;
|
|
239
238
|
readonly contract?: unknown;
|
|
240
239
|
}): Promise<SqlSchemaIR>;
|
|
241
240
|
|
|
242
241
|
inferPslContract(schemaIR: SqlSchemaIR): PslDocumentAst;
|
|
243
242
|
|
|
244
|
-
lowerAst(ast: AnyQueryAst, context: LowererContext<unknown>): LoweredStatement;
|
|
243
|
+
lowerAst(ast: AnyQueryAst | DdlNode, context: LowererContext<unknown>): LoweredStatement;
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Inserts the initial marker row for `space` (upsert on `space`).
|
|
247
|
+
* Delegates to the target control adapter's write SPI; see
|
|
248
|
+
* `SqlControlAdapter.initMarker`.
|
|
249
|
+
*/
|
|
250
|
+
initMarker(options: {
|
|
251
|
+
readonly driver: SqlControlDriverInstance<string>;
|
|
252
|
+
readonly space: string;
|
|
253
|
+
readonly destination: {
|
|
254
|
+
readonly storageHash: string;
|
|
255
|
+
readonly profileHash: string;
|
|
256
|
+
readonly invariants?: readonly string[];
|
|
257
|
+
};
|
|
258
|
+
}): Promise<void>;
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Compare-and-swap advance of the marker row for `space`. Returns `true`
|
|
262
|
+
* when the swap matched a row; see `SqlControlAdapter.updateMarker`.
|
|
263
|
+
*/
|
|
264
|
+
updateMarker(options: {
|
|
265
|
+
readonly driver: SqlControlDriverInstance<string>;
|
|
266
|
+
readonly space: string;
|
|
267
|
+
readonly expectedFrom: string;
|
|
268
|
+
readonly destination: {
|
|
269
|
+
readonly storageHash: string;
|
|
270
|
+
readonly profileHash: string;
|
|
271
|
+
readonly invariants?: readonly string[];
|
|
272
|
+
};
|
|
273
|
+
}): Promise<boolean>;
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Appends a ledger entry for `space`; see
|
|
277
|
+
* `SqlControlAdapter.writeLedgerEntry`.
|
|
278
|
+
*/
|
|
279
|
+
writeLedgerEntry(options: {
|
|
280
|
+
readonly driver: SqlControlDriverInstance<string>;
|
|
281
|
+
readonly space: string;
|
|
282
|
+
readonly entry: {
|
|
283
|
+
readonly edgeId: string;
|
|
284
|
+
readonly from: string;
|
|
285
|
+
readonly to: string;
|
|
286
|
+
readonly migrationName: string;
|
|
287
|
+
readonly migrationHash: string;
|
|
288
|
+
readonly operations: readonly unknown[];
|
|
289
|
+
};
|
|
290
|
+
}): Promise<void>;
|
|
291
|
+
|
|
292
|
+
bootstrapControlTableQueries(): readonly DdlNode[];
|
|
293
|
+
|
|
294
|
+
bootstrapSignMarkerQueries(): readonly DdlNode[];
|
|
245
295
|
|
|
246
296
|
toOperationPreview(operations: readonly MigrationPlanOperation[]): OperationPreview;
|
|
247
297
|
}
|
|
248
298
|
|
|
249
299
|
export type SqlFamilyInstance = SqlControlFamilyInstance;
|
|
250
300
|
|
|
251
|
-
function isSqlControlAdapter<TTargetId extends string>(
|
|
252
|
-
value: unknown,
|
|
253
|
-
): value is SqlControlAdapter<TTargetId> {
|
|
254
|
-
return (
|
|
255
|
-
typeof value === 'object' &&
|
|
256
|
-
value !== null &&
|
|
257
|
-
'introspect' in value &&
|
|
258
|
-
typeof (value as { introspect: unknown }).introspect === 'function' &&
|
|
259
|
-
'readMarker' in value &&
|
|
260
|
-
typeof (value as { readMarker: unknown }).readMarker === 'function' &&
|
|
261
|
-
'readAllMarkers' in value &&
|
|
262
|
-
typeof (value as { readAllMarkers: unknown }).readAllMarkers === 'function' &&
|
|
263
|
-
'lower' in value &&
|
|
264
|
-
typeof (value as { lower: unknown }).lower === 'function'
|
|
265
|
-
);
|
|
266
|
-
}
|
|
267
|
-
|
|
268
301
|
interface DescriptorWithStorageTypes {
|
|
269
302
|
readonly targetId?: string | undefined;
|
|
270
303
|
readonly types?:
|
|
@@ -314,6 +347,113 @@ function buildSqlTypeMetadataRegistry(options: {
|
|
|
314
347
|
return registry;
|
|
315
348
|
}
|
|
316
349
|
|
|
350
|
+
interface CrossSpaceFkView {
|
|
351
|
+
readonly id: string;
|
|
352
|
+
readonly contractSpace?: {
|
|
353
|
+
readonly contractJson?: {
|
|
354
|
+
readonly extensionPacks?: Readonly<Record<string, unknown>>;
|
|
355
|
+
readonly storage?: {
|
|
356
|
+
readonly namespaces?: Readonly<Record<string, unknown>>;
|
|
357
|
+
};
|
|
358
|
+
};
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Builds a map from each extension id to the set of extension ids it
|
|
364
|
+
* transitively depends on. Uses the same declared-dependency data that
|
|
365
|
+
* `buildExtensionLoadOrder` in control-stack uses.
|
|
366
|
+
*/
|
|
367
|
+
function buildTransitiveDependsOnMap(
|
|
368
|
+
extensions: readonly CrossSpaceFkView[],
|
|
369
|
+
): Map<string, Set<string>> {
|
|
370
|
+
const directDeps = new Map<string, readonly string[]>();
|
|
371
|
+
for (const ext of extensions) {
|
|
372
|
+
const packs = ext.contractSpace?.contractJson?.extensionPacks;
|
|
373
|
+
const deps = packs !== null && typeof packs === 'object' ? Object.keys(packs) : [];
|
|
374
|
+
directDeps.set(ext.id, deps);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const result = new Map<string, Set<string>>();
|
|
378
|
+
const resolve = (id: string, visiting: Set<string>): Set<string> => {
|
|
379
|
+
const cached = result.get(id);
|
|
380
|
+
if (cached !== undefined) return cached;
|
|
381
|
+
const set = new Set<string>();
|
|
382
|
+
result.set(id, set);
|
|
383
|
+
for (const depId of directDeps.get(id) ?? []) {
|
|
384
|
+
set.add(depId);
|
|
385
|
+
if (!visiting.has(depId)) {
|
|
386
|
+
visiting.add(depId);
|
|
387
|
+
for (const transitive of resolve(depId, visiting)) {
|
|
388
|
+
set.add(transitive);
|
|
389
|
+
}
|
|
390
|
+
visiting.delete(depId);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
return set;
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
for (const ext of extensions) {
|
|
397
|
+
resolve(ext.id, new Set([ext.id]));
|
|
398
|
+
}
|
|
399
|
+
return result;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Asserts that no cross-space FK in any extension points against the
|
|
404
|
+
* dependency direction.
|
|
405
|
+
*
|
|
406
|
+
* A cross-space FK (target.spaceId present) from extension A pointing at
|
|
407
|
+
* space B is a violation when B depends on A (directly or transitively),
|
|
408
|
+
* because that means A is pointing "upward" against the dependency arrows
|
|
409
|
+
* established by the extension load order.
|
|
410
|
+
*
|
|
411
|
+
* Throws with a diagnostic naming the violating extension (source), the
|
|
412
|
+
* target space, and the direction violation.
|
|
413
|
+
*/
|
|
414
|
+
function isObjectRecord(v: unknown): v is Record<string, unknown> {
|
|
415
|
+
return typeof v === 'object' && v !== null;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
export function assertNoCrossSpaceFkReverseReferences(
|
|
419
|
+
extensions: readonly CrossSpaceFkView[],
|
|
420
|
+
): void {
|
|
421
|
+
const dependsOnMap = buildTransitiveDependsOnMap(extensions);
|
|
422
|
+
|
|
423
|
+
for (const ext of extensions) {
|
|
424
|
+
const namespaces = ext.contractSpace?.contractJson?.storage?.namespaces;
|
|
425
|
+
if (!isObjectRecord(namespaces)) continue;
|
|
426
|
+
for (const ns of Object.values(namespaces)) {
|
|
427
|
+
if (!isObjectRecord(ns)) continue;
|
|
428
|
+
const entries = ns['entries'];
|
|
429
|
+
if (!isObjectRecord(entries)) continue;
|
|
430
|
+
for (const slot of Object.values(entries)) {
|
|
431
|
+
if (!isObjectRecord(slot)) continue;
|
|
432
|
+
for (const table of Object.values(slot)) {
|
|
433
|
+
if (!isObjectRecord(table)) continue;
|
|
434
|
+
const foreignKeys = table['foreignKeys'];
|
|
435
|
+
if (!Array.isArray(foreignKeys)) continue;
|
|
436
|
+
for (const fk of foreignKeys) {
|
|
437
|
+
if (!isObjectRecord(fk)) continue;
|
|
438
|
+
const target = fk['target'];
|
|
439
|
+
if (!isObjectRecord(target)) continue;
|
|
440
|
+
if (target['spaceId'] === undefined) continue;
|
|
441
|
+
const targetSpaceId = target['spaceId'];
|
|
442
|
+
if (typeof targetSpaceId !== 'string') continue;
|
|
443
|
+
// Check if targetSpaceId depends on ext.id (directly or transitively)
|
|
444
|
+
const targetDeps = dependsOnMap.get(targetSpaceId);
|
|
445
|
+
if (targetDeps?.has(ext.id)) {
|
|
446
|
+
throw new Error(
|
|
447
|
+
`Cross-space FK reverse-reference detected: extension "${ext.id}" has a cross-space FK targeting space "${targetSpaceId}", but "${targetSpaceId}" depends on "${ext.id}". Cross-space FKs must follow the dependency direction (a space can only reference spaces it depends on, not spaces that depend on it).`,
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
317
457
|
export function createSqlFamilyInstance<TTargetId extends string>(
|
|
318
458
|
stack: ControlStack<'sql', TTargetId>,
|
|
319
459
|
): SqlFamilyInstance {
|
|
@@ -353,6 +493,8 @@ export function createSqlFamilyInstance<TTargetId extends string>(
|
|
|
353
493
|
}
|
|
354
494
|
}
|
|
355
495
|
|
|
496
|
+
assertNoCrossSpaceFkReverseReferences(extensions);
|
|
497
|
+
|
|
356
498
|
const { codecTypeImports, extensionIds } = stack;
|
|
357
499
|
|
|
358
500
|
const typeMetadataRegistry = buildSqlTypeMetadataRegistry({
|
|
@@ -361,19 +503,20 @@ export function createSqlFamilyInstance<TTargetId extends string>(
|
|
|
361
503
|
extensionPacks: extensions,
|
|
362
504
|
});
|
|
363
505
|
|
|
364
|
-
//
|
|
365
|
-
//
|
|
366
|
-
//
|
|
367
|
-
//
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
506
|
+
// Lazily construct the control adapter on first use, then memoize it.
|
|
507
|
+
// Merely building a family instance must not instantiate the adapter —
|
|
508
|
+
// that would change the load/instantiate semantics of the whole stack
|
|
509
|
+
// wherever a family is created (every CLI command, emit, verify, …), not
|
|
510
|
+
// just the migration paths that actually need it. Memoizing also avoids
|
|
511
|
+
// the previous per-operation re-instantiation (a fresh adapter on every
|
|
512
|
+
// call). Family-instance methods accept `SqlControlDriverInstance<string>`
|
|
513
|
+
// (the family API isn't generic on the target id); the adapter
|
|
514
|
+
// descriptor's `create` returns the concrete `SqlControlAdapter<TTargetId>`,
|
|
515
|
+
// widened to `string` to match the family-level driver type without a
|
|
516
|
+
// per-method probe.
|
|
517
|
+
let controlAdapter: SqlControlAdapter<string> | undefined;
|
|
518
|
+
const getControlAdapter = (): SqlControlAdapter<string> =>
|
|
519
|
+
(controlAdapter ??= adapter.create(stack));
|
|
377
520
|
|
|
378
521
|
const targetSerializer = (
|
|
379
522
|
target as unknown as {
|
|
@@ -396,7 +539,7 @@ export function createSqlFamilyInstance<TTargetId extends string>(
|
|
|
396
539
|
},
|
|
397
540
|
|
|
398
541
|
async verify(verifyOptions: {
|
|
399
|
-
readonly driver:
|
|
542
|
+
readonly driver: SqlControlDriverInstance<string>;
|
|
400
543
|
readonly contract: unknown;
|
|
401
544
|
readonly expectedTargetId: string;
|
|
402
545
|
readonly contractPath: string;
|
|
@@ -544,7 +687,7 @@ export function createSqlFamilyInstance<TTargetId extends string>(
|
|
|
544
687
|
});
|
|
545
688
|
},
|
|
546
689
|
async sign(options: {
|
|
547
|
-
readonly driver:
|
|
690
|
+
readonly driver: SqlControlDriverInstance<string>;
|
|
548
691
|
readonly contract: unknown;
|
|
549
692
|
readonly contractPath: string;
|
|
550
693
|
readonly configPath?: string;
|
|
@@ -561,24 +704,24 @@ export function createSqlFamilyInstance<TTargetId extends string>(
|
|
|
561
704
|
: contractStorageHash;
|
|
562
705
|
const contractTarget = contract.target;
|
|
563
706
|
|
|
564
|
-
|
|
565
|
-
|
|
707
|
+
const controlAdapter = getControlAdapter();
|
|
708
|
+
const lowererContext = { contract };
|
|
709
|
+
for (const query of controlAdapter.bootstrapSignMarkerQueries()) {
|
|
710
|
+
const lowered = controlAdapter.lower(query, lowererContext);
|
|
711
|
+
await driver.query(lowered.sql, lowered.params);
|
|
712
|
+
}
|
|
566
713
|
|
|
567
|
-
const existingMarker = await
|
|
714
|
+
const existingMarker = await controlAdapter.readMarker(driver, APP_SPACE_ID);
|
|
568
715
|
|
|
569
716
|
let markerCreated = false;
|
|
570
717
|
let markerUpdated = false;
|
|
571
718
|
let previousHashes: { storageHash?: string; profileHash?: string } | undefined;
|
|
572
719
|
|
|
573
720
|
if (!existingMarker) {
|
|
574
|
-
|
|
575
|
-
space: APP_SPACE_ID,
|
|
721
|
+
await controlAdapter.insertMarker(driver, APP_SPACE_ID, {
|
|
576
722
|
storageHash: contractStorageHash,
|
|
577
723
|
profileHash: contractProfileHash,
|
|
578
|
-
contractJson: contractInput,
|
|
579
|
-
canonicalVersion: 1,
|
|
580
724
|
});
|
|
581
|
-
await driver.query(write.insert.sql, write.insert.params);
|
|
582
725
|
markerCreated = true;
|
|
583
726
|
} else {
|
|
584
727
|
const existingStorageHash = existingMarker.storageHash;
|
|
@@ -592,14 +735,18 @@ export function createSqlFamilyInstance<TTargetId extends string>(
|
|
|
592
735
|
storageHash: existingStorageHash,
|
|
593
736
|
profileHash: existingProfileHash,
|
|
594
737
|
};
|
|
595
|
-
const
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
738
|
+
const updated = await controlAdapter.updateMarker(
|
|
739
|
+
driver,
|
|
740
|
+
APP_SPACE_ID,
|
|
741
|
+
existingStorageHash,
|
|
742
|
+
{
|
|
743
|
+
storageHash: contractStorageHash,
|
|
744
|
+
profileHash: contractProfileHash,
|
|
745
|
+
},
|
|
746
|
+
);
|
|
747
|
+
if (!updated) {
|
|
748
|
+
throw new Error('CAS conflict: marker was modified by another process during sign');
|
|
749
|
+
}
|
|
603
750
|
markerUpdated = true;
|
|
604
751
|
}
|
|
605
752
|
}
|
|
@@ -641,18 +788,66 @@ export function createSqlFamilyInstance<TTargetId extends string>(
|
|
|
641
788
|
};
|
|
642
789
|
},
|
|
643
790
|
async readMarker(options: {
|
|
644
|
-
readonly driver:
|
|
791
|
+
readonly driver: SqlControlDriverInstance<string>;
|
|
645
792
|
readonly space: string;
|
|
646
793
|
}): Promise<ContractMarkerRecord | null> {
|
|
647
794
|
return getControlAdapter().readMarker(options.driver, options.space);
|
|
648
795
|
},
|
|
649
796
|
async readAllMarkers(options: {
|
|
650
|
-
readonly driver:
|
|
797
|
+
readonly driver: SqlControlDriverInstance<string>;
|
|
651
798
|
}): Promise<ReadonlyMap<string, ContractMarkerRecord>> {
|
|
652
799
|
return getControlAdapter().readAllMarkers(options.driver);
|
|
653
800
|
},
|
|
801
|
+
async readLedger(options: {
|
|
802
|
+
readonly driver: SqlControlDriverInstance<string>;
|
|
803
|
+
readonly space?: string;
|
|
804
|
+
}): Promise<readonly LedgerEntryRecord[]> {
|
|
805
|
+
return getControlAdapter().readLedger(options.driver, options.space);
|
|
806
|
+
},
|
|
807
|
+
async initMarker(options: {
|
|
808
|
+
readonly driver: SqlControlDriverInstance<string>;
|
|
809
|
+
readonly space: string;
|
|
810
|
+
readonly destination: {
|
|
811
|
+
readonly storageHash: string;
|
|
812
|
+
readonly profileHash: string;
|
|
813
|
+
readonly invariants?: readonly string[];
|
|
814
|
+
};
|
|
815
|
+
}): Promise<void> {
|
|
816
|
+
return getControlAdapter().initMarker(options.driver, options.space, options.destination);
|
|
817
|
+
},
|
|
818
|
+
async updateMarker(options: {
|
|
819
|
+
readonly driver: SqlControlDriverInstance<string>;
|
|
820
|
+
readonly space: string;
|
|
821
|
+
readonly expectedFrom: string;
|
|
822
|
+
readonly destination: {
|
|
823
|
+
readonly storageHash: string;
|
|
824
|
+
readonly profileHash: string;
|
|
825
|
+
readonly invariants?: readonly string[];
|
|
826
|
+
};
|
|
827
|
+
}): Promise<boolean> {
|
|
828
|
+
return getControlAdapter().updateMarker(
|
|
829
|
+
options.driver,
|
|
830
|
+
options.space,
|
|
831
|
+
options.expectedFrom,
|
|
832
|
+
options.destination,
|
|
833
|
+
);
|
|
834
|
+
},
|
|
835
|
+
async writeLedgerEntry(options: {
|
|
836
|
+
readonly driver: SqlControlDriverInstance<string>;
|
|
837
|
+
readonly space: string;
|
|
838
|
+
readonly entry: {
|
|
839
|
+
readonly edgeId: string;
|
|
840
|
+
readonly from: string;
|
|
841
|
+
readonly to: string;
|
|
842
|
+
readonly migrationName: string;
|
|
843
|
+
readonly migrationHash: string;
|
|
844
|
+
readonly operations: readonly unknown[];
|
|
845
|
+
};
|
|
846
|
+
}): Promise<void> {
|
|
847
|
+
return getControlAdapter().writeLedgerEntry(options.driver, options.space, options.entry);
|
|
848
|
+
},
|
|
654
849
|
async introspect(options: {
|
|
655
|
-
readonly driver:
|
|
850
|
+
readonly driver: SqlControlDriverInstance<string>;
|
|
656
851
|
readonly contract?: unknown;
|
|
657
852
|
}): Promise<SqlSchemaIR> {
|
|
658
853
|
return getControlAdapter().introspect(options.driver, options.contract);
|
|
@@ -662,10 +857,18 @@ export function createSqlFamilyInstance<TTargetId extends string>(
|
|
|
662
857
|
return sqlSchemaIrToPslAst(schemaIR);
|
|
663
858
|
},
|
|
664
859
|
|
|
665
|
-
lowerAst(ast: AnyQueryAst, context: LowererContext<unknown>): LoweredStatement {
|
|
860
|
+
lowerAst(ast: AnyQueryAst | DdlNode, context: LowererContext<unknown>): LoweredStatement {
|
|
666
861
|
return getControlAdapter().lower(ast, context);
|
|
667
862
|
},
|
|
668
863
|
|
|
864
|
+
bootstrapControlTableQueries(): readonly DdlNode[] {
|
|
865
|
+
return getControlAdapter().bootstrapControlTableQueries();
|
|
866
|
+
},
|
|
867
|
+
|
|
868
|
+
bootstrapSignMarkerQueries(): readonly DdlNode[] {
|
|
869
|
+
return getControlAdapter().bootstrapSignMarkerQueries();
|
|
870
|
+
},
|
|
871
|
+
|
|
669
872
|
toOperationPreview(operations: readonly MigrationPlanOperation[]): OperationPreview {
|
|
670
873
|
return sqlOperationsToPreview(operations);
|
|
671
874
|
},
|
|
@@ -16,6 +16,8 @@ import {
|
|
|
16
16
|
SqlUnboundNamespace,
|
|
17
17
|
StorageTable,
|
|
18
18
|
type StorageTableInput,
|
|
19
|
+
StorageValueSet,
|
|
20
|
+
type StorageValueSetInput,
|
|
19
21
|
} from '@prisma-next/sql-contract/types';
|
|
20
22
|
import {
|
|
21
23
|
createSqlContractSchema,
|
|
@@ -29,16 +31,16 @@ import { type Type, type } from 'arktype';
|
|
|
29
31
|
const NamespaceRawSchema = type({
|
|
30
32
|
id: 'string',
|
|
31
33
|
'kind?': 'string',
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
34
|
+
entries: type({
|
|
35
|
+
'+': 'ignore',
|
|
36
|
+
}),
|
|
35
37
|
});
|
|
36
38
|
|
|
37
39
|
function isPlainRecord(value: unknown): value is Record<string, unknown> {
|
|
38
40
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
39
41
|
}
|
|
40
42
|
|
|
41
|
-
export type SqlEntityHydrationFactory = (entry: unknown) =>
|
|
43
|
+
export type SqlEntityHydrationFactory = (entry: unknown) => unknown;
|
|
42
44
|
|
|
43
45
|
/**
|
|
44
46
|
* SQL family `ContractSerializer` abstract base. Carries the SQL-shared
|
|
@@ -70,7 +72,10 @@ export abstract class SqlContractSerializerBase<TContract extends Contract<SqlSt
|
|
|
70
72
|
private readonly contractSchema: Type<unknown> | undefined;
|
|
71
73
|
|
|
72
74
|
constructor(
|
|
73
|
-
|
|
75
|
+
protected readonly entityTypeRegistry: ReadonlyMap<
|
|
76
|
+
string,
|
|
77
|
+
SqlEntityHydrationFactory
|
|
78
|
+
> = new Map(),
|
|
74
79
|
validatorFragments?: ReadonlyMap<string, Type<unknown>>,
|
|
75
80
|
) {
|
|
76
81
|
// Only build a fragments-aware contract schema when pack contributions
|
|
@@ -158,7 +163,12 @@ export abstract class SqlContractSerializerBase<TContract extends Contract<SqlSt
|
|
|
158
163
|
const namespaceMaterialised =
|
|
159
164
|
namespaceHydrated instanceof NamespaceBase
|
|
160
165
|
? namespaceHydrated
|
|
161
|
-
: buildSqlNamespace(
|
|
166
|
+
: buildSqlNamespace(
|
|
167
|
+
blindCast<
|
|
168
|
+
SqlNamespaceTablesInput,
|
|
169
|
+
'hydrateSqlNamespaceEntry returns SqlNamespaceTablesInput when raw is not a NamespaceBase'
|
|
170
|
+
>(namespaceHydrated),
|
|
171
|
+
);
|
|
162
172
|
return [nsId, namespaceMaterialised];
|
|
163
173
|
}),
|
|
164
174
|
);
|
|
@@ -172,69 +182,72 @@ export abstract class SqlContractSerializerBase<TContract extends Contract<SqlSt
|
|
|
172
182
|
return raw;
|
|
173
183
|
}
|
|
174
184
|
const rawRecord = isPlainRecord(raw) ? raw : {};
|
|
185
|
+
if (
|
|
186
|
+
Object.hasOwn(rawRecord, 'tables') ||
|
|
187
|
+
Object.hasOwn(rawRecord, 'enum') ||
|
|
188
|
+
Object.hasOwn(rawRecord, 'collections')
|
|
189
|
+
) {
|
|
190
|
+
throw new ContractValidationError(
|
|
191
|
+
'Namespace envelope uses deprecated flat slot keys; expected `entries: { table? }`',
|
|
192
|
+
'structural',
|
|
193
|
+
);
|
|
194
|
+
}
|
|
175
195
|
const id = typeof rawRecord['id'] === 'string' ? rawRecord['id'] : nsId;
|
|
176
196
|
const parsed = NamespaceRawSchema({ ...rawRecord, id });
|
|
177
197
|
if (parsed instanceof type.errors) {
|
|
178
198
|
const messages = parsed.map((p: { message: string }) => p.message).join('; ');
|
|
179
199
|
throw new ContractValidationError(`Namespace hydration failed: ${messages}`, 'structural');
|
|
180
200
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
201
|
+
// Default to empty table; overwritten below if raw entries carry a table slot.
|
|
202
|
+
const entriesInput: {
|
|
203
|
+
table: Record<string, StorageTable>;
|
|
204
|
+
valueSet?: Record<string, StorageValueSet>;
|
|
205
|
+
} = { table: {} };
|
|
206
|
+
const entriesRaw = parsed.entries;
|
|
207
|
+
if (entriesRaw !== undefined && typeof entriesRaw === 'object' && entriesRaw !== null) {
|
|
208
|
+
const rawEntries = entriesRaw as Record<string, unknown>;
|
|
209
|
+
const tableSlot = rawEntries['table'];
|
|
210
|
+
if (tableSlot !== null && typeof tableSlot === 'object' && !Array.isArray(tableSlot)) {
|
|
211
|
+
entriesInput.table = Object.fromEntries(
|
|
212
|
+
Object.entries(tableSlot as Record<string, unknown>).map(([tableName, table]) => [
|
|
190
213
|
tableName,
|
|
191
214
|
table instanceof StorageTable ? table : new StorageTable(table as StorageTableInput),
|
|
192
215
|
]),
|
|
193
216
|
);
|
|
194
|
-
continue;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
const hydratedSlot = Object.fromEntries(
|
|
198
|
-
Object.entries(slotValue as Record<string, unknown>).map(([entryName, entry]) => {
|
|
199
|
-
if (typeof entry !== 'object' || entry === null) {
|
|
200
|
-
return [entryName, entry];
|
|
201
|
-
}
|
|
202
|
-
const kind = (entry as { kind?: unknown }).kind;
|
|
203
|
-
if (typeof kind === 'string') {
|
|
204
|
-
const factory = this.entityTypeRegistry.get(kind);
|
|
205
|
-
if (factory !== undefined) {
|
|
206
|
-
return [entryName, factory(entry)];
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
return [entryName, entry];
|
|
210
|
-
}),
|
|
211
|
-
);
|
|
212
|
-
if (Object.keys(hydratedSlot).length > 0) {
|
|
213
|
-
result[propertyKey] = hydratedSlot;
|
|
214
217
|
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
218
|
+
const valueSetSlot = rawEntries['valueSet'];
|
|
219
|
+
if (
|
|
220
|
+
valueSetSlot !== null &&
|
|
221
|
+
typeof valueSetSlot === 'object' &&
|
|
222
|
+
!Array.isArray(valueSetSlot)
|
|
223
|
+
) {
|
|
224
|
+
entriesInput.valueSet = Object.fromEntries(
|
|
225
|
+
Object.entries(
|
|
226
|
+
blindCast<
|
|
227
|
+
Record<string, unknown>,
|
|
228
|
+
'valueSet slot is a plain record after object check'
|
|
229
|
+
>(valueSetSlot),
|
|
230
|
+
).map(([vsName, vs]) => [
|
|
231
|
+
vsName,
|
|
232
|
+
vs instanceof StorageValueSet
|
|
233
|
+
? vs
|
|
234
|
+
: new StorageValueSet(
|
|
235
|
+
blindCast<
|
|
236
|
+
StorageValueSetInput,
|
|
237
|
+
'non-instance valueSet entry is StorageValueSetInput'
|
|
238
|
+
>(vs),
|
|
239
|
+
),
|
|
240
|
+
]),
|
|
241
|
+
);
|
|
228
242
|
}
|
|
243
|
+
// Target-specific slots (e.g. postgres `type`) are left for target
|
|
244
|
+
// overrides to extract from the original `raw` parameter.
|
|
229
245
|
}
|
|
230
246
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
tables,
|
|
236
|
-
...(enumSlot !== undefined ? { enum: enumSlot } : {}),
|
|
237
|
-
} as SqlNamespaceTablesInput;
|
|
247
|
+
return blindCast<SqlNamespaceTablesInput, 'hydrated namespace tables input'>({
|
|
248
|
+
id,
|
|
249
|
+
entries: entriesInput,
|
|
250
|
+
});
|
|
238
251
|
}
|
|
239
252
|
|
|
240
253
|
protected hydrateStorageTypeEntry(entry: SqlStorageTypeEntry): SqlStorageTypeEntry {
|
|
@@ -249,7 +262,10 @@ export abstract class SqlContractSerializerBase<TContract extends Contract<SqlSt
|
|
|
249
262
|
if (factory === undefined) {
|
|
250
263
|
return entry;
|
|
251
264
|
}
|
|
252
|
-
return
|
|
265
|
+
return blindCast<
|
|
266
|
+
SqlStorageTypeEntry,
|
|
267
|
+
'entity registry factory returns SqlStorageTypeEntry for storage.types entries'
|
|
268
|
+
>(factory(entry));
|
|
253
269
|
}
|
|
254
270
|
|
|
255
271
|
protected constructTargetContract(hydrated: Contract<SqlStorage>): TContract {
|