@prisma-next/family-sql 0.12.0-dev.6 → 0.12.0-dev.60

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/dist/{authoring-type-constructors-F4JpCJl7.mjs → authoring-type-constructors-D4lQ-qpj.mjs} +1 -1
  2. package/dist/{authoring-type-constructors-F4JpCJl7.mjs.map → authoring-type-constructors-D4lQ-qpj.mjs.map} +1 -1
  3. package/dist/control-adapter-76pzcIFV.d.mts +172 -0
  4. package/dist/control-adapter-76pzcIFV.d.mts.map +1 -0
  5. package/dist/control-adapter.d.mts +2 -109
  6. package/dist/control.d.mts +118 -4
  7. package/dist/control.d.mts.map +1 -1
  8. package/dist/control.mjs +305 -44
  9. package/dist/control.mjs.map +1 -1
  10. package/dist/ir.d.mts +4 -5
  11. package/dist/ir.d.mts.map +1 -1
  12. package/dist/ir.mjs +1 -1
  13. package/dist/migration.d.mts +1 -1
  14. package/dist/migration.d.mts.map +1 -1
  15. package/dist/pack.mjs +1 -1
  16. package/dist/runtime.d.mts +4 -2
  17. package/dist/runtime.d.mts.map +1 -1
  18. package/dist/runtime.mjs +4 -2
  19. package/dist/runtime.mjs.map +1 -1
  20. package/dist/schema-verify.d.mts +2 -1
  21. package/dist/schema-verify.d.mts.map +1 -1
  22. package/dist/schema-verify.mjs +1 -1
  23. package/dist/{sql-contract-serializer-8axtK4lg.mjs → sql-contract-serializer-CY7qnms7.mjs} +18 -36
  24. package/dist/sql-contract-serializer-CY7qnms7.mjs.map +1 -0
  25. package/dist/{timestamp-now-generator-BkjCQIde.mjs → timestamp-now-generator-CloimujU.mjs} +1 -1
  26. package/dist/{timestamp-now-generator-BkjCQIde.mjs.map → timestamp-now-generator-CloimujU.mjs.map} +1 -1
  27. package/dist/{types-CeeCStqw.d.mts → types-CRpx_fk5.d.mts} +69 -15
  28. package/dist/types-CRpx_fk5.d.mts.map +1 -0
  29. package/dist/{verify-Crewz6hG.mjs → verify-C-G0obRm.mjs} +1 -1
  30. package/dist/{verify-Crewz6hG.mjs.map → verify-C-G0obRm.mjs.map} +1 -1
  31. package/dist/{verify-sql-schema-CN7pPoTC.d.mts → verify-sql-schema-DcMaT5Zj.d.mts} +1 -1
  32. package/dist/{verify-sql-schema-CN7pPoTC.d.mts.map → verify-sql-schema-DcMaT5Zj.d.mts.map} +1 -1
  33. package/dist/{verify-sql-schema-CYLsGCFO.mjs → verify-sql-schema-d3P9NA_8.mjs} +400 -317
  34. package/dist/verify-sql-schema-d3P9NA_8.mjs.map +1 -0
  35. package/dist/verify.mjs +1 -1
  36. package/package.json +23 -23
  37. package/src/core/control-adapter.ts +105 -7
  38. package/src/core/control-instance.ts +260 -66
  39. package/src/core/default-namespace.ts +9 -0
  40. package/src/core/ir/sql-contract-serializer-base.ts +72 -56
  41. package/src/core/migrations/contract-to-schema-ir.ts +13 -9
  42. package/src/core/migrations/control-policy.ts +322 -0
  43. package/src/core/migrations/field-event-planner.ts +2 -2
  44. package/src/core/migrations/plan-helpers.ts +16 -0
  45. package/src/core/migrations/types.ts +16 -6
  46. package/src/core/schema-verify/control-verify-emit.ts +46 -0
  47. package/src/core/schema-verify/verifier-disposition.ts +53 -0
  48. package/src/core/schema-verify/verify-helpers.ts +151 -110
  49. package/src/core/schema-verify/verify-sql-schema.ts +281 -178
  50. package/src/exports/control.ts +6 -0
  51. package/src/exports/runtime.ts +7 -0
  52. package/dist/control-adapter.d.mts.map +0 -1
  53. package/dist/sql-contract-serializer-8axtK4lg.mjs.map +0 -1
  54. package/dist/types-CeeCStqw.d.mts.map +0 -1
  55. package/dist/verify-sql-schema-CYLsGCFO.mjs.map +0 -1
@@ -1,10 +1,13 @@
1
- import type { Contract, ContractMarkerRecord } from '@prisma-next/contract/types';
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 tables?: Readonly<Record<string, unknown>> }
71
+ { readonly entries: { readonly table: Readonly<Record<string, unknown>> } }
73
72
  >;
74
73
  for (const ns of Object.values(namespaces)) {
75
- const tbls = ns.tables;
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: ControlDriverInstance<'sql', string>;
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: ControlDriverInstance<'sql', string>;
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: ControlDriverInstance<'sql', string>;
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,11 @@ export function createSqlFamilyInstance<TTargetId extends string>(
361
503
  extensionPacks: extensions,
362
504
  });
363
505
 
364
- // Family-instance methods accept `ControlDriverInstance<'sql', string>` —
365
- // the family API isn't generic on the target id. Letting `isSqlControlAdapter`
366
- // default its type parameter narrows the adapter to `SqlControlAdapter<string>`,
367
- // which matches the family-level driver type without any cast at call sites.
368
- const getControlAdapter = () => {
369
- const controlAdapter = adapter.create(stack);
370
- if (!isSqlControlAdapter(controlAdapter)) {
371
- throw new Error(
372
- 'Adapter does not implement SqlControlAdapter (missing introspect, readMarker, or readAllMarkers)',
373
- );
374
- }
375
- return controlAdapter;
376
- };
506
+ // Family-instance methods accept `SqlControlDriverInstance<string>` — the
507
+ // family API isn't generic on the target id. The adapter descriptor's `create`
508
+ // returns the concrete `SqlControlAdapter<TTargetId>`; widening the target id to
509
+ // `string` here matches the family-level driver type without a per-method probe.
510
+ const getControlAdapter = (): SqlControlAdapter<string> => adapter.create(stack);
377
511
 
378
512
  const targetSerializer = (
379
513
  target as unknown as {
@@ -396,7 +530,7 @@ export function createSqlFamilyInstance<TTargetId extends string>(
396
530
  },
397
531
 
398
532
  async verify(verifyOptions: {
399
- readonly driver: ControlDriverInstance<'sql', string>;
533
+ readonly driver: SqlControlDriverInstance<string>;
400
534
  readonly contract: unknown;
401
535
  readonly expectedTargetId: string;
402
536
  readonly contractPath: string;
@@ -544,7 +678,7 @@ export function createSqlFamilyInstance<TTargetId extends string>(
544
678
  });
545
679
  },
546
680
  async sign(options: {
547
- readonly driver: ControlDriverInstance<'sql', string>;
681
+ readonly driver: SqlControlDriverInstance<string>;
548
682
  readonly contract: unknown;
549
683
  readonly contractPath: string;
550
684
  readonly configPath?: string;
@@ -561,24 +695,24 @@ export function createSqlFamilyInstance<TTargetId extends string>(
561
695
  : contractStorageHash;
562
696
  const contractTarget = contract.target;
563
697
 
564
- await driver.query(ensureSchemaStatement.sql, ensureSchemaStatement.params);
565
- await driver.query(ensureTableStatement.sql, ensureTableStatement.params);
698
+ const controlAdapter = getControlAdapter();
699
+ const lowererContext = { contract };
700
+ for (const query of controlAdapter.bootstrapSignMarkerQueries()) {
701
+ const lowered = controlAdapter.lower(query, lowererContext);
702
+ await driver.query(lowered.sql, lowered.params);
703
+ }
566
704
 
567
- const existingMarker = await getControlAdapter().readMarker(driver, APP_SPACE_ID);
705
+ const existingMarker = await controlAdapter.readMarker(driver, APP_SPACE_ID);
568
706
 
569
707
  let markerCreated = false;
570
708
  let markerUpdated = false;
571
709
  let previousHashes: { storageHash?: string; profileHash?: string } | undefined;
572
710
 
573
711
  if (!existingMarker) {
574
- const write = writeContractMarker({
575
- space: APP_SPACE_ID,
712
+ await controlAdapter.insertMarker(driver, APP_SPACE_ID, {
576
713
  storageHash: contractStorageHash,
577
714
  profileHash: contractProfileHash,
578
- contractJson: contractInput,
579
- canonicalVersion: 1,
580
715
  });
581
- await driver.query(write.insert.sql, write.insert.params);
582
716
  markerCreated = true;
583
717
  } else {
584
718
  const existingStorageHash = existingMarker.storageHash;
@@ -592,14 +726,18 @@ export function createSqlFamilyInstance<TTargetId extends string>(
592
726
  storageHash: existingStorageHash,
593
727
  profileHash: existingProfileHash,
594
728
  };
595
- const write = writeContractMarker({
596
- space: APP_SPACE_ID,
597
- storageHash: contractStorageHash,
598
- profileHash: contractProfileHash,
599
- contractJson: contractInput,
600
- canonicalVersion: existingMarker.canonicalVersion ?? 1,
601
- });
602
- await driver.query(write.update.sql, write.update.params);
729
+ const updated = await controlAdapter.updateMarker(
730
+ driver,
731
+ APP_SPACE_ID,
732
+ existingStorageHash,
733
+ {
734
+ storageHash: contractStorageHash,
735
+ profileHash: contractProfileHash,
736
+ },
737
+ );
738
+ if (!updated) {
739
+ throw new Error('CAS conflict: marker was modified by another process during sign');
740
+ }
603
741
  markerUpdated = true;
604
742
  }
605
743
  }
@@ -641,18 +779,66 @@ export function createSqlFamilyInstance<TTargetId extends string>(
641
779
  };
642
780
  },
643
781
  async readMarker(options: {
644
- readonly driver: ControlDriverInstance<'sql', string>;
782
+ readonly driver: SqlControlDriverInstance<string>;
645
783
  readonly space: string;
646
784
  }): Promise<ContractMarkerRecord | null> {
647
785
  return getControlAdapter().readMarker(options.driver, options.space);
648
786
  },
649
787
  async readAllMarkers(options: {
650
- readonly driver: ControlDriverInstance<'sql', string>;
788
+ readonly driver: SqlControlDriverInstance<string>;
651
789
  }): Promise<ReadonlyMap<string, ContractMarkerRecord>> {
652
790
  return getControlAdapter().readAllMarkers(options.driver);
653
791
  },
792
+ async readLedger(options: {
793
+ readonly driver: SqlControlDriverInstance<string>;
794
+ readonly space?: string;
795
+ }): Promise<readonly LedgerEntryRecord[]> {
796
+ return getControlAdapter().readLedger(options.driver, options.space);
797
+ },
798
+ async initMarker(options: {
799
+ readonly driver: SqlControlDriverInstance<string>;
800
+ readonly space: string;
801
+ readonly destination: {
802
+ readonly storageHash: string;
803
+ readonly profileHash: string;
804
+ readonly invariants?: readonly string[];
805
+ };
806
+ }): Promise<void> {
807
+ return getControlAdapter().initMarker(options.driver, options.space, options.destination);
808
+ },
809
+ async updateMarker(options: {
810
+ readonly driver: SqlControlDriverInstance<string>;
811
+ readonly space: string;
812
+ readonly expectedFrom: string;
813
+ readonly destination: {
814
+ readonly storageHash: string;
815
+ readonly profileHash: string;
816
+ readonly invariants?: readonly string[];
817
+ };
818
+ }): Promise<boolean> {
819
+ return getControlAdapter().updateMarker(
820
+ options.driver,
821
+ options.space,
822
+ options.expectedFrom,
823
+ options.destination,
824
+ );
825
+ },
826
+ async writeLedgerEntry(options: {
827
+ readonly driver: SqlControlDriverInstance<string>;
828
+ readonly space: string;
829
+ readonly entry: {
830
+ readonly edgeId: string;
831
+ readonly from: string;
832
+ readonly to: string;
833
+ readonly migrationName: string;
834
+ readonly migrationHash: string;
835
+ readonly operations: readonly unknown[];
836
+ };
837
+ }): Promise<void> {
838
+ return getControlAdapter().writeLedgerEntry(options.driver, options.space, options.entry);
839
+ },
654
840
  async introspect(options: {
655
- readonly driver: ControlDriverInstance<'sql', string>;
841
+ readonly driver: SqlControlDriverInstance<string>;
656
842
  readonly contract?: unknown;
657
843
  }): Promise<SqlSchemaIR> {
658
844
  return getControlAdapter().introspect(options.driver, options.contract);
@@ -662,10 +848,18 @@ export function createSqlFamilyInstance<TTargetId extends string>(
662
848
  return sqlSchemaIrToPslAst(schemaIR);
663
849
  },
664
850
 
665
- lowerAst(ast: AnyQueryAst, context: LowererContext<unknown>): LoweredStatement {
851
+ lowerAst(ast: AnyQueryAst | DdlNode, context: LowererContext<unknown>): LoweredStatement {
666
852
  return getControlAdapter().lower(ast, context);
667
853
  },
668
854
 
855
+ bootstrapControlTableQueries(): readonly DdlNode[] {
856
+ return getControlAdapter().bootstrapControlTableQueries();
857
+ },
858
+
859
+ bootstrapSignMarkerQueries(): readonly DdlNode[] {
860
+ return getControlAdapter().bootstrapSignMarkerQueries();
861
+ },
862
+
669
863
  toOperationPreview(operations: readonly MigrationPlanOperation[]): OperationPreview {
670
864
  return sqlOperationsToPreview(operations);
671
865
  },
@@ -0,0 +1,9 @@
1
+ export {
2
+ type ResolvedDomainModel,
3
+ resolveDomainModel,
4
+ UNBOUND_DOMAIN_NAMESPACE_ID,
5
+ } from '@prisma-next/contract/types';
6
+ export {
7
+ type ResolvedStorageTable,
8
+ resolveStorageTable,
9
+ } from '@prisma-next/sql-contract/resolve-storage-table';
@@ -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
- // Undeclared keys (`tables`, `enum`, and any pack-contributed slot maps)
33
- // intentionally pass through; the slot loop below iterates them by name.
34
- '+': 'ignore',
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) => SqlStorageTypeEntry;
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
- private readonly entityTypeRegistry: ReadonlyMap<string, SqlEntityHydrationFactory> = new Map(),
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(namespaceHydrated);
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
- const result: Record<string, unknown> = { id };
182
-
183
- for (const [propertyKey, slotValue] of Object.entries(parsed)) {
184
- if (propertyKey === 'id') continue;
185
- if (slotValue === null || typeof slotValue !== 'object') continue;
186
-
187
- if (propertyKey === 'tables') {
188
- result['tables'] = Object.fromEntries(
189
- Object.entries(slotValue as Record<string, unknown>).map(([tableName, table]) => [
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
- const enumRaw = rawRecord['enum'];
218
- if (enumRaw !== undefined && typeof enumRaw === 'object' && enumRaw !== null) {
219
- for (const entry of Object.values(enumRaw as Record<string, unknown>)) {
220
- if (typeof entry !== 'object' || entry === null) continue;
221
- const kind = (entry as { kind?: unknown }).kind;
222
- if (typeof kind === 'string' && this.entityTypeRegistry.get(kind) === undefined) {
223
- throw new ContractValidationError(
224
- `Entry kind '${kind}' has no registered hydration factory.`,
225
- 'structural',
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
- const tables = (result['tables'] ?? {}) as Record<string, StorageTable>;
232
- const enumSlot = result['enum'] as NonNullable<SqlNamespaceTablesInput['enum']> | undefined;
233
- return {
234
- ...result,
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 factory(entry);
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 {