@prisma-next/target-postgres 0.14.0-dev.49 → 0.14.0-dev.50

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/dist/control-policy-DUFgy9jO.mjs +1103 -0
  2. package/dist/control-policy-DUFgy9jO.mjs.map +1 -0
  3. package/dist/control.d.mts.map +1 -1
  4. package/dist/control.mjs +19 -40
  5. package/dist/control.mjs.map +1 -1
  6. package/dist/default-normalizer.mjs +247 -1
  7. package/dist/default-normalizer.mjs.map +1 -0
  8. package/dist/diff-database-schema-BmSmWcaY.mjs +313 -0
  9. package/dist/diff-database-schema-BmSmWcaY.mjs.map +1 -0
  10. package/dist/diff-database-schema.d.mts +46 -0
  11. package/dist/diff-database-schema.d.mts.map +1 -0
  12. package/dist/diff-database-schema.mjs +2 -0
  13. package/dist/issue-planner.d.mts +34 -10
  14. package/dist/issue-planner.d.mts.map +1 -1
  15. package/dist/issue-planner.mjs +2 -2
  16. package/dist/migration.d.mts +2 -2
  17. package/dist/migration.mjs +2 -2
  18. package/dist/native-type-normalizer.mjs +37 -1
  19. package/dist/native-type-normalizer.mjs.map +1 -0
  20. package/dist/{op-factory-call-BuoGT5UI.mjs → op-factory-call-BKRnMgrB.mjs} +2 -2
  21. package/dist/op-factory-call-BKRnMgrB.mjs.map +1 -0
  22. package/dist/{op-factory-call-Caqus-Qg.d.mts → op-factory-call-Dt-Jx5JS.d.mts} +2 -2
  23. package/dist/{op-factory-call-Caqus-Qg.d.mts.map → op-factory-call-Dt-Jx5JS.d.mts.map} +1 -1
  24. package/dist/op-factory-call.d.mts +2 -2
  25. package/dist/op-factory-call.mjs +2 -2
  26. package/dist/planner-BMBRPzsv.mjs +367 -0
  27. package/dist/planner-BMBRPzsv.mjs.map +1 -0
  28. package/dist/planner-ddl-builders-B8Nn6nHN.mjs.map +1 -1
  29. package/dist/planner-ddl-builders.d.mts +2 -2
  30. package/dist/planner-ddl-builders.d.mts.map +1 -1
  31. package/dist/planner-identity-values-CJPha2Sz.mjs.map +1 -1
  32. package/dist/planner-identity-values.d.mts +7 -1
  33. package/dist/planner-identity-values.d.mts.map +1 -1
  34. package/dist/planner-identity-values.mjs +2 -2
  35. package/dist/{planner-produced-postgres-migration-CtJi4sgS.d.mts → planner-produced-postgres-migration-C0a7I1-e.d.mts} +2 -2
  36. package/dist/{planner-produced-postgres-migration-CtJi4sgS.d.mts.map → planner-produced-postgres-migration-C0a7I1-e.d.mts.map} +1 -1
  37. package/dist/{planner-produced-postgres-migration-C2_H6Vvl.mjs → planner-produced-postgres-migration-Dp6IO_gj.mjs} +2 -2
  38. package/dist/{planner-produced-postgres-migration-C2_H6Vvl.mjs.map → planner-produced-postgres-migration-Dp6IO_gj.mjs.map} +1 -1
  39. package/dist/planner-produced-postgres-migration.d.mts +1 -1
  40. package/dist/planner-produced-postgres-migration.mjs +1 -1
  41. package/dist/planner-sql-checks-Cze3vMfe.mjs.map +1 -1
  42. package/dist/planner-sql-checks.d.mts +1 -1
  43. package/dist/planner-sql-checks.d.mts.map +1 -1
  44. package/dist/planner.d.mts +27 -67
  45. package/dist/planner.d.mts.map +1 -1
  46. package/dist/planner.mjs +3 -2
  47. package/dist/{postgres-database-schema-node-os8ogEap.d.mts → postgres-database-schema-node-C9bTuMrd.d.mts} +14 -7
  48. package/dist/postgres-database-schema-node-C9bTuMrd.d.mts.map +1 -0
  49. package/dist/{postgres-migration-D0QIVE-p.mjs → postgres-migration-D0jwzYDT.mjs} +2 -2
  50. package/dist/{postgres-migration-D0QIVE-p.mjs.map → postgres-migration-D0jwzYDT.mjs.map} +1 -1
  51. package/dist/{postgres-migration-CfyKDT7b.d.mts → postgres-migration-DFaxuRyI.d.mts} +2 -2
  52. package/dist/{postgres-migration-CfyKDT7b.d.mts.map → postgres-migration-DFaxuRyI.d.mts.map} +1 -1
  53. package/dist/{postgres-table-schema-node-Clei_xel.mjs → postgres-table-schema-node-BgaSrxYN.mjs} +30 -29
  54. package/dist/postgres-table-schema-node-BgaSrxYN.mjs.map +1 -0
  55. package/dist/schema-node-kinds-ClScchhi.mjs +76 -0
  56. package/dist/schema-node-kinds-ClScchhi.mjs.map +1 -0
  57. package/dist/types.d.mts +1 -1
  58. package/dist/types.mjs +1 -1
  59. package/package.json +21 -20
  60. package/src/core/migrations/column-ddl-rendering.ts +117 -0
  61. package/src/core/migrations/contract-to-postgres-database-schema-node.ts +25 -1
  62. package/src/core/migrations/control-policy.ts +68 -48
  63. package/src/core/migrations/diff-database-schema.ts +242 -153
  64. package/src/core/migrations/issue-planner.ts +621 -667
  65. package/src/core/migrations/operations/constraints.ts +1 -1
  66. package/src/core/migrations/planner-ddl-builders.ts +5 -2
  67. package/src/core/migrations/planner-identity-values.ts +1 -1
  68. package/src/core/migrations/planner-sql-checks.ts +1 -1
  69. package/src/core/migrations/planner-strategies.ts +248 -208
  70. package/src/core/migrations/planner.ts +180 -74
  71. package/src/core/migrations/runner.ts +1 -4
  72. package/src/core/migrations/verify-postgres-namespaces.ts +26 -15
  73. package/src/core/postgres-schema-verifier.ts +10 -7
  74. package/src/core/psl-infer/infer-psl-contract.ts +5 -10
  75. package/src/core/schema-ir/postgres-database-schema-node.ts +3 -6
  76. package/src/core/schema-ir/postgres-namespace-schema-node.ts +8 -9
  77. package/src/core/schema-ir/postgres-policy-schema-node.ts +3 -6
  78. package/src/core/schema-ir/postgres-role-schema-node.ts +3 -6
  79. package/src/core/schema-ir/postgres-table-schema-node.ts +20 -9
  80. package/src/core/schema-ir/schema-node-kinds.ts +80 -2
  81. package/src/exports/control.ts +10 -43
  82. package/src/exports/diff-database-schema.ts +7 -0
  83. package/src/exports/issue-planner.ts +1 -1
  84. package/src/exports/op-factory-call.ts +1 -0
  85. package/src/exports/planner-identity-values.ts +4 -1
  86. package/src/exports/planner.ts +0 -1
  87. package/dist/default-normalizer-Dug8Fez9.mjs +0 -248
  88. package/dist/default-normalizer-Dug8Fez9.mjs.map +0 -1
  89. package/dist/issue-planner-Cbh-xTEr.mjs +0 -849
  90. package/dist/issue-planner-Cbh-xTEr.mjs.map +0 -1
  91. package/dist/native-type-normalizer-Bc9XJzWC.mjs +0 -38
  92. package/dist/native-type-normalizer-Bc9XJzWC.mjs.map +0 -1
  93. package/dist/op-factory-call-BuoGT5UI.mjs.map +0 -1
  94. package/dist/planner-Bq0Z2sVO.mjs +0 -641
  95. package/dist/planner-Bq0Z2sVO.mjs.map +0 -1
  96. package/dist/postgres-database-schema-node-os8ogEap.d.mts.map +0 -1
  97. package/dist/postgres-table-schema-node-Clei_xel.mjs.map +0 -1
@@ -19,24 +19,32 @@ import type {
19
19
  MigrationPlanWithAuthoringSurface,
20
20
  MigrationScaffoldContext,
21
21
  SchemaDiffIssue,
22
- SchemaIssue,
22
+ SchemaOwnership,
23
23
  } from '@prisma-next/framework-components/control';
24
24
  import { UNBOUND_NAMESPACE_ID } from '@prisma-next/framework-components/ir';
25
+ import type { SqlStorage } from '@prisma-next/sql-contract/types';
25
26
  import type { SqlSchemaIR } from '@prisma-next/sql-schema-ir/types';
26
27
  import { blindCast } from '@prisma-next/utils/casts';
27
28
  import { ifDefined } from '@prisma-next/utils/defined';
28
29
  import { PostgresRlsPolicy } from '../postgres-rls-policy';
29
30
  import { PostgresDatabaseSchemaNode } from '../schema-ir/postgres-database-schema-node';
30
31
  import { PostgresPolicySchemaNode } from '../schema-ir/postgres-policy-schema-node';
31
- import type { SqlSchemaDiffNode } from '../schema-ir/schema-node-kinds';
32
+ import { PostgresSchemaNodeKind, type SqlSchemaDiffNode } from '../schema-ir/schema-node-kinds';
32
33
  import {
33
34
  formatPostgresControlPolicySubjectLabel,
35
+ resolveNamespaceIdForDdlSchema,
34
36
  resolvePostgresCallControlPolicySubject,
35
- resolvePostgresIssueControlPolicySubject,
36
- resolvePostgresIssueCreationFactoryName,
37
+ resolvePostgresNodeIssueControlPolicySubject,
38
+ resolvePostgresNodeIssueCreationFactoryName,
37
39
  } from './control-policy';
38
- import { diffPostgresDatabaseSchema } from './diff-database-schema';
39
- import { planIssues } from './issue-planner';
40
+ import { buildPostgresPlanDiff } from './diff-database-schema';
41
+ import {
42
+ coalesceSubtreeIssues,
43
+ issueNode,
44
+ issueSchemaName,
45
+ issueTableName,
46
+ planIssues,
47
+ } from './issue-planner';
40
48
  import type { PostgresOpFactoryCall } from './op-factory-call';
41
49
  import {
42
50
  CreatePostgresRlsPolicyCall,
@@ -82,18 +90,20 @@ export type PostgresPlanResult =
82
90
  /**
83
91
  * Postgres migration planner — a thin wrapper over `planIssues`.
84
92
  *
85
- * `plan()` verifies the live schema against the target contract (producing
86
- * `SchemaIssue[]`) and delegates to `planIssues` with the unified
87
- * `postgresPlannerStrategies` list: enum-change, NOT-NULL backfill,
88
- * type-change, nullable-tightening, codec-hook storage types,
89
- * component-declared dependency installs, and shared-temp-default /
90
- * empty-table-guarded NOT-NULL add-column. The same strategy list runs for
91
- * `migration plan`, `db update`, and `db init`; behavior diverges purely on
92
- * `policy.allowedOperationClasses` (the data-safe strategies short-circuit
93
- * when `'data'` is excluded). The issue planner applies operation-class
94
- * policy gates and emits a single `PostgresOpFactoryCall[]` that drives both
95
- * the runtime-ops view (via `renderOps`) and the `renderTypeScript()`
96
- * authoring surface.
93
+ * `plan()` diffs the target contract against the live schema via the one
94
+ * differ (`buildPostgresPlanDiff`, producing node-typed `SchemaDiffIssue[]`)
95
+ * and delegates to `planIssues` with the unified `postgresPlannerStrategies`
96
+ * list: NOT-NULL backfill, type-change, nullable-tightening, codec-hook
97
+ * storage types, component-declared dependency installs, and
98
+ * shared-temp-default / empty-table-guarded NOT-NULL add-column. The same
99
+ * strategy list runs for `migration plan`, `db update`, and `db init`;
100
+ * behavior diverges purely on `policy.allowedOperationClasses` (the
101
+ * data-safe strategies short-circuit when `'data'` is excluded). The issue
102
+ * planner applies operation-class policy gates and emits a single
103
+ * `PostgresOpFactoryCall[]` that drives both the runtime-ops view (via
104
+ * `renderOps`) and the `renderTypeScript()` authoring surface. RLS policy
105
+ * drift (the structural half of the same one-differ tree) is handled
106
+ * separately via `planPostgresSchemaDiff`.
97
107
  */
98
108
  export class PostgresMigrationPlanner implements MigrationPlanner<'sql', 'postgres'> {
99
109
  readonly #lowerer: ExecuteRequestLowerer | undefined;
@@ -129,6 +139,11 @@ export class PostgresMigrationPlanner implements MigrationPlanner<'sql', 'postgr
129
139
  * the marker row by the right space.
130
140
  */
131
141
  readonly spaceId: string;
142
+ /**
143
+ * Ownership oracle over the contract-space composition — see
144
+ * {@link SqlMigrationPlannerPlanOptions.ownership}.
145
+ */
146
+ readonly ownership?: SchemaOwnership;
132
147
  }): PostgresPlanResult {
133
148
  return this.planSql(options as SqlMigrationPlannerPlanOptions);
134
149
  }
@@ -158,25 +173,60 @@ export class PostgresMigrationPlanner implements MigrationPlanner<'sql', 'postgr
158
173
  return policyResult;
159
174
  }
160
175
 
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.
176
+ // The one combined tree diff drives the whole plan: relational findings
177
+ // become structural DDL via `planIssues`, policy findings become RLS ops
178
+ // via `planPostgresSchemaDiff`. Verify runs its own full-tree node diff
179
+ // (`diffSchema`) over the same schema and rejects on a
180
+ // surviving failure.
168
181
  PostgresDatabaseSchemaNode.assert(options.schema);
169
- const rawDiff = diffPostgresDatabaseSchema({
182
+ const { issues: rawIssues } = buildPostgresPlanDiff({
170
183
  contract: options.contract,
171
184
  actualSchema: options.schema,
172
- strict:
173
- options.policy.allowedOperationClasses.includes('widening') ||
174
- options.policy.allowedOperationClasses.includes('destructive'),
175
- typeMetadataRegistry: new Map(),
176
185
  frameworkComponents: options.frameworkComponents,
177
186
  });
178
- const databaseDiff = options.keepDiffIssue ? rawDiff.filter(options.keepDiffIssue) : rawDiff;
179
- const schemaIssues = this.collectSchemaIssues(options, databaseDiff.issues);
187
+ const policyDiffIssues = rawIssues.filter((issue) => isPolicyDiffIssue(issue));
188
+ const relationalDiffIssues = rawIssues.filter((issue) => !isPolicyDiffIssue(issue));
189
+
190
+ // The generic differ is total and un-gated: strict-mode extras filtering
191
+ // (dropping `not-expected` findings outside strict mode, mirroring the
192
+ // retired coordinate walk's `if (strict) { ...extra_* } }` guards),
193
+ // subtree coalescing (a missing/extra table also emits an issue for
194
+ // every child under it — redundant once the table-level Create/Drop call
195
+ // already accounts for the whole subtree), and ownership are all post-diff
196
+ // planner steps.
197
+ //
198
+ // Ownership: a live extra is only this plan's to drop when no contract
199
+ // space owns it. The differ ran against THIS space's contract, so a table
200
+ // a sibling space owns surfaces here as `not-expected`; the planner asks
201
+ // the ownership oracle (the passive aggregate) whether any space declares
202
+ // it and, if so, leaves it alone — it is a sibling's table, not an orphan.
203
+ // A table no space owns stays a genuine extra to drop under a destructive
204
+ // policy. Ownership lives in the aggregate; the planner only asks. Absent
205
+ // oracle (single-space, none handed) keeps every extra. Coalescing MUST
206
+ // run before this so a sibling-owned table's child issues have already
207
+ // collapsed into the one table-level issue that carries the table name.
208
+ const strict =
209
+ options.policy.allowedOperationClasses.includes('widening') ||
210
+ options.policy.allowedOperationClasses.includes('destructive');
211
+ const coalesced = coalesceSubtreeIssues(relationalDiffIssues);
212
+ const owned = retainUnownedExtras(coalesced, options.ownership, options.contract);
213
+ const gated = strict ? owned : owned.filter((issue) => issue.reason !== 'not-expected');
214
+
215
+ // Namespace presence (`CREATE SCHEMA`) is a planner-only op-generation
216
+ // concern stitched in here rather than inside the shared diff — verify
217
+ // never needs it (a missing schema already surfaces as a `not-found`
218
+ // table in the relational findings). These synthesized issues are added
219
+ // AFTER coalescing/scoping (never coalesced against the table diff —
220
+ // their path is an ancestor of every table path under that schema, so
221
+ // running them through the same coalesce would swallow the table-level
222
+ // `not-found` issues that drive `CREATE TABLE`) and are NOT subject to
223
+ // sibling-space scoping, matching the retired coordinate walk exactly.
224
+ const namespaceIssues = verifyPostgresNamespacePresence({
225
+ contract: options.contract,
226
+ schema: options.schema,
227
+ });
228
+ const schemaIssues = [...namespaceIssues, ...gated];
229
+
180
230
  const codecHooks = extractCodecControlHooks(options.frameworkComponents);
181
231
  const storageTypes = options.contract.storage.types ?? {};
182
232
  // The strategy layer reads the live schema by bare table name for existence
@@ -197,8 +247,8 @@ export class PostgresMigrationPlanner implements MigrationPlanner<'sql', 'postgr
197
247
  issues: schemaIssues,
198
248
  contract: options.contract,
199
249
  resolveControlPolicySubject: (issue) =>
200
- resolvePostgresIssueControlPolicySubject(issue, options.contract),
201
- resolveCreationFactoryName: resolvePostgresIssueCreationFactoryName,
250
+ resolvePostgresNodeIssueControlPolicySubject(issue, options.contract),
251
+ resolveCreationFactoryName: resolvePostgresNodeIssueCreationFactoryName,
202
252
  formatSubjectLabel: (factoryName, subject) =>
203
253
  formatPostgresControlPolicySubjectLabel(factoryName, subject, options.contract),
204
254
  });
@@ -210,7 +260,7 @@ export class PostgresMigrationPlanner implements MigrationPlanner<'sql', 'postgr
210
260
  // `db update` / `db init`, which means data-safety strategies needing
211
261
  // from/to comparisons (unsafe type change, nullable tightening) are
212
262
  // inapplicable there — reconciliation falls through to
213
- // `mapIssueToCall`'s direct destructive handlers.
263
+ // `mapNodeIssueToCall`'s direct destructive handlers.
214
264
  fromContract: options.fromContract,
215
265
  schemaName,
216
266
  codecHooks,
@@ -225,7 +275,7 @@ export class PostgresMigrationPlanner implements MigrationPlanner<'sql', 'postgr
225
275
  return plannerFailure(result.failure);
226
276
  }
227
277
 
228
- const schemaDiffCalls = this.planPostgresSchemaDiff(options, databaseDiff.schemaDiffIssues);
278
+ const schemaDiffCalls = this.planPostgresSchemaDiff(options, policyDiffIssues);
229
279
  const schemaDiffPartition = partitionCallsByControlPolicy({
230
280
  calls: schemaDiffCalls,
231
281
  contract: options.contract,
@@ -284,10 +334,10 @@ export class PostgresMigrationPlanner implements MigrationPlanner<'sql', 'postgr
284
334
  }
285
335
 
286
336
  /**
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.
337
+ * Maps the RLS policy presence findings of the one combined tree diff
338
+ * (`buildPostgresPlanDiff`, already ownership-filtered) into `ENABLE RLS`
339
+ * / `CREATE POLICY` / `DROP POLICY` ops. It does not re-diff it consumes
340
+ * exactly the policy-node subset of the shared diff's issues.
291
341
  */
292
342
  private planPostgresSchemaDiff(
293
343
  options: PlannerOptionsWithComponents,
@@ -298,10 +348,10 @@ export class PostgresMigrationPlanner implements MigrationPlanner<'sql', 'postgr
298
348
  const seenEnableTables = new Set<string>();
299
349
 
300
350
  for (const issue of filteredDiffIssues) {
301
- // 'mismatch' is unreachable for content-addressed policies: the wire name
351
+ // 'not-equal' is unreachable for content-addressed policies: the wire name
302
352
  // encodes the body hash, so two policies sharing a local key (same name)
303
353
  // are always equal and isEqualTo never returns false.
304
- if (issue.outcome === 'missing') {
354
+ if (issue.reason === 'not-found') {
305
355
  const expected = issue.expected;
306
356
  PostgresPolicySchemaNode.assert(expected);
307
357
  // expected.namespaceId is the DDL schema name (resolved during projection);
@@ -322,7 +372,7 @@ export class PostgresMigrationPlanner implements MigrationPlanner<'sql', 'postgr
322
372
  policyNodeToContractPolicy(expected),
323
373
  ),
324
374
  );
325
- } else if (issue.outcome === 'extra' && allowsDestructive) {
375
+ } else if (issue.reason === 'not-expected' && allowsDestructive) {
326
376
  const actual = issue.actual;
327
377
  PostgresPolicySchemaNode.assert(actual);
328
378
  const schemaForTable = resolveDdlSchemaForNamespaceStorage(
@@ -348,50 +398,106 @@ export class PostgresMigrationPlanner implements MigrationPlanner<'sql', 'postgr
348
398
  }
349
399
  return null;
350
400
  }
401
+ }
351
402
 
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[] {
368
- const namespaceIssues = verifyPostgresNamespacePresence({
369
- contract: options.contract,
370
- schema: options.schema,
371
- });
372
- if (namespaceIssues.length === 0) {
373
- return relationalIssues;
374
- }
375
- return [...namespaceIssues, ...relationalIssues];
376
- }
403
+ /**
404
+ * A diff issue whose node is an RLS policy — the structural half of the one
405
+ * combined tree diff, routed to `planPostgresSchemaDiff` instead of
406
+ * `planIssues`.
407
+ */
408
+ function isPolicyDiffIssue(issue: SchemaDiffIssue<SqlSchemaDiffNode>): boolean {
409
+ const node = issue.expected ?? issue.actual;
410
+ return node !== undefined && PostgresPolicySchemaNode.is(node);
377
411
  }
378
412
 
379
413
  /**
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.
414
+ * Drops a `not-expected` issue when it is a whole extra TABLE that some
415
+ * contract space owns, asking the ownership oracle per node.
416
+ *
417
+ * The consultation applies ONLY to table-level extras a live table this
418
+ * space's contract lacks — identified by asking the issue's own node
419
+ * (`nodeKind === table`), never by counting path segments. `declaresEntity`
420
+ * answers over the whole composition (self included), keyed on the schema-IR
421
+ * entity coordinate so a genuine orphan in one namespace is never conflated
422
+ * with a same-named table a sibling space declares in another, or with a
423
+ * same-named non-table entity (an enum, an RLS policy) a sibling declares in
424
+ * the SAME namespace: a positive answer means another space owns THIS table
425
+ * in THIS namespace (a table this space owned would be in its expected tree,
426
+ * never an extra) — leave it. A negative answer means no space owns it — a
427
+ * genuine orphan to drop. `entityKind` is the literal `'table'` here: this
428
+ * function only ever asks about a node already confirmed to be
429
+ * `PostgresSchemaNodeKind.table` (checked just above), and the diff tree's
430
+ * `nodeKind` vocabulary (`'postgres-table'`) is distinct from the storage
431
+ * `entries` vocabulary `elementCoordinates` walks (`'table'`) — the literal
432
+ * is that storage-entries spelling, not the node kind.
433
+ *
434
+ * The issue path carries the *resolved DDL schema* (e.g. `public`), but a
435
+ * contract space's own declared namespace id can be the unbound sentinel
436
+ * (`__unbound__`) that resolves to that schema — the two spellings would
437
+ * never string-match. `resolveNamespaceIdForDdlSchema` recovers the raw
438
+ * namespace id THIS space's own contract would use for that DDL schema, so
439
+ * a table this space's own unbound namespace declares (and any sibling
440
+ * whose own unbound namespace resolves the same way) is compared on the
441
+ * same coordinate the aggregate's `elementCoordinates` walk yields.
442
+ *
443
+ * A DEEPER extra (an extra column/constraint on a table this space DOES own —
444
+ * only the child drifted, so the table is in the expected tree) is this
445
+ * space's own drift and is always kept for dropping; asking the oracle there
446
+ * would wrongly suppress it, because the owned table answers `true`. Shallower
447
+ * issues (namespace/root) are never ownership-scoped here. No oracle ⇒ every
448
+ * extra is kept (single-space plan).
449
+ */
450
+ function retainUnownedExtras(
451
+ issues: readonly SchemaDiffIssue<SqlSchemaDiffNode>[],
452
+ ownership: SchemaOwnership | undefined,
453
+ contract: Contract<SqlStorage>,
454
+ ): readonly SchemaDiffIssue<SqlSchemaDiffNode>[] {
455
+ if (ownership === undefined) return issues;
456
+ return issues.filter((issue) => {
457
+ if (issue.reason !== 'not-expected') return true;
458
+ const node = issueNode(issue);
459
+ if (node === undefined || node.nodeKind !== PostgresSchemaNodeKind.table) return true;
460
+ const ddlSchemaName = issueSchemaName(issue);
461
+ const tableName = issueTableName(issue);
462
+ if (ddlSchemaName === undefined || tableName === undefined) return true;
463
+ const namespaceId = resolveNamespaceIdForDdlSchema(contract, ddlSchemaName);
464
+ return !ownership.declaresEntity({ namespaceId, entityKind: 'table', entityName: tableName });
465
+ });
466
+ }
467
+
468
+ /**
469
+ * Returns the one namespace node the relational strategy layer probes for
470
+ * live-table existence and reads codec-hook context off — the namespace
471
+ * matching the planner's resolved schema name, or the first namespace when
472
+ * none matches. `undefined` when the tree has no namespaces, so the strategy
473
+ * context uses its empty-schema default.
384
474
  *
385
475
  * The relational strategies key tables by bare name, so they can only probe one
386
476
  * namespace at a time; probing across every namespace at once is future work.
477
+ *
478
+ * Returns the real `PostgresNamespaceSchemaNode` reference rather than a
479
+ * projection: `storageTypePlanCallStrategy` hands this same value to codec
480
+ * `planTypeOperations` hooks as `schema`, and hooks read the Postgres-specific
481
+ * `nativeEnumTypeNames` field off it (via `PostgresNamespaceSchemaNode.is`) to
482
+ * decide whether a native enum type already exists — a projection that only
483
+ * copies `tables` would silently drop that signal. `StrategyContext.schema`'s
484
+ * declared type (`SqlSchemaIR`, shared with SQLite's flat shape) doesn't
485
+ * capture this, so the return is `blindCast` the same way `namespaceSchemaNodes`
486
+ * in the family's relational walk already treats a namespace node as a
487
+ * structurally-`SqlSchemaIR`-shaped value.
387
488
  */
388
489
  function relationalNamespaceNode(
389
490
  schema: PostgresDatabaseSchemaNode,
390
491
  schemaName: string,
391
492
  ): SqlSchemaIR | undefined {
392
493
  const namespaceNodes = Object.values(schema.namespaces);
393
- const byName = namespaceNodes.find((node) => node.schemaName === schemaName);
394
- return byName ?? namespaceNodes[0];
494
+ const namespaceNode =
495
+ namespaceNodes.find((node) => node.schemaName === schemaName) ?? namespaceNodes[0];
496
+ if (namespaceNode === undefined) return undefined;
497
+ return blindCast<
498
+ SqlSchemaIR,
499
+ 'PostgresNamespaceSchemaNode carries tables (+ nativeEnumTypeNames, read by codec hooks via PostgresNamespaceSchemaNode.is) structurally compatible with the SqlSchemaIR shape the strategy layer declares'
500
+ >(namespaceNode);
395
501
  }
396
502
 
397
503
  /**
@@ -140,10 +140,7 @@ class PostgresMigrationRunner implements SqlMigrationRunner<PostgresPlanTargetDe
140
140
  return runnerFailure('SCHEMA_VERIFY_FAILED', schemaVerifyResult.summary, {
141
141
  why: 'The resulting database schema does not satisfy the destination contract.',
142
142
  meta: {
143
- issues: [
144
- ...schemaVerifyResult.schema.issues,
145
- ...schemaVerifyResult.schema.schemaDiffIssues,
146
- ],
143
+ issues: schemaVerifyResult.schema.issues,
147
144
  },
148
145
  });
149
146
  }
@@ -1,11 +1,13 @@
1
1
  import type { Contract } from '@prisma-next/contract/types';
2
- import type { SchemaIssue } from '@prisma-next/framework-components/control';
2
+ import type { SchemaDiffIssue } 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
5
  import type { SqlSchemaIRNode } from '@prisma-next/sql-schema-ir/types';
6
6
  import { DEFAULT_NAMESPACE_ID } from '../namespace-ids';
7
7
  import { isPostgresSchema } from '../postgres-schema';
8
8
  import { PostgresDatabaseSchemaNode } from '../schema-ir/postgres-database-schema-node';
9
+ import { PostgresNamespaceSchemaNode } from '../schema-ir/postgres-namespace-schema-node';
10
+ import type { SqlSchemaDiffNode } from '../schema-ir/schema-node-kinds';
9
11
 
10
12
  /**
11
13
  * Resolves the live-database schema name for a given namespace
@@ -48,12 +50,13 @@ function existingSchemasFromSchema(schema: SqlSchemaIRNode): readonly string[] {
48
50
  }
49
51
 
50
52
  /**
51
- * Emits a `missing_schema` issue for every contract-declared Postgres
52
- * namespace whose live container does not yet exist. The planner's
53
- * `collectSchemaIssues` prepends these to the relational findings so a
54
- * multi-schema plan emits `CREATE SCHEMA` before the tables that need it —
55
- * a planner-only concern (verify already rejects via the `missing_table` a
56
- * missing schema produces), so this is not part of the shared diff.
53
+ * Emits a `postgres-namespace` `not-found` diff issue for every
54
+ * contract-declared Postgres namespace whose live container does not yet
55
+ * exist. The planner prepends these (node-typed, synthesized) to the
56
+ * relational diff issues so a multi-schema plan emits `CREATE SCHEMA`
57
+ * before the tables that need it — a planner-only concern (verify already
58
+ * rejects via the `not-found` table issues a missing schema already
59
+ * produces), so this is not part of the shared diff.
57
60
  *
58
61
  * A namespace's live container is the schema returned by its
59
62
  * polymorphic `ddlSchemaName(storage)` method — named schemas resolve
@@ -64,29 +67,37 @@ function existingSchemasFromSchema(schema: SqlSchemaIRNode): readonly string[] {
64
67
  * introspected list. `public` is suppressed implicitly because the
65
68
  * introspection (or its sensible default) always carries it.
66
69
  *
67
- * Each emitted issue stamps `namespaceId` with the contract namespace
68
- * coordinate so the downstream `mapIssueToCall` re-resolves the DDL
69
- * schema name through the same polymorphic path keeping the
70
- * coordinate, not the resolved name, as the issue's stable identity.
70
+ * Each emitted issue's path is `['database', ddlName]` an ancestor of
71
+ * every table path under that schema, so it must never be run through
72
+ * `coalesceSubtreeIssues` alongside the table diff (it would swallow the
73
+ * table-level `not-found` issues that drive `CREATE TABLE`); the planner
74
+ * adds these AFTER coalescing the relational issues, and they are not
75
+ * subject to sibling-space ownership scoping (mirrors the retired
76
+ * coordinate walk, which prepended namespace issues after that filter).
71
77
  */
72
78
  export function verifyPostgresNamespacePresence(input: {
73
79
  readonly contract: Contract<SqlStorage>;
74
80
  readonly schema: SqlSchemaIRNode;
75
- }): readonly SchemaIssue[] {
81
+ }): readonly SchemaDiffIssue<SqlSchemaDiffNode>[] {
76
82
  const { contract, schema } = input;
77
83
  const existing = new Set(existingSchemasFromSchema(schema));
78
- const issues: SchemaIssue[] = [];
84
+ const issues: SchemaDiffIssue<SqlSchemaDiffNode>[] = [];
79
85
  const namespaceIds = Object.keys(contract.storage.namespaces).sort();
80
86
  for (const namespaceId of namespaceIds) {
81
87
  if (namespaceId === UNBOUND_NAMESPACE_ID) continue;
82
88
  const ddlName = resolveDdlSchemaName(contract.storage, namespaceId);
83
89
  if (ddlName === UNBOUND_NAMESPACE_ID) continue;
84
90
  if (existing.has(ddlName)) continue;
91
+ const namespace = new PostgresNamespaceSchemaNode({
92
+ schemaName: ddlName,
93
+ tables: {},
94
+ nativeEnumTypeNames: [],
95
+ });
85
96
  issues.push({
86
- kind: 'missing_schema',
97
+ path: ['database', ddlName],
87
98
  reason: 'not-found',
88
- namespaceId,
89
99
  message: `Schema "${ddlName}" is missing from database`,
100
+ expected: namespace,
90
101
  });
91
102
  }
92
103
  return issues;
@@ -1,16 +1,19 @@
1
1
  import type { Contract } from '@prisma-next/contract/types';
2
2
  import { SqlSchemaVerifierBase } from '@prisma-next/family-sql/ir';
3
- import type { SchemaIssue, SchemaVerifyOptions } from '@prisma-next/framework-components/control';
3
+ import type {
4
+ SchemaDiffIssue,
5
+ SchemaVerifyOptions,
6
+ } from '@prisma-next/framework-components/control';
4
7
  import type { SqlStorage } from '@prisma-next/sql-contract/types';
5
8
  import type { SqlSchemaIR } from '@prisma-next/sql-schema-ir/types';
6
9
 
7
10
  /**
8
11
  * Postgres target `SchemaVerifier` concretion. Plugs into the
9
12
  * SQL-shared verification surface; production verification today still
10
- * routes through the legacy `verifySqlSchema` entry point, which
11
- * carries options (codec hooks, normalizers, framework components)
12
- * that the framework-level `SchemaVerifyOptions` shape does not yet
13
- * surface.
13
+ * routes through the family verify verdict (`verifySqlSchemaByDiff` over
14
+ * `diffPostgresSchema`), which carries options (codec hooks,
15
+ * normalizers, framework components) that the framework-level
16
+ * `SchemaVerifyOptions` shape does not yet surface.
14
17
  *
15
18
  * The hooks return the empty list pending the call-site migration that
16
19
  * routes the existing verifier behaviour through the SPI — at that
@@ -25,13 +28,13 @@ export class PostgresSchemaVerifier extends SqlSchemaVerifierBase<
25
28
  > {
26
29
  protected verifyCommonSqlSchema(
27
30
  _options: SchemaVerifyOptions<Contract<SqlStorage>, SqlSchemaIR>,
28
- ): readonly SchemaIssue[] {
31
+ ): readonly SchemaDiffIssue[] {
29
32
  return [];
30
33
  }
31
34
 
32
35
  protected verifyTargetExtensions(
33
36
  _options: SchemaVerifyOptions<Contract<SqlStorage>, SqlSchemaIR>,
34
- ): readonly SchemaIssue[] {
37
+ ): readonly SchemaDiffIssue[] {
35
38
  return [];
36
39
  }
37
40
  }
@@ -36,12 +36,8 @@ import {
36
36
  UNSPECIFIED_PSL_NAMESPACE_ID,
37
37
  } from '@prisma-next/framework-components/psl-ast';
38
38
  import type { SqlModelStorage } from '@prisma-next/sql-contract/types';
39
- import type {
40
- SqlColumnIR,
41
- SqlForeignKeyIR,
42
- SqlSchemaIR,
43
- SqlTableIR,
44
- } from '@prisma-next/sql-schema-ir/types';
39
+ import type { SqlColumnIR, SqlForeignKeyIR } from '@prisma-next/sql-schema-ir/types';
40
+ import { SqlSchemaIR, SqlTableIR } from '@prisma-next/sql-schema-ir/types';
45
41
  import { blindCast } from '@prisma-next/utils/casts';
46
42
  import { ifDefined } from '@prisma-next/utils/defined';
47
43
  import type { PostgresDatabaseSchemaNode } from '../schema-ir/postgres-database-schema-node';
@@ -255,7 +251,7 @@ function resolveForeignKeys(
255
251
  resultTables[tableName] =
256
252
  keptForeignKeys.length === table.foreignKeys.length
257
253
  ? table
258
- : { ...table, foreignKeys: keptForeignKeys };
254
+ : new SqlTableIR({ ...table, foreignKeys: keptForeignKeys });
259
255
  }
260
256
 
261
257
  return { tables: resultTables, extraRelationsByTable, crossSpaceFieldNamesByTable };
@@ -335,16 +331,15 @@ export function inferPostgresPslContract(
335
331
  'output is a later slice).',
336
332
  );
337
333
  }
338
- tables[tableName] = table;
334
+ tables[tableName] = new SqlTableIR(table);
339
335
  }
340
336
  }
341
-
342
337
  const {
343
338
  tables: resolvedTables,
344
339
  extraRelationsByTable,
345
340
  crossSpaceFieldNamesByTable,
346
341
  } = resolveForeignKeys(tables, owners);
347
- const schemaIR: SqlSchemaIR = { tables: resolvedTables };
342
+ const schemaIR = new SqlSchemaIR({ tables: resolvedTables });
348
343
 
349
344
  const options: PslPrinterOptions = {
350
345
  typeMap: createPostgresTypeMap(new Set()),
@@ -1,6 +1,6 @@
1
1
  import type { DiffableNode } from '@prisma-next/framework-components/control';
2
2
  import { freezeNode } from '@prisma-next/framework-components/ir';
3
- import { SqlSchemaIRNode } from '@prisma-next/sql-schema-ir/types';
3
+ import { assertNode, SqlSchemaIRNode } from '@prisma-next/sql-schema-ir/types';
4
4
  import type { PostgresNamespaceSchemaNode } from './postgres-namespace-schema-node';
5
5
  import type { PostgresRoleSchemaNode } from './postgres-role-schema-node';
6
6
  import { PostgresSchemaNodeKind } from './schema-node-kinds';
@@ -23,6 +23,7 @@ export interface PostgresDatabaseSchemaNodeInput {
23
23
  */
24
24
  export class PostgresDatabaseSchemaNode extends SqlSchemaIRNode implements DiffableNode {
25
25
  override readonly nodeKind = PostgresSchemaNodeKind.database;
26
+
26
27
  readonly namespaces: Readonly<Record<string, PostgresNamespaceSchemaNode>>;
27
28
  readonly roles: readonly PostgresRoleSchemaNode[];
28
29
  readonly existingSchemas: readonly string[];
@@ -54,10 +55,6 @@ export class PostgresDatabaseSchemaNode extends SqlSchemaIRNode implements Diffa
54
55
  }
55
56
 
56
57
  static assert(node: SqlSchemaIRNode): asserts node is PostgresDatabaseSchemaNode {
57
- if (!PostgresDatabaseSchemaNode.is(node)) {
58
- throw new Error(
59
- `Expected a PostgresDatabaseSchemaNode but got nodeKind=${node.nodeKind ?? 'undefined'}`,
60
- );
61
- }
58
+ assertNode(node, 'PostgresDatabaseSchemaNode', PostgresDatabaseSchemaNode.is);
62
59
  }
63
60
  }
@@ -1,6 +1,6 @@
1
1
  import type { DiffableNode } from '@prisma-next/framework-components/control';
2
2
  import { freezeNode } from '@prisma-next/framework-components/ir';
3
- import { SqlSchemaIRNode } from '@prisma-next/sql-schema-ir/types';
3
+ import { assertNode, SqlSchemaIRNode } from '@prisma-next/sql-schema-ir/types';
4
4
  import type { PostgresTableSchemaNode } from './postgres-table-schema-node';
5
5
  import { PostgresSchemaNodeKind } from './schema-node-kinds';
6
6
 
@@ -12,9 +12,11 @@ export interface PostgresNamespaceSchemaNodeInput {
12
12
 
13
13
  /**
14
14
  * One-per-Postgres-schema diff-tree node. Groups the tables belonging to a
15
- * single namespace and satisfies the `SqlSchemaIR` shape so legacy per-schema
16
- * consumers (verifySqlSchema, the relational planner, toSchemaView) can
17
- * accept it unchanged in Unit 6.
15
+ * single namespace. Per-schema consumers (the relational planner,
16
+ * toSchemaView) read this node's `tables` field structurally via
17
+ * `blindCast`/`SqlSchemaIRNode` not through a static `SqlSchemaIR`
18
+ * assignment — because `nodeKind` now carries this node's own literal
19
+ * (`postgres-namespace`), distinct from `SqlSchemaIR`'s own (`sql-schema`).
18
20
  *
19
21
  * `id` is the schema name. `isEqualTo` is identity — two namespace nodes are
20
22
  * equal iff their ids (schema names) match. `children()` returns the table
@@ -23,6 +25,7 @@ export interface PostgresNamespaceSchemaNodeInput {
23
25
  */
24
26
  export class PostgresNamespaceSchemaNode extends SqlSchemaIRNode implements DiffableNode {
25
27
  override readonly nodeKind = PostgresSchemaNodeKind.namespace;
28
+
26
29
  readonly schemaName: string;
27
30
  readonly tables: Readonly<Record<string, PostgresTableSchemaNode>>;
28
31
  readonly nativeEnumTypeNames: readonly string[];
@@ -52,10 +55,6 @@ export class PostgresNamespaceSchemaNode extends SqlSchemaIRNode implements Diff
52
55
  }
53
56
 
54
57
  static assert(node: SqlSchemaIRNode): asserts node is PostgresNamespaceSchemaNode {
55
- if (!PostgresNamespaceSchemaNode.is(node)) {
56
- throw new Error(
57
- `Expected a PostgresNamespaceSchemaNode but got nodeKind=${node.nodeKind ?? 'undefined'}`,
58
- );
59
- }
58
+ assertNode(node, 'PostgresNamespaceSchemaNode', PostgresNamespaceSchemaNode.is);
60
59
  }
61
60
  }
@@ -1,6 +1,6 @@
1
1
  import type { DiffableNode } from '@prisma-next/framework-components/control';
2
2
  import { freezeNode } from '@prisma-next/framework-components/ir';
3
- import { SqlSchemaIRNode } from '@prisma-next/sql-schema-ir/types';
3
+ import { assertNode, SqlSchemaIRNode } from '@prisma-next/sql-schema-ir/types';
4
4
  import { blindCast } from '@prisma-next/utils/casts';
5
5
  import type { RlsPolicyOperation } from '../postgres-rls-policy';
6
6
  import { PostgresSchemaNodeKind } from './schema-node-kinds';
@@ -38,6 +38,7 @@ export interface PostgresPolicySchemaNodeInput {
38
38
  */
39
39
  export class PostgresPolicySchemaNode extends SqlSchemaIRNode implements DiffableNode {
40
40
  override readonly nodeKind = PostgresSchemaNodeKind.policy;
41
+
41
42
  readonly name: string;
42
43
  readonly prefix: string;
43
44
  readonly tableName: string;
@@ -84,10 +85,6 @@ export class PostgresPolicySchemaNode extends SqlSchemaIRNode implements Diffabl
84
85
  }
85
86
 
86
87
  static assert(node: SqlSchemaIRNode | undefined): asserts node is PostgresPolicySchemaNode {
87
- if (node === undefined || !PostgresPolicySchemaNode.is(node)) {
88
- throw new Error(
89
- `Expected a PostgresPolicySchemaNode but got nodeKind=${node?.nodeKind ?? 'undefined'}`,
90
- );
91
- }
88
+ assertNode(node, 'PostgresPolicySchemaNode', PostgresPolicySchemaNode.is);
92
89
  }
93
90
  }