@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.
- package/dist/{authoring-CZmSQriw.mjs → authoring-BX1WHIdN.mjs} +2 -2
- package/dist/authoring-BX1WHIdN.mjs.map +1 -0
- package/dist/contract-free.mjs +2 -2
- package/dist/control.d.mts.map +1 -1
- package/dist/control.mjs +633 -16
- package/dist/control.mjs.map +1 -1
- package/dist/{ddl-CGf6UABz.mjs → ddl-BmLIe1z_.mjs} +2 -2
- package/dist/{ddl-CGf6UABz.mjs.map → ddl-BmLIe1z_.mjs.map} +1 -1
- package/dist/{descriptor-meta-Du6olWQI.mjs → descriptor-meta-tjatyhtZ.mjs} +3 -3
- package/dist/{descriptor-meta-Du6olWQI.mjs.map → descriptor-meta-tjatyhtZ.mjs.map} +1 -1
- package/dist/{issue-planner-C-CwZd8D.mjs → issue-planner-DbFvaAgu.mjs} +5 -5
- package/dist/issue-planner-DbFvaAgu.mjs.map +1 -0
- package/dist/issue-planner.d.mts +1 -1
- package/dist/issue-planner.mjs +1 -1
- package/dist/migration.d.mts +2 -2
- package/dist/migration.mjs +2 -2
- package/dist/{op-factory-call-DPJR7tJc.d.mts → op-factory-call-CJWUxyUW.d.mts} +2 -2
- package/dist/{op-factory-call-DPJR7tJc.d.mts.map → op-factory-call-CJWUxyUW.d.mts.map} +1 -1
- package/dist/{op-factory-call-DArx_1HM.mjs → op-factory-call-t65c4jyY.mjs} +3 -3
- package/dist/op-factory-call-t65c4jyY.mjs.map +1 -0
- package/dist/op-factory-call.d.mts +1 -1
- package/dist/op-factory-call.mjs +1 -1
- package/dist/pack.d.mts +2 -2
- package/dist/pack.mjs +1 -1
- package/dist/{planner-BQ7lSqbm.mjs → planner-CG2pLHk-.mjs} +310 -126
- package/dist/planner-CG2pLHk-.mjs.map +1 -0
- package/dist/{planner-produced-postgres-migration-3ppG0t3i.mjs → planner-produced-postgres-migration-BolD0Mvc.mjs} +2 -2
- package/dist/{planner-produced-postgres-migration-3ppG0t3i.mjs.map → planner-produced-postgres-migration-BolD0Mvc.mjs.map} +1 -1
- package/dist/{planner-produced-postgres-migration-BauTSigX.d.mts → planner-produced-postgres-migration-Cz9--XOT.d.mts} +2 -2
- package/dist/{planner-produced-postgres-migration-BauTSigX.d.mts.map → planner-produced-postgres-migration-Cz9--XOT.d.mts.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-CyjaoKhm.mjs → planner-sql-checks-DE9c4519.mjs} +2 -2
- package/dist/{planner-sql-checks-CyjaoKhm.mjs.map → planner-sql-checks-DE9c4519.mjs.map} +1 -1
- package/dist/planner-sql-checks.mjs +1 -1
- package/dist/planner.d.mts +73 -22
- package/dist/planner.d.mts.map +1 -1
- package/dist/planner.mjs +2 -2
- package/dist/{postgres-contract-view-CoRZewAp.mjs → postgres-contract-view-BegduLr6.mjs} +3 -3
- package/dist/{postgres-contract-view-CoRZewAp.mjs.map → postgres-contract-view-BegduLr6.mjs.map} +1 -1
- package/dist/postgres-database-schema-node-os8ogEap.d.mts +183 -0
- package/dist/postgres-database-schema-node-os8ogEap.d.mts.map +1 -0
- package/dist/{postgres-migration-BVjq0iFW.mjs → postgres-migration-BfMmK6tv.mjs} +3 -3
- package/dist/{postgres-migration-BVjq0iFW.mjs.map → postgres-migration-BfMmK6tv.mjs.map} +1 -1
- package/dist/{postgres-migration-UtGCjw5j.d.mts → postgres-migration-D7kNVWVG.d.mts} +2 -2
- package/dist/{postgres-migration-UtGCjw5j.d.mts.map → postgres-migration-D7kNVWVG.d.mts.map} +1 -1
- package/dist/{postgres-rls-policy-12wAEQhs.d.mts → postgres-rls-policy--CSddvMR.d.mts} +8 -16
- package/dist/postgres-rls-policy--CSddvMR.d.mts.map +1 -0
- package/dist/{postgres-schema-DLcc3W2-.mjs → postgres-schema-BLIHBEfX.mjs} +34 -64
- package/dist/postgres-schema-BLIHBEfX.mjs.map +1 -0
- package/dist/{postgres-schema-DJUEkWg-.d.mts → postgres-schema-BywoeKlt.d.mts} +9 -10
- package/dist/postgres-schema-BywoeKlt.d.mts.map +1 -0
- package/dist/postgres-table-schema-node-1nNc8XDT.mjs +248 -0
- package/dist/postgres-table-schema-node-1nNc8XDT.mjs.map +1 -0
- package/dist/resolve-ddl-schema-CL1RR1mx.mjs +20 -0
- package/dist/resolve-ddl-schema-CL1RR1mx.mjs.map +1 -0
- package/dist/runtime.mjs +1 -1
- package/dist/schema-ir-annotations.d.mts +9 -3
- package/dist/schema-ir-annotations.d.mts.map +1 -1
- package/dist/schema-ir-annotations.mjs +1 -1
- package/dist/types.d.mts +4 -4
- package/dist/types.mjs +3 -3
- package/package.json +20 -19
- package/src/core/authoring.ts +2 -2
- package/src/core/entity-kinds.ts +2 -2
- package/src/core/migrations/contract-to-postgres-database-schema-node.ts +125 -0
- package/src/core/migrations/diff-database-schema.ts +191 -0
- package/src/core/migrations/issue-planner.ts +1 -1
- package/src/core/migrations/op-factory-call.ts +1 -1
- package/src/core/migrations/operations/rls.ts +1 -1
- package/src/core/migrations/planner.ts +117 -68
- package/src/core/{schema-ir/postgres-schema-ir-annotations.ts → migrations/resolve-ddl-schema.ts} +8 -6
- package/src/core/migrations/runner.ts +13 -16
- package/src/core/migrations/verify-postgres-namespaces.ts +22 -14
- package/src/core/{schema-ir/postgres-rls-policy.ts → postgres-rls-policy.ts} +6 -43
- package/src/core/{schema-ir/postgres-role.ts → postgres-role.ts} +6 -21
- package/src/core/postgres-schema.ts +2 -2
- package/src/core/psl-infer/infer-psl-contract.ts +834 -0
- package/src/core/psl-infer/postgres-default-mapping.ts +16 -0
- package/src/core/psl-infer/postgres-type-map.ts +157 -0
- package/src/core/schema-ir/postgres-database-schema-node.ts +63 -0
- package/src/core/schema-ir/postgres-namespace-schema-node.ts +61 -0
- package/src/core/schema-ir/postgres-policy-schema-node.ts +93 -0
- package/src/core/schema-ir/postgres-role-schema-node.ts +66 -0
- package/src/core/schema-ir/{postgres-table-ir.ts → postgres-table-schema-node.ts} +29 -18
- package/src/core/schema-ir/schema-node-kinds.ts +28 -0
- package/src/exports/control.ts +30 -2
- package/src/exports/planner.ts +2 -5
- package/src/exports/schema-ir-annotations.ts +1 -1
- package/src/exports/types.ts +23 -15
- package/dist/authoring-CZmSQriw.mjs.map +0 -1
- package/dist/issue-planner-C-CwZd8D.mjs.map +0 -1
- package/dist/op-factory-call-DArx_1HM.mjs.map +0 -1
- package/dist/planner-BQ7lSqbm.mjs.map +0 -1
- package/dist/postgres-rls-policy-12wAEQhs.d.mts.map +0 -1
- package/dist/postgres-schema-DJUEkWg-.d.mts.map +0 -1
- package/dist/postgres-schema-DLcc3W2-.mjs.map +0 -1
- package/dist/postgres-schema-ir-BJl_a3J7.d.mts +0 -102
- package/dist/postgres-schema-ir-BJl_a3J7.d.mts.map +0 -1
- package/dist/postgres-schema-ir-I_FiUndw.mjs +0 -133
- package/dist/postgres-schema-ir-I_FiUndw.mjs.map +0 -1
- package/dist/postgres-schema-ir-annotations-BbDSiwx2.mjs +0 -14
- package/dist/postgres-schema-ir-annotations-BbDSiwx2.mjs.map +0 -1
- package/src/core/migrations/contract-to-postgres-schema-ir.ts +0 -91
- package/src/core/migrations/diff-postgres-schema.ts +0 -55
- 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/
|
|
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 '../
|
|
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 '../../
|
|
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 {
|
|
27
|
-
import {
|
|
28
|
-
import
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
294
|
-
|
|
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
|
-
|
|
299
|
-
options.schema,
|
|
311
|
+
expected.namespaceId,
|
|
300
312
|
);
|
|
301
|
-
const tableKey = `${schemaForTable}.${
|
|
313
|
+
const tableKey = `${schemaForTable}.${expected.tableName}`;
|
|
302
314
|
if (!seenEnableTables.has(tableKey)) {
|
|
303
315
|
seenEnableTables.add(tableKey);
|
|
304
|
-
calls.push(new EnableRowLevelSecurityCall(schemaForTable,
|
|
316
|
+
calls.push(new EnableRowLevelSecurityCall(schemaForTable, expected.tableName));
|
|
305
317
|
}
|
|
306
318
|
calls.push(
|
|
307
|
-
new CreatePostgresRlsPolicyCall(
|
|
319
|
+
new CreatePostgresRlsPolicyCall(
|
|
320
|
+
schemaForTable,
|
|
321
|
+
expected.tableName,
|
|
322
|
+
policyNodeToContractPolicy(expected),
|
|
323
|
+
),
|
|
308
324
|
);
|
|
309
325
|
} else if (issue.outcome === 'extra' && allowsDestructive) {
|
|
310
|
-
|
|
326
|
+
const actual = issue.actual;
|
|
327
|
+
PostgresPolicySchemaNode.assert(actual);
|
|
311
328
|
const schemaForTable = resolveDdlSchemaForNamespaceStorage(
|
|
312
329
|
options.contract.storage,
|
|
313
|
-
|
|
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
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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
|
|
373
|
+
return relationalIssues;
|
|
366
374
|
}
|
|
367
|
-
return [...namespaceIssues, ...
|
|
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
|
+
}
|
package/src/core/{schema-ir/postgres-schema-ir-annotations.ts → migrations/resolve-ddl-schema.ts}
RENAMED
|
@@ -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
|
-
//
|
|
128
|
-
//
|
|
129
|
-
//
|
|
130
|
-
//
|
|
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
|
|
129
|
+
const schemaNode = await this.family.introspect({
|
|
134
130
|
driver,
|
|
135
131
|
contract: options.destinationContract,
|
|
136
132
|
});
|
|
137
|
-
const schemaVerifyResult =
|
|
133
|
+
const schemaVerifyResult = this.family.verifySchema({
|
|
138
134
|
contract: options.destinationContract,
|
|
139
|
-
schema:
|
|
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: {
|
|
142
|
+
meta: {
|
|
143
|
+
issues: [
|
|
144
|
+
...schemaVerifyResult.schema.issues,
|
|
145
|
+
...schemaVerifyResult.schema.schemaDiffIssues,
|
|
146
|
+
],
|
|
147
|
+
},
|
|
151
148
|
});
|
|
152
149
|
}
|
|
153
150
|
}
|