@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.
Files changed (58) 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-CgIL9Vtx.d.mts +182 -0
  4. package/dist/control-adapter-CgIL9Vtx.d.mts.map +1 -0
  5. package/dist/control-adapter.d.mts +2 -109
  6. package/dist/control.d.mts +132 -4
  7. package/dist/control.d.mts.map +1 -1
  8. package/dist/control.mjs +277 -215
  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-r7BP5n3l.mjs → timestamp-now-generator-CloimujU.mjs} +2 -1
  26. package/dist/{timestamp-now-generator-r7BP5n3l.mjs.map → timestamp-now-generator-CloimujU.mjs.map} +1 -1
  27. package/dist/{types-CeeCStqw.d.mts → types-CbwQCzXY.d.mts} +70 -16
  28. package/dist/types-CbwQCzXY.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-DlAgBiT_.mjs} +756 -319
  34. package/dist/verify-sql-schema-DlAgBiT_.mjs.map +1 -0
  35. package/dist/verify.mjs +1 -1
  36. package/package.json +23 -23
  37. package/src/core/control-adapter.ts +116 -7
  38. package/src/core/control-instance.ts +269 -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 +75 -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 +17 -7
  46. package/src/core/psl-contract-infer/sql-schema-ir-to-psl-ast.ts +8 -6
  47. package/src/core/schema-verify/control-verify-emit.ts +46 -0
  48. package/src/core/schema-verify/verifier-disposition.ts +58 -0
  49. package/src/core/schema-verify/verify-helpers.ts +310 -111
  50. package/src/core/schema-verify/verify-sql-schema.ts +309 -178
  51. package/src/core/timestamp-now-generator.ts +1 -0
  52. package/src/exports/control-adapter.ts +5 -1
  53. package/src/exports/control.ts +7 -0
  54. package/src/exports/runtime.ts +7 -0
  55. package/dist/control-adapter.d.mts.map +0 -1
  56. package/dist/sql-contract-serializer-8axtK4lg.mjs.map +0 -1
  57. package/dist/types-CeeCStqw.d.mts.map +0 -1
  58. 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,20 @@ 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
+ // 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: ControlDriverInstance<'sql', string>;
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: ControlDriverInstance<'sql', string>;
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
- await driver.query(ensureSchemaStatement.sql, ensureSchemaStatement.params);
565
- await driver.query(ensureTableStatement.sql, ensureTableStatement.params);
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 getControlAdapter().readMarker(driver, APP_SPACE_ID);
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
- const write = writeContractMarker({
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 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);
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: ControlDriverInstance<'sql', string>;
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: ControlDriverInstance<'sql', string>;
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: ControlDriverInstance<'sql', string>;
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
  },
@@ -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 {