@supabase/pg-delta 1.0.0-alpha.17 → 1.0.0-alpha.19
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/core/expand-replace-dependencies.js +69 -0
- package/dist/core/objects/index/index.model.js +12 -2
- package/dist/core/objects/procedure/procedure.diff.js +33 -20
- package/dist/core/objects/rls-policy/changes/rls-policy.create.js +23 -0
- package/dist/core/objects/rls-policy/rls-policy.model.d.ts +49 -0
- package/dist/core/objects/rls-policy/rls-policy.model.js +122 -1
- package/dist/core/objects/table/table.diff.js +1 -0
- package/dist/core/objects/table/table.model.d.ts +4 -0
- package/dist/core/objects/table/table.model.js +2 -0
- package/dist/core/plan/sql-format/fixtures.js +8 -0
- package/dist/core/post-diff-cycle-breaking.d.ts +7 -0
- package/dist/core/post-diff-cycle-breaking.js +69 -3
- package/package.json +1 -1
- package/src/core/catalog.snapshot.test.ts +2 -0
- package/src/core/expand-replace-dependencies.test.ts +118 -0
- package/src/core/expand-replace-dependencies.ts +78 -0
- package/src/core/objects/index/index.model.test.ts +83 -0
- package/src/core/objects/index/index.model.ts +13 -4
- package/src/core/objects/procedure/procedure.diff.test.ts +100 -2
- package/src/core/objects/procedure/procedure.diff.ts +39 -21
- package/src/core/objects/rls-policy/changes/rls-policy.alter.test.ts +16 -0
- package/src/core/objects/rls-policy/changes/rls-policy.create.test.ts +128 -0
- package/src/core/objects/rls-policy/changes/rls-policy.create.ts +27 -0
- package/src/core/objects/rls-policy/changes/rls-policy.drop.test.ts +2 -0
- package/src/core/objects/rls-policy/rls-policy.diff.test.ts +2 -0
- package/src/core/objects/rls-policy/rls-policy.model.ts +134 -1
- package/src/core/objects/table/changes/table.alter.test.ts +1 -0
- package/src/core/objects/table/table.diff.test.ts +102 -0
- package/src/core/objects/table/table.diff.ts +1 -0
- package/src/core/objects/table/table.model.ts +2 -0
- package/src/core/plan/sql-format/fixtures.ts +8 -0
- package/src/core/post-diff-cycle-breaking.test.ts +142 -0
- package/src/core/post-diff-cycle-breaking.ts +83 -2
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { CreateDomain } from "./objects/domain/changes/domain.create.js";
|
|
2
2
|
import { DropDomain } from "./objects/domain/changes/domain.drop.js";
|
|
3
|
+
import { CreateIndex } from "./objects/index/changes/index.create.js";
|
|
4
|
+
import { DropIndex } from "./objects/index/changes/index.drop.js";
|
|
3
5
|
import { CreateMaterializedView } from "./objects/materialized-view/changes/materialized-view.create.js";
|
|
4
6
|
import { DropMaterializedView } from "./objects/materialized-view/changes/materialized-view.drop.js";
|
|
5
7
|
import { CreateProcedure } from "./objects/procedure/changes/procedure.create.js";
|
|
@@ -32,6 +34,25 @@ export function expandReplaceDependencies({ changes, mainCatalog, branchCatalog,
|
|
|
32
34
|
replaceRoots.add(id);
|
|
33
35
|
}
|
|
34
36
|
}
|
|
37
|
+
// Procedure stableIds are signature-qualified
|
|
38
|
+
// (`procedure:schema.name(argtypes)`), so a function whose parameter types
|
|
39
|
+
// change has different ids in `createdIds` and `droppedIds` and would not
|
|
40
|
+
// appear in the intersection above. Treat any dropped procedure whose
|
|
41
|
+
// `(schema, name)` matches a created procedure as a replace root so
|
|
42
|
+
// dependents referencing the old signature via pg_depend get promoted to
|
|
43
|
+
// DROP+CREATE.
|
|
44
|
+
const createdProcedureNames = new Set();
|
|
45
|
+
for (const id of createdIds) {
|
|
46
|
+
const key = parseProcedureSchemaName(id);
|
|
47
|
+
if (key)
|
|
48
|
+
createdProcedureNames.add(key);
|
|
49
|
+
}
|
|
50
|
+
for (const id of droppedIds) {
|
|
51
|
+
const key = parseProcedureSchemaName(id);
|
|
52
|
+
if (key && createdProcedureNames.has(key)) {
|
|
53
|
+
replaceRoots.add(id);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
35
56
|
if (replaceRoots.size === 0) {
|
|
36
57
|
return {
|
|
37
58
|
changes,
|
|
@@ -149,6 +170,14 @@ function isOwnedSequenceColumnDependency(referencedId, dependentId, mainCatalog,
|
|
|
149
170
|
return (dependentId ===
|
|
150
171
|
stableId.column(sequence.owned_by_schema, sequence.owned_by_table, sequence.owned_by_column));
|
|
151
172
|
}
|
|
173
|
+
function parseProcedureSchemaName(stableId) {
|
|
174
|
+
if (!stableId.startsWith("procedure:"))
|
|
175
|
+
return null;
|
|
176
|
+
const paren = stableId.indexOf("(");
|
|
177
|
+
if (paren === -1)
|
|
178
|
+
return null;
|
|
179
|
+
return stableId.slice("procedure:".length, paren);
|
|
180
|
+
}
|
|
152
181
|
function normalizeDependentId(dependentId) {
|
|
153
182
|
let id = dependentId;
|
|
154
183
|
while (id.startsWith("comment:")) {
|
|
@@ -195,6 +224,18 @@ function resolveObjectForStableId(stableId, mainCatalog, branchCatalog) {
|
|
|
195
224
|
const branch = branchCatalog.materializedViews[stableId];
|
|
196
225
|
return main && branch ? { kind: "materialized_view", main, branch } : null;
|
|
197
226
|
}
|
|
227
|
+
if (stableId.startsWith("index:")) {
|
|
228
|
+
const main = mainCatalog.indexes[stableId];
|
|
229
|
+
const branch = branchCatalog.indexes[stableId];
|
|
230
|
+
return main && branch
|
|
231
|
+
? {
|
|
232
|
+
kind: "index",
|
|
233
|
+
main,
|
|
234
|
+
branch,
|
|
235
|
+
branchIndexableObject: branchCatalog.indexableObjects[branch.tableStableId],
|
|
236
|
+
}
|
|
237
|
+
: null;
|
|
238
|
+
}
|
|
198
239
|
if (stableId.startsWith("procedure:")) {
|
|
199
240
|
const main = mainCatalog.procedures[stableId];
|
|
200
241
|
const branch = branchCatalog.procedures[stableId];
|
|
@@ -280,6 +321,34 @@ function buildReplaceChanges(resolved, options) {
|
|
|
280
321
|
? [new CreateMaterializedView({ materializedView: resolved.branch })]
|
|
281
322
|
: []),
|
|
282
323
|
];
|
|
324
|
+
case "index":
|
|
325
|
+
// Constraint-owned, primary, and partition-attached indexes are managed
|
|
326
|
+
// by the owning constraint or parent-index DDL, not standalone
|
|
327
|
+
// CREATE INDEX / DROP INDEX. The `case "table":` branch above already
|
|
328
|
+
// recreates constraints via AlterTableAddConstraint; emitting a
|
|
329
|
+
// standalone drop/create here would fail in PostgreSQL
|
|
330
|
+
// ("cannot drop index ... because constraint ... requires it") or
|
|
331
|
+
// duplicate the index the constraint recreates. Skip matches
|
|
332
|
+
// diffIndexes (packages/pg-delta/src/core/objects/index/index.diff.ts).
|
|
333
|
+
if (resolved.main.is_owned_by_constraint ||
|
|
334
|
+
resolved.main.is_primary ||
|
|
335
|
+
resolved.main.is_index_partition ||
|
|
336
|
+
resolved.branch.is_owned_by_constraint ||
|
|
337
|
+
resolved.branch.is_primary ||
|
|
338
|
+
resolved.branch.is_index_partition) {
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
return [
|
|
342
|
+
...(addDrop ? [new DropIndex({ index: resolved.main })] : []),
|
|
343
|
+
...(addCreate
|
|
344
|
+
? [
|
|
345
|
+
new CreateIndex({
|
|
346
|
+
index: resolved.branch,
|
|
347
|
+
indexableObject: resolved.branchIndexableObject,
|
|
348
|
+
}),
|
|
349
|
+
]
|
|
350
|
+
: []),
|
|
351
|
+
];
|
|
283
352
|
case "procedure":
|
|
284
353
|
return [
|
|
285
354
|
...(addDrop ? [new DropProcedure({ procedure: resolved.main })] : []),
|
|
@@ -36,6 +36,15 @@ const indexPropsSchema = z.object({
|
|
|
36
36
|
comment: z.string().nullable(),
|
|
37
37
|
owner: z.string(),
|
|
38
38
|
});
|
|
39
|
+
// pg_get_indexdef(oid, colno, pretty) invokes pg_get_indexdef_worker with
|
|
40
|
+
// missing_ok = true, so it can return NULL when any internal system-cache lookup
|
|
41
|
+
// fails (race with concurrent DROP, role visibility edge cases, orphaned index
|
|
42
|
+
// metadata, recovery transients). An unreadable index cannot be diffed, so we
|
|
43
|
+
// accept NULL here and filter the row out with a debug log instead of crashing
|
|
44
|
+
// the whole catalog extraction.
|
|
45
|
+
const indexRowSchema = indexPropsSchema.extend({
|
|
46
|
+
definition: z.string().nullable(),
|
|
47
|
+
});
|
|
39
48
|
export class Index extends BasePgModel {
|
|
40
49
|
schema;
|
|
41
50
|
table_name;
|
|
@@ -332,7 +341,8 @@ export async function extractIndexes(pool) {
|
|
|
332
341
|
|
|
333
342
|
order by 1, 2
|
|
334
343
|
`);
|
|
335
|
-
|
|
336
|
-
|
|
344
|
+
const validatedRows = indexRows
|
|
345
|
+
.map((row) => indexRowSchema.parse(row))
|
|
346
|
+
.filter((row) => row.definition !== null);
|
|
337
347
|
return validatedRows.map((row) => new Index(row));
|
|
338
348
|
}
|
|
@@ -17,8 +17,7 @@ import { GrantProcedurePrivileges, RevokeGrantOptionProcedurePrivileges, RevokeP
|
|
|
17
17
|
export function diffProcedures(ctx, main, branch) {
|
|
18
18
|
const { created, dropped, altered } = diffObjects(main, branch);
|
|
19
19
|
const changes = [];
|
|
20
|
-
|
|
21
|
-
const proc = branch[procedureId];
|
|
20
|
+
const appendCreateProcedureChanges = (proc) => {
|
|
22
21
|
changes.push(new CreateProcedure({ procedure: proc }));
|
|
23
22
|
// OWNER: If the procedure should be owned by someone other than the current user,
|
|
24
23
|
// emit ALTER FUNCTION/PROCEDURE ... OWNER TO after creation
|
|
@@ -53,6 +52,9 @@ export function diffProcedures(ctx, main, branch) {
|
|
|
53
52
|
Revoke: RevokeProcedurePrivileges,
|
|
54
53
|
RevokeGrantOption: RevokeGrantOptionProcedurePrivileges,
|
|
55
54
|
}, ctx.version));
|
|
55
|
+
};
|
|
56
|
+
for (const procedureId of created) {
|
|
57
|
+
appendCreateProcedureChanges(branch[procedureId]);
|
|
56
58
|
}
|
|
57
59
|
for (const procedureId of dropped) {
|
|
58
60
|
changes.push(new DropProcedure({ procedure: main[procedureId] }));
|
|
@@ -60,22 +62,18 @@ export function diffProcedures(ctx, main, branch) {
|
|
|
60
62
|
for (const procedureId of altered) {
|
|
61
63
|
const mainProcedure = main[procedureId];
|
|
62
64
|
const branchProcedure = branch[procedureId];
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
|
|
65
|
+
// Fields that are part of the function's identity/signature. PostgreSQL
|
|
66
|
+
// rejects `CREATE OR REPLACE FUNCTION` for any of these changes with
|
|
67
|
+
// errors such as:
|
|
68
|
+
// - cannot change return type of existing function
|
|
69
|
+
// - cannot change name of input parameter "..."
|
|
70
|
+
// - cannot change whether a procedure has output parameters
|
|
71
|
+
// - cannot remove parameter defaults from existing function
|
|
72
|
+
// These require `DROP FUNCTION` followed by `CREATE FUNCTION`.
|
|
73
|
+
const SIGNATURE_BREAKING_FIELDS = [
|
|
66
74
|
"kind",
|
|
67
75
|
"return_type",
|
|
68
76
|
"return_type_schema",
|
|
69
|
-
"language",
|
|
70
|
-
// The following properties are alterable in SQL, but our generator may choose
|
|
71
|
-
// to replace on changes not covered by explicit ALTER actions. Keep them out here
|
|
72
|
-
// to allow ALTER for those we implement below.
|
|
73
|
-
// security_definer,
|
|
74
|
-
// volatility,
|
|
75
|
-
// parallel_safety,
|
|
76
|
-
// is_strict,
|
|
77
|
-
// leakproof,
|
|
78
|
-
// Returns-set is part of the signature and not alterable
|
|
79
77
|
"returns_set",
|
|
80
78
|
"argument_count",
|
|
81
79
|
"argument_default_count",
|
|
@@ -84,20 +82,35 @@ export function diffProcedures(ctx, main, branch) {
|
|
|
84
82
|
"all_argument_types",
|
|
85
83
|
"argument_modes",
|
|
86
84
|
"argument_defaults",
|
|
85
|
+
];
|
|
86
|
+
// Fields where `CREATE OR REPLACE` is sufficient - body replacement only.
|
|
87
|
+
// Other fields (security_definer, volatility, parallel_safety, is_strict,
|
|
88
|
+
// leakproof, config) are alterable via dedicated ALTER actions below.
|
|
89
|
+
const OR_REPLACEABLE_NON_ALTERABLE_FIELDS = [
|
|
90
|
+
"language",
|
|
87
91
|
"source_code",
|
|
88
92
|
"binary_path",
|
|
89
93
|
"sql_body",
|
|
90
|
-
// config is alterable via SET/RESET
|
|
91
94
|
];
|
|
92
|
-
const
|
|
95
|
+
const signatureChanged = hasNonAlterableChanges(mainProcedure, branchProcedure, SIGNATURE_BREAKING_FIELDS, {
|
|
93
96
|
argument_names: deepEqual,
|
|
94
97
|
argument_types: deepEqual,
|
|
95
98
|
all_argument_types: deepEqual,
|
|
96
99
|
argument_modes: deepEqual,
|
|
97
|
-
config: deepEqual,
|
|
98
100
|
});
|
|
99
|
-
|
|
100
|
-
|
|
101
|
+
const nonAlterablePropsChanged = signatureChanged ||
|
|
102
|
+
hasNonAlterableChanges(mainProcedure, branchProcedure, OR_REPLACEABLE_NON_ALTERABLE_FIELDS);
|
|
103
|
+
if (signatureChanged) {
|
|
104
|
+
// PostgreSQL cannot change an existing function's signature via
|
|
105
|
+
// `CREATE OR REPLACE`. Drop the old signature, then recreate.
|
|
106
|
+
// `expandReplaceDependencies` will cascade the replacement to dependent
|
|
107
|
+
// objects (views, triggers, column defaults) via pg_depend edges.
|
|
108
|
+
changes.push(new DropProcedure({ procedure: mainProcedure }));
|
|
109
|
+
appendCreateProcedureChanges(branchProcedure);
|
|
110
|
+
}
|
|
111
|
+
else if (nonAlterablePropsChanged) {
|
|
112
|
+
// Body-only non-alterable change - `CREATE OR REPLACE` preserves the
|
|
113
|
+
// function OID and keeps dependent objects attached.
|
|
101
114
|
changes.push(new CreateProcedure({ procedure: branchProcedure, orReplace: true }));
|
|
102
115
|
if (mainProcedure.comment !== branchProcedure.comment) {
|
|
103
116
|
if (branchProcedure.comment === null) {
|
|
@@ -33,6 +33,29 @@ export class CreateRlsPolicy extends CreateRlsPolicyChange {
|
|
|
33
33
|
dependencies.add(stableId.table(this.policy.schema, this.policy.table_name));
|
|
34
34
|
// Owner dependency
|
|
35
35
|
dependencies.add(stableId.role(this.policy.owner));
|
|
36
|
+
// Relations and functions referenced inside USING / WITH CHECK
|
|
37
|
+
// expressions must exist before the policy is created. These come from
|
|
38
|
+
// pg_depend (populated by PostgreSQL's recordDependencyOnExpr at policy
|
|
39
|
+
// creation), not from re-parsing the expression text.
|
|
40
|
+
for (const ref of this.policy.referenced_relations) {
|
|
41
|
+
switch (ref.kind) {
|
|
42
|
+
case "table":
|
|
43
|
+
dependencies.add(stableId.table(ref.schema, ref.name));
|
|
44
|
+
break;
|
|
45
|
+
case "view":
|
|
46
|
+
dependencies.add(stableId.view(ref.schema, ref.name));
|
|
47
|
+
break;
|
|
48
|
+
case "materialized_view":
|
|
49
|
+
dependencies.add(stableId.materializedView(ref.schema, ref.name));
|
|
50
|
+
break;
|
|
51
|
+
case "foreign_table":
|
|
52
|
+
dependencies.add(stableId.foreignTable(ref.schema, ref.name));
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
for (const ref of this.policy.referenced_procedures) {
|
|
57
|
+
dependencies.add(stableId.procedure(ref.schema, ref.name, ref.argument_types.join(",")));
|
|
58
|
+
}
|
|
36
59
|
return Array.from(dependencies);
|
|
37
60
|
}
|
|
38
61
|
serialize(_options) {
|
|
@@ -1,6 +1,23 @@
|
|
|
1
1
|
import type { Pool } from "pg";
|
|
2
2
|
import z from "zod";
|
|
3
3
|
import { BasePgModel } from "../base.model.ts";
|
|
4
|
+
declare const rlsPolicyReferencedRelationSchema: z.ZodObject<{
|
|
5
|
+
kind: z.ZodEnum<{
|
|
6
|
+
foreign_table: "foreign_table";
|
|
7
|
+
materialized_view: "materialized_view";
|
|
8
|
+
table: "table";
|
|
9
|
+
view: "view";
|
|
10
|
+
}>;
|
|
11
|
+
schema: z.ZodString;
|
|
12
|
+
name: z.ZodString;
|
|
13
|
+
}, z.z.core.$strip>;
|
|
14
|
+
export type RlsPolicyReferencedRelation = z.infer<typeof rlsPolicyReferencedRelationSchema>;
|
|
15
|
+
declare const rlsPolicyReferencedProcedureSchema: z.ZodObject<{
|
|
16
|
+
schema: z.ZodString;
|
|
17
|
+
name: z.ZodString;
|
|
18
|
+
argument_types: z.ZodArray<z.ZodString>;
|
|
19
|
+
}, z.z.core.$strip>;
|
|
20
|
+
export type RlsPolicyReferencedProcedure = z.infer<typeof rlsPolicyReferencedProcedureSchema>;
|
|
4
21
|
declare const rlsPolicyPropsSchema: z.ZodObject<{
|
|
5
22
|
schema: z.ZodString;
|
|
6
23
|
name: z.ZodString;
|
|
@@ -18,6 +35,21 @@ declare const rlsPolicyPropsSchema: z.ZodObject<{
|
|
|
18
35
|
with_check_expression: z.ZodNullable<z.ZodString>;
|
|
19
36
|
owner: z.ZodString;
|
|
20
37
|
comment: z.ZodNullable<z.ZodString>;
|
|
38
|
+
referenced_relations: z.ZodArray<z.ZodObject<{
|
|
39
|
+
kind: z.ZodEnum<{
|
|
40
|
+
foreign_table: "foreign_table";
|
|
41
|
+
materialized_view: "materialized_view";
|
|
42
|
+
table: "table";
|
|
43
|
+
view: "view";
|
|
44
|
+
}>;
|
|
45
|
+
schema: z.ZodString;
|
|
46
|
+
name: z.ZodString;
|
|
47
|
+
}, z.z.core.$strip>>;
|
|
48
|
+
referenced_procedures: z.ZodArray<z.ZodObject<{
|
|
49
|
+
schema: z.ZodString;
|
|
50
|
+
name: z.ZodString;
|
|
51
|
+
argument_types: z.ZodArray<z.ZodString>;
|
|
52
|
+
}, z.z.core.$strip>>;
|
|
21
53
|
}, z.z.core.$strip>;
|
|
22
54
|
export type RlsPolicyProps = z.infer<typeof rlsPolicyPropsSchema>;
|
|
23
55
|
export declare class RlsPolicy extends BasePgModel {
|
|
@@ -31,6 +63,23 @@ export declare class RlsPolicy extends BasePgModel {
|
|
|
31
63
|
readonly with_check_expression: RlsPolicyProps["with_check_expression"];
|
|
32
64
|
readonly owner: RlsPolicyProps["owner"];
|
|
33
65
|
readonly comment: RlsPolicyProps["comment"];
|
|
66
|
+
/**
|
|
67
|
+
* Tables / views / materialized views / foreign tables that
|
|
68
|
+
* `using_expression` / `with_check_expression` reference, sourced from
|
|
69
|
+
* `pg_depend` (`recordDependencyOnExpr` at policy creation). Drives
|
|
70
|
+
* ordering dependencies in `CreateRlsPolicy.requires`. Intentionally
|
|
71
|
+
* excluded from `dataFields` — it's derived from the expression text
|
|
72
|
+
* and changes lockstep with it.
|
|
73
|
+
*/
|
|
74
|
+
readonly referenced_relations: RlsPolicyProps["referenced_relations"];
|
|
75
|
+
/**
|
|
76
|
+
* Functions / procedures that `using_expression` / `with_check_expression`
|
|
77
|
+
* reference, sourced from `pg_depend` (refclassid = `pg_proc`). The
|
|
78
|
+
* argument-type signature comes straight from `pg_proc.proargtypes` via
|
|
79
|
+
* `format_type`, so it matches the signature the procedure extractor
|
|
80
|
+
* embeds in `stableId.procedure(...)`. Not part of `dataFields`.
|
|
81
|
+
*/
|
|
82
|
+
readonly referenced_procedures: RlsPolicyProps["referenced_procedures"];
|
|
34
83
|
constructor(props: RlsPolicyProps);
|
|
35
84
|
get stableId(): `rlsPolicy:${string}`;
|
|
36
85
|
get identityFields(): {
|
|
@@ -8,6 +8,22 @@ const RlsPolicyCommandSchema = z.enum([
|
|
|
8
8
|
"d", // DELETE command
|
|
9
9
|
"*", // ALL commands
|
|
10
10
|
]);
|
|
11
|
+
const RlsPolicyReferencedRelationKindSchema = z.enum([
|
|
12
|
+
"table",
|
|
13
|
+
"view",
|
|
14
|
+
"materialized_view",
|
|
15
|
+
"foreign_table",
|
|
16
|
+
]);
|
|
17
|
+
const rlsPolicyReferencedRelationSchema = z.object({
|
|
18
|
+
kind: RlsPolicyReferencedRelationKindSchema,
|
|
19
|
+
schema: z.string(),
|
|
20
|
+
name: z.string(),
|
|
21
|
+
});
|
|
22
|
+
const rlsPolicyReferencedProcedureSchema = z.object({
|
|
23
|
+
schema: z.string(),
|
|
24
|
+
name: z.string(),
|
|
25
|
+
argument_types: z.array(z.string()),
|
|
26
|
+
});
|
|
11
27
|
const rlsPolicyPropsSchema = z.object({
|
|
12
28
|
schema: z.string(),
|
|
13
29
|
name: z.string(),
|
|
@@ -19,6 +35,8 @@ const rlsPolicyPropsSchema = z.object({
|
|
|
19
35
|
with_check_expression: z.string().nullable(),
|
|
20
36
|
owner: z.string(),
|
|
21
37
|
comment: z.string().nullable(),
|
|
38
|
+
referenced_relations: z.array(rlsPolicyReferencedRelationSchema),
|
|
39
|
+
referenced_procedures: z.array(rlsPolicyReferencedProcedureSchema),
|
|
22
40
|
});
|
|
23
41
|
export class RlsPolicy extends BasePgModel {
|
|
24
42
|
schema;
|
|
@@ -31,6 +49,23 @@ export class RlsPolicy extends BasePgModel {
|
|
|
31
49
|
with_check_expression;
|
|
32
50
|
owner;
|
|
33
51
|
comment;
|
|
52
|
+
/**
|
|
53
|
+
* Tables / views / materialized views / foreign tables that
|
|
54
|
+
* `using_expression` / `with_check_expression` reference, sourced from
|
|
55
|
+
* `pg_depend` (`recordDependencyOnExpr` at policy creation). Drives
|
|
56
|
+
* ordering dependencies in `CreateRlsPolicy.requires`. Intentionally
|
|
57
|
+
* excluded from `dataFields` — it's derived from the expression text
|
|
58
|
+
* and changes lockstep with it.
|
|
59
|
+
*/
|
|
60
|
+
referenced_relations;
|
|
61
|
+
/**
|
|
62
|
+
* Functions / procedures that `using_expression` / `with_check_expression`
|
|
63
|
+
* reference, sourced from `pg_depend` (refclassid = `pg_proc`). The
|
|
64
|
+
* argument-type signature comes straight from `pg_proc.proargtypes` via
|
|
65
|
+
* `format_type`, so it matches the signature the procedure extractor
|
|
66
|
+
* embeds in `stableId.procedure(...)`. Not part of `dataFields`.
|
|
67
|
+
*/
|
|
68
|
+
referenced_procedures;
|
|
34
69
|
constructor(props) {
|
|
35
70
|
super();
|
|
36
71
|
// Identity fields
|
|
@@ -45,6 +80,9 @@ export class RlsPolicy extends BasePgModel {
|
|
|
45
80
|
this.with_check_expression = props.with_check_expression;
|
|
46
81
|
this.owner = props.owner;
|
|
47
82
|
this.comment = props.comment;
|
|
83
|
+
// Derived metadata (not part of equality)
|
|
84
|
+
this.referenced_relations = props.referenced_relations;
|
|
85
|
+
this.referenced_procedures = props.referenced_procedures;
|
|
48
86
|
}
|
|
49
87
|
get stableId() {
|
|
50
88
|
return `rlsPolicy:${this.schema}.${this.table_name}.${this.name}`;
|
|
@@ -88,6 +126,59 @@ extension_table_oids as (
|
|
|
88
126
|
d.refclassid = 'pg_extension'::regclass
|
|
89
127
|
and d.classid = 'pg_class'::regclass
|
|
90
128
|
and d.deptype = 'e'
|
|
129
|
+
),
|
|
130
|
+
policy_relation_deps as (
|
|
131
|
+
-- Relations referenced inside polqual / polwithcheck. PostgreSQL records
|
|
132
|
+
-- these via recordDependencyOnExpr(..., DEPENDENCY_NORMAL = 'n') at
|
|
133
|
+
-- CREATE POLICY time, so pg_depend is authoritative and we don't need to
|
|
134
|
+
-- re-parse the expression text. Covers regular tables, partitioned
|
|
135
|
+
-- tables, views, materialized views, and foreign tables — any relation
|
|
136
|
+
-- kind the policy can reference in a subquery.
|
|
137
|
+
select distinct
|
|
138
|
+
d.objid as policy_oid,
|
|
139
|
+
case ref_c.relkind
|
|
140
|
+
when 'r' then 'table'
|
|
141
|
+
when 'p' then 'table'
|
|
142
|
+
when 'v' then 'view'
|
|
143
|
+
when 'm' then 'materialized_view'
|
|
144
|
+
when 'f' then 'foreign_table'
|
|
145
|
+
end as ref_kind,
|
|
146
|
+
ref_ns.nspname as ref_schema,
|
|
147
|
+
ref_c.relname as ref_name
|
|
148
|
+
from
|
|
149
|
+
pg_depend d
|
|
150
|
+
join pg_policy p on p.oid = d.objid
|
|
151
|
+
join pg_class ref_c on ref_c.oid = d.refobjid
|
|
152
|
+
join pg_namespace ref_ns on ref_ns.oid = ref_c.relnamespace
|
|
153
|
+
where
|
|
154
|
+
d.classid = 'pg_policy'::regclass
|
|
155
|
+
and d.refclassid = 'pg_class'::regclass
|
|
156
|
+
and d.deptype = 'n'
|
|
157
|
+
and ref_c.relkind in ('r', 'p', 'v', 'm', 'f')
|
|
158
|
+
and d.refobjid <> p.polrelid
|
|
159
|
+
),
|
|
160
|
+
policy_procedure_deps as (
|
|
161
|
+
-- Functions / procedures referenced inside polqual / polwithcheck. Same
|
|
162
|
+
-- pg_depend mechanism as above, just refclassid = pg_proc. proargtypes
|
|
163
|
+
-- formatted via format_type(oid, null) matches the signature produced by
|
|
164
|
+
-- the procedure extractor (see procedure.model.ts), so stableId.procedure
|
|
165
|
+
-- on both sides of the diff lines up exactly.
|
|
166
|
+
select distinct
|
|
167
|
+
d.objid as policy_oid,
|
|
168
|
+
ref_ns.nspname as ref_schema,
|
|
169
|
+
ref_p.proname as ref_name,
|
|
170
|
+
array(
|
|
171
|
+
select format_type(oid, null)
|
|
172
|
+
from unnest(ref_p.proargtypes) as oid
|
|
173
|
+
) as ref_argument_types
|
|
174
|
+
from
|
|
175
|
+
pg_depend d
|
|
176
|
+
join pg_proc ref_p on ref_p.oid = d.refobjid
|
|
177
|
+
join pg_namespace ref_ns on ref_ns.oid = ref_p.pronamespace
|
|
178
|
+
where
|
|
179
|
+
d.classid = 'pg_policy'::regclass
|
|
180
|
+
and d.refclassid = 'pg_proc'::regclass
|
|
181
|
+
and d.deptype = 'n'
|
|
91
182
|
)
|
|
92
183
|
select
|
|
93
184
|
tc.relnamespace::regnamespace::text as schema,
|
|
@@ -107,7 +198,37 @@ select
|
|
|
107
198
|
pg_get_expr(p.polqual, p.polrelid) as using_expression,
|
|
108
199
|
pg_get_expr(p.polwithcheck, p.polrelid) as with_check_expression,
|
|
109
200
|
tc.relowner::regrole::text as owner,
|
|
110
|
-
obj_description(p.oid, 'pg_policy') as comment
|
|
201
|
+
obj_description(p.oid, 'pg_policy') as comment,
|
|
202
|
+
coalesce(
|
|
203
|
+
(
|
|
204
|
+
select json_agg(
|
|
205
|
+
json_build_object(
|
|
206
|
+
'kind', prd.ref_kind,
|
|
207
|
+
'schema', prd.ref_schema,
|
|
208
|
+
'name', prd.ref_name
|
|
209
|
+
)
|
|
210
|
+
order by prd.ref_schema, prd.ref_name
|
|
211
|
+
)
|
|
212
|
+
from policy_relation_deps prd
|
|
213
|
+
where prd.policy_oid = p.oid
|
|
214
|
+
),
|
|
215
|
+
'[]'
|
|
216
|
+
) as referenced_relations,
|
|
217
|
+
coalesce(
|
|
218
|
+
(
|
|
219
|
+
select json_agg(
|
|
220
|
+
json_build_object(
|
|
221
|
+
'schema', ppd.ref_schema,
|
|
222
|
+
'name', ppd.ref_name,
|
|
223
|
+
'argument_types', ppd.ref_argument_types
|
|
224
|
+
)
|
|
225
|
+
order by ppd.ref_schema, ppd.ref_name, ppd.ref_argument_types
|
|
226
|
+
)
|
|
227
|
+
from policy_procedure_deps ppd
|
|
228
|
+
where ppd.policy_oid = p.oid
|
|
229
|
+
),
|
|
230
|
+
'[]'
|
|
231
|
+
) as referenced_procedures
|
|
111
232
|
from
|
|
112
233
|
pg_catalog.pg_policy p
|
|
113
234
|
inner join pg_catalog.pg_class tc on tc.oid = p.polrelid
|
|
@@ -66,6 +66,7 @@ function createAlterConstraintChange(mainTable, branchTable) {
|
|
|
66
66
|
mainC.validated !== branchC.validated ||
|
|
67
67
|
mainC.is_local !== branchC.is_local ||
|
|
68
68
|
mainC.no_inherit !== branchC.no_inherit ||
|
|
69
|
+
mainC.is_temporal !== branchC.is_temporal ||
|
|
69
70
|
JSON.stringify(mainC.key_columns) !==
|
|
70
71
|
JSON.stringify(branchC.key_columns) ||
|
|
71
72
|
JSON.stringify(mainC.foreign_key_columns) !==
|
|
@@ -23,6 +23,7 @@ declare const tableConstraintPropsSchema: z.ZodObject<{
|
|
|
23
23
|
validated: z.ZodBoolean;
|
|
24
24
|
is_local: z.ZodBoolean;
|
|
25
25
|
no_inherit: z.ZodBoolean;
|
|
26
|
+
is_temporal: z.ZodBoolean;
|
|
26
27
|
is_partition_clone: z.ZodBoolean;
|
|
27
28
|
parent_constraint_schema: z.ZodNullable<z.ZodString>;
|
|
28
29
|
parent_constraint_name: z.ZodNullable<z.ZodString>;
|
|
@@ -125,6 +126,7 @@ declare const tablePropsSchema: z.ZodObject<{
|
|
|
125
126
|
validated: z.ZodBoolean;
|
|
126
127
|
is_local: z.ZodBoolean;
|
|
127
128
|
no_inherit: z.ZodBoolean;
|
|
129
|
+
is_temporal: z.ZodBoolean;
|
|
128
130
|
is_partition_clone: z.ZodBoolean;
|
|
129
131
|
parent_constraint_schema: z.ZodNullable<z.ZodString>;
|
|
130
132
|
parent_constraint_name: z.ZodNullable<z.ZodString>;
|
|
@@ -239,6 +241,7 @@ export declare class Table extends BasePgModel implements TableLikeObject {
|
|
|
239
241
|
validated: boolean;
|
|
240
242
|
is_local: boolean;
|
|
241
243
|
no_inherit: boolean;
|
|
244
|
+
is_temporal: boolean;
|
|
242
245
|
is_partition_clone: boolean;
|
|
243
246
|
parent_constraint_schema: string | null;
|
|
244
247
|
parent_constraint_name: string | null;
|
|
@@ -300,6 +303,7 @@ export declare class Table extends BasePgModel implements TableLikeObject {
|
|
|
300
303
|
validated: boolean;
|
|
301
304
|
is_local: boolean;
|
|
302
305
|
no_inherit: boolean;
|
|
306
|
+
is_temporal: boolean;
|
|
303
307
|
is_partition_clone: boolean;
|
|
304
308
|
parent_constraint_schema: string | null;
|
|
305
309
|
parent_constraint_name: string | null;
|
|
@@ -42,6 +42,7 @@ const tableConstraintPropsSchema = z.object({
|
|
|
42
42
|
validated: z.boolean(),
|
|
43
43
|
is_local: z.boolean(),
|
|
44
44
|
no_inherit: z.boolean(),
|
|
45
|
+
is_temporal: z.boolean(),
|
|
45
46
|
is_partition_clone: z.boolean(),
|
|
46
47
|
parent_constraint_schema: z.string().nullable(),
|
|
47
48
|
parent_constraint_name: z.string().nullable(),
|
|
@@ -253,6 +254,7 @@ select
|
|
|
253
254
|
'validated', c.convalidated,
|
|
254
255
|
'is_local', c.conislocal,
|
|
255
256
|
'no_inherit', c.connoinherit,
|
|
257
|
+
'is_temporal', coalesce((to_jsonb(c)->>'conperiod')::boolean, false),
|
|
256
258
|
|
|
257
259
|
-- NEW: propagated-to-partition tagging (PG15+)
|
|
258
260
|
'is_partition_clone', (c.conparentid <> 0::oid),
|
|
@@ -294,6 +294,7 @@ const pkConstraint = {
|
|
|
294
294
|
validated: true,
|
|
295
295
|
is_local: true,
|
|
296
296
|
no_inherit: false,
|
|
297
|
+
is_temporal: false,
|
|
297
298
|
is_partition_clone: false,
|
|
298
299
|
parent_constraint_schema: null,
|
|
299
300
|
parent_constraint_name: null,
|
|
@@ -324,6 +325,7 @@ const uniqueConstraint = {
|
|
|
324
325
|
validated: true,
|
|
325
326
|
is_local: true,
|
|
326
327
|
no_inherit: false,
|
|
328
|
+
is_temporal: false,
|
|
327
329
|
is_partition_clone: false,
|
|
328
330
|
parent_constraint_schema: null,
|
|
329
331
|
parent_constraint_name: null,
|
|
@@ -353,6 +355,7 @@ const fkConstraint = {
|
|
|
353
355
|
validated: true,
|
|
354
356
|
is_local: true,
|
|
355
357
|
no_inherit: false,
|
|
358
|
+
is_temporal: false,
|
|
356
359
|
is_partition_clone: false,
|
|
357
360
|
parent_constraint_schema: null,
|
|
358
361
|
parent_constraint_name: null,
|
|
@@ -382,6 +385,7 @@ const checkConstraint = {
|
|
|
382
385
|
validated: true,
|
|
383
386
|
is_local: true,
|
|
384
387
|
no_inherit: true,
|
|
388
|
+
is_temporal: false,
|
|
385
389
|
is_partition_clone: false,
|
|
386
390
|
parent_constraint_schema: null,
|
|
387
391
|
parent_constraint_name: null,
|
|
@@ -692,6 +696,8 @@ const rlsPolicy = new RlsPolicy({
|
|
|
692
696
|
with_check_expression: null,
|
|
693
697
|
owner: "owner1",
|
|
694
698
|
comment: "rls policy comment",
|
|
699
|
+
referenced_relations: [],
|
|
700
|
+
referenced_procedures: [],
|
|
695
701
|
});
|
|
696
702
|
const rlsPolicyRestrictive = new RlsPolicy({
|
|
697
703
|
schema: "public",
|
|
@@ -704,6 +710,8 @@ const rlsPolicyRestrictive = new RlsPolicy({
|
|
|
704
710
|
with_check_expression: "status <> 'locked'",
|
|
705
711
|
owner: "owner1",
|
|
706
712
|
comment: null,
|
|
713
|
+
referenced_relations: [],
|
|
714
|
+
referenced_procedures: [],
|
|
707
715
|
});
|
|
708
716
|
const index = new Index({
|
|
709
717
|
schema: "public",
|
|
@@ -8,6 +8,13 @@ import type { Change } from "./change.types.ts";
|
|
|
8
8
|
* - If replace expansion added `DropTable(T)+CreateTable(T)`, targeted
|
|
9
9
|
* `AlterTableDropColumn(T.*)` / `AlterTableDropConstraint(T.*)` changes are
|
|
10
10
|
* redundant and create an unbreakable drop-phase cycle, so we elide them.
|
|
11
|
+
* - When the same `DropTable+CreateTable` pair is present, the expansion
|
|
12
|
+
* also emits one `AlterTableAddConstraint` / `AlterTableValidateConstraint`
|
|
13
|
+
* / `CreateCommentOnConstraint` per branch constraint, which may collide
|
|
14
|
+
* with the same change already emitted by `diffTables()` (for example on a
|
|
15
|
+
* shape flip or a new constraint). We dedupe these keeping only the last
|
|
16
|
+
* occurrence so the expansion's emission survives and the diffTables
|
|
17
|
+
* duplicate is removed.
|
|
11
18
|
* - If two dropped tables reference each other via FK, we insert dedicated
|
|
12
19
|
* `AlterTableDropConstraint` changes and teach the paired `DropTable`
|
|
13
20
|
* changes not to claim those FK stable IDs.
|