@prisma-next/target-postgres 0.14.0-dev.39 → 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 (106) hide show
  1. package/dist/{authoring-CZmSQriw.mjs → authoring-BX1WHIdN.mjs} +2 -2
  2. package/dist/authoring-BX1WHIdN.mjs.map +1 -0
  3. package/dist/contract-free.mjs +2 -2
  4. package/dist/control.d.mts.map +1 -1
  5. package/dist/control.mjs +633 -16
  6. package/dist/control.mjs.map +1 -1
  7. package/dist/{ddl-CGf6UABz.mjs → ddl-BmLIe1z_.mjs} +2 -2
  8. package/dist/{ddl-CGf6UABz.mjs.map → ddl-BmLIe1z_.mjs.map} +1 -1
  9. package/dist/{descriptor-meta-Du6olWQI.mjs → descriptor-meta-tjatyhtZ.mjs} +3 -3
  10. package/dist/{descriptor-meta-Du6olWQI.mjs.map → descriptor-meta-tjatyhtZ.mjs.map} +1 -1
  11. package/dist/{issue-planner-C-CwZd8D.mjs → issue-planner-DbFvaAgu.mjs} +5 -5
  12. package/dist/issue-planner-DbFvaAgu.mjs.map +1 -0
  13. package/dist/issue-planner.d.mts +1 -1
  14. package/dist/issue-planner.mjs +1 -1
  15. package/dist/migration.d.mts +2 -2
  16. package/dist/migration.mjs +2 -2
  17. package/dist/{op-factory-call-DPJR7tJc.d.mts → op-factory-call-CJWUxyUW.d.mts} +2 -2
  18. package/dist/{op-factory-call-DPJR7tJc.d.mts.map → op-factory-call-CJWUxyUW.d.mts.map} +1 -1
  19. package/dist/{op-factory-call-DArx_1HM.mjs → op-factory-call-t65c4jyY.mjs} +3 -3
  20. package/dist/op-factory-call-t65c4jyY.mjs.map +1 -0
  21. package/dist/op-factory-call.d.mts +1 -1
  22. package/dist/op-factory-call.mjs +1 -1
  23. package/dist/pack.d.mts +2 -2
  24. package/dist/pack.mjs +1 -1
  25. package/dist/{planner-BQ7lSqbm.mjs → planner-CG2pLHk-.mjs} +310 -126
  26. package/dist/planner-CG2pLHk-.mjs.map +1 -0
  27. package/dist/{planner-produced-postgres-migration-3ppG0t3i.mjs → planner-produced-postgres-migration-BolD0Mvc.mjs} +2 -2
  28. package/dist/{planner-produced-postgres-migration-3ppG0t3i.mjs.map → planner-produced-postgres-migration-BolD0Mvc.mjs.map} +1 -1
  29. package/dist/{planner-produced-postgres-migration-BauTSigX.d.mts → planner-produced-postgres-migration-Cz9--XOT.d.mts} +2 -2
  30. package/dist/{planner-produced-postgres-migration-BauTSigX.d.mts.map → planner-produced-postgres-migration-Cz9--XOT.d.mts.map} +1 -1
  31. package/dist/planner-produced-postgres-migration.d.mts +1 -1
  32. package/dist/planner-produced-postgres-migration.mjs +1 -1
  33. package/dist/{planner-sql-checks-CyjaoKhm.mjs → planner-sql-checks-DE9c4519.mjs} +2 -2
  34. package/dist/{planner-sql-checks-CyjaoKhm.mjs.map → planner-sql-checks-DE9c4519.mjs.map} +1 -1
  35. package/dist/planner-sql-checks.mjs +1 -1
  36. package/dist/planner.d.mts +73 -22
  37. package/dist/planner.d.mts.map +1 -1
  38. package/dist/planner.mjs +2 -2
  39. package/dist/{postgres-contract-view-CoRZewAp.mjs → postgres-contract-view-BegduLr6.mjs} +3 -3
  40. package/dist/{postgres-contract-view-CoRZewAp.mjs.map → postgres-contract-view-BegduLr6.mjs.map} +1 -1
  41. package/dist/postgres-database-schema-node-os8ogEap.d.mts +183 -0
  42. package/dist/postgres-database-schema-node-os8ogEap.d.mts.map +1 -0
  43. package/dist/{postgres-migration-BVjq0iFW.mjs → postgres-migration-BfMmK6tv.mjs} +3 -3
  44. package/dist/{postgres-migration-BVjq0iFW.mjs.map → postgres-migration-BfMmK6tv.mjs.map} +1 -1
  45. package/dist/{postgres-migration-UtGCjw5j.d.mts → postgres-migration-D7kNVWVG.d.mts} +2 -2
  46. package/dist/{postgres-migration-UtGCjw5j.d.mts.map → postgres-migration-D7kNVWVG.d.mts.map} +1 -1
  47. package/dist/{postgres-rls-policy-12wAEQhs.d.mts → postgres-rls-policy--CSddvMR.d.mts} +8 -16
  48. package/dist/postgres-rls-policy--CSddvMR.d.mts.map +1 -0
  49. package/dist/{postgres-schema-DLcc3W2-.mjs → postgres-schema-BLIHBEfX.mjs} +34 -64
  50. package/dist/postgres-schema-BLIHBEfX.mjs.map +1 -0
  51. package/dist/{postgres-schema-DJUEkWg-.d.mts → postgres-schema-BywoeKlt.d.mts} +9 -10
  52. package/dist/postgres-schema-BywoeKlt.d.mts.map +1 -0
  53. package/dist/postgres-table-schema-node-1nNc8XDT.mjs +248 -0
  54. package/dist/postgres-table-schema-node-1nNc8XDT.mjs.map +1 -0
  55. package/dist/resolve-ddl-schema-CL1RR1mx.mjs +20 -0
  56. package/dist/resolve-ddl-schema-CL1RR1mx.mjs.map +1 -0
  57. package/dist/runtime.mjs +1 -1
  58. package/dist/schema-ir-annotations.d.mts +9 -3
  59. package/dist/schema-ir-annotations.d.mts.map +1 -1
  60. package/dist/schema-ir-annotations.mjs +1 -1
  61. package/dist/types.d.mts +4 -4
  62. package/dist/types.mjs +3 -3
  63. package/package.json +20 -19
  64. package/src/core/authoring.ts +2 -2
  65. package/src/core/entity-kinds.ts +2 -2
  66. package/src/core/migrations/contract-to-postgres-database-schema-node.ts +125 -0
  67. package/src/core/migrations/diff-database-schema.ts +191 -0
  68. package/src/core/migrations/issue-planner.ts +1 -1
  69. package/src/core/migrations/op-factory-call.ts +1 -1
  70. package/src/core/migrations/operations/rls.ts +1 -1
  71. package/src/core/migrations/planner.ts +117 -68
  72. package/src/core/{schema-ir/postgres-schema-ir-annotations.ts → migrations/resolve-ddl-schema.ts} +8 -6
  73. package/src/core/migrations/runner.ts +13 -16
  74. package/src/core/migrations/verify-postgres-namespaces.ts +22 -14
  75. package/src/core/{schema-ir/postgres-rls-policy.ts → postgres-rls-policy.ts} +6 -43
  76. package/src/core/{schema-ir/postgres-role.ts → postgres-role.ts} +6 -21
  77. package/src/core/postgres-schema.ts +2 -2
  78. package/src/core/psl-infer/infer-psl-contract.ts +834 -0
  79. package/src/core/psl-infer/postgres-default-mapping.ts +16 -0
  80. package/src/core/psl-infer/postgres-type-map.ts +157 -0
  81. package/src/core/schema-ir/postgres-database-schema-node.ts +63 -0
  82. package/src/core/schema-ir/postgres-namespace-schema-node.ts +61 -0
  83. package/src/core/schema-ir/postgres-policy-schema-node.ts +93 -0
  84. package/src/core/schema-ir/postgres-role-schema-node.ts +66 -0
  85. package/src/core/schema-ir/{postgres-table-ir.ts → postgres-table-schema-node.ts} +29 -18
  86. package/src/core/schema-ir/schema-node-kinds.ts +28 -0
  87. package/src/exports/control.ts +30 -2
  88. package/src/exports/planner.ts +2 -5
  89. package/src/exports/schema-ir-annotations.ts +1 -1
  90. package/src/exports/types.ts +23 -15
  91. package/dist/authoring-CZmSQriw.mjs.map +0 -1
  92. package/dist/issue-planner-C-CwZd8D.mjs.map +0 -1
  93. package/dist/op-factory-call-DArx_1HM.mjs.map +0 -1
  94. package/dist/planner-BQ7lSqbm.mjs.map +0 -1
  95. package/dist/postgres-rls-policy-12wAEQhs.d.mts.map +0 -1
  96. package/dist/postgres-schema-DJUEkWg-.d.mts.map +0 -1
  97. package/dist/postgres-schema-DLcc3W2-.mjs.map +0 -1
  98. package/dist/postgres-schema-ir-BJl_a3J7.d.mts +0 -102
  99. package/dist/postgres-schema-ir-BJl_a3J7.d.mts.map +0 -1
  100. package/dist/postgres-schema-ir-I_FiUndw.mjs +0 -133
  101. package/dist/postgres-schema-ir-I_FiUndw.mjs.map +0 -1
  102. package/dist/postgres-schema-ir-annotations-BbDSiwx2.mjs +0 -14
  103. package/dist/postgres-schema-ir-annotations-BbDSiwx2.mjs.map +0 -1
  104. package/src/core/migrations/contract-to-postgres-schema-ir.ts +0 -91
  105. package/src/core/migrations/diff-postgres-schema.ts +0 -55
  106. package/src/core/schema-ir/postgres-schema-ir.ts +0 -138
@@ -0,0 +1,191 @@
1
+ import type { Contract } from '@prisma-next/contract/types';
2
+ import { verifySqlSchemaTree } from '@prisma-next/family-sql/diff';
3
+ import type { TargetBoundComponentDescriptor } from '@prisma-next/framework-components/components';
4
+ import type {
5
+ SchemaDiffIssue,
6
+ VerifyDatabaseSchemaResult,
7
+ } from '@prisma-next/framework-components/control';
8
+ import { diffSchemas, SchemaDiff } from '@prisma-next/framework-components/control';
9
+ import type { SqlStorage } from '@prisma-next/sql-contract/types';
10
+ import type { SqlSchemaIRNode } from '@prisma-next/sql-schema-ir/types';
11
+ import { blindCast } from '@prisma-next/utils/casts';
12
+ import { parsePostgresDefault } from '../default-normalizer';
13
+ import { normalizeSchemaNativeType } from '../native-type-normalizer';
14
+ import type { PostgresContract } from '../postgres-schema';
15
+ import { PostgresDatabaseSchemaNode } from '../schema-ir/postgres-database-schema-node';
16
+ import { PostgresPolicySchemaNode } from '../schema-ir/postgres-policy-schema-node';
17
+ import type { SqlSchemaDiffNode } from '../schema-ir/schema-node-kinds';
18
+ import { contractToPostgresDatabaseSchemaNode } from './contract-to-postgres-database-schema-node';
19
+
20
+ interface PostgresDiffDatabaseSchemaInput {
21
+ readonly contract: Contract<SqlStorage>;
22
+ readonly actualSchema: SqlSchemaIRNode;
23
+ readonly strict: boolean;
24
+ readonly typeMetadataRegistry: ReadonlyMap<string, { readonly nativeType?: string }>;
25
+ readonly frameworkComponents: ReadonlyArray<TargetBoundComponentDescriptor<'sql', string>>;
26
+ }
27
+
28
+ /**
29
+ * The single combined database-schema comparison — the one computation the
30
+ * migration planner and the family schema verify both consume. Composes,
31
+ * once each:
32
+ *
33
+ * - the per-namespace-paired relational diff (`verifySqlSchemaTree`) → table /
34
+ * column / constraint findings as framework `SchemaIssue`s (with the
35
+ * verification-tree `root` and pass/warn/fail counts);
36
+ * - the policy diff (`diffPostgresSchema` over the two trees) → RLS policy
37
+ * presence as `SchemaDiffIssue`s, ownership-filtered to the contract's owned
38
+ * schemas.
39
+ *
40
+ * `diffPostgresDatabaseSchema` and `verifyPostgresDatabaseSchema` both read
41
+ * this single result, so the relational walk runs once per caller — never
42
+ * once for the diff and again for the verify tree.
43
+ */
44
+ function computePostgresSchemaComparison(input: PostgresDiffDatabaseSchemaInput): {
45
+ readonly relational: VerifyDatabaseSchemaResult;
46
+ readonly schemaDiffIssues: readonly SchemaDiffIssue<SqlSchemaDiffNode>[];
47
+ } {
48
+ const postgresContract = blindCast<
49
+ PostgresContract,
50
+ 'diffPostgresDatabaseSchema is only called with a postgres contract'
51
+ >(input.contract);
52
+
53
+ // Relational diff: per-namespace-paired so a multi-schema database checks each
54
+ // contract namespace against its own actual node.
55
+ const relational = verifySqlSchemaTree({
56
+ contract: input.contract,
57
+ actualSchema: input.actualSchema,
58
+ buildExpectedSchema: (scopedContract) =>
59
+ contractToPostgresDatabaseSchemaNode(
60
+ blindCast<
61
+ PostgresContract | null,
62
+ 'the relational pairing projects a scoped postgres contract'
63
+ >(scopedContract),
64
+ { annotationNamespace: 'pg' },
65
+ ),
66
+ strict: input.strict,
67
+ typeMetadataRegistry: input.typeMetadataRegistry,
68
+ frameworkComponents: input.frameworkComponents,
69
+ normalizeDefault: parsePostgresDefault,
70
+ normalizeNativeType: normalizeSchemaNativeType,
71
+ });
72
+
73
+ // Policy diff: the generic node differ over the expected/actual policy trees,
74
+ // ownership-filtered to the schemas the contract owns (so unowned-namespace
75
+ // policies are not reported as extras). The actual schema is always the
76
+ // Postgres database root in production — assert it, matching the prior
77
+ // `collectSchemaDiffIssues` / `planPostgresSchemaDiff` behaviour.
78
+ PostgresDatabaseSchemaNode.assert(input.actualSchema);
79
+ const expected = contractToPostgresDatabaseSchemaNode(postgresContract, {
80
+ annotationNamespace: 'pg',
81
+ });
82
+ const schemaDiffIssues = filterIssuesByOwnership(
83
+ diffPostgresSchema(expected, input.actualSchema),
84
+ ownedSchemaNames(expected),
85
+ );
86
+
87
+ return { relational, schemaDiffIssues };
88
+ }
89
+
90
+ /**
91
+ * The `SchemaDiffer` for Postgres: the target's black-box comparison,
92
+ * projected to the two issue lists. Namespace presence (`missing_schema` →
93
+ * `CREATE SCHEMA`) is intentionally NOT composed here: it is a planner-only
94
+ * op-generation concern (verify rejects on the relational `missing_table` a
95
+ * missing schema already produces), so the planner stitches it in around this
96
+ * diff. Control-policy suppression of the policy issues is likewise a
97
+ * per-consumer post-step (verify filters the issues; the planner filters the
98
+ * calls).
99
+ */
100
+ export function diffPostgresDatabaseSchema(
101
+ input: PostgresDiffDatabaseSchemaInput,
102
+ ): SchemaDiff<SqlSchemaDiffNode> {
103
+ const { relational, schemaDiffIssues } = computePostgresSchemaComparison(input);
104
+ return new SchemaDiff(relational.schema.issues, schemaDiffIssues);
105
+ }
106
+
107
+ /**
108
+ * The same combined comparison as {@link diffPostgresDatabaseSchema}, wrapped
109
+ * in the verify envelope (`ok`/`summary`/`code`/`target`/`timings`) plus the
110
+ * pass/warn/fail tree the CLI renders — i.e. exactly the existing verify-result
111
+ * schema shape, so nothing downstream changes.
112
+ */
113
+ export function verifyPostgresDatabaseSchema(
114
+ input: PostgresDiffDatabaseSchemaInput,
115
+ ): VerifyDatabaseSchemaResult {
116
+ const { relational, schemaDiffIssues } = computePostgresSchemaComparison(input);
117
+ return {
118
+ ...relational,
119
+ schema: { ...relational.schema, schemaDiffIssues },
120
+ };
121
+ }
122
+
123
+ function ownedSchemaNames(expected: PostgresDatabaseSchemaNode): ReadonlySet<string> {
124
+ const policyNamespaces = Object.values(expected.namespaces).flatMap((ns) =>
125
+ Object.values(ns.tables).flatMap((t) => t.policies.map((p) => p.namespaceId)),
126
+ );
127
+ return new Set([...policyNamespaces, ...expected.existingSchemas]);
128
+ }
129
+
130
+ // Renders a display-only reference string for the diff message. If policy
131
+ // rendering grows, route it through the adapter's SQL renderer so the message
132
+ // can't diverge from the emitted policy SQL.
133
+ function renderPostgresPolicyReference(policy: PostgresPolicySchemaNode): string {
134
+ return `policy "${policy.name}" on "${policy.namespaceId}"."${policy.tableName}"`;
135
+ }
136
+
137
+ /**
138
+ * The policy node-diff — the structural half of the combined comparison above.
139
+ * Computes RLS-policy drift between two derived schema trees:
140
+ *
141
+ * 1. Runs the framework total diff over the two `PostgresDatabaseSchemaNode`
142
+ * roots (database → namespace → table → policy).
143
+ * 2. Filters to policy-subject issues only — this is transitional: the generic
144
+ * differ walks the whole tree, but the legacy relational verifier still owns
145
+ * table/column drift, so non-policy issues are dropped here.
146
+ * 3. Remaps the message to a human-readable policy reference.
147
+ *
148
+ * Both trees are `PostgresDatabaseSchemaNode`s, so every issue node is a
149
+ * `SqlSchemaDiffNode` — narrow the framework's `SchemaDiffIssue<DiffableNode>`
150
+ * output once here (the single boundary cast), so every downstream consumer
151
+ * (the ownership filter, the planner) reads the concrete node with no cast.
152
+ *
153
+ * Ownership filtering (dropping `extra` issues in namespaces a contract doesn't
154
+ * own) is the caller's responsibility — use `filterIssuesByOwnership`.
155
+ */
156
+ export function diffPostgresSchema(
157
+ expected: PostgresDatabaseSchemaNode,
158
+ actual: PostgresDatabaseSchemaNode,
159
+ ): readonly SchemaDiffIssue<SqlSchemaDiffNode>[] {
160
+ const issues = blindCast<
161
+ readonly SchemaDiffIssue<SqlSchemaDiffNode>[],
162
+ 'both trees are PostgresDatabaseSchemaNodes, so every diff-issue node is a SqlSchemaDiffNode'
163
+ >(diffSchemas(expected, actual));
164
+
165
+ return issues
166
+ .filter((i) => {
167
+ const node = i.expected ?? i.actual;
168
+ return node !== undefined && PostgresPolicySchemaNode.is(node);
169
+ })
170
+ .map((i) => {
171
+ const node = i.expected ?? i.actual;
172
+ if (node === undefined || !PostgresPolicySchemaNode.is(node)) return i;
173
+ return { ...i, message: `${i.outcome}: ${renderPostgresPolicyReference(node)}` };
174
+ });
175
+ }
176
+
177
+ /**
178
+ * Filters `extra` policy issues to those in owned namespaces. Call after
179
+ * `diffPostgresSchema` with the union of namespace ids from the expected tree's
180
+ * policies and its `existingSchemas`.
181
+ */
182
+ export function filterIssuesByOwnership(
183
+ issues: readonly SchemaDiffIssue<SqlSchemaDiffNode>[],
184
+ ownedSchemaNameSet: ReadonlySet<string>,
185
+ ): readonly SchemaDiffIssue<SqlSchemaDiffNode>[] {
186
+ return issues.filter((i) => {
187
+ if (i.outcome !== 'extra') return true;
188
+ if (i.actual === undefined) return false;
189
+ return PostgresPolicySchemaNode.is(i.actual) && ownedSchemaNameSet.has(i.actual.namespaceId);
190
+ });
191
+ }
@@ -15,7 +15,7 @@ import type {
15
15
  SqlPlannerConflict,
16
16
  SqlPlannerConflictLocation,
17
17
  } from '@prisma-next/family-sql/control';
18
- import { arraysEqual } from '@prisma-next/family-sql/schema-verify';
18
+ import { arraysEqual } from '@prisma-next/family-sql/diff';
19
19
  import type { TargetBoundComponentDescriptor } from '@prisma-next/framework-components/components';
20
20
  import type { SchemaIssue } from '@prisma-next/framework-components/control';
21
21
  import type {
@@ -40,7 +40,7 @@ import { blindCast } from '@prisma-next/utils/casts';
40
40
  import { ifDefined } from '@prisma-next/utils/defined';
41
41
  import { columnExistsAst, tableExistsAst } from '../../contract-free/checks';
42
42
  import * as contractFreeDdl from '../../contract-free/ddl';
43
- import type { PostgresRlsPolicy } from '../schema-ir/postgres-rls-policy';
43
+ import type { PostgresRlsPolicy } from '../postgres-rls-policy';
44
44
  import { escapeLiteral, quoteIdentifier } from '../sql-utils';
45
45
  import type { PostgresColumnDefault } from '../types';
46
46
  import { boundSchema } from './bound-schema';
@@ -2,7 +2,7 @@ import type { ExecuteRequestLowerer } from '@prisma-next/family-sql/control-adap
2
2
  import { ifDefined } from '@prisma-next/utils/defined';
3
3
  import { rlsEnabledAst, rlsPolicyExistsAst } from '../../../contract-free/checks';
4
4
  import { createPolicy, dropPolicy } from '../../../contract-free/ddl';
5
- import type { PostgresRlsPolicy } from '../../schema-ir/postgres-rls-policy';
5
+ import type { PostgresRlsPolicy } from '../../postgres-rls-policy';
6
6
  import { qualifyTableName } from '../planner-sql-checks';
7
7
  import { type Op, step, targetDetails } from './shared';
8
8
 
@@ -13,30 +13,29 @@ 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';
28
- import type { PostgresContract } from '../postgres-schema';
29
- import { assertPostgresRlsPolicy } from '../schema-ir/postgres-rls-policy';
30
- import { assertPostgresSchemaIR, ensurePostgresSchemaIR } from '../schema-ir/postgres-schema-ir';
31
- import { resolveDdlSchemaForNamespaceStorage } from '../schema-ir/postgres-schema-ir-annotations';
32
- import { contractToPostgresSchemaIR } from './contract-to-postgres-schema-ir';
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';
33
32
  import {
34
33
  formatPostgresControlPolicySubjectLabel,
35
34
  resolvePostgresCallControlPolicySubject,
36
35
  resolvePostgresIssueControlPolicySubject,
37
36
  resolvePostgresIssueCreationFactoryName,
38
37
  } from './control-policy';
39
- import { diffPostgresSchema, filterIssuesByOwnership } from './diff-postgres-schema';
38
+ import { diffPostgresDatabaseSchema } from './diff-database-schema';
40
39
  import { planIssues } from './issue-planner';
41
40
  import type { PostgresOpFactoryCall } from './op-factory-call';
42
41
  import {
@@ -46,6 +45,7 @@ import {
46
45
  } from './op-factory-call';
47
46
  import { TypeScriptRenderablePostgresMigration } from './planner-produced-postgres-migration';
48
47
  import { postgresPlannerStrategies } from './planner-strategies';
48
+ import { resolveDdlSchemaForNamespaceStorage } from './resolve-ddl-schema';
49
49
  import { verifyPostgresNamespacePresence } from './verify-postgres-namespaces';
50
50
 
51
51
  type PlannerFrameworkComponents = SqlMigrationPlannerPlanOptions extends {
@@ -58,10 +58,6 @@ type PlannerOptionsWithComponents = SqlMigrationPlannerPlanOptions & {
58
58
  readonly frameworkComponents: PlannerFrameworkComponents;
59
59
  };
60
60
 
61
- type VerifySqlSchemaOptionsWithComponents = Parameters<typeof verifySqlSchema>[0] & {
62
- readonly frameworkComponents: PlannerFrameworkComponents;
63
- };
64
-
65
61
  export function createPostgresMigrationPlanner(
66
62
  lowerer: ExecuteRequestLowerer,
67
63
  ): PostgresMigrationPlanner {
@@ -162,9 +158,33 @@ export class PostgresMigrationPlanner implements MigrationPlanner<'sql', 'postgr
162
158
  return policyResult;
163
159
  }
164
160
 
165
- 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);
166
180
  const codecHooks = extractCodecControlHooks(options.frameworkComponents);
167
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);
168
188
 
169
189
  // Input-side control-policy partition. `external` / `observed` subjects
170
190
  // — and non-creation issues for `tolerated` subjects — are dropped from
@@ -195,7 +215,7 @@ export class PostgresMigrationPlanner implements MigrationPlanner<'sql', 'postgr
195
215
  schemaName,
196
216
  codecHooks,
197
217
  storageTypes,
198
- schema: options.schema,
218
+ ...ifDefined('schema', relationalSchema),
199
219
  policy: options.policy,
200
220
  frameworkComponents: options.frameworkComponents,
201
221
  strategies: postgresPlannerStrategies,
@@ -205,7 +225,7 @@ export class PostgresMigrationPlanner implements MigrationPlanner<'sql', 'postgr
205
225
  return plannerFailure(result.failure);
206
226
  }
207
227
 
208
- const schemaDiffCalls = this.planPostgresSchemaDiff(options);
228
+ const schemaDiffCalls = this.planPostgresSchemaDiff(options, databaseDiff.schemaDiffIssues);
209
229
  const schemaDiffPartition = partitionCallsByControlPolicy({
210
230
  calls: schemaDiffCalls,
211
231
  contract: options.contract,
@@ -263,24 +283,16 @@ export class PostgresMigrationPlanner implements MigrationPlanner<'sql', 'postgr
263
283
  });
264
284
  }
265
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
+ */
266
292
  private planPostgresSchemaDiff(
267
293
  options: PlannerOptionsWithComponents,
294
+ filteredDiffIssues: readonly SchemaDiffIssue<SqlSchemaDiffNode>[],
268
295
  ): readonly PostgresOpFactoryCall[] {
269
- assertPostgresSchemaIR(options.schema);
270
- const expected = contractToPostgresSchemaIR(
271
- blindCast<PostgresContract, 'planPostgresSchemaDiff is only called with a postgres contract'>(
272
- options.contract,
273
- ),
274
- { annotationNamespace: 'pg' },
275
- );
276
- const actual = ensurePostgresSchemaIR(options.schema);
277
- const rawIssues = diffPostgresSchema(expected, actual);
278
- const ownedSchemaNames = new Set([
279
- ...expected.rlsPolicies.map((p) => p.namespaceId),
280
- ...expected.existingSchemas,
281
- ]);
282
- const filteredDiffIssues = filterIssuesByOwnership(rawIssues, ownedSchemaNames);
283
-
284
296
  const allowsDestructive = options.policy.allowedOperationClasses.includes('destructive');
285
297
  const calls: PostgresOpFactoryCall[] = [];
286
298
  const seenEnableTables = new Set<string>();
@@ -290,32 +302,34 @@ export class PostgresMigrationPlanner implements MigrationPlanner<'sql', 'postgr
290
302
  // encodes the body hash, so two policies sharing a local key (same name)
291
303
  // are always equal and isEqualTo never returns false.
292
304
  if (issue.outcome === 'missing') {
293
- assertPostgresRlsPolicy(issue.expected);
294
- // issue.expected.namespaceId is the DDL schema name (resolved during projection);
305
+ const expected = issue.expected;
306
+ PostgresPolicySchemaNode.assert(expected);
307
+ // expected.namespaceId is the DDL schema name (resolved during projection);
295
308
  // this re-resolution is a no-op as long as PostgresSchema.ddlSchemaName() returns this.id.
296
309
  const schemaForTable = resolveDdlSchemaForNamespaceStorage(
297
310
  options.contract.storage,
298
- issue.expected.namespaceId,
299
- options.schema,
311
+ expected.namespaceId,
300
312
  );
301
- const tableKey = `${schemaForTable}.${issue.expected.tableName}`;
313
+ const tableKey = `${schemaForTable}.${expected.tableName}`;
302
314
  if (!seenEnableTables.has(tableKey)) {
303
315
  seenEnableTables.add(tableKey);
304
- calls.push(new EnableRowLevelSecurityCall(schemaForTable, issue.expected.tableName));
316
+ calls.push(new EnableRowLevelSecurityCall(schemaForTable, expected.tableName));
305
317
  }
306
318
  calls.push(
307
- new CreatePostgresRlsPolicyCall(schemaForTable, issue.expected.tableName, issue.expected),
319
+ new CreatePostgresRlsPolicyCall(
320
+ schemaForTable,
321
+ expected.tableName,
322
+ policyNodeToContractPolicy(expected),
323
+ ),
308
324
  );
309
325
  } else if (issue.outcome === 'extra' && allowsDestructive) {
310
- assertPostgresRlsPolicy(issue.actual);
326
+ const actual = issue.actual;
327
+ PostgresPolicySchemaNode.assert(actual);
311
328
  const schemaForTable = resolveDdlSchemaForNamespaceStorage(
312
329
  options.contract.storage,
313
- issue.actual.namespaceId,
314
- options.schema,
315
- );
316
- calls.push(
317
- new DropPostgresRlsPolicyCall(schemaForTable, issue.actual.tableName, issue.actual.name),
330
+ actual.namespaceId,
318
331
  );
332
+ calls.push(new DropPostgresRlsPolicyCall(schemaForTable, actual.tableName, actual.name));
319
333
  }
320
334
  }
321
335
 
@@ -335,35 +349,70 @@ export class PostgresMigrationPlanner implements MigrationPlanner<'sql', 'postgr
335
349
  return null;
336
350
  }
337
351
 
338
- private collectSchemaIssues(options: PlannerOptionsWithComponents): readonly SchemaIssue[] {
339
- // `db init` uses additive-only policy and intentionally ignores extra
340
- // schema objects. Any reconciliation-capable policy (widening or
341
- // destructive) must inspect extras to reconcile strict equality.
342
- const allowed = options.policy.allowedOperationClasses;
343
- const strict = allowed.includes('widening') || allowed.includes('destructive');
344
- const verifyOptions: VerifySqlSchemaOptionsWithComponents = {
345
- contract: options.contract,
346
- schema: options.schema,
347
- strict,
348
- typeMetadataRegistry: new Map(),
349
- frameworkComponents: options.frameworkComponents,
350
- normalizeDefault: parsePostgresDefault,
351
- normalizeNativeType: normalizeSchemaNativeType,
352
- };
353
- const verifyResult = verifySqlSchema(verifyOptions);
354
- // Schema presence is a Postgres-specific concern (no equivalent in
355
- // SQLite / Mongo), so the issue emission lives in the target layer
356
- // rather than in the family verifier. Stitch it in here so a single
357
- // `SchemaIssue[]` flows through `planIssues` and the planner emits
358
- // CREATE SCHEMA in the dep bucket before any CreateTableCall.
359
- // Schema drift is handled separately via diffPostgresSchema → planPostgresSchemaDiff.
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[] {
360
368
  const namespaceIssues = verifyPostgresNamespacePresence({
361
369
  contract: options.contract,
362
370
  schema: options.schema,
363
371
  });
364
372
  if (namespaceIssues.length === 0) {
365
- return verifyResult.schema.issues;
373
+ return relationalIssues;
366
374
  }
367
- return [...namespaceIssues, ...verifyResult.schema.issues];
375
+ return [...namespaceIssues, ...relationalIssues];
368
376
  }
369
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
+ }
@@ -1,18 +1,20 @@
1
1
  import { UNBOUND_NAMESPACE_ID } from '@prisma-next/framework-components/ir';
2
2
  import type { SqlStorage } from '@prisma-next/sql-contract/types';
3
- import type { SqlSchemaIR } from '@prisma-next/sql-schema-ir/types';
4
3
  import { isPostgresSchema } from '../postgres-schema';
5
- import { isPostgresSchemaIR } from './postgres-schema-ir';
6
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
+ */
7
12
  export function resolveDdlSchemaForNamespaceStorage(
8
13
  storage: SqlStorage,
9
14
  namespaceId: string,
10
- schemaIr?: SqlSchemaIR,
11
15
  ): string {
12
16
  if (namespaceId === UNBOUND_NAMESPACE_ID) {
13
- return (
14
- (schemaIr && isPostgresSchemaIR(schemaIr) ? schemaIr.pgSchemaName : undefined) ?? 'public'
15
- );
17
+ return 'public';
16
18
  }
17
19
  const namespace = storage.namespaces[namespaceId];
18
20
  if (namespace && isPostgresSchema(namespace)) {
@@ -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
  }