@prisma-next/target-postgres 0.14.0-dev.8 → 0.14.0-dev.81

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 (266) hide show
  1. package/dist/authoring-ChmUI_tC.mjs +674 -0
  2. package/dist/authoring-ChmUI_tC.mjs.map +1 -0
  3. package/dist/canonicalize-D1F4vUnP.d.mts +48 -0
  4. package/dist/canonicalize-D1F4vUnP.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-BemGS69L.mjs +927 -0
  15. package/dist/codecs-BemGS69L.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 +23 -7
  24. package/dist/contract-free.mjs.map +1 -1
  25. package/dist/control-policy-BfQLCrha.mjs +1285 -0
  26. package/dist/control-policy-BfQLCrha.mjs.map +1 -0
  27. package/dist/control.d.mts +2 -2
  28. package/dist/control.d.mts.map +1 -1
  29. package/dist/control.mjs +866 -36
  30. package/dist/control.mjs.map +1 -1
  31. package/dist/data-transform-BOWpliq8.mjs.map +1 -1
  32. package/dist/{data-transform-DDgWdB5o.d.mts → data-transform-DPzwBMhW.d.mts} +3 -3
  33. package/dist/{data-transform-DDgWdB5o.d.mts.map → data-transform-DPzwBMhW.d.mts.map} +1 -1
  34. package/dist/data-transform.d.mts +1 -1
  35. package/dist/{ddl-QDyOSeLc.mjs → ddl-eBZav_MS.mjs} +128 -6
  36. package/dist/ddl-eBZav_MS.mjs.map +1 -0
  37. package/dist/ddl.d.mts +3 -2
  38. package/dist/ddl.mjs +2 -2
  39. package/dist/default-normalizer.d.mts +1 -1
  40. package/dist/default-normalizer.d.mts.map +1 -1
  41. package/dist/default-normalizer.mjs +247 -1
  42. package/dist/default-normalizer.mjs.map +1 -0
  43. package/dist/descriptor-meta-DAwRBZpA.mjs +23 -0
  44. package/dist/descriptor-meta-DAwRBZpA.mjs.map +1 -0
  45. package/dist/descriptor-meta-runtime-UwYdV8fD.mjs +12 -0
  46. package/dist/descriptor-meta-runtime-UwYdV8fD.mjs.map +1 -0
  47. package/dist/diff-database-schema-jsEOfSYT.mjs +305 -0
  48. package/dist/diff-database-schema-jsEOfSYT.mjs.map +1 -0
  49. package/dist/diff-database-schema.d.mts +46 -0
  50. package/dist/diff-database-schema.d.mts.map +1 -0
  51. package/dist/diff-database-schema.mjs +2 -0
  52. package/dist/errors-DO-w0EUS.mjs +35 -0
  53. package/dist/errors-DO-w0EUS.mjs.map +1 -0
  54. package/dist/errors.d.mts +11 -9
  55. package/dist/errors.d.mts.map +1 -1
  56. package/dist/errors.mjs +1 -1
  57. package/dist/issue-planner.d.mts +35 -11
  58. package/dist/issue-planner.d.mts.map +1 -1
  59. package/dist/issue-planner.mjs +2 -2
  60. package/dist/migration.d.mts +5 -5
  61. package/dist/migration.d.mts.map +1 -1
  62. package/dist/migration.mjs +4 -4
  63. package/dist/namespace-ids-xtp_ZY86.mjs +7 -0
  64. package/dist/namespace-ids-xtp_ZY86.mjs.map +1 -0
  65. package/dist/native-type-normalizer.mjs +37 -1
  66. package/dist/native-type-normalizer.mjs.map +1 -0
  67. package/dist/nodes-DJC5B8Id.mjs +192 -0
  68. package/dist/nodes-DJC5B8Id.mjs.map +1 -0
  69. package/dist/nodes-Dv5eCSq9.d.mts +170 -0
  70. package/dist/nodes-Dv5eCSq9.d.mts.map +1 -0
  71. package/dist/{op-factory-call-DmQEc3XV.d.mts → op-factory-call-Bn9zDx4_.d.mts} +114 -15
  72. package/dist/op-factory-call-Bn9zDx4_.d.mts.map +1 -0
  73. package/dist/{op-factory-call-D_p5vxwt.mjs → op-factory-call-Dwsb1xzl.mjs} +469 -32
  74. package/dist/op-factory-call-Dwsb1xzl.mjs.map +1 -0
  75. package/dist/op-factory-call.d.mts +2 -2
  76. package/dist/op-factory-call.mjs +2 -2
  77. package/dist/pack.d.mts +321 -3
  78. package/dist/pack.d.mts.map +1 -1
  79. package/dist/pack.mjs +1 -1
  80. package/dist/planner-ChCtpEGe.mjs +390 -0
  81. package/dist/planner-ChCtpEGe.mjs.map +1 -0
  82. package/dist/{planner-ddl-builders-B2wOwLqI.mjs → planner-ddl-builders-BKlmGDJl.mjs} +17 -28
  83. package/dist/planner-ddl-builders-BKlmGDJl.mjs.map +1 -0
  84. package/dist/planner-ddl-builders.d.mts +4 -6
  85. package/dist/planner-ddl-builders.d.mts.map +1 -1
  86. package/dist/planner-ddl-builders.mjs +2 -2
  87. package/dist/planner-identity-values-CJPha2Sz.mjs.map +1 -1
  88. package/dist/planner-identity-values.d.mts +8 -2
  89. package/dist/planner-identity-values.d.mts.map +1 -1
  90. package/dist/planner-identity-values.mjs +2 -2
  91. package/dist/{planner-produced-postgres-migration-QqHa2C2l.d.mts → planner-produced-postgres-migration-BEY1T0Xm.d.mts} +4 -4
  92. package/dist/{planner-produced-postgres-migration-QqHa2C2l.d.mts.map → planner-produced-postgres-migration-BEY1T0Xm.d.mts.map} +1 -1
  93. package/dist/{planner-produced-postgres-migration-Cji5vxUf.mjs → planner-produced-postgres-migration-Bb16yHNX.mjs} +3 -3
  94. package/dist/{planner-produced-postgres-migration-Cji5vxUf.mjs.map → planner-produced-postgres-migration-Bb16yHNX.mjs.map} +1 -1
  95. package/dist/planner-produced-postgres-migration.d.mts +1 -1
  96. package/dist/planner-produced-postgres-migration.mjs +1 -1
  97. package/dist/{planner-sql-checks-jqUUGyQR.mjs → planner-sql-checks-Cx0ATsPc.mjs} +5 -5
  98. package/dist/{planner-sql-checks-jqUUGyQR.mjs.map → planner-sql-checks-Cx0ATsPc.mjs.map} +1 -1
  99. package/dist/planner-sql-checks.d.mts +2 -2
  100. package/dist/planner-sql-checks.d.mts.map +1 -1
  101. package/dist/planner-sql-checks.mjs +1 -1
  102. package/dist/{planner-target-details-CIY6tLeo.d.mts → planner-target-details-HkP4RrRO.d.mts} +2 -2
  103. package/dist/planner-target-details-HkP4RrRO.d.mts.map +1 -0
  104. package/dist/planner-target-details.d.mts +1 -1
  105. package/dist/planner.d.mts +72 -17
  106. package/dist/planner.d.mts.map +1 -1
  107. package/dist/planner.mjs +3 -2
  108. package/dist/{postgres-contract-serializer-k3TAcPMY.mjs → postgres-contract-view-BiDhl5Ez.mjs} +45 -41
  109. package/dist/postgres-contract-view-BiDhl5Ez.mjs.map +1 -0
  110. package/dist/postgres-contract-view-C9j5cqP0.d.mts +34 -0
  111. package/dist/postgres-contract-view-C9j5cqP0.d.mts.map +1 -0
  112. package/dist/postgres-database-schema-node-CJv5bfbs.d.mts +262 -0
  113. package/dist/postgres-database-schema-node-CJv5bfbs.d.mts.map +1 -0
  114. package/dist/{postgres-migration-B5jKrXv3.mjs → postgres-migration-Cj5Tpj1a.mjs} +78 -45
  115. package/dist/postgres-migration-Cj5Tpj1a.mjs.map +1 -0
  116. package/dist/{postgres-migration-Y4YBJqkS.d.mts → postgres-migration-DbckK1su.d.mts} +58 -9
  117. package/dist/postgres-migration-DbckK1su.d.mts.map +1 -0
  118. package/dist/postgres-native-enum-BwVNf0Ku.mjs +45 -0
  119. package/dist/postgres-native-enum-BwVNf0Ku.mjs.map +1 -0
  120. package/dist/postgres-rls-policy--CSddvMR.d.mts +52 -0
  121. package/dist/postgres-rls-policy--CSddvMR.d.mts.map +1 -0
  122. package/dist/postgres-role-schema-node-Cm-j0ZGM.mjs +190 -0
  123. package/dist/postgres-role-schema-node-Cm-j0ZGM.mjs.map +1 -0
  124. package/dist/{postgres-schema-COGZ1ark.mjs → postgres-schema-ChBj22nX.mjs} +200 -11
  125. package/dist/postgres-schema-ChBj22nX.mjs.map +1 -0
  126. package/dist/postgres-schema-DKq6s1P3.d.mts +269 -0
  127. package/dist/postgres-schema-DKq6s1P3.d.mts.map +1 -0
  128. package/dist/postgres-table-schema-node-D6LvInCe.mjs +216 -0
  129. package/dist/postgres-table-schema-node-D6LvInCe.mjs.map +1 -0
  130. package/dist/render-ops.d.mts +1 -1
  131. package/dist/{render-typescript-KMgosran.mjs → render-typescript-7yqILcwr.mjs} +45 -16
  132. package/dist/render-typescript-7yqILcwr.mjs.map +1 -0
  133. package/dist/render-typescript.mjs +1 -1
  134. package/dist/resolve-ddl-schema-BCSMHFwl.mjs +22 -0
  135. package/dist/resolve-ddl-schema-BCSMHFwl.mjs.map +1 -0
  136. package/dist/rls-canonicalize.d.mts +27 -0
  137. package/dist/rls-canonicalize.d.mts.map +1 -0
  138. package/dist/rls-canonicalize.mjs +2 -0
  139. package/dist/runtime.d.mts +6 -8
  140. package/dist/runtime.d.mts.map +1 -1
  141. package/dist/runtime.mjs +3 -3
  142. package/dist/runtime.mjs.map +1 -1
  143. package/dist/schema-ir-annotations.d.mts +15 -0
  144. package/dist/schema-ir-annotations.d.mts.map +1 -0
  145. package/dist/schema-ir-annotations.mjs +2 -0
  146. package/dist/{sql-utils-DcfMz4MQ.mjs → sql-utils-SU4FDvIV.mjs} +12 -3
  147. package/dist/{sql-utils-DcfMz4MQ.mjs.map → sql-utils-SU4FDvIV.mjs.map} +1 -1
  148. package/dist/sql-utils.d.mts +8 -1
  149. package/dist/sql-utils.d.mts.map +1 -1
  150. package/dist/sql-utils.mjs +2 -2
  151. package/dist/types.d.mts +4 -140
  152. package/dist/types.mjs +5 -2
  153. package/dist/wire-name-DbQ9-2RC.mjs +104 -0
  154. package/dist/wire-name-DbQ9-2RC.mjs.map +1 -0
  155. package/package.json +26 -20
  156. package/src/contract-free/checks.ts +153 -0
  157. package/src/contract-free/control-bootstrap.ts +20 -3
  158. package/src/contract-free/ddl.ts +98 -1
  159. package/src/core/authoring.ts +726 -2
  160. package/src/core/codec-helpers.ts +33 -7
  161. package/src/core/codec-ids.ts +1 -0
  162. package/src/core/codec-type-map.ts +2 -0
  163. package/src/core/codecs.ts +193 -12
  164. package/src/core/ddl/nodes.ts +185 -2
  165. package/src/core/default-normalizer.ts +122 -2
  166. package/src/core/descriptor-meta.ts +12 -1
  167. package/src/core/entity-kinds.ts +41 -0
  168. package/src/core/errors.ts +14 -12
  169. package/src/core/migrations/bound-schema.ts +10 -0
  170. package/src/core/migrations/column-ddl-rendering.ts +117 -0
  171. package/src/core/migrations/contract-to-postgres-database-schema-node.ts +189 -0
  172. package/src/core/migrations/control-policy.ts +217 -65
  173. package/src/core/migrations/diff-database-schema.ts +252 -0
  174. package/src/core/migrations/issue-planner.ts +817 -656
  175. package/src/core/migrations/op-factory-call.ts +383 -23
  176. package/src/core/migrations/operations/columns.ts +24 -12
  177. package/src/core/migrations/operations/constraints.ts +1 -1
  178. package/src/core/migrations/operations/data-transform.ts +6 -0
  179. package/src/core/migrations/operations/dependencies.ts +2 -1
  180. package/src/core/migrations/operations/native-enum-types.ts +62 -0
  181. package/src/core/migrations/operations/rls.ts +187 -0
  182. package/src/core/migrations/operations/shared.ts +0 -30
  183. package/src/core/migrations/planner-ddl-builders.ts +31 -59
  184. package/src/core/migrations/planner-identity-values.ts +1 -1
  185. package/src/core/migrations/planner-recipes.ts +31 -13
  186. package/src/core/migrations/planner-sql-checks.ts +1 -1
  187. package/src/core/migrations/planner-strategies.ts +248 -216
  188. package/src/core/migrations/planner-target-details.ts +3 -1
  189. package/src/core/migrations/planner.ts +357 -67
  190. package/src/core/migrations/postgres-migration.ts +118 -86
  191. package/src/core/migrations/render-typescript.ts +42 -15
  192. package/src/core/migrations/resolve-ddl-schema.ts +26 -0
  193. package/src/core/migrations/runner.ts +11 -16
  194. package/src/core/migrations/verify-postgres-namespaces.ts +46 -31
  195. package/src/core/namespace-ids.ts +3 -0
  196. package/src/core/postgres-contract-serializer.ts +46 -71
  197. package/src/core/postgres-contract-view.ts +48 -0
  198. package/src/core/postgres-native-enum.ts +61 -0
  199. package/src/core/postgres-rls-enablement.ts +34 -0
  200. package/src/core/postgres-rls-policy.ts +64 -0
  201. package/src/core/postgres-role.ts +59 -0
  202. package/src/core/postgres-schema-verifier.ts +10 -7
  203. package/src/core/postgres-schema.ts +85 -15
  204. package/src/core/postgres-validators.ts +34 -0
  205. package/src/core/psl-infer/infer-psl-contract.ts +1273 -0
  206. package/src/core/psl-infer/postgres-default-mapping.ts +16 -0
  207. package/src/core/psl-infer/postgres-type-map.ts +128 -0
  208. package/src/core/rls/canonicalize.ts +65 -0
  209. package/src/core/rls/wire-name.ts +33 -0
  210. package/src/core/schema-ir/node-storage-coordinate.ts +26 -0
  211. package/src/core/schema-ir/postgres-database-schema-node.ts +61 -0
  212. package/src/core/schema-ir/postgres-namespace-schema-node.ts +70 -0
  213. package/src/core/schema-ir/postgres-native-enum-schema-node.ts +83 -0
  214. package/src/core/schema-ir/postgres-policy-schema-node.ts +90 -0
  215. package/src/core/schema-ir/postgres-role-schema-node.ts +65 -0
  216. package/src/core/schema-ir/postgres-table-schema-node.ts +140 -0
  217. package/src/core/schema-ir/schema-node-kinds.ts +122 -0
  218. package/src/core/sql-utils.ts +15 -2
  219. package/src/exports/codecs.ts +3 -0
  220. package/src/exports/contract-free.ts +11 -1
  221. package/src/exports/control.ts +27 -33
  222. package/src/exports/ddl.ts +8 -0
  223. package/src/exports/diff-database-schema.ts +7 -0
  224. package/src/exports/issue-planner.ts +1 -1
  225. package/src/exports/migration.ts +1 -0
  226. package/src/exports/op-factory-call.ts +4 -0
  227. package/src/exports/planner-ddl-builders.ts +0 -2
  228. package/src/exports/planner-identity-values.ts +4 -1
  229. package/src/exports/planner.ts +1 -0
  230. package/src/exports/rls-canonicalize.ts +12 -0
  231. package/src/exports/runtime.ts +1 -0
  232. package/src/exports/schema-ir-annotations.ts +1 -0
  233. package/src/exports/sql-utils.ts +1 -0
  234. package/src/exports/types.ts +37 -0
  235. package/dist/codec-ids-BvytN2P8.mjs.map +0 -1
  236. package/dist/codec-ids-CnXu9Qy3.d.mts.map +0 -1
  237. package/dist/codec-types-DHCkwPKE.d.mts.map +0 -1
  238. package/dist/codecs--0A5_4Bq.d.mts.map +0 -1
  239. package/dist/ddl-QDyOSeLc.mjs.map +0 -1
  240. package/dist/default-normalizer-DyyCHQWs.mjs +0 -131
  241. package/dist/default-normalizer-DyyCHQWs.mjs.map +0 -1
  242. package/dist/descriptor-meta-CpGygXpI.mjs +0 -140
  243. package/dist/descriptor-meta-CpGygXpI.mjs.map +0 -1
  244. package/dist/errors-CUk87ByX.mjs +0 -33
  245. package/dist/errors-CUk87ByX.mjs.map +0 -1
  246. package/dist/issue-planner-DL6g3CmE.mjs +0 -824
  247. package/dist/issue-planner-DL6g3CmE.mjs.map +0 -1
  248. package/dist/native-type-normalizer-Bc9XJzWC.mjs +0 -38
  249. package/dist/native-type-normalizer-Bc9XJzWC.mjs.map +0 -1
  250. package/dist/nodes-Bbhs2rwj.mjs +0 -76
  251. package/dist/nodes-Bbhs2rwj.mjs.map +0 -1
  252. package/dist/nodes-pLeLgdis.d.mts +0 -67
  253. package/dist/nodes-pLeLgdis.d.mts.map +0 -1
  254. package/dist/op-factory-call-D_p5vxwt.mjs.map +0 -1
  255. package/dist/op-factory-call-DmQEc3XV.d.mts.map +0 -1
  256. package/dist/planner-Bs_baQax.mjs +0 -324
  257. package/dist/planner-Bs_baQax.mjs.map +0 -1
  258. package/dist/planner-ddl-builders-B2wOwLqI.mjs.map +0 -1
  259. package/dist/planner-target-details-CIY6tLeo.d.mts.map +0 -1
  260. package/dist/postgres-contract-serializer-k3TAcPMY.mjs.map +0 -1
  261. package/dist/postgres-migration-B5jKrXv3.mjs.map +0 -1
  262. package/dist/postgres-migration-Y4YBJqkS.d.mts.map +0 -1
  263. package/dist/postgres-schema-COGZ1ark.mjs.map +0 -1
  264. package/dist/render-typescript-KMgosran.mjs.map +0 -1
  265. package/dist/types.d.mts.map +0 -1
  266. package/src/core/postgres-enum-type-schema.ts +0 -17
@@ -1,13 +1,407 @@
1
1
  import { temporalAuthoringPresets } from '@prisma-next/family-sql/control';
2
2
  import type {
3
+ AuthoringEntityContext,
4
+ AuthoringEntityTypeFactoryOutput,
3
5
  AuthoringEntityTypeNamespace,
4
6
  AuthoringFieldNamespace,
7
+ AuthoringModelAttributeContext,
8
+ AuthoringModelAttributeDescriptorNamespace,
9
+ AuthoringPslBlockDescriptorNamespace,
5
10
  AuthoringTypeNamespace,
11
+ PslExtensionBlock,
6
12
  } from '@prisma-next/framework-components/authoring';
13
+ import { UNBOUND_NAMESPACE_ID } from '@prisma-next/framework-components/ir';
14
+ import { modelAttribute } from '@prisma-next/psl-parser';
15
+ import type {
16
+ EntityHandleLoweringInput,
17
+ LoweredPackEntity,
18
+ ResolvedEntityHandleRef,
19
+ ResolvedPslModelRefs,
20
+ } from '@prisma-next/sql-contract/entity-handle-lowering-hook';
21
+ import type { SqlValueSetDerivingEntityTypeOutput } from '@prisma-next/sql-contract/value-set-derivation-hook';
22
+ import { assertDefined } from '@prisma-next/utils/assertions';
23
+ import { blindCast } from '@prisma-next/utils/casts';
24
+ import { ifDefined } from '@prisma-next/utils/defined';
25
+ import { PG_ENUM_CODEC_ID } from './codec-ids';
26
+ import { PostgresNativeEnum } from './postgres-native-enum';
27
+ import { PostgresRlsEnablement, type PostgresRlsEnablementInput } from './postgres-rls-enablement';
28
+ import { PostgresRlsPolicy, type RlsPolicyOperation } from './postgres-rls-policy';
29
+ import { PostgresRole } from './postgres-role';
30
+ import {
31
+ PostgresNativeEnumSchema,
32
+ PostgresRlsEnablementSchema,
33
+ PostgresRlsPolicySchema,
34
+ PostgresRoleSchema,
35
+ } from './postgres-validators';
36
+ import {
37
+ computeContentHash,
38
+ normalizePredicate,
39
+ POLICY_OPERATION_PREDICATES,
40
+ } from './rls/canonicalize';
41
+ import { formatRlsPolicyWireName } from './rls/wire-name';
42
+
43
+ /**
44
+ * `pg.enum(<ref>)` registers as an ordinary type constructor whose sole
45
+ * positional argument names a `native_enum` entity instead of carrying a
46
+ * literal value. The interpreter resolves the ref to the `native_enum`
47
+ * entity generically (driven by `entityRefArg`); the `pg/enum@1` codec
48
+ * descriptor's `columnFromEntity` hook (see `codecs.ts`) converts that
49
+ * entity into the column's `typeParams` and native type.
50
+ */
51
+ export const postgresAuthoringTypes = {
52
+ pg: {
53
+ enum: {
54
+ kind: 'typeConstructor',
55
+ entityRefArg: { index: 0, entityKind: 'native_enum' },
56
+ output: {
57
+ codecId: PG_ENUM_CODEC_ID,
58
+ },
59
+ },
60
+ },
61
+ } as const satisfies AuthoringTypeNamespace;
7
62
 
8
- export const postgresAuthoringTypes = {} as const satisfies AuthoringTypeNamespace;
63
+ export interface RlsPolicyExtensionBlock extends PslExtensionBlock {
64
+ readonly namespaceId: string;
65
+ /**
66
+ * Model refs the family interpreter resolved from the block's descriptor-
67
+ * declared `refKind: 'model'` parameters before invoking this factory
68
+ * (keyed by parameter name). An unresolved required ref is the
69
+ * interpreter's diagnostic; the factory is never called with one missing.
70
+ */
71
+ readonly resolvedModelRefs?: ResolvedPslModelRefs;
72
+ }
9
73
 
10
- export const postgresAuthoringEntityTypes = {} as const satisfies AuthoringEntityTypeNamespace;
74
+ /** A parsed `role` block annotated with its lexical namespace id by the interpreter. */
75
+ export interface RoleExtensionBlock extends PslExtensionBlock {
76
+ readonly namespaceId: string;
77
+ }
78
+
79
+ /**
80
+ * Maps a `policy_<op>` keyword to the RLS operation it authors. The keyword
81
+ * IS the operation (per the project's rejection of a `policy { operation = … }`
82
+ * conditional block).
83
+ */
84
+ const POLICY_KEYWORD_OPERATION: Readonly<Record<string, RlsPolicyOperation>> = {
85
+ policy_select: 'select',
86
+ policy_insert: 'insert',
87
+ policy_update: 'update',
88
+ policy_delete: 'delete',
89
+ policy_all: 'all',
90
+ };
91
+
92
+ function readValueParam(block: PslExtensionBlock, key: string): string | undefined {
93
+ const param = block.parameters[key];
94
+ return param?.kind === 'value' ? param.raw : undefined;
95
+ }
96
+
97
+ function readListRefParams(block: PslExtensionBlock, key: string): string[] {
98
+ const param = block.parameters[key];
99
+ if (param?.kind !== 'list') return [];
100
+ return param.items.flatMap((item) => (item.kind === 'ref' ? [item.identifier] : []));
101
+ }
102
+
103
+ /**
104
+ * Unwraps a quoted PSL string argument, inverting the printer's
105
+ * `escapePslString` escapes (`\\`, `\"`, `\n`, `\r`). An unknown escape
106
+ * sequence is kept verbatim, matching the printer-side `unescapePslString`
107
+ * convention.
108
+ */
109
+ function unwrapQuotedString(raw: string): string {
110
+ if (!(raw.startsWith('"') && raw.endsWith('"') && raw.length >= 2)) {
111
+ return raw;
112
+ }
113
+ const inner = raw.slice(1, -1);
114
+ let result = '';
115
+ for (let i = 0; i < inner.length; i++) {
116
+ if (inner[i] !== '\\' || i + 1 >= inner.length) {
117
+ result += inner[i];
118
+ continue;
119
+ }
120
+ const next = inner[i + 1];
121
+ if (next === '\\' || next === '"') {
122
+ result += next;
123
+ } else if (next === 'n') {
124
+ result += '\n';
125
+ } else if (next === 'r') {
126
+ result += '\r';
127
+ } else {
128
+ result += '\\';
129
+ result += next;
130
+ }
131
+ i++;
132
+ }
133
+ return result;
134
+ }
135
+
136
+ /**
137
+ * Assembles a {@link PostgresRlsPolicy} from lowered inputs: normalizes the
138
+ * predicates, computes the content-hash wire name, and constructs the frozen
139
+ * entity. The single hash-assembly body shared by the PSL block lowering
140
+ * ({@link lowerRlsPolicyFromBlock}) and the TS entity-handle lowering
141
+ * ({@link postgresLowerEntityHandles}) so the two surfaces cannot drift.
142
+ */
143
+ function buildRlsPolicyEntity(input: {
144
+ readonly prefix: string;
145
+ readonly tableName: string;
146
+ readonly namespaceId: string;
147
+ readonly operation: RlsPolicyOperation;
148
+ readonly roles: readonly string[];
149
+ readonly using?: string;
150
+ readonly withCheck?: string;
151
+ }): PostgresRlsPolicy {
152
+ const wireHash = computeContentHash({
153
+ ...ifDefined('using', input.using !== undefined ? normalizePredicate(input.using) : undefined),
154
+ ...ifDefined(
155
+ 'withCheck',
156
+ input.withCheck !== undefined ? normalizePredicate(input.withCheck) : undefined,
157
+ ),
158
+ roles: input.roles,
159
+ operation: input.operation,
160
+ permissive: true,
161
+ });
162
+
163
+ return new PostgresRlsPolicy({
164
+ name: formatRlsPolicyWireName(input.prefix, wireHash),
165
+ prefix: input.prefix,
166
+ tableName: input.tableName,
167
+ namespaceId: input.namespaceId,
168
+ operation: input.operation,
169
+ roles: input.roles,
170
+ ...ifDefined('using', input.using),
171
+ ...ifDefined('withCheck', input.withCheck),
172
+ permissive: true,
173
+ });
174
+ }
175
+
176
+ function lowerRlsPolicyFromBlock(
177
+ block: RlsPolicyExtensionBlock,
178
+ ctx: AuthoringEntityContext,
179
+ ): PostgresRlsPolicy | undefined {
180
+ const prefix = block.name;
181
+ const operation = POLICY_KEYWORD_OPERATION[block.keyword] ?? 'select';
182
+ // The interpreter resolves the descriptor-declared `target` model ref to
183
+ // its storage table name before invoking this factory (an unresolved or
184
+ // missing required ref is the interpreter's diagnostic), so a lookup miss
185
+ // here is structurally impossible.
186
+ const tableName = block.resolvedModelRefs?.['target']?.tableName;
187
+ assertDefined(
188
+ tableName,
189
+ `lowerRlsPolicyFromBlock: policy "${block.name}" reached the factory without a resolved \`target\` ref; the interpreter resolves same-namespace model refs before invoking entity factories.`,
190
+ );
191
+ const roles = [...readListRefParams(block, 'roles')].sort();
192
+
193
+ const usingRaw = readValueParam(block, 'using');
194
+ const withCheckRaw = readValueParam(block, 'withCheck');
195
+
196
+ // Reject a predicate the operation does not take (e.g. `using` on INSERT, or
197
+ // `withCheck` on SELECT/DELETE). The descriptor's param set already omits it,
198
+ // but the generic descriptor validator is not wired into the SQL-family
199
+ // interpreter, so the lowering enforces the per-operation predicate matrix
200
+ // directly — a wrong predicate is a load-time diagnostic, not a silent drop.
201
+ const support = POLICY_OPERATION_PREDICATES[operation];
202
+ const rejectPredicate = (predicate: 'using' | 'withCheck'): undefined => {
203
+ ctx.diagnostics?.push({
204
+ code: 'PSL_RLS_PREDICATE_NOT_FOR_OPERATION',
205
+ message: `\`${block.keyword}\` policy "${block.name}" does not take a \`${predicate}\` predicate; the ${operation.toUpperCase()} operation uses ${support.using ? '`using`' : '`withCheck`'}${support.using && support.withCheck ? ' and `withCheck`' : ' only'}.`,
206
+ sourceId: ctx.sourceId ?? 'unknown',
207
+ span: block.parameters[predicate]?.span ?? block.span,
208
+ });
209
+ return undefined;
210
+ };
211
+ if (usingRaw !== undefined && !support.using) return rejectPredicate('using');
212
+ if (withCheckRaw !== undefined && !support.withCheck) return rejectPredicate('withCheck');
213
+
214
+ const using = usingRaw !== undefined ? unwrapQuotedString(usingRaw) : undefined;
215
+ const withCheck = withCheckRaw !== undefined ? unwrapQuotedString(withCheckRaw) : undefined;
216
+
217
+ return buildRlsPolicyEntity({
218
+ prefix,
219
+ tableName,
220
+ namespaceId: block.namespaceId,
221
+ operation,
222
+ roles,
223
+ ...ifDefined('using', using),
224
+ ...ifDefined('withCheck', withCheck),
225
+ });
226
+ }
227
+
228
+ /**
229
+ * Lowers a `native_enum { memberName = "value" … @@map("type_name") }` block
230
+ * into a {@link PostgresNativeEnum}. Members must be authored as explicit
231
+ * `key = "value"` pairs — a bare (value-less) member is a diagnostic, not
232
+ * accepted (authoring-design.md §2.1). The parsed `memberName` is only used
233
+ * to duplicate-check and report diagnostics; the lowered entity carries just
234
+ * the member values (a native enum is value-only — the member "name" isn't
235
+ * a separate authoring concept from the value). `typeName` comes from
236
+ * `@@map` or defaults to the block name verbatim.
237
+ */
238
+ function lowerNativeEnumFromBlock(
239
+ block: PslExtensionBlock,
240
+ ctx: AuthoringEntityContext,
241
+ ): PostgresNativeEnum | undefined {
242
+ const sourceId = ctx.sourceId ?? 'unknown';
243
+ const diagnostics = ctx.diagnostics;
244
+
245
+ const mapAttr = block.blockAttributes.find((a) => a.name === 'map');
246
+ let typeName = block.name;
247
+ if (mapAttr) {
248
+ const rawArg = mapAttr.args[0]?.value;
249
+ const mapped = rawArg !== undefined ? unwrapQuotedString(rawArg) : undefined;
250
+ if (mapped === undefined) {
251
+ diagnostics?.push({
252
+ code: 'PSL_NATIVE_ENUM_INVALID_MAP',
253
+ message: `native_enum "${block.name}" @@map attribute must have a quoted type-name argument`,
254
+ sourceId,
255
+ span: mapAttr.span,
256
+ });
257
+ return undefined;
258
+ }
259
+ typeName = mapped;
260
+ }
261
+
262
+ let memberError = false;
263
+ const seenValues = new Set<string>();
264
+ const members: string[] = [];
265
+ for (const [memberName, paramValue] of Object.entries(block.parameters)) {
266
+ if (paramValue.kind === 'bare') {
267
+ diagnostics?.push({
268
+ code: 'PSL_NATIVE_ENUM_BARE_MEMBER',
269
+ message: `native_enum "${block.name}" member "${memberName}" has no value; members must be authored as "${memberName} = \\"value\\""`,
270
+ sourceId,
271
+ span: paramValue.span,
272
+ });
273
+ memberError = true;
274
+ continue;
275
+ }
276
+ if (paramValue.kind !== 'value') continue;
277
+
278
+ let jsonValue: unknown;
279
+ try {
280
+ jsonValue = JSON.parse(paramValue.raw);
281
+ } catch {
282
+ diagnostics?.push({
283
+ code: 'PSL_EXTENSION_INVALID_VALUE',
284
+ message: `native_enum "${block.name}" member "${memberName}" value "${paramValue.raw}" is not valid JSON`,
285
+ sourceId,
286
+ span: paramValue.span,
287
+ });
288
+ memberError = true;
289
+ continue;
290
+ }
291
+ if (typeof jsonValue !== 'string') {
292
+ diagnostics?.push({
293
+ code: 'PSL_EXTENSION_INVALID_VALUE',
294
+ message: `native_enum "${block.name}" member "${memberName}" value must be a string`,
295
+ sourceId,
296
+ span: paramValue.span,
297
+ });
298
+ memberError = true;
299
+ continue;
300
+ }
301
+ if (seenValues.has(jsonValue)) {
302
+ diagnostics?.push({
303
+ code: 'PSL_NATIVE_ENUM_DUPLICATE_MEMBER_VALUE',
304
+ message: `native_enum "${block.name}": duplicate member value "${jsonValue}"`,
305
+ sourceId,
306
+ span: paramValue.span,
307
+ });
308
+ memberError = true;
309
+ continue;
310
+ }
311
+ seenValues.add(jsonValue);
312
+ members.push(jsonValue);
313
+ }
314
+
315
+ if (memberError) return undefined;
316
+
317
+ if (members.length === 0) {
318
+ diagnostics?.push({
319
+ code: 'PSL_NATIVE_ENUM_MISSING_MEMBERS',
320
+ message: `native_enum "${block.name}" must have at least one member`,
321
+ sourceId,
322
+ span: block.span,
323
+ });
324
+ return undefined;
325
+ }
326
+
327
+ // `control` stays unset — the effective grade is resolved at read time via `effectiveControlPolicy`, like `StorageTable`/`StorageColumn`.
328
+ return new PostgresNativeEnum({ typeName, members });
329
+ }
330
+
331
+ /**
332
+ * `native_enum`'s entity-type factory output, checked separately from the assembled
333
+ * `postgresAuthoringEntityTypes` map below: `deriveValueSet` is SQL-family surface
334
+ * ({@link SqlValueSetDerivingEntityTypeOutput}), not part of the framework
335
+ * `AuthoringEntityTypeFactoryOutput` shape, so folding it directly into the map's single
336
+ * `satisfies AuthoringEntityTypeNamespace` check would trip an excess-property error. Checking it
337
+ * here against the intersection of both shapes keeps it structurally valid against each without
338
+ * widening the map's own check.
339
+ */
340
+ const nativeEnumEntityTypeOutput = {
341
+ factory: lowerNativeEnumFromBlock,
342
+ deriveValueSet: (entity: PostgresNativeEnum) => ({
343
+ kind: 'valueSet' as const,
344
+ values: [...entity.members],
345
+ }),
346
+ } satisfies AuthoringEntityTypeFactoryOutput<PslExtensionBlock, PostgresNativeEnum | undefined> &
347
+ SqlValueSetDerivingEntityTypeOutput;
348
+
349
+ /**
350
+ * Lowers a `role <name> {}` block into a {@link PostgresRole}. Roles are
351
+ * cluster-scoped in Postgres, so the block must be declared inside
352
+ * `namespace unbound { … }` — any other lexical namespace (a named schema,
353
+ * or the default bucket top-level declarations resolve to) is a load-time
354
+ * diagnostic. The lowered entity carries the unbound coordinate.
355
+ */
356
+ function lowerRoleFromBlock(
357
+ block: RoleExtensionBlock,
358
+ ctx: AuthoringEntityContext,
359
+ ): PostgresRole | undefined {
360
+ if (block.namespaceId !== UNBOUND_NAMESPACE_ID) {
361
+ ctx.diagnostics?.push({
362
+ code: 'PSL_ROLE_BLOCK_OUTSIDE_UNBOUND_NAMESPACE',
363
+ message: `\`role\` block "${block.name}" must be declared inside \`namespace unbound { }\`, not in namespace "${block.namespaceId}"`,
364
+ sourceId: ctx.sourceId ?? 'unknown',
365
+ span: block.span,
366
+ });
367
+ return undefined;
368
+ }
369
+ return new PostgresRole({ name: block.name, namespaceId: UNBOUND_NAMESPACE_ID });
370
+ }
371
+
372
+ export const postgresAuthoringEntityTypes = {
373
+ role: {
374
+ kind: 'entity',
375
+ discriminator: 'role',
376
+ validatorSchema: PostgresRoleSchema,
377
+ output: {
378
+ factory: lowerRoleFromBlock,
379
+ },
380
+ },
381
+ rls: {
382
+ kind: 'entity',
383
+ discriminator: 'rls',
384
+ validatorSchema: PostgresRlsEnablementSchema,
385
+ output: {
386
+ factory: (input: PostgresRlsEnablementInput): PostgresRlsEnablement =>
387
+ new PostgresRlsEnablement(input),
388
+ },
389
+ },
390
+ policy: {
391
+ kind: 'entity',
392
+ discriminator: 'policy',
393
+ validatorSchema: PostgresRlsPolicySchema,
394
+ output: {
395
+ factory: lowerRlsPolicyFromBlock,
396
+ },
397
+ },
398
+ native_enum: {
399
+ kind: 'entity',
400
+ discriminator: 'native_enum',
401
+ validatorSchema: PostgresNativeEnumSchema,
402
+ output: nativeEnumEntityTypeOutput,
403
+ },
404
+ } as const satisfies AuthoringEntityTypeNamespace;
11
405
 
12
406
  /**
13
407
  * Field presets contributed by the Postgres target pack.
@@ -22,6 +416,153 @@ export const postgresAuthoringEntityTypes = {} as const satisfies AuthoringEntit
22
416
  * portability use `uuidString` / `id.uuidv4String` / `id.uuidv7String` from
23
417
  * the family pack instead.
24
418
  */
419
+ /**
420
+ * Shared parameter descriptors for the five `policy_<op>` PSL block
421
+ * keywords. All five share the `policy` discriminator — the parser
422
+ * dispatches by keyword (see the framework's PSL-block SPI), and every
423
+ * keyword's factory lowers to `PostgresRlsPolicy` via `lowerRlsPolicyFromBlock`.
424
+ *
425
+ * The `roles` list uses `scope:'cross-space'` because same-namespace
426
+ * role ref resolution requires PSL namespace entries keyed by `refKind`
427
+ * (i.e. `'role'`), which in turn requires the role block discriminator to
428
+ * equal `'role'`. Aligning discriminator with refKind is tracked for
429
+ * slice 4 (cross-space roles). Until then cross-space passes validation
430
+ * unconditionally and the authored role names flow through unchanged.
431
+ */
432
+ const policyTargetParam = {
433
+ kind: 'ref',
434
+ refKind: 'model',
435
+ scope: 'same-namespace',
436
+ required: true,
437
+ } as const;
438
+ const policyRolesParam = {
439
+ kind: 'list',
440
+ of: { kind: 'ref', refKind: 'role', scope: 'cross-space' },
441
+ } as const;
442
+ const policyPredicateParam = { kind: 'value', codecId: 'pg/text@1', required: true } as const;
443
+ // A policy may only target an RLS-controlled model: the model named by
444
+ // `target` must declare `@@rls`, or the load fails with a diagnostic naming
445
+ // the model and the policy prefix.
446
+ const policyRequiresRls = { parameter: 'target', attribute: 'rls' } as const;
447
+
448
+ export const postgresAuthoringPslBlockDescriptors = {
449
+ // The predicate param set per keyword mirrors Postgres: SELECT/DELETE take
450
+ // USING only; INSERT takes WITH CHECK only; UPDATE/ALL take both. The
451
+ // per-operation predicate matrix is enforced in `lowerRlsPolicyFromBlock`
452
+ // (a wrong predicate for the operation is a load-time diagnostic there),
453
+ // since the generic descriptor validator is not wired into the SQL-family
454
+ // interpreter.
455
+ policy_select: {
456
+ kind: 'pslBlock',
457
+ keyword: 'policy_select',
458
+ discriminator: 'policy',
459
+ name: { required: true },
460
+ parameters: { target: policyTargetParam, roles: policyRolesParam, using: policyPredicateParam },
461
+ requiresModelAttribute: policyRequiresRls,
462
+ },
463
+ policy_delete: {
464
+ kind: 'pslBlock',
465
+ keyword: 'policy_delete',
466
+ discriminator: 'policy',
467
+ name: { required: true },
468
+ parameters: { target: policyTargetParam, roles: policyRolesParam, using: policyPredicateParam },
469
+ requiresModelAttribute: policyRequiresRls,
470
+ },
471
+ policy_insert: {
472
+ kind: 'pslBlock',
473
+ keyword: 'policy_insert',
474
+ discriminator: 'policy',
475
+ name: { required: true },
476
+ parameters: {
477
+ target: policyTargetParam,
478
+ roles: policyRolesParam,
479
+ withCheck: policyPredicateParam,
480
+ },
481
+ requiresModelAttribute: policyRequiresRls,
482
+ },
483
+ policy_update: {
484
+ kind: 'pslBlock',
485
+ keyword: 'policy_update',
486
+ discriminator: 'policy',
487
+ name: { required: true },
488
+ parameters: {
489
+ target: policyTargetParam,
490
+ roles: policyRolesParam,
491
+ using: policyPredicateParam,
492
+ withCheck: policyPredicateParam,
493
+ },
494
+ requiresModelAttribute: policyRequiresRls,
495
+ },
496
+ policy_all: {
497
+ kind: 'pslBlock',
498
+ keyword: 'policy_all',
499
+ discriminator: 'policy',
500
+ name: { required: true },
501
+ parameters: {
502
+ target: policyTargetParam,
503
+ roles: policyRolesParam,
504
+ using: policyPredicateParam,
505
+ withCheck: policyPredicateParam,
506
+ },
507
+ requiresModelAttribute: policyRequiresRls,
508
+ },
509
+ /**
510
+ * PSL block descriptor for `native_enum`.
511
+ *
512
+ * Reuses the existing variadic-block mechanism (the same shape the SQL
513
+ * family's `enum` block ships): the body is an open `memberName = "value"`
514
+ * list. `variadicParameters: true` opens the block to arbitrary keys
515
+ * beyond the declared (empty) `parameters` set — the lowering factory
516
+ * (`lowerNativeEnumFromBlock`) turns the variadic entries into ordered
517
+ * members and rejects a bare (value-less) member.
518
+ */
519
+ native_enum: {
520
+ kind: 'pslBlock',
521
+ keyword: 'native_enum',
522
+ discriminator: 'native_enum',
523
+ name: { required: true },
524
+ parameters: {},
525
+ variadicParameters: true,
526
+ },
527
+ /**
528
+ * PSL block descriptor for `role` (e.g. `role anon {}`). Name-only, no
529
+ * parameters and no body content. Declared inside `namespace unbound { }`
530
+ * — see {@link lowerRoleFromBlock} for the placement check and the
531
+ * coordinate the lowered entity carries.
532
+ */
533
+ role: {
534
+ kind: 'pslBlock',
535
+ keyword: 'role',
536
+ discriminator: 'role',
537
+ name: { required: true },
538
+ parameters: {},
539
+ },
540
+ } as const satisfies AuthoringPslBlockDescriptorNamespace;
541
+
542
+ /**
543
+ * `@@` model attributes contributed by the Postgres target pack.
544
+ *
545
+ * `@@rls` is argument-less: presence marks the model's table
546
+ * RLS-controlled. It lowers to a {@link PostgresRlsEnablement} marker in
547
+ * the namespace's `entries.rls`, keyed by the model's table name — the
548
+ * marker (never the policy set) is what drives
549
+ * `ENABLE`/`DISABLE ROW LEVEL SECURITY` planning.
550
+ */
551
+ export const postgresAuthoringModelAttributes = {
552
+ rls: {
553
+ kind: 'modelAttribute',
554
+ attribute: 'rls',
555
+ spec: modelAttribute('rls', {}),
556
+ lower: (_parsed: Record<never, never>, ctx: AuthoringModelAttributeContext) => ({
557
+ key: ctx.storageName,
558
+ entity: new PostgresRlsEnablement({
559
+ tableName: ctx.storageName,
560
+ namespaceId: ctx.namespaceId,
561
+ }),
562
+ }),
563
+ },
564
+ } as const satisfies AuthoringModelAttributeDescriptorNamespace;
565
+
25
566
  export const postgresAuthoringFieldPresets = {
26
567
  text: {
27
568
  kind: 'fieldPreset',
@@ -128,3 +669,186 @@ export const postgresAuthoringFieldPresets = {
128
669
  },
129
670
  },
130
671
  } as const satisfies AuthoringFieldNamespace;
672
+
673
+ /**
674
+ * Postgres identifiers cap at 63 characters and the wire name appends a
675
+ * 9-character `_<8hex>` suffix, so the authored prefix is bounded at 54.
676
+ */
677
+ const RLS_POLICY_PREFIX_MAX_LENGTH = 54;
678
+
679
+ interface RlsRoleHandleShape {
680
+ readonly entityKind: 'role';
681
+ readonly name: string;
682
+ }
683
+
684
+ interface RlsPolicyHandleShape {
685
+ readonly entityKind: 'policy';
686
+ readonly operation: RlsPolicyOperation;
687
+ readonly name: string;
688
+ readonly roles: readonly { readonly name: string }[];
689
+ readonly using?: string;
690
+ readonly withCheck?: string;
691
+ }
692
+
693
+ interface RlsTargetCoordinate {
694
+ readonly namespaceId: string;
695
+ readonly tableName: string;
696
+ }
697
+
698
+ /**
699
+ * Resolves an entity handle's `target` ref to a table coordinate of this
700
+ * contract, or throws the load-time diagnostic naming the handle: a
701
+ * cross-space target is rejected (you cannot CREATE POLICY on a table
702
+ * another space owns) and an unresolved target names the model.
703
+ */
704
+ function requireLocalTarget(
705
+ refs: Readonly<Record<string, ResolvedEntityHandleRef>>,
706
+ subject: string,
707
+ ): RlsTargetCoordinate {
708
+ const target = refs['target'];
709
+ if (target !== undefined && target.kind === 'resolved') {
710
+ return { namespaceId: target.namespaceId, tableName: target.tableName };
711
+ }
712
+ if (target !== undefined && target.kind === 'cross-space') {
713
+ throw new Error(
714
+ `defineContract: ${subject} targets model "${target.modelName ?? target.tableName}", which lives in another contract space. Policies and rlsEnabled entries must target a model declared in this contract.`,
715
+ );
716
+ }
717
+ throw new Error(
718
+ `defineContract: ${subject} targets model "${target?.modelName ?? '<anonymous>'}", which is not in the contract's models. Add the model to \`models\`.`,
719
+ );
720
+ }
721
+
722
+ /**
723
+ * The SQL-family entity-handle batch lowering hook for the Postgres pack
724
+ * (`SqlEntityHandleLoweringContribution`). Receives every `entities` handle
725
+ * whose kind this pack registered — batch, so the cross-entity diagnostics
726
+ * (duplicate prefix, policy without rlsEnabled, duplicate role) can see
727
+ * sibling handles — and lowers them with the same keying and hash assembly
728
+ * as the PSL path: `policy` keyed by prefix (wire name via
729
+ * {@link buildRlsPolicyEntity}), `rls` keyed by table name, `role` keyed by
730
+ * name and filed under the default namespace (roles are declared
731
+ * contract-wide; PSL has no role block to set a precedent).
732
+ */
733
+ export function postgresLowerEntityHandles(
734
+ input: EntityHandleLoweringInput,
735
+ ): readonly LoweredPackEntity[] {
736
+ const enablements = new Map<
737
+ string,
738
+ { coordinate: RlsTargetCoordinate; entity: PostgresRlsEnablement }
739
+ >();
740
+ const roles = new Map<string, PostgresRole>();
741
+ const policies: {
742
+ readonly handle: RlsPolicyHandleShape;
743
+ readonly refs: Readonly<Record<string, ResolvedEntityHandleRef>>;
744
+ }[] = [];
745
+ const coordinateKey = (coordinate: RlsTargetCoordinate): string =>
746
+ `${coordinate.namespaceId} ${coordinate.tableName}`;
747
+
748
+ for (const { handle, refs } of input.handles) {
749
+ switch (handle.entityKind) {
750
+ case 'rls': {
751
+ const coordinate = requireLocalTarget(refs, 'an rlsEnabled entry');
752
+ const key = coordinateKey(coordinate);
753
+ if (!enablements.has(key)) {
754
+ enablements.set(key, {
755
+ coordinate,
756
+ entity: new PostgresRlsEnablement({
757
+ tableName: coordinate.tableName,
758
+ namespaceId: coordinate.namespaceId,
759
+ }),
760
+ });
761
+ }
762
+ break;
763
+ }
764
+ case 'role': {
765
+ const roleHandle = blindCast<
766
+ RlsRoleHandleShape,
767
+ 'role handles are constructed only by the postgres contract-builder role() constructor, which enforces this shape'
768
+ >(handle);
769
+ if (roles.has(roleHandle.name)) {
770
+ throw new Error(
771
+ `defineContract: role "${roleHandle.name}" is declared more than once in the entities list.`,
772
+ );
773
+ }
774
+ // Roles are cluster-scoped in Postgres, so they always land in the
775
+ // `__unbound__` namespace — identical to a PSL `role` block
776
+ // (`lowerRoleFromBlock`) and matching the `PostgresRole` class's own
777
+ // contract. The declaring surface (TS entities vs PSL) does not move
778
+ // the slot.
779
+ roles.set(
780
+ roleHandle.name,
781
+ new PostgresRole({ name: roleHandle.name, namespaceId: UNBOUND_NAMESPACE_ID }),
782
+ );
783
+ break;
784
+ }
785
+ case 'policy': {
786
+ const policyHandle = blindCast<
787
+ RlsPolicyHandleShape,
788
+ 'policy handles are constructed only by the postgres contract-builder policy*() constructors, which enforce this shape'
789
+ >(handle);
790
+ policies.push({ handle: policyHandle, refs });
791
+ break;
792
+ }
793
+ default:
794
+ throw new Error(
795
+ `defineContract: the postgres pack does not lower "${handle.entityKind}" handles from the entities list.`,
796
+ );
797
+ }
798
+ }
799
+
800
+ const rows: LoweredPackEntity[] = [];
801
+ const seenPrefixes = new Set<string>();
802
+
803
+ for (const { handle: policy, refs } of policies) {
804
+ const prefix = policy.name;
805
+ if (prefix.length > RLS_POLICY_PREFIX_MAX_LENGTH) {
806
+ throw new Error(
807
+ `defineContract: policy prefix "${prefix}" exceeds the ${RLS_POLICY_PREFIX_MAX_LENGTH}-character maximum (Postgres identifiers cap at 63 characters and the wire name appends a 9-character hash suffix).`,
808
+ );
809
+ }
810
+ const coordinate = requireLocalTarget(refs, `policy "${prefix}"`);
811
+ if (!enablements.has(coordinateKey(coordinate))) {
812
+ const target = refs['target'];
813
+ throw new Error(
814
+ `defineContract: policy "${prefix}" targets model "${target?.modelName ?? coordinate.tableName}", whose table is not RLS-enabled. Add rlsEnabled(<model>) to the entities list.`,
815
+ );
816
+ }
817
+ const prefixKey = `${coordinate.namespaceId} ${prefix}`;
818
+ if (seenPrefixes.has(prefixKey)) {
819
+ throw new Error(
820
+ `defineContract: policy prefix "${prefix}" is declared more than once in namespace "${coordinate.namespaceId}". Policy prefixes must be unique per namespace.`,
821
+ );
822
+ }
823
+ seenPrefixes.add(prefixKey);
824
+
825
+ const roleNames = [...new Set(policy.roles.map((roleHandle) => roleHandle.name))].sort();
826
+ rows.push({
827
+ namespaceId: coordinate.namespaceId,
828
+ entityKind: 'policy',
829
+ key: prefix,
830
+ entity: buildRlsPolicyEntity({
831
+ prefix,
832
+ tableName: coordinate.tableName,
833
+ namespaceId: coordinate.namespaceId,
834
+ operation: policy.operation,
835
+ roles: roleNames,
836
+ ...ifDefined('using', policy.using),
837
+ ...ifDefined('withCheck', policy.withCheck),
838
+ }),
839
+ });
840
+ }
841
+
842
+ for (const { coordinate, entity } of enablements.values()) {
843
+ rows.push({
844
+ namespaceId: coordinate.namespaceId,
845
+ entityKind: 'rls',
846
+ key: coordinate.tableName,
847
+ entity,
848
+ });
849
+ }
850
+ for (const [name, entity] of roles) {
851
+ rows.push({ namespaceId: UNBOUND_NAMESPACE_ID, entityKind: 'role', key: name, entity });
852
+ }
853
+ return rows;
854
+ }