@prisma-next/target-postgres 0.14.0-dev.60 → 0.14.0-dev.61
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-WkqG_MkU.mjs → authoring-BLkL4fFG.mjs} +147 -38
- package/dist/authoring-BLkL4fFG.mjs.map +1 -0
- package/dist/contract-free.mjs +2 -2
- package/dist/{control-policy-Co_13Hxa.mjs → control-policy-BpmChlAK.mjs} +27 -4
- package/dist/{control-policy-Co_13Hxa.mjs.map → control-policy-BpmChlAK.mjs.map} +1 -1
- package/dist/control.mjs +6 -5
- package/dist/control.mjs.map +1 -1
- package/dist/{ddl-B-krEXY1.mjs → ddl-BovUVFVi.mjs} +2 -2
- package/dist/{ddl-B-krEXY1.mjs.map → ddl-BovUVFVi.mjs.map} +1 -1
- package/dist/{descriptor-meta-BOcKPaLE.mjs → descriptor-meta-C1T4BXJ8.mjs} +3 -3
- package/dist/{descriptor-meta-BOcKPaLE.mjs.map → descriptor-meta-C1T4BXJ8.mjs.map} +1 -1
- package/dist/{diff-database-schema-BOsDnH64.mjs → diff-database-schema-RMwnSOnz.mjs} +46 -14
- package/dist/diff-database-schema-RMwnSOnz.mjs.map +1 -0
- package/dist/diff-database-schema.d.mts +4 -4
- package/dist/diff-database-schema.d.mts.map +1 -1
- package/dist/diff-database-schema.mjs +1 -1
- package/dist/issue-planner.mjs +1 -1
- package/dist/migration.mjs +2 -2
- package/dist/{op-factory-call-DxNNKUeZ.mjs → op-factory-call-CI00nS4z.mjs} +3 -3
- package/dist/{op-factory-call-DxNNKUeZ.mjs.map → op-factory-call-CI00nS4z.mjs.map} +1 -1
- package/dist/op-factory-call.mjs +1 -1
- package/dist/pack.d.mts +146 -3
- package/dist/pack.d.mts.map +1 -1
- package/dist/pack.mjs +1 -1
- package/dist/{planner-CNrc1Odu.mjs → planner-CAHZN1Ez.mjs} +8 -8
- package/dist/planner-CAHZN1Ez.mjs.map +1 -0
- package/dist/{planner-produced-postgres-migration-NI-oGKcw.mjs → planner-produced-postgres-migration-flkG4Y7n.mjs} +2 -2
- package/dist/{planner-produced-postgres-migration-NI-oGKcw.mjs.map → planner-produced-postgres-migration-flkG4Y7n.mjs.map} +1 -1
- package/dist/planner-produced-postgres-migration.mjs +1 -1
- package/dist/{planner-sql-checks-AXoxB1Jl.mjs → planner-sql-checks-C1-Kk3Eu.mjs} +2 -2
- package/dist/{planner-sql-checks-AXoxB1Jl.mjs.map → planner-sql-checks-C1-Kk3Eu.mjs.map} +1 -1
- package/dist/planner-sql-checks.mjs +1 -1
- package/dist/planner.d.mts +2 -2
- package/dist/planner.d.mts.map +1 -1
- package/dist/planner.mjs +2 -2
- package/dist/{postgres-contract-view-Dg-N7KA7.mjs → postgres-contract-view-DWLNQbdR.mjs} +3 -3
- package/dist/{postgres-contract-view-Dg-N7KA7.mjs.map → postgres-contract-view-DWLNQbdR.mjs.map} +1 -1
- package/dist/{postgres-database-schema-node-CfdTY-Qs.d.mts → postgres-database-schema-node-BVdOffjC.d.mts} +9 -6
- package/dist/{postgres-database-schema-node-CfdTY-Qs.d.mts.map → postgres-database-schema-node-BVdOffjC.d.mts.map} +1 -1
- package/dist/{postgres-migration-ChyRsWvE.mjs → postgres-migration-C92swCzd.mjs} +3 -3
- package/dist/{postgres-migration-ChyRsWvE.mjs.map → postgres-migration-C92swCzd.mjs.map} +1 -1
- package/dist/{postgres-role-schema-node-BiSTCTNg.mjs → postgres-role-schema-node-BmCboEa_.mjs} +10 -7
- package/dist/postgres-role-schema-node-BmCboEa_.mjs.map +1 -0
- package/dist/{postgres-schema-DTLA_aRi.mjs → postgres-schema-COEca21H.mjs} +20 -3
- package/dist/postgres-schema-COEca21H.mjs.map +1 -0
- package/dist/{postgres-schema-CU4gF6QZ.d.mts → postgres-schema-DsrtYp2v.d.mts} +13 -1
- package/dist/postgres-schema-DsrtYp2v.d.mts.map +1 -0
- package/dist/postgres-table-schema-node-9FCtCUz2.mjs.map +1 -1
- package/dist/{resolve-ddl-schema-Ca88Tg6W.mjs → resolve-ddl-schema-ZkKxt2Fm.mjs} +2 -2
- package/dist/{resolve-ddl-schema-Ca88Tg6W.mjs.map → resolve-ddl-schema-ZkKxt2Fm.mjs.map} +1 -1
- package/dist/runtime.mjs +1 -1
- package/dist/schema-ir-annotations.mjs +1 -1
- package/dist/types.d.mts +2 -2
- package/dist/types.mjs +2 -2
- package/package.json +20 -20
- package/src/core/authoring.ts +136 -25
- package/src/core/migrations/control-policy.ts +29 -0
- package/src/core/migrations/diff-database-schema.ts +47 -11
- package/src/core/migrations/planner.ts +5 -0
- package/src/core/postgres-role.ts +22 -0
- package/src/core/postgres-validators.ts +1 -0
- package/src/core/psl-infer/infer-psl-contract.ts +1 -0
- package/src/core/schema-ir/postgres-database-schema-node.ts +5 -4
- package/src/core/schema-ir/postgres-role-schema-node.ts +4 -2
- package/src/core/schema-ir/schema-node-kinds.ts +7 -0
- package/dist/authoring-WkqG_MkU.mjs.map +0 -1
- package/dist/diff-database-schema-BOsDnH64.mjs.map +0 -1
- package/dist/planner-CNrc1Odu.mjs.map +0 -1
- package/dist/postgres-role-schema-node-BiSTCTNg.mjs.map +0 -1
- package/dist/postgres-schema-CU4gF6QZ.d.mts.map +0 -1
- package/dist/postgres-schema-DTLA_aRi.mjs.map +0 -1
package/src/core/authoring.ts
CHANGED
|
@@ -12,10 +12,11 @@ import type {
|
|
|
12
12
|
} from '@prisma-next/framework-components/authoring';
|
|
13
13
|
import { modelAttribute } from '@prisma-next/psl-parser';
|
|
14
14
|
import type { SqlValueSetDerivingEntityTypeOutput } from '@prisma-next/sql-contract/value-set-derivation-hook';
|
|
15
|
+
import { ifDefined } from '@prisma-next/utils/defined';
|
|
15
16
|
import { PG_ENUM_CODEC_ID } from './codec-ids';
|
|
16
17
|
import { PostgresNativeEnum } from './postgres-native-enum';
|
|
17
18
|
import { PostgresRlsEnablement, type PostgresRlsEnablementInput } from './postgres-rls-enablement';
|
|
18
|
-
import { PostgresRlsPolicy } from './postgres-rls-policy';
|
|
19
|
+
import { PostgresRlsPolicy, type RlsPolicyOperation } from './postgres-rls-policy';
|
|
19
20
|
import { PostgresRole, type PostgresRoleInput } from './postgres-role';
|
|
20
21
|
import {
|
|
21
22
|
PostgresNativeEnumSchema,
|
|
@@ -50,6 +51,35 @@ export interface RlsPolicyExtensionBlock extends PslExtensionBlock {
|
|
|
50
51
|
readonly namespaceId: string;
|
|
51
52
|
}
|
|
52
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Maps a `policy_<op>` keyword to the RLS operation it authors. The keyword
|
|
56
|
+
* IS the operation (per the project's rejection of a `policy { operation = … }`
|
|
57
|
+
* conditional block).
|
|
58
|
+
*/
|
|
59
|
+
const POLICY_KEYWORD_OPERATION: Readonly<Record<string, RlsPolicyOperation>> = {
|
|
60
|
+
policy_select: 'select',
|
|
61
|
+
policy_insert: 'insert',
|
|
62
|
+
policy_update: 'update',
|
|
63
|
+
policy_delete: 'delete',
|
|
64
|
+
policy_all: 'all',
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Which predicate each RLS operation admits, mirroring Postgres: SELECT and
|
|
69
|
+
* DELETE decide row visibility/eligibility with `USING` only; INSERT validates
|
|
70
|
+
* the new row with `WITH CHECK` only; UPDATE and ALL take both. A predicate
|
|
71
|
+
* outside this set is a load-time error (see {@link lowerRlsPolicyFromBlock}).
|
|
72
|
+
*/
|
|
73
|
+
const POLICY_OPERATION_PREDICATES: Readonly<
|
|
74
|
+
Record<RlsPolicyOperation, { readonly using: boolean; readonly withCheck: boolean }>
|
|
75
|
+
> = {
|
|
76
|
+
select: { using: true, withCheck: false },
|
|
77
|
+
insert: { using: false, withCheck: true },
|
|
78
|
+
update: { using: true, withCheck: true },
|
|
79
|
+
delete: { using: true, withCheck: false },
|
|
80
|
+
all: { using: true, withCheck: true },
|
|
81
|
+
};
|
|
82
|
+
|
|
53
83
|
function readRefParam(block: PslExtensionBlock, key: string): string | undefined {
|
|
54
84
|
const param = block.parameters[key];
|
|
55
85
|
return param?.kind === 'ref' ? param.identifier : undefined;
|
|
@@ -101,18 +131,43 @@ function unwrapQuotedString(raw: string): string {
|
|
|
101
131
|
|
|
102
132
|
function lowerRlsPolicyFromBlock(
|
|
103
133
|
block: RlsPolicyExtensionBlock,
|
|
104
|
-
|
|
105
|
-
): PostgresRlsPolicy {
|
|
134
|
+
ctx: AuthoringEntityContext,
|
|
135
|
+
): PostgresRlsPolicy | undefined {
|
|
106
136
|
const prefix = block.name;
|
|
137
|
+
const operation = POLICY_KEYWORD_OPERATION[block.keyword] ?? 'select';
|
|
107
138
|
const targetModelName = readRefParam(block, 'target') ?? '';
|
|
108
139
|
const tableName = targetModelName.charAt(0).toLowerCase() + targetModelName.slice(1);
|
|
109
140
|
const roles = [...readListRefParams(block, 'roles')].sort();
|
|
110
|
-
|
|
141
|
+
|
|
142
|
+
const usingRaw = readValueParam(block, 'using');
|
|
143
|
+
const withCheckRaw = readValueParam(block, 'withCheck');
|
|
144
|
+
|
|
145
|
+
// Reject a predicate the operation does not take (e.g. `using` on INSERT, or
|
|
146
|
+
// `withCheck` on SELECT/DELETE). The descriptor's param set already omits it,
|
|
147
|
+
// but the generic descriptor validator is not wired into the SQL-family
|
|
148
|
+
// interpreter, so the lowering enforces the per-operation predicate matrix
|
|
149
|
+
// directly — a wrong predicate is a load-time diagnostic, not a silent drop.
|
|
150
|
+
const support = POLICY_OPERATION_PREDICATES[operation];
|
|
151
|
+
const rejectPredicate = (predicate: 'using' | 'withCheck'): undefined => {
|
|
152
|
+
ctx.diagnostics?.push({
|
|
153
|
+
code: 'PSL_RLS_PREDICATE_NOT_FOR_OPERATION',
|
|
154
|
+
message: `\`${block.keyword}\` policy "${block.name}" does not take a \`${predicate}\` predicate; the ${operation.toUpperCase()} operation uses ${support.using ? '`using`' : '`withCheck`'}${support.using && support.withCheck ? ' and `withCheck`' : ' only'}.`,
|
|
155
|
+
sourceId: ctx.sourceId ?? 'unknown',
|
|
156
|
+
span: block.parameters[predicate]?.span ?? block.span,
|
|
157
|
+
});
|
|
158
|
+
return undefined;
|
|
159
|
+
};
|
|
160
|
+
if (usingRaw !== undefined && !support.using) return rejectPredicate('using');
|
|
161
|
+
if (withCheckRaw !== undefined && !support.withCheck) return rejectPredicate('withCheck');
|
|
162
|
+
|
|
163
|
+
const using = usingRaw !== undefined ? unwrapQuotedString(usingRaw) : undefined;
|
|
164
|
+
const withCheck = withCheckRaw !== undefined ? unwrapQuotedString(withCheckRaw) : undefined;
|
|
111
165
|
|
|
112
166
|
const wireHash = computeContentHash({
|
|
113
|
-
using
|
|
167
|
+
...ifDefined('using', using !== undefined ? normalizePredicate(using) : undefined),
|
|
168
|
+
...ifDefined('withCheck', withCheck !== undefined ? normalizePredicate(withCheck) : undefined),
|
|
114
169
|
roles,
|
|
115
|
-
operation
|
|
170
|
+
operation,
|
|
116
171
|
permissive: true,
|
|
117
172
|
});
|
|
118
173
|
const wireName = formatRlsPolicyWireName(prefix, wireHash);
|
|
@@ -122,9 +177,10 @@ function lowerRlsPolicyFromBlock(
|
|
|
122
177
|
prefix,
|
|
123
178
|
tableName,
|
|
124
179
|
namespaceId: block.namespaceId,
|
|
125
|
-
operation
|
|
180
|
+
operation,
|
|
126
181
|
roles,
|
|
127
|
-
using,
|
|
182
|
+
...ifDefined('using', using),
|
|
183
|
+
...ifDefined('withCheck', withCheck),
|
|
128
184
|
permissive: true,
|
|
129
185
|
});
|
|
130
186
|
}
|
|
@@ -298,13 +354,10 @@ export const postgresAuthoringEntityTypes = {
|
|
|
298
354
|
* the family pack instead.
|
|
299
355
|
*/
|
|
300
356
|
/**
|
|
301
|
-
*
|
|
302
|
-
*
|
|
303
|
-
*
|
|
304
|
-
*
|
|
305
|
-
* interpreter (a later dispatch). The `discriminator` matches
|
|
306
|
-
* `PostgresRlsPolicy.kind` so the parsed block node carries the same
|
|
307
|
-
* discriminant as the IR class it will lower to.
|
|
357
|
+
* Shared parameter descriptors for the five `policy_<op>` PSL block
|
|
358
|
+
* keywords. All five share the `policy` discriminator — the parser
|
|
359
|
+
* dispatches by keyword (see the framework's PSL-block SPI), and every
|
|
360
|
+
* keyword's factory lowers to `PostgresRlsPolicy` via `lowerRlsPolicyFromBlock`.
|
|
308
361
|
*
|
|
309
362
|
* The `roles` list uses `scope:'cross-space'` because same-namespace
|
|
310
363
|
* role ref resolution requires PSL namespace entries keyed by `refKind`
|
|
@@ -313,24 +366,82 @@ export const postgresAuthoringEntityTypes = {
|
|
|
313
366
|
* slice 4 (cross-space roles). Until then cross-space passes validation
|
|
314
367
|
* unconditionally and the authored role names flow through unchanged.
|
|
315
368
|
*/
|
|
369
|
+
const policyTargetParam = {
|
|
370
|
+
kind: 'ref',
|
|
371
|
+
refKind: 'model',
|
|
372
|
+
scope: 'same-namespace',
|
|
373
|
+
required: true,
|
|
374
|
+
} as const;
|
|
375
|
+
const policyRolesParam = {
|
|
376
|
+
kind: 'list',
|
|
377
|
+
of: { kind: 'ref', refKind: 'role', scope: 'cross-space' },
|
|
378
|
+
} as const;
|
|
379
|
+
const policyPredicateParam = { kind: 'value', codecId: 'pg/text@1', required: true } as const;
|
|
380
|
+
// A policy may only target an RLS-controlled model: the model named by
|
|
381
|
+
// `target` must declare `@@rls`, or the load fails with a diagnostic naming
|
|
382
|
+
// the model and the policy prefix.
|
|
383
|
+
const policyRequiresRls = { parameter: 'target', attribute: 'rls' } as const;
|
|
384
|
+
|
|
316
385
|
export const postgresAuthoringPslBlockDescriptors = {
|
|
386
|
+
// The predicate param set per keyword mirrors Postgres: SELECT/DELETE take
|
|
387
|
+
// USING only; INSERT takes WITH CHECK only; UPDATE/ALL take both. The
|
|
388
|
+
// per-operation predicate matrix is enforced in `lowerRlsPolicyFromBlock`
|
|
389
|
+
// (a wrong predicate for the operation is a load-time diagnostic there),
|
|
390
|
+
// since the generic descriptor validator is not wired into the SQL-family
|
|
391
|
+
// interpreter.
|
|
317
392
|
policy_select: {
|
|
318
393
|
kind: 'pslBlock',
|
|
319
394
|
keyword: 'policy_select',
|
|
320
395
|
discriminator: 'policy',
|
|
321
396
|
name: { required: true },
|
|
397
|
+
parameters: { target: policyTargetParam, roles: policyRolesParam, using: policyPredicateParam },
|
|
398
|
+
requiresModelAttribute: policyRequiresRls,
|
|
399
|
+
},
|
|
400
|
+
policy_delete: {
|
|
401
|
+
kind: 'pslBlock',
|
|
402
|
+
keyword: 'policy_delete',
|
|
403
|
+
discriminator: 'policy',
|
|
404
|
+
name: { required: true },
|
|
405
|
+
parameters: { target: policyTargetParam, roles: policyRolesParam, using: policyPredicateParam },
|
|
406
|
+
requiresModelAttribute: policyRequiresRls,
|
|
407
|
+
},
|
|
408
|
+
policy_insert: {
|
|
409
|
+
kind: 'pslBlock',
|
|
410
|
+
keyword: 'policy_insert',
|
|
411
|
+
discriminator: 'policy',
|
|
412
|
+
name: { required: true },
|
|
322
413
|
parameters: {
|
|
323
|
-
target:
|
|
324
|
-
roles:
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
414
|
+
target: policyTargetParam,
|
|
415
|
+
roles: policyRolesParam,
|
|
416
|
+
withCheck: policyPredicateParam,
|
|
417
|
+
},
|
|
418
|
+
requiresModelAttribute: policyRequiresRls,
|
|
419
|
+
},
|
|
420
|
+
policy_update: {
|
|
421
|
+
kind: 'pslBlock',
|
|
422
|
+
keyword: 'policy_update',
|
|
423
|
+
discriminator: 'policy',
|
|
424
|
+
name: { required: true },
|
|
425
|
+
parameters: {
|
|
426
|
+
target: policyTargetParam,
|
|
427
|
+
roles: policyRolesParam,
|
|
428
|
+
using: policyPredicateParam,
|
|
429
|
+
withCheck: policyPredicateParam,
|
|
430
|
+
},
|
|
431
|
+
requiresModelAttribute: policyRequiresRls,
|
|
432
|
+
},
|
|
433
|
+
policy_all: {
|
|
434
|
+
kind: 'pslBlock',
|
|
435
|
+
keyword: 'policy_all',
|
|
436
|
+
discriminator: 'policy',
|
|
437
|
+
name: { required: true },
|
|
438
|
+
parameters: {
|
|
439
|
+
target: policyTargetParam,
|
|
440
|
+
roles: policyRolesParam,
|
|
441
|
+
using: policyPredicateParam,
|
|
442
|
+
withCheck: policyPredicateParam,
|
|
329
443
|
},
|
|
330
|
-
|
|
331
|
-
// `target` must declare `@@rls`, or the load fails with a diagnostic
|
|
332
|
-
// naming the model and the policy prefix.
|
|
333
|
-
requiresModelAttribute: { parameter: 'target', attribute: 'rls' },
|
|
444
|
+
requiresModelAttribute: policyRequiresRls,
|
|
334
445
|
},
|
|
335
446
|
/**
|
|
336
447
|
* PSL block descriptor for `native_enum`.
|
|
@@ -5,6 +5,7 @@ import { entityAt, UNBOUND_NAMESPACE_ID } from '@prisma-next/framework-component
|
|
|
5
5
|
import type { SqlStorage, StorageTable } from '@prisma-next/sql-contract/types';
|
|
6
6
|
import { blindCast } from '@prisma-next/utils/casts';
|
|
7
7
|
import { ifDefined } from '@prisma-next/utils/defined';
|
|
8
|
+
import { type PostgresRole, ROLE_DEFAULT_CONTROL_POLICY } from '../postgres-role';
|
|
8
9
|
import { isPostgresSchema } from '../postgres-schema';
|
|
9
10
|
import type { PostgresNamespaceSchemaNode } from '../schema-ir/postgres-namespace-schema-node';
|
|
10
11
|
import { PostgresTableSchemaNode } from '../schema-ir/postgres-table-schema-node';
|
|
@@ -215,6 +216,17 @@ export function resolvePostgresNodeIssueCreationFactoryName(
|
|
|
215
216
|
* `namespaceId` falls back to {@link UNBOUND_NAMESPACE_ID} via
|
|
216
217
|
* `resolveNamespaceIdForTable`'s own fallback — the call-side resolver does
|
|
217
218
|
* the same for the `DropTableCall` it produces.
|
|
219
|
+
*
|
|
220
|
+
* A role node resolves against the `PostgresRole` contract entity at that
|
|
221
|
+
* role's name (roles are root-level diff subjects, so the name is the
|
|
222
|
+
* second path segment after the tree root). A `not-found` role is declared,
|
|
223
|
+
* so the entity is always found and its `control` read directly — which is
|
|
224
|
+
* always {@link ROLE_DEFAULT_CONTROL_POLICY} today, since no authoring
|
|
225
|
+
* surface sets it otherwise. A `not-expected` role has no contract entity
|
|
226
|
+
* (an undeclared live role, by definition), so the subject falls back to
|
|
227
|
+
* the same default — roles are referenced but never owned, so an
|
|
228
|
+
* undeclared role gets exactly the same governance as a declared one.
|
|
229
|
+
* Never creates a new object: role provisioning is a non-goal.
|
|
218
230
|
*/
|
|
219
231
|
export function resolvePostgresNodeIssueControlPolicySubject(
|
|
220
232
|
issue: SchemaDiffIssue<SqlSchemaDiffNode>,
|
|
@@ -234,6 +246,23 @@ export function resolvePostgresNodeIssueControlPolicySubject(
|
|
|
234
246
|
};
|
|
235
247
|
}
|
|
236
248
|
|
|
249
|
+
if (node.nodeKind === PostgresSchemaNodeKind.role) {
|
|
250
|
+
const roleName = issue.path[1];
|
|
251
|
+
const roleEntity =
|
|
252
|
+
roleName === undefined
|
|
253
|
+
? undefined
|
|
254
|
+
: entityAt<PostgresRole>(contract.storage, {
|
|
255
|
+
namespaceId: UNBOUND_NAMESPACE_ID,
|
|
256
|
+
entityKind: 'role',
|
|
257
|
+
entityName: roleName,
|
|
258
|
+
});
|
|
259
|
+
return {
|
|
260
|
+
namespaceId: UNBOUND_NAMESPACE_ID,
|
|
261
|
+
explicitNodeControlPolicy: roleEntity?.control ?? ROLE_DEFAULT_CONTROL_POLICY,
|
|
262
|
+
createsNewObject: false,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
237
266
|
const tableName = issue.path[2];
|
|
238
267
|
if (tableName === undefined) return undefined;
|
|
239
268
|
const ddlSchemaName = issue.path[1];
|
|
@@ -3,8 +3,9 @@ import type { SqlSchemaDiffResult } from '@prisma-next/family-sql/control';
|
|
|
3
3
|
import { buildNativeTypeExpander } from '@prisma-next/family-sql/control';
|
|
4
4
|
import { classifyDiffSubjectGranularity } from '@prisma-next/family-sql/diff';
|
|
5
5
|
import type { TargetBoundComponentDescriptor } from '@prisma-next/framework-components/components';
|
|
6
|
-
import type { SchemaDiffIssue } from '@prisma-next/framework-components/control';
|
|
6
|
+
import type { DiffableNode, SchemaDiffIssue } from '@prisma-next/framework-components/control';
|
|
7
7
|
import { diffSchemas } from '@prisma-next/framework-components/control';
|
|
8
|
+
import { UNBOUND_NAMESPACE_ID } from '@prisma-next/framework-components/ir';
|
|
8
9
|
import type { SqlStorage } from '@prisma-next/sql-contract/types';
|
|
9
10
|
import type { SqlSchemaIRNode } from '@prisma-next/sql-schema-ir/types';
|
|
10
11
|
import { blindCast } from '@prisma-next/utils/casts';
|
|
@@ -19,6 +20,31 @@ import {
|
|
|
19
20
|
import { contractToPostgresDatabaseSchemaNode } from './contract-to-postgres-database-schema-node';
|
|
20
21
|
import { resolvePostgresNodeIssueControlPolicySubject } from './control-policy';
|
|
21
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Whether a diff issue's subject node is cluster-scoped — it carries its own
|
|
25
|
+
* `namespaceId` field (only role and policy diff nodes do) and that
|
|
26
|
+
* coordinate is the unbound sentinel, meaning the node is not owned by any
|
|
27
|
+
* schema/namespace. A role node is always cluster-scoped (roles are
|
|
28
|
+
* cluster-level objects); a policy node's `namespaceId` is always its
|
|
29
|
+
* table's resolved DDL schema, never the sentinel. Namespace-ownership
|
|
30
|
+
* scoping (which compares `issue.path[1]` against the set of DDL schemas
|
|
31
|
+
* the contract owns) makes no sense for a cluster-scoped subject — its path
|
|
32
|
+
* segment at that index is the object's own name, not a schema name — so
|
|
33
|
+
* such an issue bypasses that scoping entirely.
|
|
34
|
+
*/
|
|
35
|
+
function isClusterScopedIssue(issue: SchemaDiffIssue): boolean {
|
|
36
|
+
const node = issue.expected ?? issue.actual;
|
|
37
|
+
return node !== undefined && nodeNamespaceId(node) === UNBOUND_NAMESPACE_ID;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function nodeNamespaceId(node: DiffableNode): string | undefined {
|
|
41
|
+
if (!Object.hasOwn(node, 'namespaceId')) return undefined;
|
|
42
|
+
return blindCast<
|
|
43
|
+
{ namespaceId: string },
|
|
44
|
+
'presence of an own namespaceId field was just checked via Object.hasOwn'
|
|
45
|
+
>(node).namespaceId;
|
|
46
|
+
}
|
|
47
|
+
|
|
22
48
|
function ownedSchemaNames(expected: PostgresDatabaseSchemaNode): ReadonlySet<string> {
|
|
23
49
|
const policyNamespaces = Object.values(expected.namespaces).flatMap((ns) =>
|
|
24
50
|
Object.values(ns.tables).flatMap((t) => t.policies.map((p) => p.namespaceId)),
|
|
@@ -53,10 +79,17 @@ function pruneTableLessNamespaces(
|
|
|
53
79
|
}
|
|
54
80
|
|
|
55
81
|
/**
|
|
56
|
-
* Resolves a verdict-diff issue's subject
|
|
57
|
-
*
|
|
82
|
+
* Resolves a verdict-diff issue's subject's declared control policy directly
|
|
83
|
+
* from the contract, by delegating to the same node-typed resolver
|
|
58
84
|
* ({@link resolvePostgresNodeIssueControlPolicySubject}) the planner uses to
|
|
59
|
-
* gate DDL calls. `undefined` when the issue resolves to no contract
|
|
85
|
+
* gate DDL calls. `undefined` when the issue resolves to no contract subject.
|
|
86
|
+
*
|
|
87
|
+
* A role issue resolves through that same resolver to `external`
|
|
88
|
+
* unconditionally (see its role branch), regardless of the contract's own
|
|
89
|
+
* default policy: a role is referenced by the contract but not owned, and
|
|
90
|
+
* `external`'s existing semantics — a missing declared subject still fails,
|
|
91
|
+
* every extra is suppressed — are exactly the wanted asymmetric grading for
|
|
92
|
+
* a cluster object the framework does not own.
|
|
60
93
|
*/
|
|
61
94
|
function resolveControlPolicy(
|
|
62
95
|
issue: SchemaDiffIssue,
|
|
@@ -75,8 +108,9 @@ function resolveControlPolicy(
|
|
|
75
108
|
* the expected tree (resolved leaf values, expander threaded, FK schemas
|
|
76
109
|
* resolved, table-less namespaces pruned), run the generic
|
|
77
110
|
* differ over the trees as derived, and scope out `not-expected` findings under namespaces the
|
|
78
|
-
* contract does not own. Ownership
|
|
79
|
-
*
|
|
111
|
+
* contract does not own. Ownership scoping bypasses cluster-scoped subjects
|
|
112
|
+
* (roles today), mirroring the legacy decomposition: relational extras check
|
|
113
|
+
* the PRUNED owned set (the legacy
|
|
80
114
|
* per-namespace walk never visited a table-less namespace, so its live
|
|
81
115
|
* relational contents are invisible), while `structural` extras (RLS
|
|
82
116
|
* policies) check the FULL owned set (the legacy policy diff governed
|
|
@@ -107,9 +141,10 @@ export function diffPostgresSchema(input: {
|
|
|
107
141
|
const structuralOwned = ownedSchemaNames(fullExpected);
|
|
108
142
|
const issues = diffSchemas(expected, actual).filter((issue) => {
|
|
109
143
|
if (issue.reason !== 'not-expected') return true;
|
|
144
|
+
if (isClusterScopedIssue(issue)) return true;
|
|
145
|
+
const granularity = classifyDiffSubjectGranularity(issue, postgresDiffSubjectGranularity);
|
|
110
146
|
const namespaceSegment = issue.path[1];
|
|
111
147
|
if (namespaceSegment === undefined) return true;
|
|
112
|
-
const granularity = classifyDiffSubjectGranularity(issue, postgresDiffSubjectGranularity);
|
|
113
148
|
const owned = granularity === 'structural' ? structuralOwned : relationalOwned;
|
|
114
149
|
return owned.has(namespaceSegment);
|
|
115
150
|
});
|
|
@@ -165,15 +200,15 @@ export interface PostgresPlanDiff {
|
|
|
165
200
|
readonly expected: PostgresDatabaseSchemaNode;
|
|
166
201
|
/** The live ("start") tree, padded with empty namespaces so a missing schema's tables pair. */
|
|
167
202
|
readonly actual: PostgresDatabaseSchemaNode;
|
|
168
|
-
/** The one node diff over the two trees: relational + policy drift,
|
|
203
|
+
/** The one node diff over the two trees: relational + policy drift, cluster-scope-aware ownership filtered. */
|
|
169
204
|
readonly issues: readonly SchemaDiffIssue<SqlSchemaDiffNode>[];
|
|
170
205
|
}
|
|
171
206
|
|
|
172
207
|
/**
|
|
173
208
|
* The Postgres planner's diff input: the SAME tree-building
|
|
174
209
|
* `diffPostgresSchema` uses (expander threaded, FK schemas resolved,
|
|
175
|
-
* table-less namespaces pruned,
|
|
176
|
-
* namespace padding (so a missing
|
|
210
|
+
* table-less namespaces pruned, cluster-scope-aware ownership filter) plus
|
|
211
|
+
* actual namespace padding (so a missing
|
|
177
212
|
* schema's tables surface as `not-found` instead of a swallowed namespace
|
|
178
213
|
* `not-found`). One differ drives both verify and plan; this is the
|
|
179
214
|
* plan-side derivation. The single issue list covers tables / columns /
|
|
@@ -207,9 +242,10 @@ export function buildPostgresPlanDiff(input: {
|
|
|
207
242
|
'both trees are PostgresDatabaseSchemaNodes, so every diff-issue node is a SqlSchemaDiffNode'
|
|
208
243
|
>(diffSchemas(expected, paddedActual)).filter((issue) => {
|
|
209
244
|
if (issue.reason !== 'not-expected') return true;
|
|
245
|
+
if (isClusterScopedIssue(issue)) return true;
|
|
246
|
+
const granularity = classifyDiffSubjectGranularity(issue, postgresDiffSubjectGranularity);
|
|
210
247
|
const namespaceSegment = issue.path[1];
|
|
211
248
|
if (namespaceSegment === undefined) return true;
|
|
212
|
-
const granularity = classifyDiffSubjectGranularity(issue, postgresDiffSubjectGranularity);
|
|
213
249
|
const owned = granularity === 'structural' ? structuralOwned : relationalOwned;
|
|
214
250
|
return owned.has(namespaceSegment);
|
|
215
251
|
});
|
|
@@ -186,6 +186,11 @@ export class PostgresMigrationPlanner implements MigrationPlanner<'sql', 'postgr
|
|
|
186
186
|
frameworkComponents: options.frameworkComponents,
|
|
187
187
|
});
|
|
188
188
|
const policyDiffIssues = rawIssues.filter((issue) => isPolicyDiffIssue(issue));
|
|
189
|
+
// Role diff issues resolve to the `external` control policy (see
|
|
190
|
+
// `resolvePostgresNodeIssueControlPolicySubject`'s role branch), so the
|
|
191
|
+
// control-policy partition below suppresses them to zero ops on its own,
|
|
192
|
+
// before `mapNodeIssueToCall` ever sees them — no separate exclusion
|
|
193
|
+
// needed here.
|
|
189
194
|
const relationalDiffIssues = rawIssues.filter((issue) => !isPolicyDiffIssue(issue));
|
|
190
195
|
|
|
191
196
|
// The generic differ is total and un-gated: strict-mode extras filtering
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import { freezeNode } from '@prisma-next/framework-components/ir';
|
|
2
2
|
import { SqlNode } from '@prisma-next/sql-contract/types';
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Roles are referenced by the contract but never owned — the framework
|
|
6
|
+
* issues no `CREATE`/`DROP ROLE`, so a role's effective governance is always
|
|
7
|
+
* `external`: a missing declared role fails verify, an extra live role is
|
|
8
|
+
* tolerated, and the planner emits zero role DDL. `external` is the only
|
|
9
|
+
* value a role's control can ever carry.
|
|
10
|
+
*/
|
|
11
|
+
export const ROLE_DEFAULT_CONTROL_POLICY = 'external';
|
|
12
|
+
|
|
4
13
|
export interface PostgresRoleInput {
|
|
5
14
|
readonly name: string;
|
|
6
15
|
/**
|
|
@@ -8,6 +17,11 @@ export interface PostgresRoleInput {
|
|
|
8
17
|
* from `@prisma-next/framework-components/ir`.
|
|
9
18
|
*/
|
|
10
19
|
readonly namespaceId: string;
|
|
20
|
+
/**
|
|
21
|
+
* Defaults to {@link ROLE_DEFAULT_CONTROL_POLICY} when omitted. Roles are
|
|
22
|
+
* referenced but never owned, so `'external'` is the only allowed value.
|
|
23
|
+
*/
|
|
24
|
+
readonly control?: 'external';
|
|
11
25
|
}
|
|
12
26
|
|
|
13
27
|
/**
|
|
@@ -22,16 +36,24 @@ export interface PostgresRoleInput {
|
|
|
22
36
|
* Extends `SqlNode` directly, frozen at construction via `freezeNode(this)`.
|
|
23
37
|
* The `kind: 'role'` discriminant is enumerable so it survives JSON.
|
|
24
38
|
* Matches the entries key (one-string rule).
|
|
39
|
+
*
|
|
40
|
+
* `control` always carries a value (never omitted): it defaults to
|
|
41
|
+
* {@link ROLE_DEFAULT_CONTROL_POLICY} in the constructor, so both a
|
|
42
|
+
* freshly-authored role and one hydrated from an older `contract.json`
|
|
43
|
+
* lacking the field resolve the same way — the control-policy resolver
|
|
44
|
+
* reads it directly off the entity instead of special-casing role issues.
|
|
25
45
|
*/
|
|
26
46
|
export class PostgresRole extends SqlNode {
|
|
27
47
|
override readonly kind = 'role' as const;
|
|
28
48
|
readonly name: string;
|
|
29
49
|
readonly namespaceId: string;
|
|
50
|
+
readonly control: 'external';
|
|
30
51
|
|
|
31
52
|
constructor(input: PostgresRoleInput) {
|
|
32
53
|
super();
|
|
33
54
|
this.name = input.name;
|
|
34
55
|
this.namespaceId = input.namespaceId;
|
|
56
|
+
this.control = input.control ?? ROLE_DEFAULT_CONTROL_POLICY;
|
|
35
57
|
freezeNode(this);
|
|
36
58
|
}
|
|
37
59
|
}
|
|
@@ -17,9 +17,10 @@ export interface PostgresDatabaseSchemaNodeInput {
|
|
|
17
17
|
*
|
|
18
18
|
* `id` is the fixed sentinel `'database'` — the root has no siblings and
|
|
19
19
|
* the value is never emitted into migration paths. `isEqualTo` is identity
|
|
20
|
-
* (roots always share the `'database'` id). `children()`
|
|
21
|
-
* nodes
|
|
22
|
-
*
|
|
20
|
+
* (roots always share the `'database'` id). `children()` yields the namespace
|
|
21
|
+
* nodes followed by the role nodes — both are root-level diff subjects. The
|
|
22
|
+
* differ pairs siblings by `(nodeKind, id)`, so a role and a namespace may
|
|
23
|
+
* share a name without colliding.
|
|
23
24
|
*/
|
|
24
25
|
export class PostgresDatabaseSchemaNode extends SqlSchemaIRNode implements DiffableNode {
|
|
25
26
|
override readonly nodeKind = PostgresSchemaNodeKind.database;
|
|
@@ -47,7 +48,7 @@ export class PostgresDatabaseSchemaNode extends SqlSchemaIRNode implements Diffa
|
|
|
47
48
|
}
|
|
48
49
|
|
|
49
50
|
children(): readonly DiffableNode[] {
|
|
50
|
-
return Object.values(this.namespaces);
|
|
51
|
+
return [...Object.values(this.namespaces), ...this.roles];
|
|
51
52
|
}
|
|
52
53
|
|
|
53
54
|
static is(node: SqlSchemaIRNode): node is PostgresDatabaseSchemaNode {
|
|
@@ -20,8 +20,10 @@ export interface PostgresRoleSchemaNodeInput {
|
|
|
20
20
|
* Built by project-from-contract and project-from-database from their respective
|
|
21
21
|
* `PostgresRole` contract entities / introspected rows.
|
|
22
22
|
*
|
|
23
|
-
* Roles are cluster-scoped, so `id` is the role name
|
|
24
|
-
*
|
|
23
|
+
* Roles are cluster-scoped, so `id` is the bare role name. The differ pairs
|
|
24
|
+
* siblings by `(nodeKind, id)`, so a role and a namespace may share a name
|
|
25
|
+
* (role `public`, schema `public`) without colliding. `isEqualTo` compares
|
|
26
|
+
* ids — name-equality is role-equality for cluster-scoped objects.
|
|
25
27
|
*/
|
|
26
28
|
export class PostgresRoleSchemaNode extends SqlSchemaIRNode implements DiffableNode {
|
|
27
29
|
override readonly nodeKind = PostgresSchemaNodeKind.role;
|
|
@@ -45,6 +45,13 @@ const POSTGRES_NODE_GRANULARITY: Readonly<Record<PostgresSchemaNodeKind, DiffSub
|
|
|
45
45
|
[PostgresSchemaNodeKind.database]: 'structural',
|
|
46
46
|
[PostgresSchemaNodeKind.namespace]: 'namespace',
|
|
47
47
|
[PostgresSchemaNodeKind.table]: 'entity',
|
|
48
|
+
// A policy set is owned by the managed table it attaches to; a role is
|
|
49
|
+
// referenced by the contract but not owned (the framework names it in a
|
|
50
|
+
// policy but never owns the cluster's role list). Both classify as
|
|
51
|
+
// `structural`. The asymmetry for roles — a missing declared one fails, an
|
|
52
|
+
// undeclared live one is tolerated unconditionally — comes from the
|
|
53
|
+
// control policy the diff resolves a role issue against (`external`),
|
|
54
|
+
// not from the granularity.
|
|
48
55
|
[PostgresSchemaNodeKind.policy]: 'structural',
|
|
49
56
|
[PostgresSchemaNodeKind.role]: 'structural',
|
|
50
57
|
};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"authoring-WkqG_MkU.mjs","names":[],"sources":["../src/core/authoring.ts"],"sourcesContent":["import { temporalAuthoringPresets } from '@prisma-next/family-sql/control';\nimport type {\n AuthoringEntityContext,\n AuthoringEntityTypeFactoryOutput,\n AuthoringEntityTypeNamespace,\n AuthoringFieldNamespace,\n AuthoringModelAttributeContext,\n AuthoringModelAttributeDescriptorNamespace,\n AuthoringPslBlockDescriptorNamespace,\n AuthoringTypeNamespace,\n PslExtensionBlock,\n} from '@prisma-next/framework-components/authoring';\nimport { modelAttribute } from '@prisma-next/psl-parser';\nimport type { SqlValueSetDerivingEntityTypeOutput } from '@prisma-next/sql-contract/value-set-derivation-hook';\nimport { PG_ENUM_CODEC_ID } from './codec-ids';\nimport { PostgresNativeEnum } from './postgres-native-enum';\nimport { PostgresRlsEnablement, type PostgresRlsEnablementInput } from './postgres-rls-enablement';\nimport { PostgresRlsPolicy } from './postgres-rls-policy';\nimport { PostgresRole, type PostgresRoleInput } from './postgres-role';\nimport {\n PostgresNativeEnumSchema,\n PostgresRlsEnablementSchema,\n PostgresRlsPolicySchema,\n PostgresRoleSchema,\n} from './postgres-validators';\nimport { computeContentHash, normalizePredicate } from './rls/canonicalize';\nimport { formatRlsPolicyWireName } from './rls/wire-name';\n\n/**\n * `pg.enum(<ref>)` registers as an ordinary type constructor whose sole\n * positional argument names a `native_enum` entity instead of carrying a\n * literal value. The interpreter resolves the ref to the `native_enum`\n * entity generically (driven by `entityRefArg`); the `pg/enum@1` codec\n * descriptor's `columnFromEntity` hook (see `codecs.ts`) converts that\n * entity into the column's `typeParams` and native type.\n */\nexport const postgresAuthoringTypes = {\n pg: {\n enum: {\n kind: 'typeConstructor',\n entityRefArg: { index: 0, entityKind: 'native_enum' },\n output: {\n codecId: PG_ENUM_CODEC_ID,\n },\n },\n },\n} as const satisfies AuthoringTypeNamespace;\n\nexport interface RlsPolicyExtensionBlock extends PslExtensionBlock {\n readonly namespaceId: string;\n}\n\nfunction readRefParam(block: PslExtensionBlock, key: string): string | undefined {\n const param = block.parameters[key];\n return param?.kind === 'ref' ? param.identifier : undefined;\n}\n\nfunction readValueParam(block: PslExtensionBlock, key: string): string | undefined {\n const param = block.parameters[key];\n return param?.kind === 'value' ? param.raw : undefined;\n}\n\nfunction readListRefParams(block: PslExtensionBlock, key: string): string[] {\n const param = block.parameters[key];\n if (param?.kind !== 'list') return [];\n return param.items.flatMap((item) => (item.kind === 'ref' ? [item.identifier] : []));\n}\n\n/**\n * Unwraps a quoted PSL string argument, inverting the printer's\n * `escapePslString` escapes (`\\\\`, `\\\"`, `\\n`, `\\r`). An unknown escape\n * sequence is kept verbatim, matching the printer-side `unescapePslString`\n * convention.\n */\nfunction unwrapQuotedString(raw: string): string {\n if (!(raw.startsWith('\"') && raw.endsWith('\"') && raw.length >= 2)) {\n return raw;\n }\n const inner = raw.slice(1, -1);\n let result = '';\n for (let i = 0; i < inner.length; i++) {\n if (inner[i] !== '\\\\' || i + 1 >= inner.length) {\n result += inner[i];\n continue;\n }\n const next = inner[i + 1];\n if (next === '\\\\' || next === '\"') {\n result += next;\n } else if (next === 'n') {\n result += '\\n';\n } else if (next === 'r') {\n result += '\\r';\n } else {\n result += '\\\\';\n result += next;\n }\n i++;\n }\n return result;\n}\n\nfunction lowerRlsPolicyFromBlock(\n block: RlsPolicyExtensionBlock,\n _ctx: AuthoringEntityContext,\n): PostgresRlsPolicy {\n const prefix = block.name;\n const targetModelName = readRefParam(block, 'target') ?? '';\n const tableName = targetModelName.charAt(0).toLowerCase() + targetModelName.slice(1);\n const roles = [...readListRefParams(block, 'roles')].sort();\n const using = unwrapQuotedString(readValueParam(block, 'using') ?? '');\n\n const wireHash = computeContentHash({\n using: normalizePredicate(using),\n roles,\n operation: 'select',\n permissive: true,\n });\n const wireName = formatRlsPolicyWireName(prefix, wireHash);\n\n return new PostgresRlsPolicy({\n name: wireName,\n prefix,\n tableName,\n namespaceId: block.namespaceId,\n operation: 'select',\n roles,\n using,\n permissive: true,\n });\n}\n\n/**\n * Lowers a `native_enum { memberName = \"value\" … @@map(\"type_name\") }` block\n * into a {@link PostgresNativeEnum}. Members must be authored as explicit\n * `key = \"value\"` pairs — a bare (value-less) member is a diagnostic, not\n * accepted (authoring-design.md §2.1). The parsed `memberName` is only used\n * to duplicate-check and report diagnostics; the lowered entity carries just\n * the member values (a native enum is value-only — the member \"name\" isn't\n * a separate authoring concept from the value). `typeName` comes from\n * `@@map` or defaults to the block name verbatim.\n */\nfunction lowerNativeEnumFromBlock(\n block: PslExtensionBlock,\n ctx: AuthoringEntityContext,\n): PostgresNativeEnum | undefined {\n const sourceId = ctx.sourceId ?? 'unknown';\n const diagnostics = ctx.diagnostics;\n\n const mapAttr = block.blockAttributes.find((a) => a.name === 'map');\n let typeName = block.name;\n if (mapAttr) {\n const rawArg = mapAttr.args[0]?.value;\n const mapped = rawArg !== undefined ? unwrapQuotedString(rawArg) : undefined;\n if (mapped === undefined) {\n diagnostics?.push({\n code: 'PSL_NATIVE_ENUM_INVALID_MAP',\n message: `native_enum \"${block.name}\" @@map attribute must have a quoted type-name argument`,\n sourceId,\n span: mapAttr.span,\n });\n return undefined;\n }\n typeName = mapped;\n }\n\n let memberError = false;\n const seenValues = new Set<string>();\n const members: string[] = [];\n for (const [memberName, paramValue] of Object.entries(block.parameters)) {\n if (paramValue.kind === 'bare') {\n diagnostics?.push({\n code: 'PSL_NATIVE_ENUM_BARE_MEMBER',\n message: `native_enum \"${block.name}\" member \"${memberName}\" has no value; members must be authored as \"${memberName} = \\\\\"value\\\\\"\"`,\n sourceId,\n span: paramValue.span,\n });\n memberError = true;\n continue;\n }\n if (paramValue.kind !== 'value') continue;\n\n let jsonValue: unknown;\n try {\n jsonValue = JSON.parse(paramValue.raw);\n } catch {\n diagnostics?.push({\n code: 'PSL_EXTENSION_INVALID_VALUE',\n message: `native_enum \"${block.name}\" member \"${memberName}\" value \"${paramValue.raw}\" is not valid JSON`,\n sourceId,\n span: paramValue.span,\n });\n memberError = true;\n continue;\n }\n if (typeof jsonValue !== 'string') {\n diagnostics?.push({\n code: 'PSL_EXTENSION_INVALID_VALUE',\n message: `native_enum \"${block.name}\" member \"${memberName}\" value must be a string`,\n sourceId,\n span: paramValue.span,\n });\n memberError = true;\n continue;\n }\n if (seenValues.has(jsonValue)) {\n diagnostics?.push({\n code: 'PSL_NATIVE_ENUM_DUPLICATE_MEMBER_VALUE',\n message: `native_enum \"${block.name}\": duplicate member value \"${jsonValue}\"`,\n sourceId,\n span: paramValue.span,\n });\n memberError = true;\n continue;\n }\n seenValues.add(jsonValue);\n members.push(jsonValue);\n }\n\n if (memberError) return undefined;\n\n if (members.length === 0) {\n diagnostics?.push({\n code: 'PSL_NATIVE_ENUM_MISSING_MEMBERS',\n message: `native_enum \"${block.name}\" must have at least one member`,\n sourceId,\n span: block.span,\n });\n return undefined;\n }\n\n // `control` stays unset — the effective grade is resolved at read time via `effectiveControlPolicy`, like `StorageTable`/`StorageColumn`.\n return new PostgresNativeEnum({ typeName, members });\n}\n\n/**\n * `native_enum`'s entity-type factory output, checked separately from the assembled\n * `postgresAuthoringEntityTypes` map below: `deriveValueSet` is SQL-family surface\n * ({@link SqlValueSetDerivingEntityTypeOutput}), not part of the framework\n * `AuthoringEntityTypeFactoryOutput` shape, so folding it directly into the map's single\n * `satisfies AuthoringEntityTypeNamespace` check would trip an excess-property error. Checking it\n * here against the intersection of both shapes keeps it structurally valid against each without\n * widening the map's own check.\n */\nconst nativeEnumEntityTypeOutput = {\n factory: lowerNativeEnumFromBlock,\n deriveValueSet: (entity: PostgresNativeEnum) => ({\n kind: 'valueSet' as const,\n values: [...entity.members],\n }),\n} satisfies AuthoringEntityTypeFactoryOutput<PslExtensionBlock, PostgresNativeEnum | undefined> &\n SqlValueSetDerivingEntityTypeOutput;\n\nexport const postgresAuthoringEntityTypes = {\n role: {\n kind: 'entity',\n discriminator: 'role',\n validatorSchema: PostgresRoleSchema,\n output: {\n factory: (input: PostgresRoleInput): PostgresRole => new PostgresRole(input),\n },\n },\n rls: {\n kind: 'entity',\n discriminator: 'rls',\n validatorSchema: PostgresRlsEnablementSchema,\n output: {\n factory: (input: PostgresRlsEnablementInput): PostgresRlsEnablement =>\n new PostgresRlsEnablement(input),\n },\n },\n policy: {\n kind: 'entity',\n discriminator: 'policy',\n validatorSchema: PostgresRlsPolicySchema,\n output: {\n factory: lowerRlsPolicyFromBlock,\n },\n },\n native_enum: {\n kind: 'entity',\n discriminator: 'native_enum',\n validatorSchema: PostgresNativeEnumSchema,\n output: nativeEnumEntityTypeOutput,\n },\n} as const satisfies AuthoringEntityTypeNamespace;\n\n/**\n * Field presets contributed by the Postgres target pack.\n *\n * These mirror the PSL scalar-to-codec mapping used by the Postgres adapter\n * (see `createPostgresPslScalarTypeDescriptors`), so that authoring a field\n * via the TS callback surface (e.g. `field.int()`) and via the PSL scalar\n * surface (e.g. `Int`) lowers to byte-identical contracts.\n *\n * The `uuidNative` / `id.uuidv4Native` / `id.uuidv7Native` presets use the\n * native Postgres `uuid` type (codecId `pg/uuid@1`). For cross-target\n * portability use `uuidString` / `id.uuidv4String` / `id.uuidv7String` from\n * the family pack instead.\n */\n/**\n * PSL block descriptor for `policy_select`.\n *\n * The parser learns the block shape from this descriptor; lowering from\n * `PslExtensionBlock` to `PostgresRlsPolicy` is wired in the PSL\n * interpreter (a later dispatch). The `discriminator` matches\n * `PostgresRlsPolicy.kind` so the parsed block node carries the same\n * discriminant as the IR class it will lower to.\n *\n * The `roles` list uses `scope:'cross-space'` because same-namespace\n * role ref resolution requires PSL namespace entries keyed by `refKind`\n * (i.e. `'role'`), which in turn requires the role block discriminator to\n * equal `'role'`. Aligning discriminator with refKind is tracked for\n * slice 4 (cross-space roles). Until then cross-space passes validation\n * unconditionally and the authored role names flow through unchanged.\n */\nexport const postgresAuthoringPslBlockDescriptors = {\n policy_select: {\n kind: 'pslBlock',\n keyword: 'policy_select',\n discriminator: 'policy',\n name: { required: true },\n parameters: {\n target: { kind: 'ref', refKind: 'model', scope: 'same-namespace', required: true },\n roles: {\n kind: 'list',\n of: { kind: 'ref', refKind: 'role', scope: 'cross-space' },\n },\n using: { kind: 'value', codecId: 'pg/text@1', required: true },\n },\n // A policy may only target an RLS-controlled model: the model named by\n // `target` must declare `@@rls`, or the load fails with a diagnostic\n // naming the model and the policy prefix.\n requiresModelAttribute: { parameter: 'target', attribute: 'rls' },\n },\n /**\n * PSL block descriptor for `native_enum`.\n *\n * Reuses the existing variadic-block mechanism (the same shape the SQL\n * family's `enum` block ships): the body is an open `memberName = \"value\"`\n * list. `variadicParameters: true` opens the block to arbitrary keys\n * beyond the declared (empty) `parameters` set — the lowering factory\n * (`lowerNativeEnumFromBlock`) turns the variadic entries into ordered\n * members and rejects a bare (value-less) member.\n */\n native_enum: {\n kind: 'pslBlock',\n keyword: 'native_enum',\n discriminator: 'native_enum',\n name: { required: true },\n parameters: {},\n variadicParameters: true,\n },\n} as const satisfies AuthoringPslBlockDescriptorNamespace;\n\n/**\n * `@@` model attributes contributed by the Postgres target pack.\n *\n * `@@rls` is argument-less: presence marks the model's table\n * RLS-controlled. It lowers to a {@link PostgresRlsEnablement} marker in\n * the namespace's `entries.rls`, keyed by the model's table name — the\n * marker (never the policy set) is what drives\n * `ENABLE`/`DISABLE ROW LEVEL SECURITY` planning.\n */\nexport const postgresAuthoringModelAttributes = {\n rls: {\n kind: 'modelAttribute',\n attribute: 'rls',\n spec: modelAttribute('rls', {}),\n lower: (_parsed: Record<never, never>, ctx: AuthoringModelAttributeContext) => ({\n key: ctx.storageName,\n entity: new PostgresRlsEnablement({\n tableName: ctx.storageName,\n namespaceId: ctx.namespaceId,\n }),\n }),\n },\n} as const satisfies AuthoringModelAttributeDescriptorNamespace;\n\nexport const postgresAuthoringFieldPresets = {\n text: {\n kind: 'fieldPreset',\n output: {\n codecId: 'pg/text@1',\n nativeType: 'text',\n },\n },\n int: {\n kind: 'fieldPreset',\n output: {\n codecId: 'pg/int4@1',\n nativeType: 'int4',\n },\n },\n bigint: {\n kind: 'fieldPreset',\n output: {\n codecId: 'pg/int8@1',\n nativeType: 'int8',\n },\n },\n float: {\n kind: 'fieldPreset',\n output: {\n codecId: 'pg/float8@1',\n nativeType: 'float8',\n },\n },\n decimal: {\n kind: 'fieldPreset',\n output: {\n codecId: 'pg/numeric@1',\n nativeType: 'numeric',\n },\n },\n boolean: {\n kind: 'fieldPreset',\n output: {\n codecId: 'pg/bool@1',\n nativeType: 'bool',\n },\n },\n json: {\n kind: 'fieldPreset',\n output: {\n codecId: 'pg/jsonb@1',\n nativeType: 'jsonb',\n },\n },\n bytes: {\n kind: 'fieldPreset',\n output: {\n codecId: 'pg/bytea@1',\n nativeType: 'bytea',\n },\n },\n dateTime: {\n kind: 'fieldPreset',\n output: {\n codecId: 'pg/timestamptz@1',\n nativeType: 'timestamptz',\n },\n },\n temporal: /* @__PURE__ */ temporalAuthoringPresets({\n codecId: 'pg/timestamptz@1',\n nativeType: 'timestamptz',\n }),\n uuidNative: {\n kind: 'fieldPreset',\n output: {\n codecId: 'pg/uuid@1',\n nativeType: 'uuid',\n },\n },\n id: {\n uuidv4Native: {\n kind: 'fieldPreset',\n output: {\n codecId: 'pg/uuid@1',\n nativeType: 'uuid',\n executionDefaults: {\n onCreate: {\n kind: 'generator',\n id: 'uuidv4',\n },\n },\n id: true,\n },\n },\n uuidv7Native: {\n kind: 'fieldPreset',\n output: {\n codecId: 'pg/uuid@1',\n nativeType: 'uuid',\n executionDefaults: {\n onCreate: {\n kind: 'generator',\n id: 'uuidv7',\n },\n },\n id: true,\n },\n },\n },\n} as const satisfies AuthoringFieldNamespace;\n"],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"diff-database-schema-BOsDnH64.mjs","names":[],"sources":["../src/core/migrations/contract-to-postgres-database-schema-node.ts","../src/core/migrations/diff-database-schema.ts"],"sourcesContent":["import type { ContractToSchemaIROptions } from '@prisma-next/family-sql/control';\nimport { contractNamespaceToSchemaIR } from '@prisma-next/family-sql/control';\nimport { UNBOUND_NAMESPACE_ID } from '@prisma-next/framework-components/ir';\nimport { SqlForeignKeyIR } from '@prisma-next/sql-schema-ir/types';\nimport { ifDefined } from '@prisma-next/utils/defined';\nimport type { PostgresRlsPolicy } from '../postgres-rls-policy';\nimport type { PostgresContract } from '../postgres-schema';\nimport { isPostgresSchema } from '../postgres-schema';\nimport { PostgresDatabaseSchemaNode } from '../schema-ir/postgres-database-schema-node';\nimport { PostgresNamespaceSchemaNode } from '../schema-ir/postgres-namespace-schema-node';\nimport { PostgresPolicySchemaNode } from '../schema-ir/postgres-policy-schema-node';\nimport { PostgresRoleSchemaNode } from '../schema-ir/postgres-role-schema-node';\nimport { PostgresTableSchemaNode } from '../schema-ir/postgres-table-schema-node';\nimport { resolveDdlSchemaForNamespaceStorage } from './resolve-ddl-schema';\n\nfunction toPolicyNode(policy: PostgresRlsPolicy, namespaceId: string): PostgresPolicySchemaNode {\n return new PostgresPolicySchemaNode({\n name: policy.name,\n prefix: policy.prefix,\n tableName: policy.tableName,\n namespaceId,\n operation: policy.operation,\n roles: [...policy.roles],\n ...ifDefined('using', policy.using),\n ...ifDefined('withCheck', policy.withCheck),\n permissive: policy.permissive,\n });\n}\n\n/**\n * Projects a Postgres contract into the expected schema-diff tree: a\n * `PostgresDatabaseSchemaNode` root holding one `PostgresNamespaceSchemaNode`\n * per Postgres namespace, each holding its `PostgresTableSchemaNode`s with\n * their `PostgresPolicySchemaNode`s, plus the database roles on the root.\n *\n * Not a duplicate of the family's `contractToSchemaIR`: that builds a flat,\n * single `{ tables }` map (and throws on cross-namespace name collisions, with\n * no RLS/role concept) for SQLite's single-schema world. This is the\n * Postgres-specific *tree* shape — multi-schema, RLS-policy-aware, role-aware.\n * It reuses the family's per-namespace table conversion (`contractNamespaceToSchemaIR`)\n * for column/FK/index building and only adds the Postgres tree/policy/role shape.\n *\n * Tables are grouped by their owning namespace (resolved DDL schema name) so\n * the tree mirrors Postgres's object hierarchy. The DDL schema name is\n * resolved once per namespace.\n *\n * A policy that references a table absent from its namespace is a malformed\n * contract — the loop throws rather than fabricating a stub table.\n */\nexport function contractToPostgresDatabaseSchemaNode(\n contract: PostgresContract | null,\n options: ContractToSchemaIROptions,\n): PostgresDatabaseSchemaNode {\n if (contract === null) {\n return new PostgresDatabaseSchemaNode({\n namespaces: {},\n roles: [],\n existingSchemas: [],\n pgVersion: '',\n });\n }\n\n const namespaces: Record<string, PostgresNamespaceSchemaNode> = {};\n const roles: PostgresRoleSchemaNode[] = [];\n const ownedSchemas: string[] = [];\n\n for (const ns of Object.values(contract.storage.namespaces)) {\n if (!isPostgresSchema(ns)) continue;\n const ddlSchema = resolveDdlSchemaForNamespaceStorage(contract.storage, ns.id);\n ownedSchemas.push(ddlSchema);\n\n // Convert only THIS namespace's tables (passing the full storage for\n // type/value-set/enum resolution that spans namespaces), so the same table\n // name can exist in two schemas without colliding in a bare-keyed record.\n const sqlTables = contractNamespaceToSchemaIR(contract.storage, ns.id, options).tables;\n\n const policiesByTable = new Map<string, PostgresPolicySchemaNode[]>();\n for (const policy of Object.values(ns.policy)) {\n const list = policiesByTable.get(policy.tableName) ?? [];\n list.push(toPolicyNode(policy, ddlSchema));\n policiesByTable.set(policy.tableName, list);\n }\n\n const tables: Record<string, PostgresTableSchemaNode> = {};\n for (const tableName of Object.keys(ns.table)) {\n const sqlTable = sqlTables[tableName];\n if (sqlTable === undefined) continue;\n // The family conversion stamps `referencedSchema` only for bound FK\n // targets; an absent value means the FK targets the unbound namespace.\n // Postgres restores its own coordinate for that slot (the unbound\n // singleton's id) so the raw coordinate keeps qualifying REFERENCES\n // clauses, and resolves the real live DDL schema — introspected FKs\n // already carry the live schema, so this is what lets an expected FK\n // pair (by diff-node id) with its introspected counterpart.\n const foreignKeys = sqlTable.foreignKeys.map(\n (fk) =>\n new SqlForeignKeyIR({\n columns: fk.columns,\n referencedTable: fk.referencedTable,\n referencedColumns: fk.referencedColumns,\n referencedSchema: fk.referencedSchema ?? UNBOUND_NAMESPACE_ID,\n ...ifDefined('name', fk.name),\n ...ifDefined('onDelete', fk.onDelete),\n ...ifDefined('onUpdate', fk.onUpdate),\n ...ifDefined('annotations', fk.annotations),\n resolvedReferencedNamespace: resolveDdlSchemaForNamespaceStorage(\n contract.storage,\n fk.referencedSchema ?? UNBOUND_NAMESPACE_ID,\n ),\n }),\n );\n tables[tableName] = new PostgresTableSchemaNode({\n name: sqlTable.name,\n columns: sqlTable.columns,\n foreignKeys,\n uniques: sqlTable.uniques,\n indexes: sqlTable.indexes,\n ...ifDefined('primaryKey', sqlTable.primaryKey),\n ...ifDefined('annotations', sqlTable.annotations),\n ...ifDefined('checks', sqlTable.checks),\n policies: policiesByTable.get(tableName) ?? [],\n // Marker-driven, never derived from the policy set: the `rls` entry\n // is the single authored source of enablement.\n rlsEnabled: Object.hasOwn(ns.rls, tableName),\n });\n }\n\n for (const [tableName, tablePolicies] of policiesByTable) {\n if (!(tableName in tables)) {\n const policyName = tablePolicies[0]?.name ?? '(unknown)';\n throw new Error(\n `contract-to-postgres-database-schema-node: policy \"${policyName}\" references table \"${tableName}\" not present in namespace \"${ddlSchema}\"`,\n );\n }\n if (!Object.hasOwn(ns.rls, tableName)) {\n const policyPrefix = tablePolicies[0]?.prefix ?? '(unknown)';\n throw new Error(\n `contract-to-postgres-database-schema-node: policy \"${policyPrefix}\" targets table \"${tableName}\" in namespace \"${ddlSchema}\", which is not RLS-controlled. Mark the model with @@rls (entries.rls[\"${tableName}\"]) or remove the policy.`,\n );\n }\n }\n\n namespaces[ddlSchema] = new PostgresNamespaceSchemaNode({\n schemaName: ddlSchema,\n tables,\n nativeEnumTypeNames: [],\n });\n\n for (const role of Object.values(ns.role)) {\n roles.push(new PostgresRoleSchemaNode({ name: role.name, namespaceId: role.namespaceId }));\n }\n }\n\n return new PostgresDatabaseSchemaNode({\n namespaces,\n roles,\n existingSchemas: ownedSchemas,\n pgVersion: '',\n });\n}\n","import type { Contract, ControlPolicy } from '@prisma-next/contract/types';\nimport type { SqlSchemaDiffResult } from '@prisma-next/family-sql/control';\nimport { buildNativeTypeExpander } from '@prisma-next/family-sql/control';\nimport { classifyDiffSubjectGranularity } from '@prisma-next/family-sql/diff';\nimport type { TargetBoundComponentDescriptor } from '@prisma-next/framework-components/components';\nimport type { SchemaDiffIssue } from '@prisma-next/framework-components/control';\nimport { diffSchemas } from '@prisma-next/framework-components/control';\nimport type { SqlStorage } from '@prisma-next/sql-contract/types';\nimport type { SqlSchemaIRNode } from '@prisma-next/sql-schema-ir/types';\nimport { blindCast } from '@prisma-next/utils/casts';\nimport { ifDefined } from '@prisma-next/utils/defined';\nimport type { PostgresContract } from '../postgres-schema';\nimport { PostgresDatabaseSchemaNode } from '../schema-ir/postgres-database-schema-node';\nimport { PostgresNamespaceSchemaNode } from '../schema-ir/postgres-namespace-schema-node';\nimport {\n postgresDiffSubjectGranularity,\n type SqlSchemaDiffNode,\n} from '../schema-ir/schema-node-kinds';\nimport { contractToPostgresDatabaseSchemaNode } from './contract-to-postgres-database-schema-node';\nimport { resolvePostgresNodeIssueControlPolicySubject } from './control-policy';\n\nfunction ownedSchemaNames(expected: PostgresDatabaseSchemaNode): ReadonlySet<string> {\n const policyNamespaces = Object.values(expected.namespaces).flatMap((ns) =>\n Object.values(ns.tables).flatMap((t) => t.policies.map((p) => p.namespaceId)),\n );\n return new Set([...policyNamespaces, ...expected.existingSchemas]);\n}\n\n/**\n * Drops contract namespaces that declare no tables from the verdict-diff\n * expected tree and the relational owned-schema set. The legacy relational\n * walk skipped a table-less namespace (e.g. an enums-only schema) before\n * pairing, so neither its DDL schema's absence nor that schema's live\n * relational contents ever reached the verdict — the pruned tree\n * reproduces that. The prune loses no expected policies: policies attach\n * to tables, so a table-less namespace carries none (the projection\n * throws on a policy referencing an absent table). Live policies in a\n * pruned schema remain governed via the full owned set (see\n * {@link diffPostgresSchema}).\n */\nfunction pruneTableLessNamespaces(\n expected: PostgresDatabaseSchemaNode,\n): PostgresDatabaseSchemaNode {\n const namespaces = Object.fromEntries(\n Object.entries(expected.namespaces).filter(([, ns]) => Object.keys(ns.tables).length > 0),\n );\n return new PostgresDatabaseSchemaNode({\n namespaces,\n roles: [...expected.roles],\n existingSchemas: expected.existingSchemas.filter((s) => namespaces[s] !== undefined),\n pgVersion: expected.pgVersion,\n });\n}\n\n/**\n * Resolves a verdict-diff issue's subject table's declared control policy\n * directly from the contract, by delegating to the same node-typed resolver\n * ({@link resolvePostgresNodeIssueControlPolicySubject}) the planner uses to\n * gate DDL calls. `undefined` when the issue resolves to no contract table.\n */\nfunction resolveControlPolicy(\n issue: SchemaDiffIssue,\n contract: Contract<SqlStorage>,\n): ControlPolicy | undefined {\n const nodeIssue = blindCast<\n SchemaDiffIssue<SqlSchemaDiffNode>,\n 'every node in a Postgres schema diff tree is a SqlSchemaDiffNode'\n >(issue);\n return resolvePostgresNodeIssueControlPolicySubject(nodeIssue, contract)\n ?.explicitNodeControlPolicy;\n}\n\n/**\n * The Postgres full-tree node diff for the family verify verdict: derive\n * the expected tree (resolved leaf values, expander threaded, FK schemas\n * resolved, table-less namespaces pruned), run the generic\n * differ over the trees as derived, and scope out `not-expected` findings under namespaces the\n * contract does not own. Ownership is role-aware, mirroring the legacy\n * decomposition: relational extras check the PRUNED owned set (the legacy\n * per-namespace walk never visited a table-less namespace, so its live\n * relational contents are invisible), while `structural` extras (RLS\n * policies) check the FULL owned set (the legacy policy diff governed\n * every contract schema regardless of tables — RLS governance does not\n * shrink because a namespace declares no tables). The codec `verifyType`\n * hooks run once per contract namespace with tables against that\n * namespace's paired actual node (the hooks read namespace-scoped state\n * such as `nativeEnumTypeNames`).\n */\nexport function diffPostgresSchema(input: {\n readonly contract: Contract<SqlStorage>;\n readonly schema: SqlSchemaIRNode;\n readonly frameworkComponents: ReadonlyArray<TargetBoundComponentDescriptor<'sql', string>>;\n}): SqlSchemaDiffResult {\n const postgresContract = blindCast<\n PostgresContract,\n 'diffPostgresSchema is only called with a postgres contract'\n >(input.contract);\n PostgresDatabaseSchemaNode.assert(input.schema);\n const actual = input.schema;\n const expandNativeType = buildNativeTypeExpander(input.frameworkComponents);\n const fullExpected = contractToPostgresDatabaseSchemaNode(postgresContract, {\n annotationNamespace: 'pg',\n ...ifDefined('expandNativeType', expandNativeType),\n });\n const expected = pruneTableLessNamespaces(fullExpected);\n const relationalOwned = ownedSchemaNames(expected);\n const structuralOwned = ownedSchemaNames(fullExpected);\n const issues = diffSchemas(expected, actual).filter((issue) => {\n if (issue.reason !== 'not-expected') return true;\n const namespaceSegment = issue.path[1];\n if (namespaceSegment === undefined) return true;\n const granularity = classifyDiffSubjectGranularity(issue, postgresDiffSubjectGranularity);\n const owned = granularity === 'structural' ? structuralOwned : relationalOwned;\n return owned.has(namespaceSegment);\n });\n const namespacePairs = Object.values(expected.namespaces).map((ns) => ({\n actual: actual.namespaces[ns.schemaName],\n }));\n return {\n issues,\n resolveControlPolicy: (issue) => resolveControlPolicy(issue, postgresContract),\n namespacePairs,\n };\n}\n\n/**\n * Adds an empty namespace node to the actual tree for every expected namespace\n * absent from it. The relational plan diff pairs on namespace: a contract\n * namespace whose live schema does not exist yet must surface each of its\n * tables as `not-found` (→ `CREATE TABLE`), NOT as a single namespace\n * `not-found` that subtree-coalescing would collapse (leaving `CREATE SCHEMA`\n * with no tables). Padding makes the namespaces pair, so only table/column/\n * policy drift surfaces; `CREATE SCHEMA` comes separately from the synthesized\n * namespace-presence stitch (`verifyPostgresNamespacePresence`), never from the\n * tree diff — matching the retired per-namespace-paired relational walk, which\n * paired a missing schema against an empty namespace node.\n */\nfunction padActualNamespaces(\n expected: PostgresDatabaseSchemaNode,\n actual: PostgresDatabaseSchemaNode,\n): PostgresDatabaseSchemaNode {\n const namespaces: Record<string, PostgresNamespaceSchemaNode> = { ...actual.namespaces };\n let padded = false;\n for (const schemaName of Object.keys(expected.namespaces)) {\n if (namespaces[schemaName] === undefined) {\n namespaces[schemaName] = new PostgresNamespaceSchemaNode({\n schemaName,\n tables: {},\n nativeEnumTypeNames: [],\n });\n padded = true;\n }\n }\n if (!padded) return actual;\n return new PostgresDatabaseSchemaNode({\n namespaces,\n roles: [...actual.roles],\n existingSchemas: [...actual.existingSchemas],\n pgVersion: actual.pgVersion,\n });\n}\n\nexport interface PostgresPlanDiff {\n /** The desired (\"end\") tree — resolved leaf values (incl. `codecRef`) on every column, table-less namespaces pruned. */\n readonly expected: PostgresDatabaseSchemaNode;\n /** The live (\"start\") tree, padded with empty namespaces so a missing schema's tables pair. */\n readonly actual: PostgresDatabaseSchemaNode;\n /** The one node diff over the two trees: relational + policy drift, role-aware ownership filtered. */\n readonly issues: readonly SchemaDiffIssue<SqlSchemaDiffNode>[];\n}\n\n/**\n * The Postgres planner's diff input: the SAME tree-building\n * `diffPostgresSchema` uses (expander threaded, FK schemas resolved,\n * table-less namespaces pruned, role-aware ownership filter) plus actual\n * namespace padding (so a missing\n * schema's tables surface as `not-found` instead of a swallowed namespace\n * `not-found`). One differ drives both verify and plan; this is the\n * plan-side derivation. The single issue list covers tables / columns /\n * constraints / indexes / defaults AND policies — the caller splits it\n * (relational → `mapNodeIssueToCall`; policy → RLS ops) and stitches in\n * `CREATE SCHEMA` separately.\n */\nexport function buildPostgresPlanDiff(input: {\n readonly contract: Contract<SqlStorage>;\n readonly actualSchema: SqlSchemaIRNode;\n readonly frameworkComponents: ReadonlyArray<TargetBoundComponentDescriptor<'sql', string>>;\n}): PostgresPlanDiff {\n const postgresContract = blindCast<\n PostgresContract,\n 'buildPostgresPlanDiff is only called with a postgres contract'\n >(input.contract);\n PostgresDatabaseSchemaNode.assert(input.actualSchema);\n const actual = input.actualSchema;\n const expandNativeType = buildNativeTypeExpander(input.frameworkComponents);\n const projectionOptions = {\n annotationNamespace: 'pg',\n ...ifDefined('expandNativeType', expandNativeType),\n };\n const fullExpected = contractToPostgresDatabaseSchemaNode(postgresContract, projectionOptions);\n const expected = pruneTableLessNamespaces(fullExpected);\n const paddedActual = padActualNamespaces(expected, actual);\n const relationalOwned = ownedSchemaNames(expected);\n const structuralOwned = ownedSchemaNames(fullExpected);\n const issues = blindCast<\n readonly SchemaDiffIssue<SqlSchemaDiffNode>[],\n 'both trees are PostgresDatabaseSchemaNodes, so every diff-issue node is a SqlSchemaDiffNode'\n >(diffSchemas(expected, paddedActual)).filter((issue) => {\n if (issue.reason !== 'not-expected') return true;\n const namespaceSegment = issue.path[1];\n if (namespaceSegment === undefined) return true;\n const granularity = classifyDiffSubjectGranularity(issue, postgresDiffSubjectGranularity);\n const owned = granularity === 'structural' ? structuralOwned : relationalOwned;\n return owned.has(namespaceSegment);\n });\n return { expected, actual: paddedActual, issues };\n}\n"],"mappings":"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"}
|