@prisma-next/target-postgres 0.14.0-dev.4 → 0.14.0-dev.40

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 (191) hide show
  1. package/dist/authoring-BX1WHIdN.mjs +220 -0
  2. package/dist/authoring-BX1WHIdN.mjs.map +1 -0
  3. package/dist/canonicalize-Bn3BM1Cn.mjs +46 -0
  4. package/dist/canonicalize-Bn3BM1Cn.mjs.map +1 -0
  5. package/dist/canonicalize-CAbXtVtB.d.mts +37 -0
  6. package/dist/canonicalize-CAbXtVtB.d.mts.map +1 -0
  7. package/dist/{codec-types-DHCkwPKE.d.mts → codec-types-CueCoGD0.d.mts} +2 -2
  8. package/dist/{codec-types-DHCkwPKE.d.mts.map → codec-types-CueCoGD0.d.mts.map} +1 -1
  9. package/dist/codec-types.d.mts +1 -1
  10. package/dist/{codecs--0A5_4Bq.d.mts → codecs-Ca8upop6.d.mts} +12 -1
  11. package/dist/codecs-Ca8upop6.d.mts.map +1 -0
  12. package/dist/codecs.d.mts +1 -1
  13. package/dist/codecs.mjs +34 -1
  14. package/dist/codecs.mjs.map +1 -1
  15. package/dist/contract-free.d.mts +8 -3
  16. package/dist/contract-free.d.mts.map +1 -1
  17. package/dist/contract-free.mjs +3 -3
  18. package/dist/control.d.mts +1 -1
  19. package/dist/control.d.mts.map +1 -1
  20. package/dist/control.mjs +639 -21
  21. package/dist/control.mjs.map +1 -1
  22. package/dist/data-transform-BOWpliq8.mjs.map +1 -1
  23. package/dist/{data-transform-DDgWdB5o.d.mts → data-transform-qKy1U5V9.d.mts} +2 -2
  24. package/dist/{data-transform-DDgWdB5o.d.mts.map → data-transform-qKy1U5V9.d.mts.map} +1 -1
  25. package/dist/data-transform.d.mts +1 -1
  26. package/dist/{ddl-QDyOSeLc.mjs → ddl-BmLIe1z_.mjs} +62 -5
  27. package/dist/ddl-BmLIe1z_.mjs.map +1 -0
  28. package/dist/ddl.d.mts +3 -2
  29. package/dist/ddl.mjs +2 -2
  30. package/dist/{default-normalizer-DyyCHQWs.mjs → default-normalizer-Dug8Fez9.mjs} +119 -2
  31. package/dist/default-normalizer-Dug8Fez9.mjs.map +1 -0
  32. package/dist/default-normalizer.d.mts +1 -1
  33. package/dist/default-normalizer.d.mts.map +1 -1
  34. package/dist/default-normalizer.mjs +1 -1
  35. package/dist/descriptor-meta-runtime-UwYdV8fD.mjs +12 -0
  36. package/dist/descriptor-meta-runtime-UwYdV8fD.mjs.map +1 -0
  37. package/dist/descriptor-meta-tjatyhtZ.mjs +18 -0
  38. package/dist/descriptor-meta-tjatyhtZ.mjs.map +1 -0
  39. package/dist/{issue-planner-DL6g3CmE.mjs → issue-planner-DbFvaAgu.mjs} +35 -10
  40. package/dist/issue-planner-DbFvaAgu.mjs.map +1 -0
  41. package/dist/issue-planner.d.mts +3 -3
  42. package/dist/issue-planner.d.mts.map +1 -1
  43. package/dist/issue-planner.mjs +1 -1
  44. package/dist/migration.d.mts +5 -5
  45. package/dist/migration.mjs +4 -4
  46. package/dist/nodes-CSkPX3rI.d.mts +120 -0
  47. package/dist/nodes-CSkPX3rI.d.mts.map +1 -0
  48. package/dist/{nodes-Bbhs2rwj.mjs → nodes-Dh__NS58.mjs} +56 -2
  49. package/dist/nodes-Dh__NS58.mjs.map +1 -0
  50. package/dist/{op-factory-call-DmQEc3XV.d.mts → op-factory-call-CJWUxyUW.d.mts} +44 -13
  51. package/dist/op-factory-call-CJWUxyUW.d.mts.map +1 -0
  52. package/dist/{op-factory-call-D_p5vxwt.mjs → op-factory-call-t65c4jyY.mjs} +201 -28
  53. package/dist/op-factory-call-t65c4jyY.mjs.map +1 -0
  54. package/dist/op-factory-call.d.mts +1 -1
  55. package/dist/op-factory-call.mjs +1 -1
  56. package/dist/pack.d.mts +74 -2
  57. package/dist/pack.d.mts.map +1 -1
  58. package/dist/pack.mjs +1 -1
  59. package/dist/planner-CG2pLHk-.mjs +640 -0
  60. package/dist/planner-CG2pLHk-.mjs.map +1 -0
  61. package/dist/{planner-ddl-builders-B2wOwLqI.mjs → planner-ddl-builders-B8Nn6nHN.mjs} +12 -27
  62. package/dist/planner-ddl-builders-B8Nn6nHN.mjs.map +1 -0
  63. package/dist/planner-ddl-builders.d.mts +2 -4
  64. package/dist/planner-ddl-builders.d.mts.map +1 -1
  65. package/dist/planner-ddl-builders.mjs +2 -2
  66. package/dist/{planner-produced-postgres-migration-Cji5vxUf.mjs → planner-produced-postgres-migration-BolD0Mvc.mjs} +3 -3
  67. package/dist/{planner-produced-postgres-migration-Cji5vxUf.mjs.map → planner-produced-postgres-migration-BolD0Mvc.mjs.map} +1 -1
  68. package/dist/{planner-produced-postgres-migration-QqHa2C2l.d.mts → planner-produced-postgres-migration-Cz9--XOT.d.mts} +4 -4
  69. package/dist/{planner-produced-postgres-migration-QqHa2C2l.d.mts.map → planner-produced-postgres-migration-Cz9--XOT.d.mts.map} +1 -1
  70. package/dist/planner-produced-postgres-migration.d.mts +1 -1
  71. package/dist/planner-produced-postgres-migration.mjs +1 -1
  72. package/dist/{planner-sql-checks-jqUUGyQR.mjs → planner-sql-checks-DE9c4519.mjs} +2 -2
  73. package/dist/{planner-sql-checks-jqUUGyQR.mjs.map → planner-sql-checks-DE9c4519.mjs.map} +1 -1
  74. package/dist/planner-sql-checks.mjs +1 -1
  75. package/dist/{planner-target-details-CIY6tLeo.d.mts → planner-target-details-HkP4RrRO.d.mts} +2 -2
  76. package/dist/planner-target-details-HkP4RrRO.d.mts.map +1 -0
  77. package/dist/planner-target-details.d.mts +1 -1
  78. package/dist/planner.d.mts +82 -4
  79. package/dist/planner.d.mts.map +1 -1
  80. package/dist/planner.mjs +2 -2
  81. package/dist/{postgres-contract-serializer-k3TAcPMY.mjs → postgres-contract-view-BegduLr6.mjs} +50 -23
  82. package/dist/postgres-contract-view-BegduLr6.mjs.map +1 -0
  83. package/dist/postgres-contract-view-C9j5cqP0.d.mts +34 -0
  84. package/dist/postgres-contract-view-C9j5cqP0.d.mts.map +1 -0
  85. package/dist/postgres-database-schema-node-os8ogEap.d.mts +183 -0
  86. package/dist/postgres-database-schema-node-os8ogEap.d.mts.map +1 -0
  87. package/dist/{postgres-migration-B5jKrXv3.mjs → postgres-migration-BfMmK6tv.mjs} +21 -2
  88. package/dist/postgres-migration-BfMmK6tv.mjs.map +1 -0
  89. package/dist/{postgres-migration-Y4YBJqkS.d.mts → postgres-migration-D7kNVWVG.d.mts} +19 -6
  90. package/dist/postgres-migration-D7kNVWVG.d.mts.map +1 -0
  91. package/dist/postgres-rls-policy--CSddvMR.d.mts +52 -0
  92. package/dist/postgres-rls-policy--CSddvMR.d.mts.map +1 -0
  93. package/dist/{postgres-schema-COGZ1ark.mjs → postgres-schema-BLIHBEfX.mjs} +108 -5
  94. package/dist/postgres-schema-BLIHBEfX.mjs.map +1 -0
  95. package/dist/postgres-schema-BywoeKlt.d.mts +189 -0
  96. package/dist/postgres-schema-BywoeKlt.d.mts.map +1 -0
  97. package/dist/postgres-table-schema-node-1nNc8XDT.mjs +248 -0
  98. package/dist/postgres-table-schema-node-1nNc8XDT.mjs.map +1 -0
  99. package/dist/render-ops.d.mts +1 -1
  100. package/dist/{render-typescript-KMgosran.mjs → render-typescript-7yqILcwr.mjs} +45 -16
  101. package/dist/render-typescript-7yqILcwr.mjs.map +1 -0
  102. package/dist/render-typescript.mjs +1 -1
  103. package/dist/resolve-ddl-schema-CL1RR1mx.mjs +20 -0
  104. package/dist/resolve-ddl-schema-CL1RR1mx.mjs.map +1 -0
  105. package/dist/rls-canonicalize.d.mts +2 -0
  106. package/dist/rls-canonicalize.mjs +2 -0
  107. package/dist/runtime.d.mts +7 -6
  108. package/dist/runtime.d.mts.map +1 -1
  109. package/dist/runtime.mjs +3 -3
  110. package/dist/runtime.mjs.map +1 -1
  111. package/dist/schema-ir-annotations.d.mts +14 -0
  112. package/dist/schema-ir-annotations.d.mts.map +1 -0
  113. package/dist/schema-ir-annotations.mjs +2 -0
  114. package/dist/types.d.mts +4 -140
  115. package/dist/types.mjs +3 -2
  116. package/package.json +22 -17
  117. package/src/contract-free/checks.ts +73 -0
  118. package/src/contract-free/ddl.ts +45 -1
  119. package/src/core/authoring.ts +115 -1
  120. package/src/core/codecs.ts +35 -1
  121. package/src/core/ddl/nodes.ts +94 -2
  122. package/src/core/default-normalizer.ts +122 -2
  123. package/src/core/descriptor-meta.ts +4 -0
  124. package/src/core/entity-kinds.ts +16 -0
  125. package/src/core/migrations/bound-schema.ts +10 -0
  126. package/src/core/migrations/contract-to-postgres-database-schema-node.ts +125 -0
  127. package/src/core/migrations/control-policy.ts +3 -0
  128. package/src/core/migrations/diff-database-schema.ts +191 -0
  129. package/src/core/migrations/issue-planner.ts +44 -3
  130. package/src/core/migrations/op-factory-call.ts +109 -21
  131. package/src/core/migrations/operations/columns.ts +24 -12
  132. package/src/core/migrations/operations/data-transform.ts +6 -0
  133. package/src/core/migrations/operations/rls.ts +106 -0
  134. package/src/core/migrations/operations/shared.ts +0 -30
  135. package/src/core/migrations/planner-ddl-builders.ts +15 -56
  136. package/src/core/migrations/planner-recipes.ts +31 -13
  137. package/src/core/migrations/planner-strategies.ts +3 -11
  138. package/src/core/migrations/planner-target-details.ts +3 -1
  139. package/src/core/migrations/planner.ts +165 -33
  140. package/src/core/migrations/postgres-migration.ts +34 -4
  141. package/src/core/migrations/render-typescript.ts +42 -15
  142. package/src/core/migrations/resolve-ddl-schema.ts +24 -0
  143. package/src/core/migrations/runner.ts +13 -16
  144. package/src/core/migrations/verify-postgres-namespaces.ts +24 -19
  145. package/src/core/postgres-contract-serializer.ts +50 -33
  146. package/src/core/postgres-contract-view.ts +48 -0
  147. package/src/core/postgres-rls-policy.ts +64 -0
  148. package/src/core/postgres-role.ts +37 -0
  149. package/src/core/postgres-schema.ts +37 -13
  150. package/src/core/postgres-validators.ts +20 -0
  151. package/src/core/psl-infer/infer-psl-contract.ts +834 -0
  152. package/src/core/psl-infer/postgres-default-mapping.ts +16 -0
  153. package/src/core/psl-infer/postgres-type-map.ts +157 -0
  154. package/src/core/rls/canonicalize.ts +48 -0
  155. package/src/core/schema-ir/postgres-database-schema-node.ts +63 -0
  156. package/src/core/schema-ir/postgres-namespace-schema-node.ts +61 -0
  157. package/src/core/schema-ir/postgres-policy-schema-node.ts +93 -0
  158. package/src/core/schema-ir/postgres-role-schema-node.ts +66 -0
  159. package/src/core/schema-ir/postgres-table-schema-node.ts +107 -0
  160. package/src/core/schema-ir/schema-node-kinds.ts +28 -0
  161. package/src/exports/contract-free.ts +7 -1
  162. package/src/exports/control.ts +39 -12
  163. package/src/exports/ddl.ts +4 -0
  164. package/src/exports/migration.ts +1 -0
  165. package/src/exports/planner-ddl-builders.ts +0 -2
  166. package/src/exports/planner.ts +2 -0
  167. package/src/exports/rls-canonicalize.ts +6 -0
  168. package/src/exports/runtime.ts +1 -0
  169. package/src/exports/schema-ir-annotations.ts +1 -0
  170. package/src/exports/types.ts +28 -0
  171. package/dist/codecs--0A5_4Bq.d.mts.map +0 -1
  172. package/dist/ddl-QDyOSeLc.mjs.map +0 -1
  173. package/dist/default-normalizer-DyyCHQWs.mjs.map +0 -1
  174. package/dist/descriptor-meta-CpGygXpI.mjs +0 -140
  175. package/dist/descriptor-meta-CpGygXpI.mjs.map +0 -1
  176. package/dist/issue-planner-DL6g3CmE.mjs.map +0 -1
  177. package/dist/nodes-Bbhs2rwj.mjs.map +0 -1
  178. package/dist/nodes-pLeLgdis.d.mts +0 -67
  179. package/dist/nodes-pLeLgdis.d.mts.map +0 -1
  180. package/dist/op-factory-call-D_p5vxwt.mjs.map +0 -1
  181. package/dist/op-factory-call-DmQEc3XV.d.mts.map +0 -1
  182. package/dist/planner-Bs_baQax.mjs +0 -324
  183. package/dist/planner-Bs_baQax.mjs.map +0 -1
  184. package/dist/planner-ddl-builders-B2wOwLqI.mjs.map +0 -1
  185. package/dist/planner-target-details-CIY6tLeo.d.mts.map +0 -1
  186. package/dist/postgres-contract-serializer-k3TAcPMY.mjs.map +0 -1
  187. package/dist/postgres-migration-B5jKrXv3.mjs.map +0 -1
  188. package/dist/postgres-migration-Y4YBJqkS.d.mts.map +0 -1
  189. package/dist/postgres-schema-COGZ1ark.mjs.map +0 -1
  190. package/dist/render-typescript-KMgosran.mjs.map +0 -1
  191. package/dist/types.d.mts.map +0 -1
@@ -13,28 +13,39 @@ import {
13
13
  plannerFailure,
14
14
  } from '@prisma-next/family-sql/control';
15
15
  import type { ExecuteRequestLowerer } from '@prisma-next/family-sql/control-adapter';
16
- import { verifySqlSchema } from '@prisma-next/family-sql/schema-verify';
17
16
  import type { TargetBoundComponentDescriptor } from '@prisma-next/framework-components/components';
18
17
  import type {
19
18
  MigrationPlanner,
20
19
  MigrationPlanWithAuthoringSurface,
21
20
  MigrationScaffoldContext,
21
+ SchemaDiffIssue,
22
22
  SchemaIssue,
23
23
  } from '@prisma-next/framework-components/control';
24
24
  import { UNBOUND_NAMESPACE_ID } from '@prisma-next/framework-components/ir';
25
+ import type { SqlSchemaIR } from '@prisma-next/sql-schema-ir/types';
25
26
  import { blindCast } from '@prisma-next/utils/casts';
26
- import { parsePostgresDefault } from '../default-normalizer';
27
- import { normalizeSchemaNativeType } from '../native-type-normalizer';
27
+ import { ifDefined } from '@prisma-next/utils/defined';
28
+ import { PostgresRlsPolicy } from '../postgres-rls-policy';
29
+ import { PostgresDatabaseSchemaNode } from '../schema-ir/postgres-database-schema-node';
30
+ import { PostgresPolicySchemaNode } from '../schema-ir/postgres-policy-schema-node';
31
+ import type { SqlSchemaDiffNode } from '../schema-ir/schema-node-kinds';
28
32
  import {
29
33
  formatPostgresControlPolicySubjectLabel,
30
34
  resolvePostgresCallControlPolicySubject,
31
35
  resolvePostgresIssueControlPolicySubject,
32
36
  resolvePostgresIssueCreationFactoryName,
33
37
  } from './control-policy';
38
+ import { diffPostgresDatabaseSchema } from './diff-database-schema';
34
39
  import { planIssues } from './issue-planner';
35
40
  import type { PostgresOpFactoryCall } from './op-factory-call';
41
+ import {
42
+ CreatePostgresRlsPolicyCall,
43
+ DropPostgresRlsPolicyCall,
44
+ EnableRowLevelSecurityCall,
45
+ } from './op-factory-call';
36
46
  import { TypeScriptRenderablePostgresMigration } from './planner-produced-postgres-migration';
37
47
  import { postgresPlannerStrategies } from './planner-strategies';
48
+ import { resolveDdlSchemaForNamespaceStorage } from './resolve-ddl-schema';
38
49
  import { verifyPostgresNamespacePresence } from './verify-postgres-namespaces';
39
50
 
40
51
  type PlannerFrameworkComponents = SqlMigrationPlannerPlanOptions extends {
@@ -47,10 +58,6 @@ type PlannerOptionsWithComponents = SqlMigrationPlannerPlanOptions & {
47
58
  readonly frameworkComponents: PlannerFrameworkComponents;
48
59
  };
49
60
 
50
- type VerifySqlSchemaOptionsWithComponents = Parameters<typeof verifySqlSchema>[0] & {
51
- readonly frameworkComponents: PlannerFrameworkComponents;
52
- };
53
-
54
61
  export function createPostgresMigrationPlanner(
55
62
  lowerer: ExecuteRequestLowerer,
56
63
  ): PostgresMigrationPlanner {
@@ -151,9 +158,33 @@ export class PostgresMigrationPlanner implements MigrationPlanner<'sql', 'postgr
151
158
  return policyResult;
152
159
  }
153
160
 
154
- const schemaIssues = this.collectSchemaIssues(options);
161
+ // One combined database-schema diff drives the whole plan: the relational
162
+ // findings (+ namespace presence) become structural DDL via `planIssues`,
163
+ // the policy findings become RLS ops via `planPostgresSchemaDiff`. Verify
164
+ // runs the same underlying comparison (via `verifyDatabaseSchema`) and
165
+ // rejects on non-empty. The caller-supplied `keepDiffIssue` predicate is
166
+ // applied blindly — any scoping (e.g. multi-space ownership) is the
167
+ // orchestration's, never worked out here.
168
+ PostgresDatabaseSchemaNode.assert(options.schema);
169
+ const rawDiff = diffPostgresDatabaseSchema({
170
+ contract: options.contract,
171
+ actualSchema: options.schema,
172
+ strict:
173
+ options.policy.allowedOperationClasses.includes('widening') ||
174
+ options.policy.allowedOperationClasses.includes('destructive'),
175
+ typeMetadataRegistry: new Map(),
176
+ frameworkComponents: options.frameworkComponents,
177
+ });
178
+ const databaseDiff = options.keepDiffIssue ? rawDiff.filter(options.keepDiffIssue) : rawDiff;
179
+ const schemaIssues = this.collectSchemaIssues(options, databaseDiff.issues);
155
180
  const codecHooks = extractCodecControlHooks(options.frameworkComponents);
156
181
  const storageTypes = options.contract.storage.types ?? {};
182
+ // The strategy layer reads the live schema by bare table name for existence
183
+ // checks (shared-temp-default safety, FK/unique probes), so it takes one
184
+ // per-schema namespace node — never the whole tree root, and never a flat
185
+ // merge of every namespace (which would collide same-named tables across
186
+ // schemas). Probing more than one namespace at once is future work.
187
+ const relationalSchema = relationalNamespaceNode(options.schema, schemaName);
157
188
 
158
189
  // Input-side control-policy partition. `external` / `observed` subjects
159
190
  // — and non-creation issues for `tolerated` subjects — are dropped from
@@ -184,7 +215,7 @@ export class PostgresMigrationPlanner implements MigrationPlanner<'sql', 'postgr
184
215
  schemaName,
185
216
  codecHooks,
186
217
  storageTypes,
187
- schema: options.schema,
218
+ ...ifDefined('schema', relationalSchema),
188
219
  policy: options.policy,
189
220
  frameworkComponents: options.frameworkComponents,
190
221
  strategies: postgresPlannerStrategies,
@@ -194,6 +225,17 @@ export class PostgresMigrationPlanner implements MigrationPlanner<'sql', 'postgr
194
225
  return plannerFailure(result.failure);
195
226
  }
196
227
 
228
+ const schemaDiffCalls = this.planPostgresSchemaDiff(options, databaseDiff.schemaDiffIssues);
229
+ const schemaDiffPartition = partitionCallsByControlPolicy({
230
+ calls: schemaDiffCalls,
231
+ contract: options.contract,
232
+ resolveControlPolicySubject: (call) =>
233
+ resolvePostgresCallControlPolicySubject(call, options.contract),
234
+ resolveFactoryName: (call) => call.factoryName,
235
+ formatSubjectLabel: (factoryName, subject) =>
236
+ formatPostgresControlPolicySubjectLabel(factoryName, subject, options.contract),
237
+ });
238
+
197
239
  // Inline `onFieldEvent`-emitted ops after structural DDL. The fixed
198
240
  // ordering is `structural → added → dropped → altered`, with
199
241
  // within-group sorting by `(tableName, fieldName)` so re-emits are
@@ -219,9 +261,10 @@ export class PostgresMigrationPlanner implements MigrationPlanner<'sql', 'postgr
219
261
  formatSubjectLabel: (factoryName, subject) =>
220
262
  formatPostgresControlPolicySubjectLabel(factoryName, subject, options.contract),
221
263
  });
222
- const calls = [...result.value.calls, ...fieldEventPartition.kept];
264
+ const calls = [...result.value.calls, ...schemaDiffPartition.kept, ...fieldEventPartition.kept];
223
265
  const warnings: SqlPlannerConflict[] = [
224
266
  ...issuePartition.warnings,
267
+ ...schemaDiffPartition.warnings,
225
268
  ...fieldEventPartition.warnings,
226
269
  ];
227
270
 
@@ -240,6 +283,59 @@ export class PostgresMigrationPlanner implements MigrationPlanner<'sql', 'postgr
240
283
  });
241
284
  }
242
285
 
286
+ /**
287
+ * Maps the RLS policy presence findings of the shared
288
+ * `diffPostgresDatabaseSchema` (already ownership-filtered) into
289
+ * `ENABLE RLS` / `CREATE POLICY` / `DROP POLICY` ops. It no longer re-diffs —
290
+ * it consumes the `schemaDiffIssues` of the one combined diff.
291
+ */
292
+ private planPostgresSchemaDiff(
293
+ options: PlannerOptionsWithComponents,
294
+ filteredDiffIssues: readonly SchemaDiffIssue<SqlSchemaDiffNode>[],
295
+ ): readonly PostgresOpFactoryCall[] {
296
+ const allowsDestructive = options.policy.allowedOperationClasses.includes('destructive');
297
+ const calls: PostgresOpFactoryCall[] = [];
298
+ const seenEnableTables = new Set<string>();
299
+
300
+ for (const issue of filteredDiffIssues) {
301
+ // 'mismatch' is unreachable for content-addressed policies: the wire name
302
+ // encodes the body hash, so two policies sharing a local key (same name)
303
+ // are always equal and isEqualTo never returns false.
304
+ if (issue.outcome === 'missing') {
305
+ const expected = issue.expected;
306
+ PostgresPolicySchemaNode.assert(expected);
307
+ // expected.namespaceId is the DDL schema name (resolved during projection);
308
+ // this re-resolution is a no-op as long as PostgresSchema.ddlSchemaName() returns this.id.
309
+ const schemaForTable = resolveDdlSchemaForNamespaceStorage(
310
+ options.contract.storage,
311
+ expected.namespaceId,
312
+ );
313
+ const tableKey = `${schemaForTable}.${expected.tableName}`;
314
+ if (!seenEnableTables.has(tableKey)) {
315
+ seenEnableTables.add(tableKey);
316
+ calls.push(new EnableRowLevelSecurityCall(schemaForTable, expected.tableName));
317
+ }
318
+ calls.push(
319
+ new CreatePostgresRlsPolicyCall(
320
+ schemaForTable,
321
+ expected.tableName,
322
+ policyNodeToContractPolicy(expected),
323
+ ),
324
+ );
325
+ } else if (issue.outcome === 'extra' && allowsDestructive) {
326
+ const actual = issue.actual;
327
+ PostgresPolicySchemaNode.assert(actual);
328
+ const schemaForTable = resolveDdlSchemaForNamespaceStorage(
329
+ options.contract.storage,
330
+ actual.namespaceId,
331
+ );
332
+ calls.push(new DropPostgresRlsPolicyCall(schemaForTable, actual.tableName, actual.name));
333
+ }
334
+ }
335
+
336
+ return calls;
337
+ }
338
+
243
339
  private ensureAdditivePolicy(policy: MigrationOperationPolicy) {
244
340
  if (!policy.allowedOperationClasses.includes('additive')) {
245
341
  return plannerFailure([
@@ -253,34 +349,70 @@ export class PostgresMigrationPlanner implements MigrationPlanner<'sql', 'postgr
253
349
  return null;
254
350
  }
255
351
 
256
- private collectSchemaIssues(options: PlannerOptionsWithComponents): readonly SchemaIssue[] {
257
- // `db init` uses additive-only policy and intentionally ignores extra
258
- // schema objects. Any reconciliation-capable policy (widening or
259
- // destructive) must inspect extras to reconcile strict equality.
260
- const allowed = options.policy.allowedOperationClasses;
261
- const strict = allowed.includes('widening') || allowed.includes('destructive');
262
- const verifyOptions: VerifySqlSchemaOptionsWithComponents = {
263
- contract: options.contract,
264
- schema: options.schema,
265
- strict,
266
- typeMetadataRegistry: new Map(),
267
- frameworkComponents: options.frameworkComponents,
268
- normalizeDefault: parsePostgresDefault,
269
- normalizeNativeType: normalizeSchemaNativeType,
270
- };
271
- const verifyResult = verifySqlSchema(verifyOptions);
272
- // Schema presence is a Postgres-specific concern (no equivalent in
273
- // SQLite / Mongo), so the issue emission lives in the target layer
274
- // rather than in the family verifier. Stitch it in here so a single
275
- // `SchemaIssue[]` flows through `planIssues` and the planner emits
276
- // CREATE SCHEMA in the dep bucket before any CreateTableCall.
352
+ /**
353
+ * The structural issue list `planIssues` consumes: the relational findings
354
+ * from the shared `diffPostgresDatabaseSchema` plus namespace presence.
355
+ *
356
+ * Schema presence (`missing_schema` → `CREATE SCHEMA`) is a planner-only
357
+ * op-generation concern, so it is stitched in here rather than inside the
358
+ * shared diff verify never needs it (a missing schema already surfaces as
359
+ * `missing_table` in the relational findings). It reads `existingSchemas` off
360
+ * the database root, so it takes the whole tree. Policy drift is handled
361
+ * separately via `planPostgresSchemaDiff` from the same shared diff's
362
+ * `schemaDiffIssues`.
363
+ */
364
+ private collectSchemaIssues(
365
+ options: PlannerOptionsWithComponents,
366
+ relationalIssues: readonly SchemaIssue[],
367
+ ): readonly SchemaIssue[] {
277
368
  const namespaceIssues = verifyPostgresNamespacePresence({
278
369
  contract: options.contract,
279
370
  schema: options.schema,
280
371
  });
281
372
  if (namespaceIssues.length === 0) {
282
- return verifyResult.schema.issues;
373
+ return relationalIssues;
283
374
  }
284
- return [...namespaceIssues, ...verifyResult.schema.issues];
375
+ return [...namespaceIssues, ...relationalIssues];
285
376
  }
286
377
  }
378
+
379
+ /**
380
+ * Returns the one namespace node whose tables the relational strategy layer
381
+ * probes for live-table existence — the node matching the planner's resolved
382
+ * schema name, or the first namespace when none matches. `undefined` when the
383
+ * tree has no namespaces, so the strategy context uses its empty-schema default.
384
+ *
385
+ * The relational strategies key tables by bare name, so they can only probe one
386
+ * namespace at a time; probing across every namespace at once is future work.
387
+ */
388
+ function relationalNamespaceNode(
389
+ schema: PostgresDatabaseSchemaNode,
390
+ schemaName: string,
391
+ ): SqlSchemaIR | undefined {
392
+ const namespaceNodes = Object.values(schema.namespaces);
393
+ const byName = namespaceNodes.find((node) => node.schemaName === schemaName);
394
+ return byName ?? namespaceNodes[0];
395
+ }
396
+
397
+ /**
398
+ * Rebuilds the `PostgresRlsPolicy` contract entity `CreatePostgresRlsPolicyCall`
399
+ * carries (its `renderTypeScript`/`createRlsPolicy` paths serialize the whole
400
+ * entity, `namespaceId` included). This reconstructs rather than looking the
401
+ * original up in the contract on purpose: the diff node's `namespaceId` is the
402
+ * *resolved DDL schema* (set when the expected tree was built), which is the
403
+ * value the emitted op must carry; the contract-stored entity holds the raw,
404
+ * pre-resolution coordinate, so a lookup would change the migration output.
405
+ */
406
+ function policyNodeToContractPolicy(node: PostgresPolicySchemaNode): PostgresRlsPolicy {
407
+ return new PostgresRlsPolicy({
408
+ name: node.name,
409
+ prefix: node.prefix,
410
+ tableName: node.tableName,
411
+ namespaceId: node.namespaceId,
412
+ operation: node.operation,
413
+ roles: [...node.roles],
414
+ ...ifDefined('using', node.using),
415
+ ...ifDefined('withCheck', node.withCheck),
416
+ permissive: node.permissive,
417
+ });
418
+ }
@@ -3,9 +3,11 @@ import type { SqlMigrationPlanOperation } from '@prisma-next/family-sql/control'
3
3
  import type { SqlControlAdapter } from '@prisma-next/family-sql/control-adapter';
4
4
  import { Migration as SqlMigration } from '@prisma-next/family-sql/migration';
5
5
  import type { ControlStack } from '@prisma-next/framework-components/control';
6
+ import { MigrationContractViews } from '@prisma-next/migration-tools/migration';
6
7
  import type { SqlStorage } from '@prisma-next/sql-contract/types';
7
8
  import type { DdlColumn, DdlTableConstraint } from '@prisma-next/sql-relational-core/ast';
8
9
  import { errorPostgresMigrationStackMissing } from '../errors';
10
+ import { PostgresContractView } from '../postgres-contract-view';
9
11
  import {
10
12
  AddCheckConstraintCall,
11
13
  AddColumnCall,
@@ -60,10 +62,10 @@ import type { PostgresPlanTargetDetails } from './planner-target-details';
60
62
  * is an antipattern in a migration. (The unbound/unspecified namespace concept
61
63
  * remains for SQLite, which has no schemas, and for Mongo's connection `db`.)
62
64
  */
63
- export abstract class PostgresMigration extends SqlMigration<
64
- PostgresPlanTargetDetails,
65
- 'postgres'
66
- > {
65
+ export abstract class PostgresMigration<
66
+ Start extends Contract<SqlStorage> = Contract<SqlStorage>,
67
+ End extends Contract<SqlStorage> = Contract<SqlStorage>,
68
+ > extends SqlMigration<PostgresPlanTargetDetails, 'postgres', Start, End> {
67
69
  readonly targetId = 'postgres' as const;
68
70
 
69
71
  /**
@@ -74,6 +76,17 @@ export abstract class PostgresMigration extends SqlMigration<
74
76
  */
75
77
  protected readonly controlAdapter: SqlControlAdapter<'postgres'> | undefined;
76
78
 
79
+ #endView = new MigrationContractViews<PostgresContractView<End>>(
80
+ this,
81
+ 'PostgresMigration',
82
+ (json) => PostgresContractView.fromJson<End>(json),
83
+ );
84
+ #startView = new MigrationContractViews<PostgresContractView<Start>>(
85
+ this,
86
+ 'PostgresMigration',
87
+ (json) => PostgresContractView.fromJson<Start>(json),
88
+ );
89
+
77
90
  constructor(stack?: ControlStack<'sql', 'postgres'>) {
78
91
  super(stack);
79
92
  // The descriptor `create()` is typed as the wider `ControlAdapterInstance`;
@@ -84,6 +97,23 @@ export abstract class PostgresMigration extends SqlMigration<
84
97
  : undefined;
85
98
  }
86
99
 
100
+ /**
101
+ * The typed, schema-qualified Postgres view over this migration's end-state
102
+ * contract — `this.endContract.namespace.<schema>.table.<name>`, etc. Throws
103
+ * if no `endContractJson` was provided.
104
+ */
105
+ get endContract(): PostgresContractView<End> {
106
+ return this.#endView.endContract;
107
+ }
108
+
109
+ /**
110
+ * The typed Postgres view over this migration's start-state contract, or
111
+ * `null` for a baseline migration (no `startContractJson`).
112
+ */
113
+ get startContract(): PostgresContractView<Start> | null {
114
+ return this.#startView.startContract;
115
+ }
116
+
87
117
  /**
88
118
  * Instance-method wrapper around the free `dataTransform` factory that
89
119
  * supplies the stored control adapter. Authors call this from inside
@@ -45,15 +45,19 @@ export function renderCallsToTypeScript(
45
45
  calls: ReadonlyArray<OpFactoryCall>,
46
46
  meta: RenderMigrationMeta,
47
47
  ): string {
48
- const imports = buildImports(calls);
48
+ const imports = buildImports(calls, meta);
49
49
  const operationsBody = calls.map((c) => renderCall(c)).join(',\n');
50
+ const hasStart = meta.from !== null;
51
+ const startField = hasStart ? [' override readonly startContractJson = startContract;'] : [];
50
52
 
51
53
  return [
52
54
  shebangLineFor(detectScaffoldRuntime()),
53
55
  imports,
54
56
  '',
55
- 'export default class M extends Migration {',
56
- buildDescribeMethod(meta),
57
+ `export default class M extends Migration<${hasStart ? 'Start' : 'never'}, End> {`,
58
+ ...startField,
59
+ ' override readonly endContractJson = endContract;',
60
+ '',
57
61
  ' override get operations() {',
58
62
  ' return [',
59
63
  indent(operationsBody, 6),
@@ -70,8 +74,8 @@ function renderCall(call: OpFactoryCall): string {
70
74
  return call.renderTypeScript();
71
75
  }
72
76
 
73
- function buildImports(calls: ReadonlyArray<OpFactoryCall>): string {
74
- const requirements: ImportRequirement[] = [...BASE_IMPORTS];
77
+ function buildImports(calls: ReadonlyArray<OpFactoryCall>, meta: RenderMigrationMeta): string {
78
+ const requirements: ImportRequirement[] = [...BASE_IMPORTS, ...contractImports(meta)];
75
79
  for (const call of calls) {
76
80
  for (const req of call.importRequirements()) {
77
81
  requirements.push(req);
@@ -80,16 +84,39 @@ function buildImports(calls: ReadonlyArray<OpFactoryCall>): string {
80
84
  return renderImports(requirements);
81
85
  }
82
86
 
83
- function buildDescribeMethod(meta: RenderMigrationMeta): string {
84
- const lines: string[] = [];
85
- lines.push(' override describe() {');
86
- lines.push(' return {');
87
- lines.push(` from: ${JSON.stringify(meta.from)},`);
88
- lines.push(` to: ${JSON.stringify(meta.to)},`);
89
- lines.push(' };');
90
- lines.push(' }');
91
- lines.push('');
92
- return lines.join('\n');
87
+ /**
88
+ * The committed contract-JSON imports the scaffold reads its from/to identity
89
+ * from. `end-contract.json` is always present; `start-contract.json` is added
90
+ * only for a non-baseline migration (`meta.from !== null`). The matching
91
+ * `Contract` type imports (aliased `Start`/`End`) feed the
92
+ * `Migration<Start, End>` generics. Baseline emits `Migration<never, End>` with
93
+ * no start imports — `never` is the honest "no prior contract" Start.
94
+ */
95
+ function contractImports(meta: RenderMigrationMeta): readonly ImportRequirement[] {
96
+ const reqs: ImportRequirement[] = [
97
+ {
98
+ moduleSpecifier: './end-contract.json',
99
+ symbol: 'endContract',
100
+ kind: 'default',
101
+ attributes: { type: 'json' },
102
+ },
103
+ { moduleSpecifier: './end-contract', symbol: 'Contract', alias: 'End', typeOnly: true },
104
+ ];
105
+ if (meta.from !== null) {
106
+ reqs.push({
107
+ moduleSpecifier: './start-contract.json',
108
+ symbol: 'startContract',
109
+ kind: 'default',
110
+ attributes: { type: 'json' },
111
+ });
112
+ reqs.push({
113
+ moduleSpecifier: './start-contract',
114
+ symbol: 'Contract',
115
+ alias: 'Start',
116
+ typeOnly: true,
117
+ });
118
+ }
119
+ return reqs;
93
120
  }
94
121
 
95
122
  function indent(text: string, spaces: number): string {
@@ -0,0 +1,24 @@
1
+ import { UNBOUND_NAMESPACE_ID } from '@prisma-next/framework-components/ir';
2
+ import type { SqlStorage } from '@prisma-next/sql-contract/types';
3
+ import { isPostgresSchema } from '../postgres-schema';
4
+
5
+ /**
6
+ * Given the contract's storage and a namespace id, returns the live Postgres
7
+ * DDL schema name that namespace maps to. A named Postgres namespace dispatches
8
+ * to its `ddlSchemaName(storage)`; the unbound sentinel resolves to `public`
9
+ * (the search-path default for offline planning); a bare object payload (used
10
+ * by some tests) falls back to the namespace id itself.
11
+ */
12
+ export function resolveDdlSchemaForNamespaceStorage(
13
+ storage: SqlStorage,
14
+ namespaceId: string,
15
+ ): string {
16
+ if (namespaceId === UNBOUND_NAMESPACE_ID) {
17
+ return 'public';
18
+ }
19
+ const namespace = storage.namespaces[namespaceId];
20
+ if (namespace && isPostgresSchema(namespace)) {
21
+ return namespace.ddlSchemaName(storage);
22
+ }
23
+ return namespaceId;
24
+ }
@@ -12,7 +12,6 @@ import type {
12
12
  SqlMigrationRunnerSuccessValue,
13
13
  } from '@prisma-next/family-sql/control';
14
14
  import { runnerFailure, runnerSuccess } from '@prisma-next/family-sql/control';
15
- import { verifySqlSchema } from '@prisma-next/family-sql/schema-verify';
16
15
  import type { MigrationRunnerResult } from '@prisma-next/framework-components/control';
17
16
  import { APP_SPACE_ID } from '@prisma-next/framework-components/control';
18
17
  import { UNBOUND_NAMESPACE_ID } from '@prisma-next/framework-components/ir';
@@ -23,8 +22,6 @@ import { blindCast } from '@prisma-next/utils/casts';
23
22
  import { ifDefined } from '@prisma-next/utils/defined';
24
23
  import type { Result } from '@prisma-next/utils/result';
25
24
  import { notOk, ok, okVoid } from '@prisma-next/utils/result';
26
- import { parsePostgresDefault } from '../default-normalizer';
27
- import { normalizeSchemaNativeType } from '../native-type-normalizer';
28
25
  import type { PostgresPlanTargetDetails } from './planner-target-details';
29
26
 
30
27
  interface ApplyPlanSuccessValue {
@@ -124,30 +121,30 @@ class PostgresMigrationRunner implements SqlMigrationRunner<PostgresPlanTargetDe
124
121
  applyValue = applyResult.value;
125
122
  }
126
123
 
127
- // Schema verification on app-space only extension spaces don't
128
- // own user-facing tables in the live schema, and `verifySqlSchema`
129
- // matches the destination contract against the database, which
130
- // would flag every app-space table as "extra" when called against
131
- // an extension contract.
124
+ // Verify the schema on app-space only: extension spaces don't own
125
+ // user-facing tables, so verifying the destination contract against the
126
+ // database would flag every app-space table as "extra". Delegates to the
127
+ // family `verifySchema` the same comparison the CLI verify runs.
132
128
  if (space === APP_SPACE_ID) {
133
- const schemaIR = await this.family.introspect({
129
+ const schemaNode = await this.family.introspect({
134
130
  driver,
135
131
  contract: options.destinationContract,
136
132
  });
137
- const schemaVerifyResult = verifySqlSchema({
133
+ const schemaVerifyResult = this.family.verifySchema({
138
134
  contract: options.destinationContract,
139
- schema: schemaIR,
135
+ schema: schemaNode,
140
136
  strict: options.strictVerification ?? true,
141
- context: options.context ?? {},
142
- typeMetadataRegistry: this.family.typeMetadataRegistry,
143
137
  frameworkComponents: options.frameworkComponents,
144
- normalizeDefault: parsePostgresDefault,
145
- normalizeNativeType: normalizeSchemaNativeType,
146
138
  });
147
139
  if (!schemaVerifyResult.ok) {
148
140
  return runnerFailure('SCHEMA_VERIFY_FAILED', schemaVerifyResult.summary, {
149
141
  why: 'The resulting database schema does not satisfy the destination contract.',
150
- meta: { issues: schemaVerifyResult.schema.issues },
142
+ meta: {
143
+ issues: [
144
+ ...schemaVerifyResult.schema.issues,
145
+ ...schemaVerifyResult.schema.schemaDiffIssues,
146
+ ],
147
+ },
151
148
  });
152
149
  }
153
150
  }
@@ -2,8 +2,9 @@ import type { Contract } from '@prisma-next/contract/types';
2
2
  import type { SchemaIssue } from '@prisma-next/framework-components/control';
3
3
  import { UNBOUND_NAMESPACE_ID } from '@prisma-next/framework-components/ir';
4
4
  import type { SqlStorage } from '@prisma-next/sql-contract/types';
5
- import type { SqlSchemaIR } from '@prisma-next/sql-schema-ir/types';
5
+ import type { SqlSchemaIRNode } from '@prisma-next/sql-schema-ir/types';
6
6
  import { isPostgresSchema } from '../postgres-schema';
7
+ import { PostgresDatabaseSchemaNode } from '../schema-ir/postgres-database-schema-node';
7
8
 
8
9
  /**
9
10
  * Resolves the live-database schema name for a given namespace
@@ -23,32 +24,35 @@ function resolveDdlSchemaName(storage: SqlStorage, namespaceId: string): string
23
24
  }
24
25
 
25
26
  /**
26
- * Reads the introspected list of schema names from the Postgres-flavoured
27
- * annotations slot on the schema IR. Defaults to the always-present
28
- * `public` schema when introspection did not populate the slot a fresh
29
- * Postgres database always carries `public` (unless an operator dropped
30
- * it manually), so any verifier path that runs without an enriched
31
- * introspection still suppresses the redundant `CREATE SCHEMA "public"`.
27
+ * Reads the introspected list of schema names from the database-root schema
28
+ * node. `existingSchemas` is database-level, so it lives on the
29
+ * `PostgresDatabaseSchemaNode` root not on the per-schema namespace nodes.
30
+ *
31
+ * Defaults to the always-present `public` schema when the node is not the
32
+ * database root a fresh Postgres database always carries `public` (unless an
33
+ * operator dropped it manually), so any verifier path that runs without an
34
+ * enriched introspection still suppresses the redundant `CREATE SCHEMA
35
+ * "public"`.
32
36
  *
33
37
  * Production introspection (`PostgresControlAdapter.introspect`) is the
34
- * authoritative source: it queries `pg_namespace` and writes every
35
- * non-system schema into `annotations.pg.existingSchemas`. Tests that
36
- * want to assert against a richer initial state pass the slot
37
- * explicitly via the schema IR.
38
+ * authoritative source: it queries `pg_namespace` and sets `existingSchemas`
39
+ * on the returned root. Tests that want to assert against a richer initial
40
+ * state construct a `PostgresDatabaseSchemaNode` explicitly.
38
41
  */
39
- function existingSchemasFromSchema(schema: SqlSchemaIR): readonly string[] {
40
- const annotations = (schema as { annotations?: { pg?: { existingSchemas?: unknown } } })
41
- .annotations;
42
- const slot = annotations?.pg?.existingSchemas;
43
- if (Array.isArray(slot)) {
44
- return slot.filter((s): s is string => typeof s === 'string');
42
+ function existingSchemasFromSchema(schema: SqlSchemaIRNode): readonly string[] {
43
+ if (PostgresDatabaseSchemaNode.is(schema)) {
44
+ return schema.existingSchemas;
45
45
  }
46
46
  return ['public'];
47
47
  }
48
48
 
49
49
  /**
50
50
  * Emits a `missing_schema` issue for every contract-declared Postgres
51
- * namespace whose live container does not yet exist.
51
+ * namespace whose live container does not yet exist. The planner's
52
+ * `collectSchemaIssues` prepends these to the relational findings so a
53
+ * multi-schema plan emits `CREATE SCHEMA` before the tables that need it —
54
+ * a planner-only concern (verify already rejects via the `missing_table` a
55
+ * missing schema produces), so this is not part of the shared diff.
52
56
  *
53
57
  * A namespace's live container is the schema returned by its
54
58
  * polymorphic `ddlSchemaName(storage)` method — named schemas resolve
@@ -66,7 +70,7 @@ function existingSchemasFromSchema(schema: SqlSchemaIR): readonly string[] {
66
70
  */
67
71
  export function verifyPostgresNamespacePresence(input: {
68
72
  readonly contract: Contract<SqlStorage>;
69
- readonly schema: SqlSchemaIR;
73
+ readonly schema: SqlSchemaIRNode;
70
74
  }): readonly SchemaIssue[] {
71
75
  const { contract, schema } = input;
72
76
  const existing = new Set(existingSchemasFromSchema(schema));
@@ -79,6 +83,7 @@ export function verifyPostgresNamespacePresence(input: {
79
83
  if (existing.has(ddlName)) continue;
80
84
  issues.push({
81
85
  kind: 'missing_schema',
86
+ reason: 'not-found',
82
87
  namespaceId,
83
88
  message: `Schema "${ddlName}" is missing from database`,
84
89
  });