@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.
- package/dist/control-policy-DUFgy9jO.mjs +1103 -0
- package/dist/control-policy-DUFgy9jO.mjs.map +1 -0
- package/dist/control.d.mts.map +1 -1
- package/dist/control.mjs +19 -40
- package/dist/control.mjs.map +1 -1
- package/dist/default-normalizer.mjs +247 -1
- package/dist/default-normalizer.mjs.map +1 -0
- package/dist/diff-database-schema-BmSmWcaY.mjs +313 -0
- package/dist/diff-database-schema-BmSmWcaY.mjs.map +1 -0
- package/dist/diff-database-schema.d.mts +46 -0
- package/dist/diff-database-schema.d.mts.map +1 -0
- package/dist/diff-database-schema.mjs +2 -0
- package/dist/issue-planner.d.mts +34 -10
- package/dist/issue-planner.d.mts.map +1 -1
- package/dist/issue-planner.mjs +2 -2
- package/dist/migration.d.mts +2 -2
- package/dist/migration.mjs +2 -2
- package/dist/native-type-normalizer.mjs +37 -1
- package/dist/native-type-normalizer.mjs.map +1 -0
- package/dist/{op-factory-call-BuoGT5UI.mjs → op-factory-call-BKRnMgrB.mjs} +2 -2
- package/dist/op-factory-call-BKRnMgrB.mjs.map +1 -0
- package/dist/{op-factory-call-Caqus-Qg.d.mts → op-factory-call-Dt-Jx5JS.d.mts} +2 -2
- package/dist/{op-factory-call-Caqus-Qg.d.mts.map → op-factory-call-Dt-Jx5JS.d.mts.map} +1 -1
- package/dist/op-factory-call.d.mts +2 -2
- package/dist/op-factory-call.mjs +2 -2
- package/dist/planner-BMBRPzsv.mjs +367 -0
- package/dist/planner-BMBRPzsv.mjs.map +1 -0
- package/dist/planner-ddl-builders-B8Nn6nHN.mjs.map +1 -1
- package/dist/planner-ddl-builders.d.mts +2 -2
- package/dist/planner-ddl-builders.d.mts.map +1 -1
- package/dist/planner-identity-values-CJPha2Sz.mjs.map +1 -1
- package/dist/planner-identity-values.d.mts +7 -1
- package/dist/planner-identity-values.d.mts.map +1 -1
- package/dist/planner-identity-values.mjs +2 -2
- package/dist/{planner-produced-postgres-migration-CtJi4sgS.d.mts → planner-produced-postgres-migration-C0a7I1-e.d.mts} +2 -2
- package/dist/{planner-produced-postgres-migration-CtJi4sgS.d.mts.map → planner-produced-postgres-migration-C0a7I1-e.d.mts.map} +1 -1
- package/dist/{planner-produced-postgres-migration-C2_H6Vvl.mjs → planner-produced-postgres-migration-Dp6IO_gj.mjs} +2 -2
- package/dist/{planner-produced-postgres-migration-C2_H6Vvl.mjs.map → planner-produced-postgres-migration-Dp6IO_gj.mjs.map} +1 -1
- package/dist/planner-produced-postgres-migration.d.mts +1 -1
- package/dist/planner-produced-postgres-migration.mjs +1 -1
- package/dist/planner-sql-checks-Cze3vMfe.mjs.map +1 -1
- package/dist/planner-sql-checks.d.mts +1 -1
- package/dist/planner-sql-checks.d.mts.map +1 -1
- package/dist/planner.d.mts +27 -67
- package/dist/planner.d.mts.map +1 -1
- package/dist/planner.mjs +3 -2
- package/dist/{postgres-database-schema-node-os8ogEap.d.mts → postgres-database-schema-node-C9bTuMrd.d.mts} +14 -7
- package/dist/postgres-database-schema-node-C9bTuMrd.d.mts.map +1 -0
- package/dist/{postgres-migration-D0QIVE-p.mjs → postgres-migration-D0jwzYDT.mjs} +2 -2
- package/dist/{postgres-migration-D0QIVE-p.mjs.map → postgres-migration-D0jwzYDT.mjs.map} +1 -1
- package/dist/{postgres-migration-CfyKDT7b.d.mts → postgres-migration-DFaxuRyI.d.mts} +2 -2
- package/dist/{postgres-migration-CfyKDT7b.d.mts.map → postgres-migration-DFaxuRyI.d.mts.map} +1 -1
- package/dist/{postgres-table-schema-node-Clei_xel.mjs → postgres-table-schema-node-BgaSrxYN.mjs} +30 -29
- package/dist/postgres-table-schema-node-BgaSrxYN.mjs.map +1 -0
- package/dist/schema-node-kinds-ClScchhi.mjs +76 -0
- package/dist/schema-node-kinds-ClScchhi.mjs.map +1 -0
- package/dist/types.d.mts +1 -1
- package/dist/types.mjs +1 -1
- package/package.json +21 -20
- package/src/core/migrations/column-ddl-rendering.ts +117 -0
- package/src/core/migrations/contract-to-postgres-database-schema-node.ts +25 -1
- package/src/core/migrations/control-policy.ts +68 -48
- package/src/core/migrations/diff-database-schema.ts +242 -153
- package/src/core/migrations/issue-planner.ts +621 -667
- package/src/core/migrations/operations/constraints.ts +1 -1
- package/src/core/migrations/planner-ddl-builders.ts +5 -2
- package/src/core/migrations/planner-identity-values.ts +1 -1
- package/src/core/migrations/planner-sql-checks.ts +1 -1
- package/src/core/migrations/planner-strategies.ts +248 -208
- package/src/core/migrations/planner.ts +180 -74
- package/src/core/migrations/runner.ts +1 -4
- package/src/core/migrations/verify-postgres-namespaces.ts +26 -15
- package/src/core/postgres-schema-verifier.ts +10 -7
- package/src/core/psl-infer/infer-psl-contract.ts +5 -10
- package/src/core/schema-ir/postgres-database-schema-node.ts +3 -6
- package/src/core/schema-ir/postgres-namespace-schema-node.ts +8 -9
- package/src/core/schema-ir/postgres-policy-schema-node.ts +3 -6
- package/src/core/schema-ir/postgres-role-schema-node.ts +3 -6
- package/src/core/schema-ir/postgres-table-schema-node.ts +20 -9
- package/src/core/schema-ir/schema-node-kinds.ts +80 -2
- package/src/exports/control.ts +10 -43
- package/src/exports/diff-database-schema.ts +7 -0
- package/src/exports/issue-planner.ts +1 -1
- package/src/exports/op-factory-call.ts +1 -0
- package/src/exports/planner-identity-values.ts +4 -1
- package/src/exports/planner.ts +0 -1
- package/dist/default-normalizer-Dug8Fez9.mjs +0 -248
- package/dist/default-normalizer-Dug8Fez9.mjs.map +0 -1
- package/dist/issue-planner-Cbh-xTEr.mjs +0 -849
- package/dist/issue-planner-Cbh-xTEr.mjs.map +0 -1
- package/dist/native-type-normalizer-Bc9XJzWC.mjs +0 -38
- package/dist/native-type-normalizer-Bc9XJzWC.mjs.map +0 -1
- package/dist/op-factory-call-BuoGT5UI.mjs.map +0 -1
- package/dist/planner-Bq0Z2sVO.mjs +0 -641
- package/dist/planner-Bq0Z2sVO.mjs.map +0 -1
- package/dist/postgres-database-schema-node-os8ogEap.d.mts.map +0 -1
- package/dist/postgres-table-schema-node-Clei_xel.mjs.map +0 -1
|
@@ -1,191 +1,280 @@
|
|
|
1
|
-
import type { Contract } from '@prisma-next/contract/types';
|
|
2
|
-
import {
|
|
1
|
+
import type { Contract, ControlPolicy } from '@prisma-next/contract/types';
|
|
2
|
+
import type { SqlSchemaDiffResult } from '@prisma-next/family-sql/control';
|
|
3
|
+
import { buildNativeTypeExpander } from '@prisma-next/family-sql/control';
|
|
4
|
+
import {
|
|
5
|
+
classifyDiffSubjectGranularity,
|
|
6
|
+
resolveSemanticSatisfaction,
|
|
7
|
+
} from '@prisma-next/family-sql/diff';
|
|
3
8
|
import type { TargetBoundComponentDescriptor } from '@prisma-next/framework-components/components';
|
|
4
|
-
import type {
|
|
5
|
-
|
|
6
|
-
VerifyDatabaseSchemaResult,
|
|
7
|
-
} from '@prisma-next/framework-components/control';
|
|
8
|
-
import { diffSchemas, SchemaDiff } from '@prisma-next/framework-components/control';
|
|
9
|
+
import type { SchemaDiffIssue } from '@prisma-next/framework-components/control';
|
|
10
|
+
import { diffSchemas } from '@prisma-next/framework-components/control';
|
|
9
11
|
import type { SqlStorage } from '@prisma-next/sql-contract/types';
|
|
10
12
|
import type { SqlSchemaIRNode } from '@prisma-next/sql-schema-ir/types';
|
|
11
13
|
import { blindCast } from '@prisma-next/utils/casts';
|
|
12
|
-
import {
|
|
13
|
-
import { normalizeSchemaNativeType } from '../native-type-normalizer';
|
|
14
|
+
import { ifDefined } from '@prisma-next/utils/defined';
|
|
14
15
|
import type { PostgresContract } from '../postgres-schema';
|
|
15
16
|
import { PostgresDatabaseSchemaNode } from '../schema-ir/postgres-database-schema-node';
|
|
16
|
-
import {
|
|
17
|
-
import
|
|
17
|
+
import { PostgresNamespaceSchemaNode } from '../schema-ir/postgres-namespace-schema-node';
|
|
18
|
+
import { PostgresTableSchemaNode } from '../schema-ir/postgres-table-schema-node';
|
|
19
|
+
import {
|
|
20
|
+
postgresDiffSubjectGranularity,
|
|
21
|
+
type SqlSchemaDiffNode,
|
|
22
|
+
} from '../schema-ir/schema-node-kinds';
|
|
18
23
|
import { contractToPostgresDatabaseSchemaNode } from './contract-to-postgres-database-schema-node';
|
|
24
|
+
import { resolvePostgresNodeIssueControlPolicySubject } from './control-policy';
|
|
19
25
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
readonly frameworkComponents: ReadonlyArray<TargetBoundComponentDescriptor<'sql', string>>;
|
|
26
|
+
function ownedSchemaNames(expected: PostgresDatabaseSchemaNode): ReadonlySet<string> {
|
|
27
|
+
const policyNamespaces = Object.values(expected.namespaces).flatMap((ns) =>
|
|
28
|
+
Object.values(ns.tables).flatMap((t) => t.policies.map((p) => p.namespaceId)),
|
|
29
|
+
);
|
|
30
|
+
return new Set([...policyNamespaces, ...expected.existingSchemas]);
|
|
26
31
|
}
|
|
27
32
|
|
|
28
33
|
/**
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
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.
|
|
34
|
+
* Applies the family's semantic-satisfaction normalization across a Postgres
|
|
35
|
+
* tree pair: every actual table with an expected counterpart (paired by
|
|
36
|
+
* namespace id, then table id) gets its unique/index child lists adjusted;
|
|
37
|
+
* everything else passes through untouched.
|
|
43
38
|
*/
|
|
44
|
-
function
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
39
|
+
export function normalizePostgresActualForDiff(
|
|
40
|
+
expected: PostgresDatabaseSchemaNode,
|
|
41
|
+
actual: PostgresDatabaseSchemaNode,
|
|
42
|
+
): PostgresDatabaseSchemaNode {
|
|
43
|
+
const namespaces: Record<string, PostgresNamespaceSchemaNode> = {};
|
|
44
|
+
for (const [nsId, actualNs] of Object.entries(actual.namespaces)) {
|
|
45
|
+
const expectedNs = expected.namespaces[nsId];
|
|
46
|
+
if (expectedNs === undefined) {
|
|
47
|
+
namespaces[nsId] = actualNs;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
const tables: Record<string, PostgresTableSchemaNode> = {};
|
|
51
|
+
for (const [tableName, actualTable] of Object.entries(actualNs.tables)) {
|
|
52
|
+
const expectedTable = expectedNs.tables[tableName];
|
|
53
|
+
if (expectedTable === undefined) {
|
|
54
|
+
tables[tableName] = actualTable;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
const adjusted = resolveSemanticSatisfaction({
|
|
58
|
+
expectedUniques: expectedTable.uniques,
|
|
59
|
+
expectedIndexes: expectedTable.indexes,
|
|
60
|
+
actualUniques: actualTable.uniques,
|
|
61
|
+
actualIndexes: actualTable.indexes,
|
|
62
|
+
});
|
|
63
|
+
tables[tableName] = new PostgresTableSchemaNode({
|
|
64
|
+
name: actualTable.name,
|
|
65
|
+
columns: actualTable.columns,
|
|
66
|
+
foreignKeys: actualTable.foreignKeys,
|
|
67
|
+
uniques: adjusted.actualUniques,
|
|
68
|
+
indexes: adjusted.actualIndexes,
|
|
69
|
+
...ifDefined('primaryKey', actualTable.primaryKey),
|
|
70
|
+
...ifDefined('annotations', actualTable.annotations),
|
|
71
|
+
...ifDefined('checks', actualTable.checks),
|
|
72
|
+
policies: [...actualTable.policies],
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
namespaces[nsId] = new PostgresNamespaceSchemaNode({
|
|
76
|
+
schemaName: actualNs.schemaName,
|
|
77
|
+
tables,
|
|
78
|
+
nativeEnumTypeNames: actualNs.nativeEnumTypeNames,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
return new PostgresDatabaseSchemaNode({
|
|
82
|
+
namespaces,
|
|
83
|
+
roles: [...actual.roles],
|
|
84
|
+
existingSchemas: [...actual.existingSchemas],
|
|
85
|
+
pgVersion: actual.pgVersion,
|
|
71
86
|
});
|
|
87
|
+
}
|
|
72
88
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
89
|
+
/**
|
|
90
|
+
* Drops contract namespaces that declare no tables from the verdict-diff
|
|
91
|
+
* expected tree and the relational owned-schema set. The legacy relational
|
|
92
|
+
* walk skipped a table-less namespace (e.g. an enums-only schema) before
|
|
93
|
+
* pairing, so neither its DDL schema's absence nor that schema's live
|
|
94
|
+
* relational contents ever reached the verdict — the pruned tree
|
|
95
|
+
* reproduces that. The prune loses no expected policies: policies attach
|
|
96
|
+
* to tables, so a table-less namespace carries none (the projection
|
|
97
|
+
* throws on a policy referencing an absent table). Live policies in a
|
|
98
|
+
* pruned schema remain governed via the full owned set (see
|
|
99
|
+
* {@link diffPostgresSchema}).
|
|
100
|
+
*/
|
|
101
|
+
function pruneTableLessNamespaces(
|
|
102
|
+
expected: PostgresDatabaseSchemaNode,
|
|
103
|
+
): PostgresDatabaseSchemaNode {
|
|
104
|
+
const namespaces = Object.fromEntries(
|
|
105
|
+
Object.entries(expected.namespaces).filter(([, ns]) => Object.keys(ns.tables).length > 0),
|
|
85
106
|
);
|
|
86
|
-
|
|
87
|
-
|
|
107
|
+
return new PostgresDatabaseSchemaNode({
|
|
108
|
+
namespaces,
|
|
109
|
+
roles: [...expected.roles],
|
|
110
|
+
existingSchemas: expected.existingSchemas.filter((s) => namespaces[s] !== undefined),
|
|
111
|
+
pgVersion: expected.pgVersion,
|
|
112
|
+
});
|
|
88
113
|
}
|
|
89
114
|
|
|
90
115
|
/**
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
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).
|
|
116
|
+
* Resolves a verdict-diff issue's subject table's declared control policy
|
|
117
|
+
* directly from the contract, by delegating to the same node-typed resolver
|
|
118
|
+
* ({@link resolvePostgresNodeIssueControlPolicySubject}) the planner uses to
|
|
119
|
+
* gate DDL calls. `undefined` when the issue resolves to no contract table.
|
|
99
120
|
*/
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
121
|
+
function resolveControlPolicy(
|
|
122
|
+
issue: SchemaDiffIssue,
|
|
123
|
+
contract: Contract<SqlStorage>,
|
|
124
|
+
): ControlPolicy | undefined {
|
|
125
|
+
const nodeIssue = blindCast<
|
|
126
|
+
SchemaDiffIssue<SqlSchemaDiffNode>,
|
|
127
|
+
'every node in a Postgres schema diff tree is a SqlSchemaDiffNode'
|
|
128
|
+
>(issue);
|
|
129
|
+
return resolvePostgresNodeIssueControlPolicySubject(nodeIssue, contract)
|
|
130
|
+
?.explicitNodeControlPolicy;
|
|
105
131
|
}
|
|
106
132
|
|
|
107
133
|
/**
|
|
108
|
-
* The
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
134
|
+
* The Postgres full-tree node diff for the family verify verdict: derive
|
|
135
|
+
* the expected tree (resolved leaf values, expander threaded, FK schemas
|
|
136
|
+
* resolved, table-less namespaces pruned), normalize the actual tree for
|
|
137
|
+
* semantic satisfaction, run the generic
|
|
138
|
+
* differ, and scope out `not-expected` findings under namespaces the
|
|
139
|
+
* contract does not own. Ownership is role-aware, mirroring the legacy
|
|
140
|
+
* decomposition: relational extras check the PRUNED owned set (the legacy
|
|
141
|
+
* per-namespace walk never visited a table-less namespace, so its live
|
|
142
|
+
* relational contents are invisible), while `structural` extras (RLS
|
|
143
|
+
* policies) check the FULL owned set (the legacy policy diff governed
|
|
144
|
+
* every contract schema regardless of tables — RLS governance does not
|
|
145
|
+
* shrink because a namespace declares no tables). The codec `verifyType`
|
|
146
|
+
* hooks run once per contract namespace with tables against that
|
|
147
|
+
* namespace's paired actual node (the hooks read namespace-scoped state
|
|
148
|
+
* such as `nativeEnumTypeNames`).
|
|
112
149
|
*/
|
|
113
|
-
export function
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
150
|
+
export function diffPostgresSchema(input: {
|
|
151
|
+
readonly contract: Contract<SqlStorage>;
|
|
152
|
+
readonly schema: SqlSchemaIRNode;
|
|
153
|
+
readonly frameworkComponents: ReadonlyArray<TargetBoundComponentDescriptor<'sql', string>>;
|
|
154
|
+
}): SqlSchemaDiffResult {
|
|
155
|
+
const postgresContract = blindCast<
|
|
156
|
+
PostgresContract,
|
|
157
|
+
'diffPostgresSchema is only called with a postgres contract'
|
|
158
|
+
>(input.contract);
|
|
159
|
+
PostgresDatabaseSchemaNode.assert(input.schema);
|
|
160
|
+
const actual = input.schema;
|
|
161
|
+
const expandNativeType = buildNativeTypeExpander(input.frameworkComponents);
|
|
162
|
+
const fullExpected = contractToPostgresDatabaseSchemaNode(postgresContract, {
|
|
163
|
+
annotationNamespace: 'pg',
|
|
164
|
+
...ifDefined('expandNativeType', expandNativeType),
|
|
165
|
+
});
|
|
166
|
+
const expected = pruneTableLessNamespaces(fullExpected);
|
|
167
|
+
const normalizedActual = normalizePostgresActualForDiff(expected, actual);
|
|
168
|
+
const relationalOwned = ownedSchemaNames(expected);
|
|
169
|
+
const structuralOwned = ownedSchemaNames(fullExpected);
|
|
170
|
+
const issues = diffSchemas(expected, normalizedActual).filter((issue) => {
|
|
171
|
+
if (issue.reason !== 'not-expected') return true;
|
|
172
|
+
const namespaceSegment = issue.path[1];
|
|
173
|
+
if (namespaceSegment === undefined) return true;
|
|
174
|
+
const granularity = classifyDiffSubjectGranularity(issue, postgresDiffSubjectGranularity);
|
|
175
|
+
const owned = granularity === 'structural' ? structuralOwned : relationalOwned;
|
|
176
|
+
return owned.has(namespaceSegment);
|
|
177
|
+
});
|
|
178
|
+
const namespacePairs = Object.values(expected.namespaces).map((ns) => ({
|
|
179
|
+
actual: actual.namespaces[ns.schemaName],
|
|
180
|
+
}));
|
|
117
181
|
return {
|
|
118
|
-
|
|
119
|
-
|
|
182
|
+
issues,
|
|
183
|
+
resolveControlPolicy: (issue) => resolveControlPolicy(issue, postgresContract),
|
|
184
|
+
namespacePairs,
|
|
120
185
|
};
|
|
121
186
|
}
|
|
122
187
|
|
|
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
188
|
/**
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
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`.
|
|
189
|
+
* Adds an empty namespace node to the actual tree for every expected namespace
|
|
190
|
+
* absent from it. The relational plan diff pairs on namespace: a contract
|
|
191
|
+
* namespace whose live schema does not exist yet must surface each of its
|
|
192
|
+
* tables as `not-found` (→ `CREATE TABLE`), NOT as a single namespace
|
|
193
|
+
* `not-found` that subtree-coalescing would collapse (leaving `CREATE SCHEMA`
|
|
194
|
+
* with no tables). Padding makes the namespaces pair, so only table/column/
|
|
195
|
+
* policy drift surfaces; `CREATE SCHEMA` comes separately from the synthesized
|
|
196
|
+
* namespace-presence stitch (`verifyPostgresNamespacePresence`), never from the
|
|
197
|
+
* tree diff — matching the retired per-namespace-paired relational walk, which
|
|
198
|
+
* paired a missing schema against an empty namespace node.
|
|
155
199
|
*/
|
|
156
|
-
|
|
200
|
+
function padActualNamespaces(
|
|
157
201
|
expected: PostgresDatabaseSchemaNode,
|
|
158
202
|
actual: PostgresDatabaseSchemaNode,
|
|
159
|
-
):
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
203
|
+
): PostgresDatabaseSchemaNode {
|
|
204
|
+
const namespaces: Record<string, PostgresNamespaceSchemaNode> = { ...actual.namespaces };
|
|
205
|
+
let padded = false;
|
|
206
|
+
for (const schemaName of Object.keys(expected.namespaces)) {
|
|
207
|
+
if (namespaces[schemaName] === undefined) {
|
|
208
|
+
namespaces[schemaName] = new PostgresNamespaceSchemaNode({
|
|
209
|
+
schemaName,
|
|
210
|
+
tables: {},
|
|
211
|
+
nativeEnumTypeNames: [],
|
|
212
|
+
});
|
|
213
|
+
padded = true;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
if (!padded) return actual;
|
|
217
|
+
return new PostgresDatabaseSchemaNode({
|
|
218
|
+
namespaces,
|
|
219
|
+
roles: [...actual.roles],
|
|
220
|
+
existingSchemas: [...actual.existingSchemas],
|
|
221
|
+
pgVersion: actual.pgVersion,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
164
224
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
if (node === undefined || !PostgresPolicySchemaNode.is(node)) return i;
|
|
173
|
-
return { ...i, message: `${i.outcome}: ${renderPostgresPolicyReference(node)}` };
|
|
174
|
-
});
|
|
225
|
+
export interface PostgresPlanDiff {
|
|
226
|
+
/** The desired ("end") tree — resolved leaf values (incl. `codecRef`) on every column, table-less namespaces pruned. */
|
|
227
|
+
readonly expected: PostgresDatabaseSchemaNode;
|
|
228
|
+
/** The live ("start") tree, padded with empty namespaces and normalized for semantic satisfaction against `expected`. */
|
|
229
|
+
readonly actual: PostgresDatabaseSchemaNode;
|
|
230
|
+
/** The one node diff over the two trees: relational + policy drift, role-aware ownership filtered. */
|
|
231
|
+
readonly issues: readonly SchemaDiffIssue<SqlSchemaDiffNode>[];
|
|
175
232
|
}
|
|
176
233
|
|
|
177
234
|
/**
|
|
178
|
-
*
|
|
179
|
-
* `diffPostgresSchema`
|
|
180
|
-
*
|
|
235
|
+
* The Postgres planner's diff input: the SAME tree-building
|
|
236
|
+
* `diffPostgresSchema` uses (expander threaded, FK schemas resolved,
|
|
237
|
+
* table-less namespaces pruned, actual normalized for semantic satisfaction,
|
|
238
|
+
* role-aware ownership filter) plus actual namespace padding (so a missing
|
|
239
|
+
* schema's tables surface as `not-found` instead of a swallowed namespace
|
|
240
|
+
* `not-found`). One differ drives both verify and plan; this is the
|
|
241
|
+
* plan-side derivation. The single issue list covers tables / columns /
|
|
242
|
+
* constraints / indexes / defaults AND policies — the caller splits it
|
|
243
|
+
* (relational → `mapNodeIssueToCall`; policy → RLS ops) and stitches in
|
|
244
|
+
* `CREATE SCHEMA` separately.
|
|
181
245
|
*/
|
|
182
|
-
export function
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
246
|
+
export function buildPostgresPlanDiff(input: {
|
|
247
|
+
readonly contract: Contract<SqlStorage>;
|
|
248
|
+
readonly actualSchema: SqlSchemaIRNode;
|
|
249
|
+
readonly frameworkComponents: ReadonlyArray<TargetBoundComponentDescriptor<'sql', string>>;
|
|
250
|
+
}): PostgresPlanDiff {
|
|
251
|
+
const postgresContract = blindCast<
|
|
252
|
+
PostgresContract,
|
|
253
|
+
'buildPostgresPlanDiff is only called with a postgres contract'
|
|
254
|
+
>(input.contract);
|
|
255
|
+
PostgresDatabaseSchemaNode.assert(input.actualSchema);
|
|
256
|
+
const actual = input.actualSchema;
|
|
257
|
+
const expandNativeType = buildNativeTypeExpander(input.frameworkComponents);
|
|
258
|
+
const projectionOptions = {
|
|
259
|
+
annotationNamespace: 'pg',
|
|
260
|
+
...ifDefined('expandNativeType', expandNativeType),
|
|
261
|
+
};
|
|
262
|
+
const fullExpected = contractToPostgresDatabaseSchemaNode(postgresContract, projectionOptions);
|
|
263
|
+
const expected = pruneTableLessNamespaces(fullExpected);
|
|
264
|
+
const paddedActual = padActualNamespaces(expected, actual);
|
|
265
|
+
const normalizedActual = normalizePostgresActualForDiff(expected, paddedActual);
|
|
266
|
+
const relationalOwned = ownedSchemaNames(expected);
|
|
267
|
+
const structuralOwned = ownedSchemaNames(fullExpected);
|
|
268
|
+
const issues = blindCast<
|
|
269
|
+
readonly SchemaDiffIssue<SqlSchemaDiffNode>[],
|
|
270
|
+
'both trees are PostgresDatabaseSchemaNodes, so every diff-issue node is a SqlSchemaDiffNode'
|
|
271
|
+
>(diffSchemas(expected, normalizedActual)).filter((issue) => {
|
|
272
|
+
if (issue.reason !== 'not-expected') return true;
|
|
273
|
+
const namespaceSegment = issue.path[1];
|
|
274
|
+
if (namespaceSegment === undefined) return true;
|
|
275
|
+
const granularity = classifyDiffSubjectGranularity(issue, postgresDiffSubjectGranularity);
|
|
276
|
+
const owned = granularity === 'structural' ? structuralOwned : relationalOwned;
|
|
277
|
+
return owned.has(namespaceSegment);
|
|
190
278
|
});
|
|
279
|
+
return { expected, actual: normalizedActual, issues };
|
|
191
280
|
}
|