@prisma-next/target-postgres 0.14.0-dev.6 → 0.14.0-dev.61

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 (253) hide show
  1. package/dist/authoring-BLkL4fFG.mjs +528 -0
  2. package/dist/authoring-BLkL4fFG.mjs.map +1 -0
  3. package/dist/canonicalize-CAbXtVtB.d.mts +37 -0
  4. package/dist/canonicalize-CAbXtVtB.d.mts.map +1 -0
  5. package/dist/{codec-ids-CnXu9Qy3.d.mts → codec-ids-BzrFF-I4.d.mts} +3 -2
  6. package/dist/codec-ids-BzrFF-I4.d.mts.map +1 -0
  7. package/dist/{codec-ids-BvytN2P8.mjs → codec-ids-C_-Hj6bL.mjs} +3 -2
  8. package/dist/codec-ids-C_-Hj6bL.mjs.map +1 -0
  9. package/dist/codec-ids.d.mts +2 -2
  10. package/dist/codec-ids.mjs +2 -2
  11. package/dist/{codec-types-DHCkwPKE.d.mts → codec-types-BiIWziRO.d.mts} +3 -2
  12. package/dist/codec-types-BiIWziRO.d.mts.map +1 -0
  13. package/dist/codec-types.d.mts +1 -1
  14. package/dist/codecs-CkVQrZ4G.mjs +913 -0
  15. package/dist/codecs-CkVQrZ4G.mjs.map +1 -0
  16. package/dist/{codecs--0A5_4Bq.d.mts → codecs-Cm-nS4mQ.d.mts} +111 -4
  17. package/dist/codecs-Cm-nS4mQ.d.mts.map +1 -0
  18. package/dist/codecs.d.mts +2 -2
  19. package/dist/codecs.mjs +3 -757
  20. package/dist/codecs.mjs.map +1 -1
  21. package/dist/contract-free.d.mts +33 -3
  22. package/dist/contract-free.d.mts.map +1 -1
  23. package/dist/contract-free.mjs +4 -4
  24. package/dist/control-policy-BpmChlAK.mjs +1151 -0
  25. package/dist/control-policy-BpmChlAK.mjs.map +1 -0
  26. package/dist/control.d.mts +2 -2
  27. package/dist/control.d.mts.map +1 -1
  28. package/dist/control.mjs +901 -35
  29. package/dist/control.mjs.map +1 -1
  30. package/dist/data-transform-BOWpliq8.mjs.map +1 -1
  31. package/dist/{data-transform-DDgWdB5o.d.mts → data-transform-DPzwBMhW.d.mts} +3 -3
  32. package/dist/{data-transform-DDgWdB5o.d.mts.map → data-transform-DPzwBMhW.d.mts.map} +1 -1
  33. package/dist/data-transform.d.mts +1 -1
  34. package/dist/{ddl-QDyOSeLc.mjs → ddl-BovUVFVi.mjs} +77 -6
  35. package/dist/ddl-BovUVFVi.mjs.map +1 -0
  36. package/dist/ddl.d.mts +3 -2
  37. package/dist/ddl.mjs +2 -2
  38. package/dist/default-normalizer.d.mts +1 -1
  39. package/dist/default-normalizer.d.mts.map +1 -1
  40. package/dist/default-normalizer.mjs +247 -1
  41. package/dist/default-normalizer.mjs.map +1 -0
  42. package/dist/descriptor-meta-C1T4BXJ8.mjs +22 -0
  43. package/dist/descriptor-meta-C1T4BXJ8.mjs.map +1 -0
  44. package/dist/descriptor-meta-runtime-UwYdV8fD.mjs +12 -0
  45. package/dist/descriptor-meta-runtime-UwYdV8fD.mjs.map +1 -0
  46. package/dist/diff-database-schema-RMwnSOnz.mjs +298 -0
  47. package/dist/diff-database-schema-RMwnSOnz.mjs.map +1 -0
  48. package/dist/diff-database-schema.d.mts +46 -0
  49. package/dist/diff-database-schema.d.mts.map +1 -0
  50. package/dist/diff-database-schema.mjs +2 -0
  51. package/dist/errors-DO-w0EUS.mjs +35 -0
  52. package/dist/errors-DO-w0EUS.mjs.map +1 -0
  53. package/dist/errors.d.mts +11 -9
  54. package/dist/errors.d.mts.map +1 -1
  55. package/dist/errors.mjs +1 -1
  56. package/dist/issue-planner.d.mts +35 -11
  57. package/dist/issue-planner.d.mts.map +1 -1
  58. package/dist/issue-planner.mjs +2 -2
  59. package/dist/migration.d.mts +5 -5
  60. package/dist/migration.d.mts.map +1 -1
  61. package/dist/migration.mjs +4 -4
  62. package/dist/namespace-ids-xtp_ZY86.mjs +7 -0
  63. package/dist/namespace-ids-xtp_ZY86.mjs.map +1 -0
  64. package/dist/native-type-normalizer.mjs +37 -1
  65. package/dist/native-type-normalizer.mjs.map +1 -0
  66. package/dist/nodes-B815pj_X.mjs +162 -0
  67. package/dist/nodes-B815pj_X.mjs.map +1 -0
  68. package/dist/nodes-BtQyZha6.d.mts +146 -0
  69. package/dist/nodes-BtQyZha6.d.mts.map +1 -0
  70. package/dist/{op-factory-call-D_p5vxwt.mjs → op-factory-call-CI00nS4z.mjs} +300 -31
  71. package/dist/op-factory-call-CI00nS4z.mjs.map +1 -0
  72. package/dist/{op-factory-call-DmQEc3XV.d.mts → op-factory-call-D6_7-7Vc.d.mts} +68 -15
  73. package/dist/op-factory-call-D6_7-7Vc.d.mts.map +1 -0
  74. package/dist/op-factory-call.d.mts +2 -2
  75. package/dist/op-factory-call.mjs +2 -2
  76. package/dist/pack.d.mts +287 -3
  77. package/dist/pack.d.mts.map +1 -1
  78. package/dist/pack.mjs +1 -1
  79. package/dist/planner-CAHZN1Ez.mjs +422 -0
  80. package/dist/planner-CAHZN1Ez.mjs.map +1 -0
  81. package/dist/{planner-ddl-builders-B2wOwLqI.mjs → planner-ddl-builders-B8Nn6nHN.mjs} +12 -27
  82. package/dist/planner-ddl-builders-B8Nn6nHN.mjs.map +1 -0
  83. package/dist/planner-ddl-builders.d.mts +4 -6
  84. package/dist/planner-ddl-builders.d.mts.map +1 -1
  85. package/dist/planner-ddl-builders.mjs +2 -2
  86. package/dist/planner-identity-values-CJPha2Sz.mjs.map +1 -1
  87. package/dist/planner-identity-values.d.mts +8 -2
  88. package/dist/planner-identity-values.d.mts.map +1 -1
  89. package/dist/planner-identity-values.mjs +2 -2
  90. package/dist/{planner-produced-postgres-migration-QqHa2C2l.d.mts → planner-produced-postgres-migration-a-tjfKsA.d.mts} +4 -4
  91. package/dist/{planner-produced-postgres-migration-QqHa2C2l.d.mts.map → planner-produced-postgres-migration-a-tjfKsA.d.mts.map} +1 -1
  92. package/dist/{planner-produced-postgres-migration-Cji5vxUf.mjs → planner-produced-postgres-migration-flkG4Y7n.mjs} +3 -3
  93. package/dist/{planner-produced-postgres-migration-Cji5vxUf.mjs.map → planner-produced-postgres-migration-flkG4Y7n.mjs.map} +1 -1
  94. package/dist/planner-produced-postgres-migration.d.mts +1 -1
  95. package/dist/planner-produced-postgres-migration.mjs +1 -1
  96. package/dist/{planner-sql-checks-jqUUGyQR.mjs → planner-sql-checks-C1-Kk3Eu.mjs} +2 -2
  97. package/dist/{planner-sql-checks-jqUUGyQR.mjs.map → planner-sql-checks-C1-Kk3Eu.mjs.map} +1 -1
  98. package/dist/planner-sql-checks.d.mts +2 -2
  99. package/dist/planner-sql-checks.d.mts.map +1 -1
  100. package/dist/planner-sql-checks.mjs +1 -1
  101. package/dist/{planner-target-details-CIY6tLeo.d.mts → planner-target-details-HkP4RrRO.d.mts} +2 -2
  102. package/dist/planner-target-details-HkP4RrRO.d.mts.map +1 -0
  103. package/dist/planner-target-details.d.mts +1 -1
  104. package/dist/planner.d.mts +72 -17
  105. package/dist/planner.d.mts.map +1 -1
  106. package/dist/planner.mjs +3 -2
  107. package/dist/postgres-contract-view-C9j5cqP0.d.mts +34 -0
  108. package/dist/postgres-contract-view-C9j5cqP0.d.mts.map +1 -0
  109. package/dist/{postgres-contract-serializer-k3TAcPMY.mjs → postgres-contract-view-DWLNQbdR.mjs} +45 -41
  110. package/dist/postgres-contract-view-DWLNQbdR.mjs.map +1 -0
  111. package/dist/postgres-database-schema-node-BVdOffjC.d.mts +221 -0
  112. package/dist/postgres-database-schema-node-BVdOffjC.d.mts.map +1 -0
  113. package/dist/{postgres-migration-Y4YBJqkS.d.mts → postgres-migration-Bzzi0Asr.d.mts} +28 -9
  114. package/dist/postgres-migration-Bzzi0Asr.d.mts.map +1 -0
  115. package/dist/{postgres-migration-B5jKrXv3.mjs → postgres-migration-C92swCzd.mjs} +53 -45
  116. package/dist/postgres-migration-C92swCzd.mjs.map +1 -0
  117. package/dist/postgres-native-enum-xdiqmBf_.mjs +41 -0
  118. package/dist/postgres-native-enum-xdiqmBf_.mjs.map +1 -0
  119. package/dist/postgres-rls-policy--CSddvMR.d.mts +52 -0
  120. package/dist/postgres-rls-policy--CSddvMR.d.mts.map +1 -0
  121. package/dist/postgres-role-schema-node-BmCboEa_.mjs +198 -0
  122. package/dist/postgres-role-schema-node-BmCboEa_.mjs.map +1 -0
  123. package/dist/{postgres-schema-COGZ1ark.mjs → postgres-schema-COEca21H.mjs} +188 -10
  124. package/dist/postgres-schema-COEca21H.mjs.map +1 -0
  125. package/dist/postgres-schema-DsrtYp2v.d.mts +269 -0
  126. package/dist/postgres-schema-DsrtYp2v.d.mts.map +1 -0
  127. package/dist/postgres-table-schema-node-9FCtCUz2.mjs +153 -0
  128. package/dist/postgres-table-schema-node-9FCtCUz2.mjs.map +1 -0
  129. package/dist/render-ops.d.mts +1 -1
  130. package/dist/{render-typescript-KMgosran.mjs → render-typescript-7yqILcwr.mjs} +45 -16
  131. package/dist/render-typescript-7yqILcwr.mjs.map +1 -0
  132. package/dist/render-typescript.mjs +1 -1
  133. package/dist/resolve-ddl-schema-ZkKxt2Fm.mjs +22 -0
  134. package/dist/resolve-ddl-schema-ZkKxt2Fm.mjs.map +1 -0
  135. package/dist/rls-canonicalize.d.mts +20 -0
  136. package/dist/rls-canonicalize.d.mts.map +1 -0
  137. package/dist/rls-canonicalize.mjs +2 -0
  138. package/dist/runtime.d.mts +6 -8
  139. package/dist/runtime.d.mts.map +1 -1
  140. package/dist/runtime.mjs +3 -3
  141. package/dist/runtime.mjs.map +1 -1
  142. package/dist/schema-ir-annotations.d.mts +15 -0
  143. package/dist/schema-ir-annotations.d.mts.map +1 -0
  144. package/dist/schema-ir-annotations.mjs +2 -0
  145. package/dist/types.d.mts +4 -140
  146. package/dist/types.mjs +5 -2
  147. package/dist/wire-name-rHHtr1h0.mjs +75 -0
  148. package/dist/wire-name-rHHtr1h0.mjs.map +1 -0
  149. package/package.json +23 -17
  150. package/src/contract-free/checks.ts +73 -0
  151. package/src/contract-free/ddl.ts +71 -1
  152. package/src/core/authoring.ts +467 -2
  153. package/src/core/codec-ids.ts +1 -0
  154. package/src/core/codec-type-map.ts +2 -0
  155. package/src/core/codecs.ts +180 -1
  156. package/src/core/ddl/nodes.ts +141 -2
  157. package/src/core/default-normalizer.ts +122 -2
  158. package/src/core/descriptor-meta.ts +10 -1
  159. package/src/core/entity-kinds.ts +41 -0
  160. package/src/core/errors.ts +14 -12
  161. package/src/core/migrations/bound-schema.ts +10 -0
  162. package/src/core/migrations/column-ddl-rendering.ts +117 -0
  163. package/src/core/migrations/contract-to-postgres-database-schema-node.ts +160 -0
  164. package/src/core/migrations/control-policy.ts +126 -44
  165. package/src/core/migrations/diff-database-schema.ts +253 -0
  166. package/src/core/migrations/issue-planner.ts +692 -652
  167. package/src/core/migrations/op-factory-call.ts +189 -21
  168. package/src/core/migrations/operations/columns.ts +24 -12
  169. package/src/core/migrations/operations/constraints.ts +1 -1
  170. package/src/core/migrations/operations/data-transform.ts +6 -0
  171. package/src/core/migrations/operations/dependencies.ts +2 -1
  172. package/src/core/migrations/operations/rls.ts +187 -0
  173. package/src/core/migrations/operations/shared.ts +0 -30
  174. package/src/core/migrations/planner-ddl-builders.ts +20 -58
  175. package/src/core/migrations/planner-identity-values.ts +1 -1
  176. package/src/core/migrations/planner-recipes.ts +31 -13
  177. package/src/core/migrations/planner-sql-checks.ts +1 -1
  178. package/src/core/migrations/planner-strategies.ts +248 -216
  179. package/src/core/migrations/planner-target-details.ts +3 -1
  180. package/src/core/migrations/planner.ts +386 -59
  181. package/src/core/migrations/postgres-migration.ts +70 -86
  182. package/src/core/migrations/render-typescript.ts +42 -15
  183. package/src/core/migrations/resolve-ddl-schema.ts +26 -0
  184. package/src/core/migrations/runner.ts +10 -16
  185. package/src/core/migrations/verify-postgres-namespaces.ts +47 -31
  186. package/src/core/namespace-ids.ts +3 -0
  187. package/src/core/postgres-contract-serializer.ts +46 -71
  188. package/src/core/postgres-contract-view.ts +48 -0
  189. package/src/core/postgres-native-enum.ts +51 -0
  190. package/src/core/postgres-rls-enablement.ts +34 -0
  191. package/src/core/postgres-rls-policy.ts +64 -0
  192. package/src/core/postgres-role.ts +59 -0
  193. package/src/core/postgres-schema-verifier.ts +10 -7
  194. package/src/core/postgres-schema.ts +64 -15
  195. package/src/core/postgres-validators.ts +34 -0
  196. package/src/core/psl-infer/infer-psl-contract.ts +1326 -0
  197. package/src/core/psl-infer/postgres-default-mapping.ts +16 -0
  198. package/src/core/psl-infer/postgres-type-map.ts +128 -0
  199. package/src/core/rls/canonicalize.ts +48 -0
  200. package/src/core/rls/wire-name.ts +33 -0
  201. package/src/core/schema-ir/postgres-database-schema-node.ts +61 -0
  202. package/src/core/schema-ir/postgres-namespace-schema-node.ts +79 -0
  203. package/src/core/schema-ir/postgres-policy-schema-node.ts +90 -0
  204. package/src/core/schema-ir/postgres-role-schema-node.ts +65 -0
  205. package/src/core/schema-ir/postgres-table-schema-node.ts +140 -0
  206. package/src/core/schema-ir/schema-node-kinds.ts +113 -0
  207. package/src/exports/codecs.ts +3 -0
  208. package/src/exports/contract-free.ts +11 -1
  209. package/src/exports/control.ts +27 -33
  210. package/src/exports/ddl.ts +6 -0
  211. package/src/exports/diff-database-schema.ts +7 -0
  212. package/src/exports/issue-planner.ts +1 -1
  213. package/src/exports/migration.ts +1 -0
  214. package/src/exports/op-factory-call.ts +1 -0
  215. package/src/exports/planner-ddl-builders.ts +0 -2
  216. package/src/exports/planner-identity-values.ts +4 -1
  217. package/src/exports/planner.ts +1 -0
  218. package/src/exports/rls-canonicalize.ts +7 -0
  219. package/src/exports/runtime.ts +1 -0
  220. package/src/exports/schema-ir-annotations.ts +1 -0
  221. package/src/exports/types.ts +34 -0
  222. package/dist/codec-ids-BvytN2P8.mjs.map +0 -1
  223. package/dist/codec-ids-CnXu9Qy3.d.mts.map +0 -1
  224. package/dist/codec-types-DHCkwPKE.d.mts.map +0 -1
  225. package/dist/codecs--0A5_4Bq.d.mts.map +0 -1
  226. package/dist/ddl-QDyOSeLc.mjs.map +0 -1
  227. package/dist/default-normalizer-DyyCHQWs.mjs +0 -131
  228. package/dist/default-normalizer-DyyCHQWs.mjs.map +0 -1
  229. package/dist/descriptor-meta-CpGygXpI.mjs +0 -140
  230. package/dist/descriptor-meta-CpGygXpI.mjs.map +0 -1
  231. package/dist/errors-CUk87ByX.mjs +0 -33
  232. package/dist/errors-CUk87ByX.mjs.map +0 -1
  233. package/dist/issue-planner-DL6g3CmE.mjs +0 -824
  234. package/dist/issue-planner-DL6g3CmE.mjs.map +0 -1
  235. package/dist/native-type-normalizer-Bc9XJzWC.mjs +0 -38
  236. package/dist/native-type-normalizer-Bc9XJzWC.mjs.map +0 -1
  237. package/dist/nodes-Bbhs2rwj.mjs +0 -76
  238. package/dist/nodes-Bbhs2rwj.mjs.map +0 -1
  239. package/dist/nodes-pLeLgdis.d.mts +0 -67
  240. package/dist/nodes-pLeLgdis.d.mts.map +0 -1
  241. package/dist/op-factory-call-D_p5vxwt.mjs.map +0 -1
  242. package/dist/op-factory-call-DmQEc3XV.d.mts.map +0 -1
  243. package/dist/planner-Bs_baQax.mjs +0 -324
  244. package/dist/planner-Bs_baQax.mjs.map +0 -1
  245. package/dist/planner-ddl-builders-B2wOwLqI.mjs.map +0 -1
  246. package/dist/planner-target-details-CIY6tLeo.d.mts.map +0 -1
  247. package/dist/postgres-contract-serializer-k3TAcPMY.mjs.map +0 -1
  248. package/dist/postgres-migration-B5jKrXv3.mjs.map +0 -1
  249. package/dist/postgres-migration-Y4YBJqkS.d.mts.map +0 -1
  250. package/dist/postgres-schema-COGZ1ark.mjs.map +0 -1
  251. package/dist/render-typescript-KMgosran.mjs.map +0 -1
  252. package/dist/types.d.mts.map +0 -1
  253. package/src/core/postgres-enum-type-schema.ts +0 -17
@@ -0,0 +1,1326 @@
1
+ import type { ColumnDefault } from '@prisma-next/contract/types';
2
+ import type { SqlDescribedContractSpace } from '@prisma-next/family-sql/control';
3
+ import type {
4
+ DefaultMappingOptions,
5
+ EnumInfo,
6
+ PslNativeTypeAttribute,
7
+ PslPrinterOptions,
8
+ PslTypeMap,
9
+ RelationField,
10
+ } from '@prisma-next/family-sql/psl-infer';
11
+ import {
12
+ buildChildRelationField,
13
+ deriveRelationFieldName,
14
+ inferRelations,
15
+ mapDefault,
16
+ parseRawDefault,
17
+ toEnumMemberName,
18
+ toEnumName,
19
+ toFieldName,
20
+ toModelName,
21
+ toNamedTypeName,
22
+ } from '@prisma-next/family-sql/psl-infer';
23
+ import {
24
+ coordinateKey,
25
+ elementCoordinates,
26
+ isPlainRecord,
27
+ } from '@prisma-next/framework-components/ir';
28
+ import type {
29
+ PslAttribute,
30
+ PslAttributeArgument,
31
+ PslDocumentAst,
32
+ PslExtensionBlock,
33
+ PslExtensionBlockParamValue,
34
+ PslField,
35
+ PslFieldAttribute,
36
+ PslModel,
37
+ PslModelAttribute,
38
+ PslNamedTypeDeclaration,
39
+ PslSpan,
40
+ PslTypeConstructorCall,
41
+ PslTypesBlock,
42
+ } from '@prisma-next/framework-components/psl-ast';
43
+ import {
44
+ makePslNamespace,
45
+ makePslNamespaceEntries,
46
+ UNSPECIFIED_PSL_NAMESPACE_ID,
47
+ } from '@prisma-next/framework-components/psl-ast';
48
+ import type { SqlModelStorage } from '@prisma-next/sql-contract/types';
49
+ import type { SqlColumnIR, SqlForeignKeyIR } from '@prisma-next/sql-schema-ir/types';
50
+ import { SqlSchemaIR, SqlTableIR } from '@prisma-next/sql-schema-ir/types';
51
+ import { blindCast } from '@prisma-next/utils/casts';
52
+ import { ifDefined } from '@prisma-next/utils/defined';
53
+ import { PostgresNativeEnum } from '../postgres-native-enum';
54
+ import type { PostgresDatabaseSchemaNode } from '../schema-ir/postgres-database-schema-node';
55
+ import { createPostgresDefaultMapping } from './postgres-default-mapping';
56
+ import { createPostgresTypeMap } from './postgres-type-map';
57
+
58
+ const SYNTHETIC_SPAN: PslSpan = {
59
+ start: { offset: 0, line: 1, column: 1 },
60
+ end: { offset: 0, line: 1, column: 1 },
61
+ };
62
+
63
+ const PSL_SCALAR_TYPE_NAMES = new Set([
64
+ 'String',
65
+ 'Boolean',
66
+ 'Int',
67
+ 'BigInt',
68
+ 'Float',
69
+ 'Decimal',
70
+ 'DateTime',
71
+ 'Json',
72
+ 'Bytes',
73
+ ]);
74
+
75
+ type ResolvedColumnFieldName = {
76
+ readonly fieldName: string;
77
+ readonly fieldMap?: string | undefined;
78
+ };
79
+
80
+ type TableColumnFieldNameMap = ReadonlyMap<string, ResolvedColumnFieldName>;
81
+
82
+ type NamedTypeRegistryEntry = {
83
+ readonly name: string;
84
+ readonly baseType: string;
85
+ readonly nativeTypeAttribute: PslNativeTypeAttribute;
86
+ };
87
+
88
+ type NamedTypeRegistry = {
89
+ readonly entriesByKey: Map<string, NamedTypeRegistryEntry>;
90
+ readonly usedNames: Set<string>;
91
+ };
92
+
93
+ type TopLevelNameResult = {
94
+ readonly name: string;
95
+ readonly map?: string | undefined;
96
+ };
97
+
98
+ /**
99
+ * Coordinates every element a set of already-assembled contracts declare,
100
+ * mapped to the {@link SqlDescribedContractSpace} that owns it and keyed by
101
+ * the shared {@link coordinateKey} helper. `contract infer` uses this to omit
102
+ * database elements a stack extension pack's contract space already
103
+ * describes — reusing the same coordinate walk the contract-space aggregate
104
+ * and cross-space collision check use (`elementCoordinates`), rather than a
105
+ * bespoke per-entity-kind membership test — and, for a foreign key whose
106
+ * referenced table an entry owns, to resolve the qualified cross-space
107
+ * relation it describes.
108
+ */
109
+ function describedContractOwners(
110
+ describedContracts: readonly SqlDescribedContractSpace[],
111
+ ): ReadonlyMap<string, SqlDescribedContractSpace> {
112
+ const owners = new Map<string, SqlDescribedContractSpace>();
113
+ for (const entry of describedContracts) {
114
+ for (const coordinate of elementCoordinates(entry.contract.storage)) {
115
+ owners.set(coordinateKey(coordinate), entry);
116
+ }
117
+ }
118
+ return owners;
119
+ }
120
+
121
+ /**
122
+ * The domain model a described contract maps a `(namespaceId, tableName)`
123
+ * storage coordinate to, plus the pack's own space id and column→field-name
124
+ * mapping — everything `buildModel`/`buildRelationField` need to emit the
125
+ * cross-space relation `<spaceId>:<namespaceId>.<modelName>` and resolve its
126
+ * `references` argument to the pack's own field names rather than a generic
127
+ * column-name guess.
128
+ */
129
+ type CrossSpaceTarget = {
130
+ readonly spaceId: string;
131
+ readonly namespaceId: string;
132
+ readonly modelName: string;
133
+ readonly fieldNamesByColumn: TableColumnFieldNameMap;
134
+ };
135
+
136
+ function resolveCrossSpaceTarget(
137
+ owner: SqlDescribedContractSpace,
138
+ namespaceId: string,
139
+ tableName: string,
140
+ ): CrossSpaceTarget | undefined {
141
+ const domainNamespace = owner.contract.domain.namespaces[namespaceId];
142
+ if (domainNamespace === undefined) {
143
+ return undefined;
144
+ }
145
+
146
+ for (const [modelName, model] of Object.entries(domainNamespace.models)) {
147
+ const storage = blindCast<SqlModelStorage, 'SQL contract model storage'>(model.storage);
148
+ if (storage.namespaceId !== namespaceId || storage.table !== tableName) {
149
+ continue;
150
+ }
151
+
152
+ const fieldNamesByColumn = new Map<string, ResolvedColumnFieldName>();
153
+ for (const [fieldName, fieldStorage] of Object.entries(storage.fields)) {
154
+ fieldNamesByColumn.set(fieldStorage.column, { fieldName });
155
+ }
156
+
157
+ return { spaceId: owner.spaceId, namespaceId, modelName, fieldNamesByColumn };
158
+ }
159
+
160
+ return undefined;
161
+ }
162
+
163
+ type ForeignKeyResolution = {
164
+ /** `tables`, with every cross-space or dangling foreign key removed. */
165
+ readonly tables: Record<string, SqlTableIR>;
166
+ /** Cross-space relation fields to merge onto `inferRelations`'s output, keyed by host table name. */
167
+ readonly extraRelationsByTable: ReadonlyMap<string, readonly RelationField[]>;
168
+ /** Synthetic field-name maps for cross-space-referenced pack tables, merged into `fieldNamesByTable`. */
169
+ readonly crossSpaceFieldNamesByTable: ReadonlyMap<string, TableColumnFieldNameMap>;
170
+ };
171
+
172
+ /**
173
+ * Classifies every foreign key on a surviving table into one of three cases.
174
+ * A foreign key that carries a `referencedSchema` is checked against the
175
+ * pack-owned coordinates first, so a pack-owned target wins even when a local
176
+ * table happens to share its bare name; only foreign keys with no owned
177
+ * coordinate fall through to the local/dangling distinction.
178
+ *
179
+ * - **Cross-space**: `referencedSchema` is set and a described contract owns
180
+ * the coordinate `(referencedSchema, 'table', referencedTable)`. The
181
+ * referenced table is absent from the tree (omitted because the pack
182
+ * describes it, or never introspected — `contract infer` walks a single
183
+ * namespace). Removed from `foreignKeys` (so `inferRelations` never falls
184
+ * back to a bare, unqualified table name for it) and replaced with a
185
+ * `RelationField` qualified with the owning pack's space id and namespace
186
+ * id. `owners` holds only pack-declared coordinates, so an app's own table
187
+ * (e.g. `public.users`) is never owned and cannot be captured here.
188
+ * - **Local**: not a pack-owned coordinate, and the referenced table survived
189
+ * introspection — left untouched, `inferRelations` handles it as before.
190
+ * - **Dangling**: not a pack-owned coordinate, and the referenced table is
191
+ * neither in the tree nor owned by any described contract. Removed from
192
+ * `foreignKeys`, keeping the scalar column, rather than emitting a relation
193
+ * to a model that was never defined.
194
+ *
195
+ * A pack that owns the referenced coordinate but declares no domain model
196
+ * mapped to it is malformed; that case throws rather than degrading to a
197
+ * silent drop, which would contradict the dangling definition above.
198
+ */
199
+ function resolveForeignKeys(
200
+ tables: Readonly<Record<string, SqlTableIR>>,
201
+ owners: ReadonlyMap<string, SqlDescribedContractSpace>,
202
+ ): ForeignKeyResolution {
203
+ const resultTables: Record<string, SqlTableIR> = {};
204
+ const extraRelationsByTable = new Map<string, RelationField[]>();
205
+ const crossSpaceFieldNamesByTable = new Map<string, TableColumnFieldNameMap>();
206
+
207
+ for (const [tableName, table] of Object.entries(tables)) {
208
+ const keptForeignKeys: SqlForeignKeyIR[] = [];
209
+
210
+ for (const fk of table.foreignKeys) {
211
+ if (fk.referencedSchema !== undefined) {
212
+ const owner = owners.get(
213
+ coordinateKey({
214
+ namespaceId: fk.referencedSchema,
215
+ entityKind: 'table',
216
+ entityName: fk.referencedTable,
217
+ }),
218
+ );
219
+ if (owner !== undefined) {
220
+ const target = resolveCrossSpaceTarget(owner, fk.referencedSchema, fk.referencedTable);
221
+ if (target === undefined) {
222
+ throw new Error(
223
+ `contract infer: described contract space "${owner.spaceId}" owns storage ` +
224
+ `coordinate "${fk.referencedSchema}.${fk.referencedTable}" but declares no ` +
225
+ 'domain model mapped to it. A pack that describes a table must also declare the ' +
226
+ 'domain model it maps to; this pack is malformed.',
227
+ );
228
+ }
229
+
230
+ if (!crossSpaceFieldNamesByTable.has(fk.referencedTable)) {
231
+ crossSpaceFieldNamesByTable.set(fk.referencedTable, target.fieldNamesByColumn);
232
+ }
233
+
234
+ const fieldName = deriveRelationFieldName(fk.columns, fk.referencedTable);
235
+ const optional = fk.columns.some(
236
+ (columnName) => table.columns[columnName]?.nullable ?? false,
237
+ );
238
+ const relationField: RelationField = {
239
+ ...buildChildRelationField(fieldName, target.modelName, fk, optional),
240
+ typeNamespaceId: target.namespaceId,
241
+ typeContractSpaceId: target.spaceId,
242
+ };
243
+
244
+ const existingRelations = extraRelationsByTable.get(tableName);
245
+ if (existingRelations) {
246
+ existingRelations.push(relationField);
247
+ } else {
248
+ extraRelationsByTable.set(tableName, [relationField]);
249
+ }
250
+ continue;
251
+ }
252
+ }
253
+
254
+ // Not a pack-owned coordinate: keep the foreign key if the referenced
255
+ // table survived introspection (local), otherwise drop it while keeping
256
+ // the scalar column (dangling).
257
+ if (tables[fk.referencedTable] !== undefined) {
258
+ keptForeignKeys.push(fk);
259
+ }
260
+ }
261
+
262
+ resultTables[tableName] =
263
+ keptForeignKeys.length === table.foreignKeys.length
264
+ ? table
265
+ : new SqlTableIR({ ...table, foreignKeys: keptForeignKeys });
266
+ }
267
+
268
+ return { tables: resultTables, extraRelationsByTable, crossSpaceFieldNamesByTable };
269
+ }
270
+
271
+ /**
272
+ * Infers a PSL AST (for `printPsl`) from an introspected Postgres schema tree.
273
+ *
274
+ * Target-owned inference: it walks the `PostgresDatabaseSchemaNode` tree and
275
+ * owns the Postgres dialect knowledge — the native type map and default map.
276
+ * Relation inference, name transforms, generic default mapping, and raw-default
277
+ * parsing are shape-neutral utilities imported from the SQL family.
278
+ *
279
+ * This slice emits relational-only PSL, byte-identical to the prior flat
280
+ * inference: the tree's tables (across its namespaces — `contract infer`
281
+ * introspects a single live namespace) are gathered into the model set and
282
+ * emitted as one `UNSPECIFIED_PSL_NAMESPACE_ID` bucket. Top-level entities
283
+ * (policies/roles → PSL extension blocks) are a later slice.
284
+ *
285
+ * `describedContracts` — the stack's extension packs' already-assembled
286
+ * contracts, each paired with its space id — is consulted while gathering
287
+ * tables: a table whose coordinate `(schemaName, 'table', tableName)` one of
288
+ * those contracts already declares is omitted, before the duplicate-name
289
+ * check below and before relation inference, so it cannot spuriously
290
+ * collide with an app table and never contributes a bare relation field. A
291
+ * surviving table's foreign key into an omitted, pack-owned table is not
292
+ * dropped: {@link resolveForeignKeys} rewrites it into a relation qualified
293
+ * with the pack's space id (`<spaceId>:<namespaceId>.<Model>`) instead.
294
+ */
295
+ export function inferPostgresPslContract(
296
+ tree: PostgresDatabaseSchemaNode,
297
+ describedContracts?: readonly SqlDescribedContractSpace[],
298
+ ): PslDocumentAst {
299
+ const namespaces = Object.values(tree.namespaces);
300
+ const owners = describedContractOwners(describedContracts ?? []);
301
+ const enumOwners = describedNativeEnumOwnersByTypeName(describedContracts ?? []);
302
+
303
+ // Native enum adoption: each namespace's introspected `{ typeName, values }`
304
+ // entries become `native_enum` blocks + `pg.enum(<Name>)` columns, minus the
305
+ // types a described pack contract already declares (matched by TYPE NAME —
306
+ // see describedNativeEnumOwnersByTypeName). Transitional grade gap: an
307
+ // inferred block carries no explicit `control` and inherits the contract's
308
+ // `defaultControl`; under `defaultControl: 'managed'` the planner does not
309
+ // yet enforce the native enum lifecycle (CREATE TYPE / DROP TYPE / ADD
310
+ // VALUE) — that lands with Phase 2 of projects/native-postgres-enums/spec.md,
311
+ // making the grade true retroactively with no contract change.
312
+ const enumDefinitions = new Map<string, readonly string[]>();
313
+ const packOwnedEnumTypesByNamespace = new Map<string, Map<string, string>>();
314
+ const enumNamespaceNames = new Set<string>();
315
+ for (const namespace of namespaces) {
316
+ const definitionsForNamespace = new Set(namespace.nativeEnums.map((e) => e.typeName));
317
+ for (const typeName of namespace.nativeEnumTypeNames) {
318
+ if (!definitionsForNamespace.has(typeName)) {
319
+ throw new Error(
320
+ `contract infer: introspection reported native enum type "${typeName}" in schema ` +
321
+ `"${namespace.schemaName}" without member values — the introspected tree's ` +
322
+ '`nativeEnumTypeNames` and `nativeEnums` are out of sync (internal invariant).',
323
+ );
324
+ }
325
+ }
326
+ for (const { typeName, values } of namespace.nativeEnums) {
327
+ const owner = enumOwners.get(
328
+ coordinateKey({
329
+ namespaceId: namespace.schemaName,
330
+ entityKind: 'native_enum',
331
+ entityName: typeName,
332
+ }),
333
+ );
334
+ if (owner !== undefined) {
335
+ const owned = packOwnedEnumTypesByNamespace.get(namespace.schemaName) ?? new Map();
336
+ // Columns reference the type either bare or schema-qualified —
337
+ // `format_type` qualifies a type outside the connection's
338
+ // search_path — so both spellings are owned.
339
+ owned.set(typeName, owner.spaceId);
340
+ owned.set(`${namespace.schemaName}.${typeName}`, owner.spaceId);
341
+ packOwnedEnumTypesByNamespace.set(namespace.schemaName, owned);
342
+ continue;
343
+ }
344
+ enumDefinitions.set(typeName, values);
345
+ enumNamespaceNames.add(namespace.schemaName);
346
+ }
347
+ }
348
+
349
+ // Stopgap (TML-2958): flatten the schema-IR *tree* into the single `{ tables }`
350
+ // map the PSL writer expects. The writer still walks a flat table map, so this
351
+ // is a read-only projection — it does not reintroduce a stored flat schema.
352
+ // The real fix is to extend the PSL writer to walk the namespace tree and emit
353
+ // per-namespace `namespace { … }` blocks; until then `contract infer` handles a
354
+ // single introspected namespace, and a same-named table in two schemas has no
355
+ // unambiguous single-bucket model, so we throw rather than silently drop one.
356
+ const tables: Record<string, SqlTableIR> = {};
357
+ const tableNamespaceNames = new Set<string>();
358
+ for (const namespace of namespaces) {
359
+ const ownedEnumTypes = packOwnedEnumTypesByNamespace.get(namespace.schemaName);
360
+ for (const [tableName, table] of Object.entries(namespace.tables)) {
361
+ if (
362
+ owners.has(
363
+ coordinateKey({
364
+ namespaceId: namespace.schemaName,
365
+ entityKind: 'table',
366
+ entityName: tableName,
367
+ }),
368
+ )
369
+ ) {
370
+ continue;
371
+ }
372
+ if (tables[tableName] !== undefined) {
373
+ throw new Error(
374
+ `contract infer: duplicate table name "${tableName}" across schemas is not yet supported ` +
375
+ '(single-namespace PSL inference emits one flat bucket; multi-namespace `namespace { … }` ' +
376
+ 'output is a later slice).',
377
+ );
378
+ }
379
+ if (ownedEnumTypes !== undefined) {
380
+ for (const column of Object.values(table.columns)) {
381
+ const owningSpaceId = ownedEnumTypes.get(column.nativeType);
382
+ if (owningSpaceId !== undefined) {
383
+ throw new Error(
384
+ `contract infer: column "${tableName}"."${column.name}" is typed by native enum ` +
385
+ `type "${column.nativeType}", which extension pack space "${owningSpaceId}" ` +
386
+ 'already describes. A cross-space enum-typed column has no authorable PSL form ' +
387
+ "yet; describe the table in that pack's contract or retype the column before " +
388
+ 're-running contract infer.',
389
+ );
390
+ }
391
+ }
392
+ }
393
+ tables[tableName] = new SqlTableIR(table);
394
+ tableNamespaceNames.add(namespace.schemaName);
395
+ }
396
+ }
397
+
398
+ // Namespace wrap (pinned during shaping): a `native_enum` block only lowers
399
+ // inside an explicit `namespace { … }` block — the interpreter skips
400
+ // extension entities in the unspecified top-level bucket — so enum-bearing
401
+ // output wraps everything in the introspected schema's name. Enum-free
402
+ // output stays flat and byte-identical to the prior inference.
403
+ let wrapNamespaceName: string | undefined;
404
+ if (enumDefinitions.size > 0) {
405
+ const contentNamespaces = new Set([...enumNamespaceNames, ...tableNamespaceNames]);
406
+ if (contentNamespaces.size > 1) {
407
+ throw new Error(
408
+ 'contract infer: native enum adoption with content across multiple schemas is not yet ' +
409
+ 'supported (single-namespace PSL inference emits one `namespace { … }` block; ' +
410
+ `multi-namespace output is a later slice). Schemas: ${[...contentNamespaces]
411
+ .sort()
412
+ .join(', ')}.`,
413
+ );
414
+ }
415
+ wrapNamespaceName = [...contentNamespaces][0];
416
+ }
417
+
418
+ const {
419
+ tables: resolvedTables,
420
+ extraRelationsByTable,
421
+ crossSpaceFieldNamesByTable,
422
+ } = resolveForeignKeys(tables, owners);
423
+ const schemaIR = new SqlSchemaIR({ tables: resolvedTables });
424
+
425
+ // Live introspection reports an enum column's nativeType schema-qualified
426
+ // whenever the type sits outside the connection's search_path (`format_type`
427
+ // semantics; e.g. `auth.aal_level`), while `pg_type.typname` — the
428
+ // definitions key — is always bare. Register the qualified spelling as an
429
+ // alias so those columns resolve; block emission stays keyed on the bare
430
+ // name (one block per type).
431
+ const enumTypeNames = new Set(enumDefinitions.keys());
432
+ if (wrapNamespaceName !== undefined) {
433
+ for (const typeName of enumDefinitions.keys()) {
434
+ enumTypeNames.add(`${wrapNamespaceName}.${typeName}`);
435
+ }
436
+ }
437
+ const enumInfo: EnumInfo = {
438
+ typeNames: enumTypeNames,
439
+ definitions: enumDefinitions,
440
+ };
441
+ const options: PslPrinterOptions = {
442
+ typeMap: createPostgresTypeMap(enumInfo.typeNames),
443
+ defaultMapping: createPostgresDefaultMapping(),
444
+ parseRawDefault,
445
+ ...(enumDefinitions.size > 0 ? { enumInfo } : {}),
446
+ };
447
+
448
+ return buildPslDocumentAst(
449
+ schemaIR,
450
+ options,
451
+ {
452
+ extraRelationsByTable,
453
+ crossSpaceFieldNamesByTable,
454
+ },
455
+ wrapNamespaceName,
456
+ );
457
+ }
458
+
459
+ /**
460
+ * Native-enum ownership index for pack subtraction, keyed by TYPE NAME
461
+ * coordinate. A pack contract keys `entries.native_enum` by HANDLE name
462
+ * (`AalLevel`) while introspection sees the Postgres type name (`aal_level`);
463
+ * the entity's `typeName` field carries the mapping, so this walk indexes by
464
+ * it rather than reusing {@link describedContractOwners}' entries-key
465
+ * coordinates.
466
+ *
467
+ * Reads `entries.native_enum` directly: the kind map is carried
468
+ * non-enumerable on `PostgresSchema.entries`, so the generic
469
+ * `elementCoordinates` walk never yields it. Serialized pack contracts do
470
+ * not carry `native_enum` entries at all (`PostgresContractSerializer`
471
+ * strips them; only the derived `valueSet` survives), so this subtraction
472
+ * currently matches in-memory described contracts only.
473
+ */
474
+ function describedNativeEnumOwnersByTypeName(
475
+ describedContracts: readonly SqlDescribedContractSpace[],
476
+ ): ReadonlyMap<string, SqlDescribedContractSpace> {
477
+ const owners = new Map<string, SqlDescribedContractSpace>();
478
+ for (const entry of describedContracts) {
479
+ for (const [namespaceId, ns] of Object.entries(entry.contract.storage.namespaces)) {
480
+ const kindMap = ns.entries['native_enum'];
481
+ if (!isPlainRecord(kindMap)) continue;
482
+ for (const entity of Object.values(kindMap)) {
483
+ if (!PostgresNativeEnum.is(entity)) continue;
484
+ owners.set(
485
+ coordinateKey({
486
+ namespaceId,
487
+ entityKind: 'native_enum',
488
+ entityName: entity.typeName,
489
+ }),
490
+ entry,
491
+ );
492
+ }
493
+ }
494
+ }
495
+ return owners;
496
+ }
497
+
498
+ export function buildPslDocumentAst(
499
+ schemaIR: SqlSchemaIR,
500
+ options: PslPrinterOptions,
501
+ foreignKeyExtras: Pick<
502
+ ForeignKeyResolution,
503
+ 'extraRelationsByTable' | 'crossSpaceFieldNamesByTable'
504
+ >,
505
+ namespaceName?: string,
506
+ ): PslDocumentAst {
507
+ const { typeMap, defaultMapping, parseRawDefault: rawDefaultParser } = options;
508
+ const { extraRelationsByTable, crossSpaceFieldNamesByTable } = foreignKeyExtras;
509
+
510
+ const modelNames = buildTopLevelNameMap(
511
+ Object.keys(schemaIR.tables),
512
+ toModelName,
513
+ 'model',
514
+ 'table',
515
+ );
516
+
517
+ const modelNameMap = new Map(
518
+ [...modelNames].map(([tableName, result]) => [tableName, result.name]),
519
+ );
520
+
521
+ const { enumNameMap: bareEnumNameMap, enumBlocks } = buildNativeEnumBlocks(
522
+ options.enumInfo?.definitions ?? new Map(),
523
+ modelNames,
524
+ );
525
+
526
+ // Columns reference an enum type bare or schema-qualified (`format_type`
527
+ // qualifies types outside the search_path); alias the qualified spelling
528
+ // onto the same PSL name. Blocks stay keyed on the bare name.
529
+ const enumNameMap = new Map(bareEnumNameMap);
530
+ if (namespaceName !== undefined) {
531
+ for (const [typeName, pslName] of bareEnumNameMap) {
532
+ enumNameMap.set(`${namespaceName}.${typeName}`, pslName);
533
+ }
534
+ }
535
+
536
+ const reservedNamedTypeNames = createReservedNamedTypeNames(modelNames, enumNameMap);
537
+
538
+ // Cross-space entries are seeded first so a real local table of the same
539
+ // bare name (an existing single-namespace-flat-bucket limitation, not new
540
+ // here) always wins the merge, matching `resolveForeignKeys`'s own
541
+ // precedence: a surviving local table is never treated as cross-space.
542
+ const fieldNamesByTable = new Map([
543
+ ...crossSpaceFieldNamesByTable,
544
+ ...buildFieldNamesByTable(schemaIR.tables),
545
+ ]);
546
+ const { relationsByTable } = inferRelations(schemaIR.tables, modelNameMap);
547
+ const namedTypes = seedNamedTypeRegistry(schemaIR, typeMap, enumNameMap, reservedNamedTypeNames);
548
+
549
+ const models: PslModel[] = [];
550
+ for (const table of Object.values(schemaIR.tables)) {
551
+ models.push(
552
+ buildModel(
553
+ table,
554
+ typeMap,
555
+ enumNameMap,
556
+ fieldNamesByTable,
557
+ namedTypes,
558
+ defaultMapping,
559
+ rawDefaultParser,
560
+ [
561
+ ...(relationsByTable.get(table.name) ?? []),
562
+ ...(extraRelationsByTable.get(table.name) ?? []),
563
+ ],
564
+ ),
565
+ );
566
+ }
567
+
568
+ const sortedModels = topologicalSort(models, schemaIR.tables, modelNameMap);
569
+
570
+ const namedTypeEntries = [...namedTypes.entriesByKey.values()].sort((a, b) =>
571
+ a.name.localeCompare(b.name),
572
+ );
573
+ const types: PslTypesBlock | undefined =
574
+ namedTypeEntries.length > 0
575
+ ? {
576
+ kind: 'types',
577
+ declarations: namedTypeEntries.map(buildNamedTypeDeclaration),
578
+ span: SYNTHETIC_SPAN,
579
+ }
580
+ : undefined;
581
+
582
+ // Inferred PSL nodes will eventually be routed into per-namespace buckets
583
+ // matching the source storage; for now everything lands in a single bucket.
584
+ // Without a `namespaceName` that bucket is the synthesised `__unspecified__`
585
+ // one, which the framework printer emits at top level with no
586
+ // `namespace { … }` wrapper — preserving the existing flat introspection
587
+ // output verbatim. With a `namespaceName` (enum-bearing output) the bucket
588
+ // is a real named namespace, printed as an explicit block.
589
+ const ast: PslDocumentAst = {
590
+ kind: 'document',
591
+ sourceId: '<sql-schema-ir>',
592
+ namespaces: [
593
+ makePslNamespace({
594
+ kind: 'namespace',
595
+ name: namespaceName ?? UNSPECIFIED_PSL_NAMESPACE_ID,
596
+ entries: makePslNamespaceEntries(sortedModels, [], enumBlocks),
597
+ span: SYNTHETIC_SPAN,
598
+ }),
599
+ ],
600
+ ...(types ? { types } : {}),
601
+ span: SYNTHETIC_SPAN,
602
+ };
603
+
604
+ return ast;
605
+ }
606
+
607
+ type NativeEnumBlockResult = {
608
+ /** Native enum type name → PSL block name, for `pg.enum(<Name>)` field refs. */
609
+ readonly enumNameMap: ReadonlyMap<string, string>;
610
+ readonly enumBlocks: readonly PslExtensionBlock[];
611
+ };
612
+
613
+ /**
614
+ * Builds one `native_enum` extension-block AST node per introspected enum
615
+ * definition. Block names go through the shared top-level transform
616
+ * (`toEnumName`, intra-enum collisions throw like model collisions) and are
617
+ * then reserved against the model names — an enum whose PSL name a model
618
+ * already claims gets a numeric suffix, with `@@map` carrying the real type
619
+ * name. Members print as explicit `member = "value"` pairs: the member name
620
+ * is the sanitized value (deduplicated within the block), the JSON-encoded
621
+ * value carries the truth verbatim.
622
+ */
623
+ function buildNativeEnumBlocks(
624
+ definitions: ReadonlyMap<string, readonly string[]>,
625
+ modelNames: ReadonlyMap<string, TopLevelNameResult>,
626
+ ): NativeEnumBlockResult {
627
+ const enumNames = buildTopLevelNameMap(
628
+ [...definitions.keys()].sort(),
629
+ toEnumName,
630
+ 'enum',
631
+ 'enum type',
632
+ );
633
+
634
+ const usedTopLevelNames = new Set<string>(PSL_SCALAR_TYPE_NAMES);
635
+ for (const result of modelNames.values()) {
636
+ usedTopLevelNames.add(result.name);
637
+ }
638
+
639
+ const enumNameMap = new Map<string, string>();
640
+ const enumBlocks: PslExtensionBlock[] = [];
641
+ for (const [typeName, result] of enumNames) {
642
+ const name = createUniqueFieldName(result.name, usedTopLevelNames);
643
+ usedTopLevelNames.add(name);
644
+ enumNameMap.set(typeName, name);
645
+ enumBlocks.push(buildNativeEnumBlock(name, typeName, definitions.get(typeName) ?? []));
646
+ }
647
+
648
+ return { enumNameMap, enumBlocks };
649
+ }
650
+
651
+ function buildNativeEnumBlock(
652
+ name: string,
653
+ typeName: string,
654
+ values: readonly string[],
655
+ ): PslExtensionBlock {
656
+ const usedMemberNames = new Set<string>();
657
+ const parameters: Record<string, PslExtensionBlockParamValue> = {};
658
+ for (const value of values) {
659
+ const memberName = createUniqueFieldName(toEnumMemberName(value), usedMemberNames);
660
+ usedMemberNames.add(memberName);
661
+ parameters[memberName] = { kind: 'value', raw: JSON.stringify(value), span: SYNTHETIC_SPAN };
662
+ }
663
+
664
+ return {
665
+ kind: 'native_enum',
666
+ keyword: 'native_enum',
667
+ name,
668
+ parameters,
669
+ blockAttributes:
670
+ name === typeName
671
+ ? []
672
+ : [
673
+ {
674
+ name: 'map',
675
+ args: [
676
+ {
677
+ kind: 'positional',
678
+ value: `"${escapePslString(typeName)}"`,
679
+ span: SYNTHETIC_SPAN,
680
+ },
681
+ ],
682
+ span: SYNTHETIC_SPAN,
683
+ },
684
+ ],
685
+ span: SYNTHETIC_SPAN,
686
+ };
687
+ }
688
+
689
+ function buildModel(
690
+ table: SqlTableIR,
691
+ typeMap: PslTypeMap,
692
+ enumNameMap: ReadonlyMap<string, string>,
693
+ fieldNamesByTable: ReadonlyMap<string, TableColumnFieldNameMap>,
694
+ namedTypes: NamedTypeRegistry,
695
+ defaultMapping: DefaultMappingOptions | undefined,
696
+ rawDefaultParser: PslPrinterOptions['parseRawDefault'],
697
+ relationFields: readonly RelationField[],
698
+ ): PslModel {
699
+ const { name: modelName, map: mapName } = toModelName(table.name);
700
+ const fieldNameMap = fieldNamesByTable.get(table.name);
701
+
702
+ const pkColumns = new Set(table.primaryKey?.columns ?? []);
703
+ const isSinglePk = pkColumns.size === 1;
704
+ const singlePkConstraintName = isSinglePk ? table.primaryKey?.name : undefined;
705
+
706
+ const uniqueColumns = new Map<string, string | undefined>();
707
+ for (const unique of table.uniques) {
708
+ if (unique.columns.length === 1) {
709
+ const [columnName = ''] = unique.columns;
710
+ const existingConstraintName = uniqueColumns.get(columnName);
711
+ if (!uniqueColumns.has(columnName) || (existingConstraintName === undefined && unique.name)) {
712
+ uniqueColumns.set(columnName, unique.name);
713
+ }
714
+ }
715
+ }
716
+
717
+ const fields: PslField[] = [];
718
+ for (const column of Object.values(table.columns)) {
719
+ fields.push(
720
+ buildScalarField(
721
+ column,
722
+ table,
723
+ typeMap,
724
+ enumNameMap,
725
+ fieldNameMap,
726
+ namedTypes,
727
+ defaultMapping,
728
+ rawDefaultParser,
729
+ pkColumns,
730
+ isSinglePk,
731
+ singlePkConstraintName,
732
+ uniqueColumns,
733
+ ),
734
+ );
735
+ }
736
+
737
+ const usedFieldNames = new Set(fields.map((field) => field.name));
738
+ for (const rel of relationFields) {
739
+ fields.push(buildRelationField(rel, table.name, fieldNamesByTable, usedFieldNames));
740
+ }
741
+
742
+ const modelAttributes: PslModelAttribute[] = [];
743
+
744
+ if (table.primaryKey && table.primaryKey.columns.length > 1) {
745
+ const pkFieldNames = table.primaryKey.columns.map((columnName) =>
746
+ resolveColumnFieldName(fieldNamesByTable, table.name, columnName),
747
+ );
748
+ modelAttributes.push(buildModelConstraintAttribute('id', pkFieldNames, table.primaryKey.name));
749
+ }
750
+
751
+ for (const unique of table.uniques) {
752
+ if (unique.columns.length > 1) {
753
+ const uniqueFieldNames = unique.columns.map((columnName) =>
754
+ resolveColumnFieldName(fieldNamesByTable, table.name, columnName),
755
+ );
756
+ modelAttributes.push(buildModelConstraintAttribute('unique', uniqueFieldNames, unique.name));
757
+ }
758
+ }
759
+
760
+ for (const index of table.indexes) {
761
+ if (!index.unique) {
762
+ const indexFieldNames = index.columns.map((columnName) =>
763
+ resolveColumnFieldName(fieldNamesByTable, table.name, columnName),
764
+ );
765
+ modelAttributes.push(buildModelConstraintAttribute('index', indexFieldNames, index.name));
766
+ }
767
+ }
768
+
769
+ if (mapName) {
770
+ modelAttributes.push(buildMapAttribute('model', mapName));
771
+ }
772
+
773
+ // Surface introspection advisory: tables without a primary key cannot serve
774
+ // as the right-hand side of a `findUnique`-style query downstream, so the
775
+ // user should add an `@id`. This warning is part of the emitted SQL output
776
+ // and is asserted byte-for-byte, so keep the exact wording.
777
+ const comment = table.primaryKey
778
+ ? undefined
779
+ : '// WARNING: This table has no primary key in the database';
780
+
781
+ return {
782
+ kind: 'model',
783
+ name: modelName,
784
+ fields,
785
+ attributes: modelAttributes,
786
+ span: SYNTHETIC_SPAN,
787
+ ...(comment !== undefined ? { comment } : {}),
788
+ };
789
+ }
790
+
791
+ function buildScalarField(
792
+ column: SqlColumnIR,
793
+ table: SqlTableIR,
794
+ typeMap: PslTypeMap,
795
+ enumNameMap: ReadonlyMap<string, string>,
796
+ fieldNameMap: TableColumnFieldNameMap | undefined,
797
+ namedTypes: NamedTypeRegistry,
798
+ defaultMapping: DefaultMappingOptions | undefined,
799
+ rawDefaultParser: PslPrinterOptions['parseRawDefault'],
800
+ pkColumns: ReadonlySet<string>,
801
+ isSinglePk: boolean,
802
+ singlePkConstraintName: string | undefined,
803
+ uniqueColumns: ReadonlyMap<string, string | undefined>,
804
+ ): PslField {
805
+ const resolvedField = fieldNameMap?.get(column.name);
806
+ const fieldName = resolvedField?.fieldName ?? toFieldName(column.name).name;
807
+ const fieldMap = resolvedField?.fieldMap;
808
+
809
+ const resolution = typeMap.resolve(column.nativeType, table.annotations);
810
+
811
+ if ('unsupported' in resolution) {
812
+ const attrs: PslFieldAttribute[] = [];
813
+ if (fieldMap !== undefined) {
814
+ attrs.push(buildMapAttribute('field', fieldMap));
815
+ }
816
+ return {
817
+ kind: 'field',
818
+ name: fieldName,
819
+ typeName: `Unsupported("${escapePslString(resolution.nativeType)}")`,
820
+ optional: column.nullable,
821
+ list: column.many === true,
822
+ attributes: attrs,
823
+ span: SYNTHETIC_SPAN,
824
+ };
825
+ }
826
+
827
+ // An enum-typed column emits the `pg.enum(<Name>)` type-constructor call —
828
+ // the Phase-1 authoring form a `native_enum` ref field takes — not a bare
829
+ // name substitution. The printer renders `typeConstructor` when present and
830
+ // composes `?`/`[]` exactly like any other field type.
831
+ let typeName = resolution.pslType;
832
+ let typeConstructor: PslTypeConstructorCall | undefined;
833
+ const enumPslName = enumNameMap.get(column.nativeType);
834
+ if (enumPslName) {
835
+ typeName = enumPslName;
836
+ typeConstructor = {
837
+ kind: 'typeConstructor',
838
+ path: ['pg', 'enum'],
839
+ args: [positionalArg(enumPslName)],
840
+ span: SYNTHETIC_SPAN,
841
+ };
842
+ }
843
+ if (resolution.nativeTypeAttribute && !enumPslName) {
844
+ typeName = resolveNamedTypeName(namedTypes, resolution);
845
+ }
846
+
847
+ const attributes: PslFieldAttribute[] = [];
848
+ const isId = isSinglePk && pkColumns.has(column.name);
849
+ if (isId) {
850
+ attributes.push(buildSimpleConstraintFieldAttribute('id', singlePkConstraintName));
851
+ }
852
+
853
+ if (column.default !== undefined) {
854
+ const parsed = parseColumnDefault(column.default, column.nativeType, rawDefaultParser);
855
+ if (parsed) {
856
+ const result = mapDefault(parsed, defaultMapping);
857
+ if ('attribute' in result) {
858
+ attributes.push(parseDefaultAttributeString(result.attribute));
859
+ }
860
+ // 'comment' fallback (unrecognized raw default) is dropped — the
861
+ // M1 legacy path emitted a `// Raw default: ...` line above the field via
862
+ // `PrinterField.comment`. M2 drops this since it would require comment
863
+ // nodes in the AST.
864
+ }
865
+ }
866
+
867
+ if (uniqueColumns.has(column.name) && !isId) {
868
+ const uniqueConstraintName = uniqueColumns.get(column.name);
869
+ attributes.push(buildSimpleConstraintFieldAttribute('unique', uniqueConstraintName));
870
+ }
871
+
872
+ if (fieldMap !== undefined) {
873
+ attributes.push(buildMapAttribute('field', fieldMap));
874
+ }
875
+
876
+ return {
877
+ kind: 'field',
878
+ name: fieldName,
879
+ typeName,
880
+ ...ifDefined('typeConstructor', typeConstructor),
881
+ optional: column.nullable,
882
+ list: column.many === true,
883
+ attributes,
884
+ span: SYNTHETIC_SPAN,
885
+ };
886
+ }
887
+
888
+ function buildRelationField(
889
+ rel: RelationField,
890
+ hostTableName: string,
891
+ fieldNamesByTable: ReadonlyMap<string, TableColumnFieldNameMap>,
892
+ usedFieldNames: Set<string>,
893
+ ): PslField {
894
+ const fieldName = createUniqueFieldName(rel.fieldName, usedFieldNames);
895
+ usedFieldNames.add(fieldName);
896
+
897
+ const args: PslAttributeArgument[] = [];
898
+
899
+ if (rel.fields && rel.references) {
900
+ if (rel.relationName) {
901
+ args.push(namedArg('name', `"${escapePslString(rel.relationName)}"`));
902
+ }
903
+ args.push(
904
+ namedArg(
905
+ 'fields',
906
+ `[${rel.fields
907
+ .map((columnName) => resolveColumnFieldName(fieldNamesByTable, hostTableName, columnName))
908
+ .join(', ')}]`,
909
+ ),
910
+ );
911
+ args.push(
912
+ namedArg(
913
+ 'references',
914
+ `[${rel.references
915
+ .map((columnName) =>
916
+ resolveColumnFieldName(fieldNamesByTable, rel.referencedTableName ?? '', columnName),
917
+ )
918
+ .join(', ')}]`,
919
+ ),
920
+ );
921
+ if (rel.onDelete) {
922
+ args.push(namedArg('onDelete', rel.onDelete));
923
+ }
924
+ if (rel.onUpdate) {
925
+ args.push(namedArg('onUpdate', rel.onUpdate));
926
+ }
927
+ if (rel.fkName) {
928
+ args.push(namedArg('map', `"${escapePslString(rel.fkName)}"`));
929
+ }
930
+ } else if (rel.relationName) {
931
+ args.push(namedArg('name', `"${escapePslString(rel.relationName)}"`));
932
+ }
933
+
934
+ const attrs: PslFieldAttribute[] =
935
+ args.length > 0 ? [buildAttribute('field', 'relation', args)] : [];
936
+
937
+ return {
938
+ kind: 'field',
939
+ name: fieldName,
940
+ typeName: rel.typeName,
941
+ ...ifDefined('typeNamespaceId', rel.typeNamespaceId),
942
+ ...ifDefined('typeContractSpaceId', rel.typeContractSpaceId),
943
+ optional: rel.optional,
944
+ list: rel.list,
945
+ attributes: attrs,
946
+ span: SYNTHETIC_SPAN,
947
+ };
948
+ }
949
+
950
+ function buildModelConstraintAttribute(
951
+ name: 'id' | 'unique' | 'index',
952
+ fields: readonly string[],
953
+ constraintName?: string,
954
+ ): PslModelAttribute {
955
+ const args: PslAttributeArgument[] = [positionalArg(`[${fields.join(', ')}]`)];
956
+ if (constraintName !== undefined) {
957
+ args.push(namedArg('map', `"${escapePslString(constraintName)}"`));
958
+ }
959
+ return buildAttribute('model', name, args);
960
+ }
961
+
962
+ function buildSimpleConstraintFieldAttribute(
963
+ name: 'id' | 'unique',
964
+ constraintName: string | undefined,
965
+ ): PslFieldAttribute {
966
+ if (constraintName === undefined) {
967
+ return buildAttribute('field', name, []);
968
+ }
969
+ return buildAttribute('field', name, [namedArg('map', `"${escapePslString(constraintName)}"`)]);
970
+ }
971
+
972
+ function parseDefaultAttributeString(attributeText: string): PslFieldAttribute {
973
+ // Strip leading "@default(" and trailing ")" — `mapDefault` always returns one
974
+ // top-level positional expression.
975
+ const inner = attributeText.replace(/^@default\(/, '').replace(/\)$/, '');
976
+ return buildAttribute('field', 'default', [positionalArg(inner)]);
977
+ }
978
+
979
+ function buildMapAttribute(target: 'model' | 'field' | 'enum', mapName: string): PslAttribute {
980
+ return buildAttribute(target, 'map', [positionalArg(`"${escapePslString(mapName)}"`)]);
981
+ }
982
+
983
+ function buildAttribute(
984
+ target: PslAttribute['target'],
985
+ name: string,
986
+ args: readonly PslAttributeArgument[],
987
+ ): PslAttribute {
988
+ return {
989
+ kind: 'attribute',
990
+ target,
991
+ name,
992
+ args,
993
+ span: SYNTHETIC_SPAN,
994
+ };
995
+ }
996
+
997
+ function positionalArg(value: string): PslAttributeArgument {
998
+ return { kind: 'positional', value, span: SYNTHETIC_SPAN };
999
+ }
1000
+
1001
+ function namedArg(name: string, value: string): PslAttributeArgument {
1002
+ return { kind: 'named', name, value, span: SYNTHETIC_SPAN };
1003
+ }
1004
+
1005
+ function buildNamedTypeDeclaration(entry: NamedTypeRegistryEntry): PslNamedTypeDeclaration {
1006
+ const attribute = buildAttribute(
1007
+ 'namedType',
1008
+ entry.nativeTypeAttribute.name,
1009
+ (entry.nativeTypeAttribute.args ?? []).map(positionalArg),
1010
+ );
1011
+ return {
1012
+ kind: 'namedType',
1013
+ name: entry.name,
1014
+ baseType: entry.baseType,
1015
+ attributes: [attribute],
1016
+ span: SYNTHETIC_SPAN,
1017
+ };
1018
+ }
1019
+
1020
+ function escapePslString(value: string): string {
1021
+ return value
1022
+ .replace(/\\/g, '\\\\')
1023
+ .replace(/"/g, '\\"')
1024
+ .replace(/\n/g, '\\n')
1025
+ .replace(/\r/g, '\\r');
1026
+ }
1027
+
1028
+ /**
1029
+ * Resolves a `SqlColumnIR.default` value into a normalized {@link ColumnDefault}.
1030
+ *
1031
+ * `SqlSchemaIR` types the column default as `string` (a raw database default
1032
+ * expression). Some legacy fixtures and tests still pass already-normalized
1033
+ * `ColumnDefault` objects in the same slot, so we accept either shape
1034
+ * defensively at runtime.
1035
+ */
1036
+ function parseColumnDefault(
1037
+ value: unknown,
1038
+ nativeType: string | undefined,
1039
+ rawDefaultParser: PslPrinterOptions['parseRawDefault'],
1040
+ ): ColumnDefault | undefined {
1041
+ if (typeof value === 'string') {
1042
+ return rawDefaultParser ? rawDefaultParser(value, nativeType) : undefined;
1043
+ }
1044
+ if (value !== null && typeof value === 'object' && 'kind' in (value as Record<string, unknown>)) {
1045
+ return value as ColumnDefault;
1046
+ }
1047
+ return undefined;
1048
+ }
1049
+
1050
+ function buildFieldNamesByTable(
1051
+ tables: Record<string, SqlTableIR>,
1052
+ ): ReadonlyMap<string, TableColumnFieldNameMap> {
1053
+ const fieldNamesByTable = new Map<string, TableColumnFieldNameMap>();
1054
+
1055
+ for (const table of Object.values(tables)) {
1056
+ const columns = Object.values(table.columns).map((column, index) => {
1057
+ const { name, map } = toFieldName(column.name);
1058
+ return {
1059
+ columnName: column.name,
1060
+ desiredFieldName: name,
1061
+ fieldMap: map,
1062
+ index,
1063
+ };
1064
+ });
1065
+
1066
+ const assignmentOrder = [...columns].sort((left, right) => {
1067
+ const mapComparison =
1068
+ Number(left.fieldMap !== undefined) - Number(right.fieldMap !== undefined);
1069
+ if (mapComparison !== 0) {
1070
+ return mapComparison;
1071
+ }
1072
+ return left.index - right.index;
1073
+ });
1074
+
1075
+ const usedFieldNames = new Set<string>();
1076
+ const tableFieldNames = new Map<string, ResolvedColumnFieldName>();
1077
+
1078
+ for (const column of assignmentOrder) {
1079
+ const fieldName = createUniqueFieldName(column.desiredFieldName, usedFieldNames);
1080
+ usedFieldNames.add(fieldName);
1081
+ tableFieldNames.set(column.columnName, {
1082
+ fieldName,
1083
+ fieldMap: column.fieldMap,
1084
+ });
1085
+ }
1086
+
1087
+ fieldNamesByTable.set(table.name, tableFieldNames);
1088
+ }
1089
+
1090
+ return fieldNamesByTable;
1091
+ }
1092
+
1093
+ function resolveColumnFieldName(
1094
+ fieldNamesByTable: ReadonlyMap<string, TableColumnFieldNameMap>,
1095
+ tableName: string,
1096
+ columnName: string,
1097
+ ): string {
1098
+ return (
1099
+ fieldNamesByTable.get(tableName)?.get(columnName)?.fieldName ?? toFieldName(columnName).name
1100
+ );
1101
+ }
1102
+
1103
+ function createUniqueFieldName(desiredName: string, usedFieldNames: ReadonlySet<string>): string {
1104
+ if (!usedFieldNames.has(desiredName)) {
1105
+ return desiredName;
1106
+ }
1107
+
1108
+ let counter = 2;
1109
+ while (usedFieldNames.has(`${desiredName}${counter}`)) {
1110
+ counter++;
1111
+ }
1112
+ return `${desiredName}${counter}`;
1113
+ }
1114
+
1115
+ function buildTopLevelNameMap(
1116
+ sources: Iterable<string>,
1117
+ normalize: (source: string) => TopLevelNameResult,
1118
+ kind: 'model' | 'enum',
1119
+ sourceKind: 'table' | 'enum type',
1120
+ ): Map<string, TopLevelNameResult> {
1121
+ const results = new Map<string, TopLevelNameResult>();
1122
+ const normalizedToSources = new Map<string, string[]>();
1123
+
1124
+ for (const source of sources) {
1125
+ const normalized = normalize(source);
1126
+ results.set(source, normalized);
1127
+ normalizedToSources.set(normalized.name, [
1128
+ ...(normalizedToSources.get(normalized.name) ?? []),
1129
+ source,
1130
+ ]);
1131
+ }
1132
+
1133
+ const duplicates = [...normalizedToSources.entries()].filter(
1134
+ ([, conflictingSources]) => conflictingSources.length > 1,
1135
+ );
1136
+ if (duplicates.length > 0) {
1137
+ const details = duplicates.map(
1138
+ ([normalizedName, conflictingSources]) =>
1139
+ `- ${kind} "${normalizedName}" from ${sourceKind}s ${conflictingSources
1140
+ .map((source) => `"${source}"`)
1141
+ .join(', ')}`,
1142
+ );
1143
+ throw new Error(`PSL ${kind} name collisions detected:\n${details.join('\n')}`);
1144
+ }
1145
+
1146
+ return results;
1147
+ }
1148
+
1149
+ function createReservedNamedTypeNames(
1150
+ modelNames: ReadonlyMap<string, TopLevelNameResult>,
1151
+ enumNameMap: ReadonlyMap<string, string>,
1152
+ ): Set<string> {
1153
+ const reservedNames = new Set<string>(PSL_SCALAR_TYPE_NAMES);
1154
+
1155
+ for (const result of modelNames.values()) {
1156
+ reservedNames.add(result.name);
1157
+ }
1158
+ for (const enumPslName of enumNameMap.values()) {
1159
+ reservedNames.add(enumPslName);
1160
+ }
1161
+
1162
+ return reservedNames;
1163
+ }
1164
+
1165
+ function seedNamedTypeRegistry(
1166
+ schemaIR: SqlSchemaIR,
1167
+ typeMap: PslTypeMap,
1168
+ enumNameMap: ReadonlyMap<string, string>,
1169
+ reservedNames: ReadonlySet<string>,
1170
+ ): NamedTypeRegistry {
1171
+ type Seed = {
1172
+ readonly baseType: string;
1173
+ readonly desiredName: string;
1174
+ readonly nativeTypeAttribute: PslNativeTypeAttribute;
1175
+ };
1176
+
1177
+ const seeds = new Map<string, Seed>();
1178
+
1179
+ for (const tableName of Object.keys(schemaIR.tables).sort()) {
1180
+ const table = schemaIR.tables[tableName];
1181
+ if (!table) {
1182
+ continue;
1183
+ }
1184
+
1185
+ for (const columnName of Object.keys(table.columns).sort()) {
1186
+ const column = table.columns[columnName];
1187
+ if (!column) {
1188
+ continue;
1189
+ }
1190
+
1191
+ const resolution = typeMap.resolve(column.nativeType, table.annotations);
1192
+ if (
1193
+ 'unsupported' in resolution ||
1194
+ enumNameMap.has(column.nativeType) ||
1195
+ !resolution.nativeTypeAttribute
1196
+ ) {
1197
+ continue;
1198
+ }
1199
+
1200
+ const signatureKey = createNamedTypeSignatureKey(resolution);
1201
+ if (!seeds.has(signatureKey)) {
1202
+ seeds.set(signatureKey, {
1203
+ baseType: resolution.pslType,
1204
+ desiredName: toNamedTypeName(column.name),
1205
+ nativeTypeAttribute: resolution.nativeTypeAttribute,
1206
+ });
1207
+ }
1208
+ }
1209
+ }
1210
+
1211
+ const registry: NamedTypeRegistry = {
1212
+ entriesByKey: new Map<string, NamedTypeRegistryEntry>(),
1213
+ usedNames: new Set<string>(reservedNames),
1214
+ };
1215
+
1216
+ const sortedSeeds = [...seeds.entries()].sort((left, right) => {
1217
+ const desiredNameComparison = left[1].desiredName.localeCompare(right[1].desiredName);
1218
+ if (desiredNameComparison !== 0) {
1219
+ return desiredNameComparison;
1220
+ }
1221
+ return left[0].localeCompare(right[0]);
1222
+ });
1223
+
1224
+ for (const [signatureKey, seed] of sortedSeeds) {
1225
+ const name = createUniqueFieldName(seed.desiredName, registry.usedNames);
1226
+ registry.entriesByKey.set(signatureKey, {
1227
+ name,
1228
+ baseType: seed.baseType,
1229
+ nativeTypeAttribute: seed.nativeTypeAttribute,
1230
+ });
1231
+ registry.usedNames.add(name);
1232
+ }
1233
+
1234
+ return registry;
1235
+ }
1236
+
1237
+ function resolveNamedTypeName(
1238
+ registry: NamedTypeRegistry,
1239
+ resolution: {
1240
+ readonly pslType: string;
1241
+ readonly nativeType: string;
1242
+ readonly typeParams?: Record<string, unknown>;
1243
+ readonly nativeTypeAttribute?: PslNativeTypeAttribute;
1244
+ },
1245
+ ): string {
1246
+ const key = createNamedTypeSignatureKey(resolution);
1247
+ const existing = registry.entriesByKey.get(key);
1248
+ if (existing) {
1249
+ return existing.name;
1250
+ }
1251
+
1252
+ throw new Error(`Named type registry was not seeded for native type "${resolution.nativeType}"`);
1253
+ }
1254
+
1255
+ function createNamedTypeSignatureKey(resolution: {
1256
+ readonly pslType: string;
1257
+ readonly nativeType: string;
1258
+ readonly typeParams?: Record<string, unknown>;
1259
+ readonly nativeTypeAttribute?: PslNativeTypeAttribute;
1260
+ }): string {
1261
+ return JSON.stringify({
1262
+ baseType: resolution.pslType,
1263
+ nativeTypeAttribute: resolution.nativeTypeAttribute
1264
+ ? {
1265
+ name: resolution.nativeTypeAttribute.name,
1266
+ args: resolution.nativeTypeAttribute.args ?? null,
1267
+ }
1268
+ : null,
1269
+ });
1270
+ }
1271
+
1272
+ function topologicalSort(
1273
+ models: PslModel[],
1274
+ tables: Record<string, SqlTableIR>,
1275
+ modelNameMap: ReadonlyMap<string, string>,
1276
+ ): PslModel[] {
1277
+ const modelByName = new Map<string, PslModel>();
1278
+ for (const model of models) {
1279
+ modelByName.set(model.name, model);
1280
+ }
1281
+
1282
+ const deps = new Map<string, Set<string>>();
1283
+ const tableToModel = new Map<string, string>();
1284
+ for (const tableName of Object.keys(tables)) {
1285
+ const modelName = modelNameMap.get(tableName) as string;
1286
+ tableToModel.set(tableName, modelName);
1287
+ deps.set(modelName, new Set());
1288
+ }
1289
+
1290
+ for (const [tableName, table] of Object.entries(tables)) {
1291
+ const modelName = tableToModel.get(tableName) as string;
1292
+ for (const fk of table.foreignKeys) {
1293
+ const refModelName = tableToModel.get(fk.referencedTable);
1294
+ if (refModelName && refModelName !== modelName) {
1295
+ (deps.get(modelName) as Set<string>).add(refModelName);
1296
+ }
1297
+ }
1298
+ }
1299
+
1300
+ const result: PslModel[] = [];
1301
+ const visited = new Set<string>();
1302
+ const visiting = new Set<string>();
1303
+
1304
+ const sortedNames = [...deps.keys()].sort();
1305
+
1306
+ function visit(name: string): void {
1307
+ if (visited.has(name)) return;
1308
+ if (visiting.has(name)) return;
1309
+ visiting.add(name);
1310
+
1311
+ const sortedDeps = [...(deps.get(name) as Set<string>)].sort();
1312
+ for (const dep of sortedDeps) {
1313
+ visit(dep);
1314
+ }
1315
+
1316
+ visiting.delete(name);
1317
+ visited.add(name);
1318
+ result.push(modelByName.get(name) as PslModel);
1319
+ }
1320
+
1321
+ for (const name of sortedNames) {
1322
+ visit(name);
1323
+ }
1324
+
1325
+ return result;
1326
+ }