@prisma-next/family-sql 0.14.0-dev.49 → 0.14.0-dev.50

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 (46) hide show
  1. package/dist/{control-adapter-DHYFuOBy.d.mts → control-adapter-BVOj4gXI.d.mts} +34 -5
  2. package/dist/control-adapter-BVOj4gXI.d.mts.map +1 -0
  3. package/dist/control-adapter.d.mts +1 -1
  4. package/dist/control.d.mts +65 -50
  5. package/dist/control.d.mts.map +1 -1
  6. package/dist/control.mjs +308 -56
  7. package/dist/control.mjs.map +1 -1
  8. package/dist/diff.d.mts +141 -30
  9. package/dist/diff.d.mts.map +1 -1
  10. package/dist/diff.mjs +155 -1384
  11. package/dist/diff.mjs.map +1 -1
  12. package/dist/ir.d.mts +5 -5
  13. package/dist/ir.d.mts.map +1 -1
  14. package/dist/ir.mjs +2 -2
  15. package/dist/ir.mjs.map +1 -1
  16. package/dist/migration.d.mts +1 -1
  17. package/dist/schema-differ-DnoopSXm.d.mts +45 -0
  18. package/dist/schema-differ-DnoopSXm.d.mts.map +1 -0
  19. package/dist/schema-verify-W3r631Jh.mjs +226 -0
  20. package/dist/schema-verify-W3r631Jh.mjs.map +1 -0
  21. package/dist/{sql-contract-serializer-DwUSJ4PO.mjs → sql-contract-serializer-C75cfMSS.mjs} +2 -2
  22. package/dist/{sql-contract-serializer-DwUSJ4PO.mjs.map → sql-contract-serializer-C75cfMSS.mjs.map} +1 -1
  23. package/dist/{types-DBxrwN1d.d.mts → types-BW7pzb2j.d.mts} +24 -15
  24. package/dist/types-BW7pzb2j.d.mts.map +1 -0
  25. package/package.json +21 -21
  26. package/src/core/control-instance.ts +99 -80
  27. package/src/core/control-target-descriptor.ts +29 -14
  28. package/src/core/diff/diff-tree-normalization.ts +211 -0
  29. package/src/core/diff/schema-verify.ts +324 -0
  30. package/src/core/diff/sql-schema-diff.ts +40 -1466
  31. package/src/core/diff/verifier-disposition.ts +14 -42
  32. package/src/core/ir/sql-schema-verifier-base.ts +5 -5
  33. package/src/core/migrations/contract-to-schema-ir.ts +63 -20
  34. package/src/core/migrations/native-type-expander.ts +28 -0
  35. package/src/core/migrations/schema-differ.ts +30 -29
  36. package/src/core/migrations/types.ts +23 -14
  37. package/src/exports/control.ts +6 -0
  38. package/src/exports/diff.ts +28 -8
  39. package/dist/contract-to-schema-ir-S-evq8E6.mjs +0 -264
  40. package/dist/contract-to-schema-ir-S-evq8E6.mjs.map +0 -1
  41. package/dist/control-adapter-DHYFuOBy.d.mts.map +0 -1
  42. package/dist/sql-schema-diff-6z36dZt6.d.mts +0 -105
  43. package/dist/sql-schema-diff-6z36dZt6.d.mts.map +0 -1
  44. package/dist/types-DBxrwN1d.d.mts.map +0 -1
  45. package/src/core/diff/control-verify-emit.ts +0 -46
  46. package/src/core/diff/verify-helpers.ts +0 -832
package/dist/control.mjs CHANGED
@@ -1,18 +1,19 @@
1
1
  import { i as sqlFamilyPslBlockDescriptors, n as sqlFamilyAuthoringFieldPresets, r as sqlFamilyEntityTypes, t as sqlFamilyAuthoringTypes } from "./authoring-type-constructors-CXd-8ydc.mjs";
2
- import { t as SqlContractSerializer } from "./sql-contract-serializer-DwUSJ4PO.mjs";
2
+ import { n as classifyDiffSubjectGranularity, o as verifySqlSchemaByDiff, s as extractCodecControlHooks, t as classifyDiffEntityKind } from "./schema-verify-W3r631Jh.mjs";
3
+ import { t as SqlContractSerializer } from "./sql-contract-serializer-C75cfMSS.mjs";
3
4
  import { t as collectSupportedCodecTypeIds } from "./verify-C-G0obRm.mjs";
4
- import { a as extractCodecControlHooks, i as resolveValueSetValues, n as contractToSchemaIR, r as detectDestructiveChanges, t as contractNamespaceToSchemaIR } from "./contract-to-schema-ir-S-evq8E6.mjs";
5
5
  import { n as temporalAuthoringPresets, r as timestampNowControlDescriptor } from "./timestamp-now-generator-CloimujU.mjs";
6
6
  import { sqlEmission } from "@prisma-next/sql-contract-emitter";
7
7
  import { blindCast } from "@prisma-next/utils/casts";
8
- import { effectiveControlPolicy } from "@prisma-next/contract/types";
9
- import { APP_SPACE_ID, SchemaTreeNode, VERIFY_CODE_HASH_MISMATCH, VERIFY_CODE_MARKER_MISSING, VERIFY_CODE_TARGET_MISMATCH, assembleAuthoringContributions, dispositionForCategory } from "@prisma-next/framework-components/control";
8
+ import { APP_SPACE_ID, SchemaTreeNode, VERIFY_CODE_HASH_MISMATCH, VERIFY_CODE_MARKER_MISSING, VERIFY_CODE_TARGET_MISMATCH, assembleAuthoringContributions } from "@prisma-next/framework-components/control";
10
9
  import { isPlainRecord } from "@prisma-next/framework-components/ir";
11
10
  import { assertDescriptorSelfConsistency } from "@prisma-next/migration-tools/spaces";
12
11
  import { sqlContractCanonicalizationHooks } from "@prisma-next/sql-contract/canonicalization-hooks";
13
12
  import { defaultIndexName } from "@prisma-next/sql-schema-ir/naming";
14
13
  import { ifDefined } from "@prisma-next/utils/defined";
15
- import { StorageTable } from "@prisma-next/sql-contract/types";
14
+ import { effectiveControlPolicy } from "@prisma-next/contract/types";
15
+ import { StorageTable, isStorageTypeInstance } from "@prisma-next/sql-contract/types";
16
+ import { SqlSchemaIR, SqlTableIR } from "@prisma-next/sql-schema-ir/types";
16
17
  import { notOk, ok } from "@prisma-next/utils/result";
17
18
  //#region src/core/operation-preview.ts
18
19
  function isDdlStatement(sqlStatement) {
@@ -210,12 +211,13 @@ function createSqlFamilyInstance(stack) {
210
211
  const getControlAdapter = () => controlAdapter ??= adapter.create(stack);
211
212
  const targetSerializer = target.contractSerializer;
212
213
  const targetInferPslContract = blindCast(target).inferPslContract;
214
+ const diffSchema = blindCast(target).diffSchema;
215
+ const targetGranularityOf = blindCast(target).classifySubjectGranularity;
216
+ const targetEntityKindOf = blindCast(target).classifyEntityKind;
213
217
  const describedContracts = extensions.flatMap((extension) => extension.contractSpace ? [{
214
218
  spaceId: extension.id,
215
219
  contract: extension.contractSpace.contractJson
216
220
  }] : []);
217
- const verifyDatabaseSchema = blindCast(target).verifyDatabaseSchema;
218
- if (!verifyDatabaseSchema) throw new Error(`SQL target "${target.targetId}" is missing the required verifyDatabaseSchema descriptor operation`);
219
221
  const deserializeWithTargetSerializer = (contractOrJson) => {
220
222
  const serializer = targetSerializer ?? new SqlContractSerializer();
221
223
  const json = targetSerializer !== void 0 && !isPlainRecord(contractOrJson) ? targetSerializer.serializeContract(blindCast(contractOrJson)) : contractOrJson;
@@ -322,40 +324,40 @@ function createSqlFamilyInstance(stack) {
322
324
  },
323
325
  verifySchema(options) {
324
326
  const contract = deserializeWithTargetSerializer(options.contract);
325
- const sqlResult = verifyDatabaseSchema({
327
+ if (!diffSchema) throw new Error(`SQL target "${target.targetId}" is missing the required diffSchema descriptor operation`);
328
+ if (!targetGranularityOf) throw new Error(`SQL target "${target.targetId}" is missing the required classifySubjectGranularity descriptor operation`);
329
+ return verifySqlSchemaByDiff({
326
330
  contract,
327
331
  schema: options.schema,
328
332
  strict: options.strict,
329
- typeMetadataRegistry,
330
- frameworkComponents: options.frameworkComponents
333
+ frameworkComponents: options.frameworkComponents,
334
+ diffSchema,
335
+ granularityOf: targetGranularityOf
331
336
  });
332
- const schemaDiffIssues = filterSchemaDiffIssues(sqlResult.schema.schemaDiffIssues, contract.defaultControlPolicy);
333
- const relationalFails = sqlResult.schema.counts.fail;
334
- if (schemaDiffIssues.length === 0) {
335
- if (schemaDiffIssues === sqlResult.schema.schemaDiffIssues) return sqlResult;
336
- return {
337
- ...sqlResult,
338
- schema: {
339
- ...sqlResult.schema,
340
- schemaDiffIssues
341
- }
342
- };
343
- }
344
- const totalFails = relationalFails + schemaDiffIssues.length;
345
- return {
346
- ...sqlResult,
347
- ok: false,
348
- code: sqlResult.code ?? "PN-RUN-3010",
349
- summary: `Database schema does not satisfy contract (${totalFails} failure${totalFails === 1 ? "" : "s"})`,
350
- schema: {
351
- ...sqlResult.schema,
352
- schemaDiffIssues,
353
- counts: {
354
- ...sqlResult.schema.counts,
355
- fail: totalFails
356
- }
357
- }
358
- };
337
+ },
338
+ /**
339
+ * Classifies a diff issue's subject granularity on demand, by resolving
340
+ * its node's `nodeKind` through the target's classifier — the
341
+ * {@link import('@prisma-next/framework-components/control').SchemaSubjectClassifierCapable}
342
+ * capability. Framework consumers spanning contract spaces (the
343
+ * migration aggregate's unclaimed-elements sweep) detect and call this
344
+ * instead of reaching into the concrete schema-IR node, which they
345
+ * cannot read; nothing is stamped on the issue or the node.
346
+ */
347
+ classifySubjectGranularity(issue) {
348
+ if (!targetGranularityOf) throw new Error(`SQL target "${target.targetId}" is missing the required classifySubjectGranularity descriptor operation`);
349
+ return classifyDiffSubjectGranularity(issue, targetGranularityOf);
350
+ },
351
+ /**
352
+ * Classifies a diff issue's subject storage `entityKind` on demand, by
353
+ * resolving its node's `nodeKind` through the target's classifier —
354
+ * the sibling of `classifySubjectGranularity` above, and part of the
355
+ * same {@link import('@prisma-next/framework-components/control').SchemaSubjectClassifierCapable}
356
+ * capability.
357
+ */
358
+ classifyEntityKind(issue) {
359
+ if (!targetEntityKindOf) throw new Error(`SQL target "${target.targetId}" is missing the required classifyEntityKind descriptor operation`);
360
+ return classifyDiffEntityKind(issue, targetEntityKindOf);
359
361
  },
360
362
  async sign(options) {
361
363
  const { driver, contract: contractInput, contractPath, configPath } = options;
@@ -553,24 +555,6 @@ function createSqlFamilyInstance(stack) {
553
555
  }
554
556
  };
555
557
  }
556
- /**
557
- * Filters the structural schema-diff issues (from `diffDatabaseSchema`) through
558
- * the contract's `defaultControlPolicy`. Issues whose outcome maps to a suppressed
559
- * category under the effective policy are removed. This mirrors the control-policy
560
- * filtering applied by `verifySqlSchema` for table/column-level findings.
561
- *
562
- * Outcome → category mapping:
563
- * - `'extra'` → `'extraAuxiliary'` (an extra auxiliary entity, e.g. an RLS policy)
564
- * - `'missing'` → `'declaredMissing'`
565
- * - `'mismatch'` → `'declaredIncompatible'`
566
- */
567
- function filterSchemaDiffIssues(issues, defaultControlPolicy) {
568
- if (issues.length === 0) return issues;
569
- const policy = effectiveControlPolicy(void 0, defaultControlPolicy);
570
- return issues.filter((issue) => {
571
- return dispositionForCategory(policy, issue.outcome === "extra" ? "extraAuxiliary" : issue.outcome === "missing" ? "declaredMissing" : "declaredIncompatible") !== "suppress";
572
- });
573
- }
574
558
  //#endregion
575
559
  //#region src/core/control-descriptor.ts
576
560
  var SqlFamilyDescriptor = class {
@@ -590,6 +574,255 @@ var SqlFamilyDescriptor = class {
590
574
  }
591
575
  };
592
576
  //#endregion
577
+ //#region src/core/migrations/contract-to-schema-ir.ts
578
+ function convertColumn(name, column, storageTypes, expandNativeType, renderDefault) {
579
+ const resolved = resolveColumnTypeMetadata(column, storageTypes);
580
+ const baseNativeType = expandNativeType ? expandNativeType({
581
+ nativeType: resolved.nativeType,
582
+ codecId: resolved.codecId,
583
+ ...ifDefined("typeParams", resolved.typeParams)
584
+ }) : resolved.nativeType;
585
+ const nativeType = column.many ? `${baseNativeType}[]` : baseNativeType;
586
+ return {
587
+ name,
588
+ nativeType,
589
+ nullable: column.nullable,
590
+ ...ifDefined("default", column.default != null && renderDefault ? renderDefault(column.default, column) : void 0),
591
+ resolvedNativeType: nativeType,
592
+ ...ifDefined("resolvedDefault", column.default ?? void 0),
593
+ codecRef: buildColumnCodecRef(resolved, column.many),
594
+ codecBaseNativeType: resolved.nativeType,
595
+ ...column.typeRef !== void 0 ? { codecNamedType: true } : {}
596
+ };
597
+ }
598
+ /**
599
+ * Builds the column's `CodecRef` from its resolved (post-`typeRef`) codec
600
+ * identity — the same construction the query AST and the migration DDL
601
+ * renderer already use (TML-2456, TML-2918).
602
+ */
603
+ function buildColumnCodecRef(resolved, many) {
604
+ return {
605
+ codecId: resolved.codecId,
606
+ ...ifDefined("typeParams", resolved.typeParams !== void 0 ? blindCast(resolved.typeParams) : void 0),
607
+ ...ifDefined("many", many)
608
+ };
609
+ }
610
+ function resolveColumnTypeMetadata(column, storageTypes) {
611
+ if (!column.typeRef) return column;
612
+ const referenced = storageTypes[column.typeRef];
613
+ if (!referenced) throw new Error(`Column references storage type "${column.typeRef}" but it is not defined in storage.types.`);
614
+ if (isStorageTypeInstance(referenced)) return {
615
+ codecId: referenced.codecId,
616
+ nativeType: referenced.nativeType,
617
+ typeParams: referenced.typeParams
618
+ };
619
+ throw new Error(`Storage type "${column.typeRef}" has an unknown polymorphic kind; expected a codec-typed StorageTypeInstance.`);
620
+ }
621
+ /**
622
+ * Resolves a `ValueSetRef` to its permitted values from the contract storage.
623
+ *
624
+ * Throws when the referenced namespace or value-set is absent — this indicates
625
+ * the contract was built incorrectly (the check and the value-set must be
626
+ * co-emitted by the lowering step). Used by `convertCheck` (schema-IR
627
+ * projection), `verifyCheckConstraints` (verification), and
628
+ * `checkConstraintPlanCallStrategy` (migration planning) so all three agree on
629
+ * the resolved values and the error behavior on a missing reference.
630
+ */
631
+ function allStrings(values) {
632
+ return values.every((value) => typeof value === "string");
633
+ }
634
+ function resolveValueSetValues(ref, storage, contextLabel) {
635
+ const ns = storage.namespaces[ref.namespaceId];
636
+ if (!ns) throw new Error(`resolveValueSetValues: namespace "${ref.namespaceId}" not found in storage (${contextLabel})`);
637
+ const valueSet = ns.entries.valueSet?.[ref.entityName];
638
+ if (!valueSet) throw new Error(`resolveValueSetValues: value-set "${ref.entityName}" not found in namespace "${ref.namespaceId}" (${contextLabel})`);
639
+ const values = valueSet.values;
640
+ if (!allStrings(values)) throw new Error(`resolveValueSetValues: value-set "${ref.entityName}" in namespace "${ref.namespaceId}" has a non-string value; numeric-enum CHECK constraints are not yet supported (${contextLabel})`);
641
+ return values;
642
+ }
643
+ /**
644
+ * Projects a `CheckConstraint` IR into an `SqlCheckConstraintIRInput` by
645
+ * resolving the permitted values from the storage value-set it references.
646
+ *
647
+ * The `CheckConstraint.valueSet` ref points to
648
+ * `storage.namespaces[namespaceId].entries.valueSet[name]`. The resolved
649
+ * values are lifted directly from `StorageValueSet.values` so verification
650
+ * compares value sets, not SQL predicate strings.
651
+ *
652
+ * Throws if the referenced namespace or value-set is absent — this
653
+ * indicates the contract was built incorrectly (the check and the
654
+ * value-set must be co-emitted by the lowering step).
655
+ */
656
+ function convertCheck(check, storage) {
657
+ const permittedValues = resolveValueSetValues(check.valueSet, storage, `check "${check.name}"`);
658
+ return {
659
+ name: check.name,
660
+ column: check.column,
661
+ permittedValues
662
+ };
663
+ }
664
+ function convertUnique(unique) {
665
+ return {
666
+ columns: unique.columns,
667
+ ...ifDefined("name", unique.name)
668
+ };
669
+ }
670
+ function convertIndex(index) {
671
+ return {
672
+ columns: index.columns,
673
+ unique: false,
674
+ ...ifDefined("name", index.name),
675
+ ...ifDefined("type", index.type),
676
+ ...ifDefined("options", index.options)
677
+ };
678
+ }
679
+ function convertForeignKey(fk) {
680
+ return {
681
+ columns: fk.source.columns,
682
+ referencedTable: fk.target.tableName,
683
+ referencedSchema: fk.target.namespaceId,
684
+ referencedColumns: fk.target.columns,
685
+ ...ifDefined("name", fk.name),
686
+ ...ifDefined("onDelete", fk.onDelete),
687
+ ...ifDefined("onUpdate", fk.onUpdate)
688
+ };
689
+ }
690
+ function convertTable(name, table, storageTypes, expandNativeType, renderDefault, storage) {
691
+ const columns = {};
692
+ for (const [colName, colDef] of Object.entries(table.columns)) columns[colName] = convertColumn(colName, colDef, storageTypes, expandNativeType, renderDefault);
693
+ const satisfiedIndexColumns = new Set([
694
+ ...table.indexes.map((idx) => idx.columns.join(",")),
695
+ ...table.uniques.map((unique) => unique.columns.join(",")),
696
+ ...table.primaryKey ? [table.primaryKey.columns.join(",")] : []
697
+ ]);
698
+ const fkBackingIndexes = [];
699
+ for (const fk of table.foreignKeys) {
700
+ if (fk.index === false) continue;
701
+ const key = fk.source.columns.join(",");
702
+ if (satisfiedIndexColumns.has(key)) continue;
703
+ fkBackingIndexes.push({
704
+ columns: fk.source.columns,
705
+ unique: false,
706
+ name: defaultIndexName(name, fk.source.columns)
707
+ });
708
+ satisfiedIndexColumns.add(key);
709
+ }
710
+ const checks = table.checks && table.checks.length > 0 ? table.checks.map((c) => convertCheck(c, storage)) : void 0;
711
+ return new SqlTableIR({
712
+ name,
713
+ columns,
714
+ ...ifDefined("primaryKey", table.primaryKey),
715
+ foreignKeys: table.foreignKeys.filter((fk) => fk.constraint !== false).map(convertForeignKey),
716
+ uniques: table.uniques.map(convertUnique),
717
+ indexes: [...table.indexes.map(convertIndex), ...fkBackingIndexes],
718
+ ...ifDefined("checks", checks)
719
+ });
720
+ }
721
+ /**
722
+ * Detects destructive changes between two contract storages.
723
+ *
724
+ * The additive-only planner silently ignores removals (tables, columns).
725
+ * This function detects those removals so callers can report them as conflicts
726
+ * rather than silently producing an empty plan.
727
+ *
728
+ * Returns an empty array if no destructive changes are found.
729
+ */
730
+ function detectDestructiveChanges(from, to) {
731
+ if (!from) return [];
732
+ const hasOwn = (value, key) => Object.hasOwn(value, key);
733
+ const conflicts = [];
734
+ const namespaceIds = [...new Set([...Object.keys(from.namespaces), ...Object.keys(to.namespaces)])].sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
735
+ for (const namespaceId of namespaceIds) {
736
+ const fromNs = from.namespaces[namespaceId];
737
+ const toNs = to.namespaces[namespaceId];
738
+ const fromTables = fromNs?.entries.table;
739
+ if (!fromTables) continue;
740
+ for (const tableName of Object.keys(fromTables)) {
741
+ const toTableRaw = toNs?.entries.table?.[tableName];
742
+ if (!StorageTable.is(toTableRaw)) {
743
+ conflicts.push({
744
+ kind: "tableRemoved",
745
+ summary: `Table "${tableName}" was removed`
746
+ });
747
+ continue;
748
+ }
749
+ const toTable = toTableRaw;
750
+ const fromTableRaw = fromTables[tableName];
751
+ if (!StorageTable.is(fromTableRaw)) continue;
752
+ const fromTable = fromTableRaw;
753
+ for (const columnName of Object.keys(fromTable.columns)) if (!hasOwn(toTable.columns, columnName)) conflicts.push({
754
+ kind: "columnRemoved",
755
+ summary: `Column "${tableName}"."${columnName}" was removed`
756
+ });
757
+ }
758
+ }
759
+ return conflicts;
760
+ }
761
+ /**
762
+ * Converts a `Contract` to `SqlSchemaIR`.
763
+ *
764
+ * Reads `contract.storage` for tables and `contract.storage.types` for type
765
+ * annotations. Storage-type annotations are written under
766
+ * `options.annotationNamespace`.
767
+ *
768
+ * Drops codec metadata (`codecId`, `typeRef`) since the schema IR only represents
769
+ * structural information. When `expandNativeType` is provided, parameterized types
770
+ * are expanded (e.g. `character` + `{ length: 36 }` → `character(36)`) so the
771
+ * resulting IR compares correctly against the "to" contract during planning.
772
+ *
773
+ * Returns an empty schema IR when `contract` is `null` (new project).
774
+ */
775
+ /**
776
+ * Converts the tables of a single namespace into a `SqlSchemaIR`, keyed by
777
+ * table name within that namespace. Unlike {@link contractToSchemaIR}, which
778
+ * flattens every namespace's tables into one bare-keyed record (and throws on a
779
+ * cross-namespace name collision), this scopes the table iteration to one
780
+ * namespace so the same table name can exist in two schemas.
781
+ *
782
+ * The full `storage` is still passed to `convertTable`, so value-set / enum /
783
+ * type resolution that legitimately spans namespaces is unaffected. Foreign
784
+ * keys are built purely from the FK descriptor (`fk.target`), so cross-namespace
785
+ * FKs survive per-namespace conversion. The `annotations` block (storage-type
786
+ * derived) is omitted here — the per-namespace tree consumer reads only the
787
+ * per-table fields.
788
+ */
789
+ function contractNamespaceToSchemaIR(storage, namespaceId, options) {
790
+ if (options.annotationNamespace.length === 0) throw new Error("annotationNamespace must be a non-empty string");
791
+ const namespace = storage.namespaces[namespaceId];
792
+ if (!namespace) return new SqlSchemaIR({ tables: {} });
793
+ const storageTypes = { ...storage.types ?? {} };
794
+ const tables = {};
795
+ for (const [tableName, tableDefRaw] of Object.entries(namespace.entries.table ?? {})) {
796
+ StorageTable.assert(tableDefRaw, `namespaces.${namespaceId}.entries.table.${tableName}`);
797
+ tables[tableName] = convertTable(tableName, tableDefRaw, storageTypes, options.expandNativeType, options.renderDefault, storage);
798
+ }
799
+ return new SqlSchemaIR({ tables });
800
+ }
801
+ function contractToSchemaIR(contract, options) {
802
+ if (options.annotationNamespace.length === 0) throw new Error("annotationNamespace must be a non-empty string");
803
+ if (!contract) return new SqlSchemaIR({ tables: {} });
804
+ const storage = contract.storage;
805
+ const storageTypes = { ...storage.types ?? {} };
806
+ const tables = {};
807
+ for (const ns of Object.values(storage.namespaces)) for (const [tableName, tableDefRaw] of Object.entries(ns.entries.table ?? {})) {
808
+ StorageTable.assert(tableDefRaw, `namespaces.${ns.id}.entries.table.${tableName}`);
809
+ const tableDef = tableDefRaw;
810
+ if (tables[tableName] !== void 0) throw new Error(`contractToSchemaIR: duplicate SQL table name "${tableName}" across namespaces (ambiguous for flat SqlSchemaIR.tables).`);
811
+ tables[tableName] = convertTable(tableName, tableDef, storageTypes, options.expandNativeType, options.renderDefault, storage);
812
+ }
813
+ return new SqlSchemaIR({
814
+ tables,
815
+ ...ifDefined("annotations", deriveAnnotations(storage, options.annotationNamespace, options.resolveEnumNamespaceSchema))
816
+ });
817
+ }
818
+ function deriveAnnotations(storage, annotationNamespace, _resolveEnumNamespaceSchema) {
819
+ const storageTypes = {};
820
+ for (const typeInstance of Object.values(storage.types ?? {})) if (isStorageTypeInstance(typeInstance)) storageTypes[typeInstance.nativeType] = typeInstance;
821
+ const envelope = { ...Object.keys(storageTypes).length > 0 ? { storageTypes } : {} };
822
+ if (Object.keys(envelope).length === 0) return void 0;
823
+ return { [annotationNamespace]: envelope };
824
+ }
825
+ //#endregion
593
826
  //#region src/core/migrations/control-policy.ts
594
827
  /**
595
828
  * The control policy that governs a single call. The `external` default is an
@@ -884,6 +1117,25 @@ function unionSorted(a, b) {
884
1117
  return [...set].sort((x, y) => x < y ? -1 : x > y ? 1 : 0);
885
1118
  }
886
1119
  //#endregion
1120
+ //#region src/core/migrations/native-type-expander.ts
1121
+ /**
1122
+ * Builds the codec-hook-composed `expandNativeType` callback the contract→IR
1123
+ * derivation uses to expand parameterized native types (e.g. `character` +
1124
+ * `{ length: 36 }` → `character(36)`). Returns `undefined` when no framework
1125
+ * components are supplied, so callers can omit the option entirely.
1126
+ */
1127
+ function buildNativeTypeExpander(frameworkComponents) {
1128
+ if (!frameworkComponents) return;
1129
+ const codecHooks = extractCodecControlHooks(frameworkComponents);
1130
+ return (input) => {
1131
+ if (!input.typeParams) return input.nativeType;
1132
+ if (!input.codecId) return input.nativeType;
1133
+ const hooks = codecHooks.get(input.codecId);
1134
+ if (!hooks?.expandNativeType) return input.nativeType;
1135
+ return hooks.expandNativeType(input);
1136
+ };
1137
+ }
1138
+ //#endregion
887
1139
  //#region src/core/migrations/plan-helpers.ts
888
1140
  const readOnlyEmptyObject = Object.freeze({});
889
1141
  function cloneRecord(value) {
@@ -995,6 +1247,6 @@ const INIT_ADDITIVE_POLICY = Object.freeze({ allowedOperationClasses: Object.fre
995
1247
  //#region src/exports/control.ts
996
1248
  var control_default = new SqlFamilyDescriptor();
997
1249
  //#endregion
998
- export { INIT_ADDITIVE_POLICY, assembleAuthoringContributions, contractNamespaceToSchemaIR, contractToSchemaIR, controlPolicyForCall, createMigrationPlan, control_default as default, detectDestructiveChanges, extractCodecControlHooks, partitionCallsByControlPolicy, partitionIssuesByControlPolicy, planFieldEventOperations, plannerFailure, plannerSuccess, resolveValueSetValues, runnerFailure, runnerSuccess, temporalAuthoringPresets, timestampNowControlDescriptor };
1250
+ export { INIT_ADDITIVE_POLICY, assembleAuthoringContributions, buildNativeTypeExpander, contractNamespaceToSchemaIR, contractToSchemaIR, controlPolicyForCall, createMigrationPlan, control_default as default, detectDestructiveChanges, extractCodecControlHooks, partitionCallsByControlPolicy, partitionIssuesByControlPolicy, planFieldEventOperations, plannerFailure, plannerSuccess, resolveValueSetValues, runnerFailure, runnerSuccess, temporalAuthoringPresets, timestampNowControlDescriptor };
999
1251
 
1000
1252
  //# sourceMappingURL=control.mjs.map