@supabase/pg-delta 1.0.0-alpha.18 → 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 +42 -0
- package/dist/core/objects/index/index.model.js +12 -2
- 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/plan/sql-format/fixtures.js +4 -0
- package/package.json +1 -1
- package/src/core/catalog.snapshot.test.ts +2 -0
- package/src/core/expand-replace-dependencies.ts +52 -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/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/plan/sql-format/fixtures.ts +4 -0
|
@@ -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";
|
|
@@ -222,6 +224,18 @@ function resolveObjectForStableId(stableId, mainCatalog, branchCatalog) {
|
|
|
222
224
|
const branch = branchCatalog.materializedViews[stableId];
|
|
223
225
|
return main && branch ? { kind: "materialized_view", main, branch } : null;
|
|
224
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
|
+
}
|
|
225
239
|
if (stableId.startsWith("procedure:")) {
|
|
226
240
|
const main = mainCatalog.procedures[stableId];
|
|
227
241
|
const branch = branchCatalog.procedures[stableId];
|
|
@@ -307,6 +321,34 @@ function buildReplaceChanges(resolved, options) {
|
|
|
307
321
|
? [new CreateMaterializedView({ materializedView: resolved.branch })]
|
|
308
322
|
: []),
|
|
309
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
|
+
];
|
|
310
352
|
case "procedure":
|
|
311
353
|
return [
|
|
312
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
|
}
|
|
@@ -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
|
|
@@ -696,6 +696,8 @@ const rlsPolicy = new RlsPolicy({
|
|
|
696
696
|
with_check_expression: null,
|
|
697
697
|
owner: "owner1",
|
|
698
698
|
comment: "rls policy comment",
|
|
699
|
+
referenced_relations: [],
|
|
700
|
+
referenced_procedures: [],
|
|
699
701
|
});
|
|
700
702
|
const rlsPolicyRestrictive = new RlsPolicy({
|
|
701
703
|
schema: "public",
|
|
@@ -708,6 +710,8 @@ const rlsPolicyRestrictive = new RlsPolicy({
|
|
|
708
710
|
with_check_expression: "status <> 'locked'",
|
|
709
711
|
owner: "owner1",
|
|
710
712
|
comment: null,
|
|
713
|
+
referenced_relations: [],
|
|
714
|
+
referenced_procedures: [],
|
|
711
715
|
});
|
|
712
716
|
const index = new Index({
|
|
713
717
|
schema: "public",
|
package/package.json
CHANGED
|
@@ -2,6 +2,8 @@ import type { Catalog } from "./catalog.model.ts";
|
|
|
2
2
|
import type { Change } from "./change.types.ts";
|
|
3
3
|
import { CreateDomain } from "./objects/domain/changes/domain.create.ts";
|
|
4
4
|
import { DropDomain } from "./objects/domain/changes/domain.drop.ts";
|
|
5
|
+
import { CreateIndex } from "./objects/index/changes/index.create.ts";
|
|
6
|
+
import { DropIndex } from "./objects/index/changes/index.drop.ts";
|
|
5
7
|
import { CreateMaterializedView } from "./objects/materialized-view/changes/materialized-view.create.ts";
|
|
6
8
|
import { DropMaterializedView } from "./objects/materialized-view/changes/materialized-view.drop.ts";
|
|
7
9
|
import { CreateProcedure } from "./objects/procedure/changes/procedure.create.ts";
|
|
@@ -34,6 +36,12 @@ type ResolvedObject =
|
|
|
34
36
|
main: Catalog["views"][string];
|
|
35
37
|
branch: Catalog["views"][string];
|
|
36
38
|
}
|
|
39
|
+
| {
|
|
40
|
+
kind: "index";
|
|
41
|
+
main: Catalog["indexes"][string];
|
|
42
|
+
branch: Catalog["indexes"][string];
|
|
43
|
+
branchIndexableObject: Catalog["indexableObjects"][string] | undefined;
|
|
44
|
+
}
|
|
37
45
|
| {
|
|
38
46
|
kind: "materialized_view";
|
|
39
47
|
main: Catalog["materializedViews"][string];
|
|
@@ -346,6 +354,20 @@ function resolveObjectForStableId(
|
|
|
346
354
|
return main && branch ? { kind: "materialized_view", main, branch } : null;
|
|
347
355
|
}
|
|
348
356
|
|
|
357
|
+
if (stableId.startsWith("index:")) {
|
|
358
|
+
const main = mainCatalog.indexes[stableId];
|
|
359
|
+
const branch = branchCatalog.indexes[stableId];
|
|
360
|
+
return main && branch
|
|
361
|
+
? {
|
|
362
|
+
kind: "index",
|
|
363
|
+
main,
|
|
364
|
+
branch,
|
|
365
|
+
branchIndexableObject:
|
|
366
|
+
branchCatalog.indexableObjects[branch.tableStableId],
|
|
367
|
+
}
|
|
368
|
+
: null;
|
|
369
|
+
}
|
|
370
|
+
|
|
349
371
|
if (stableId.startsWith("procedure:")) {
|
|
350
372
|
const main = mainCatalog.procedures[stableId];
|
|
351
373
|
const branch = branchCatalog.procedures[stableId];
|
|
@@ -447,6 +469,36 @@ function buildReplaceChanges(
|
|
|
447
469
|
? [new CreateMaterializedView({ materializedView: resolved.branch })]
|
|
448
470
|
: []),
|
|
449
471
|
];
|
|
472
|
+
case "index":
|
|
473
|
+
// Constraint-owned, primary, and partition-attached indexes are managed
|
|
474
|
+
// by the owning constraint or parent-index DDL, not standalone
|
|
475
|
+
// CREATE INDEX / DROP INDEX. The `case "table":` branch above already
|
|
476
|
+
// recreates constraints via AlterTableAddConstraint; emitting a
|
|
477
|
+
// standalone drop/create here would fail in PostgreSQL
|
|
478
|
+
// ("cannot drop index ... because constraint ... requires it") or
|
|
479
|
+
// duplicate the index the constraint recreates. Skip matches
|
|
480
|
+
// diffIndexes (packages/pg-delta/src/core/objects/index/index.diff.ts).
|
|
481
|
+
if (
|
|
482
|
+
resolved.main.is_owned_by_constraint ||
|
|
483
|
+
resolved.main.is_primary ||
|
|
484
|
+
resolved.main.is_index_partition ||
|
|
485
|
+
resolved.branch.is_owned_by_constraint ||
|
|
486
|
+
resolved.branch.is_primary ||
|
|
487
|
+
resolved.branch.is_index_partition
|
|
488
|
+
) {
|
|
489
|
+
return null;
|
|
490
|
+
}
|
|
491
|
+
return [
|
|
492
|
+
...(addDrop ? [new DropIndex({ index: resolved.main })] : []),
|
|
493
|
+
...(addCreate
|
|
494
|
+
? [
|
|
495
|
+
new CreateIndex({
|
|
496
|
+
index: resolved.branch,
|
|
497
|
+
indexableObject: resolved.branchIndexableObject,
|
|
498
|
+
}),
|
|
499
|
+
]
|
|
500
|
+
: []),
|
|
501
|
+
];
|
|
450
502
|
case "procedure":
|
|
451
503
|
return [
|
|
452
504
|
...(addDrop ? [new DropProcedure({ procedure: resolved.main })] : []),
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import type { Pool } from "pg";
|
|
3
|
+
import { extractIndexes, Index } from "./index.model.ts";
|
|
4
|
+
|
|
5
|
+
// Minimal fields required by indexPropsSchema; individual tests override the
|
|
6
|
+
// fields relevant to each scenario.
|
|
7
|
+
const baseRow = {
|
|
8
|
+
schema: "public",
|
|
9
|
+
table_name: '"users"',
|
|
10
|
+
storage_params: [] as string[],
|
|
11
|
+
statistics_target: [] as number[],
|
|
12
|
+
index_type: "btree",
|
|
13
|
+
tablespace: null,
|
|
14
|
+
is_unique: false,
|
|
15
|
+
is_primary: false,
|
|
16
|
+
is_exclusion: false,
|
|
17
|
+
nulls_not_distinct: false,
|
|
18
|
+
immediate: true,
|
|
19
|
+
is_clustered: false,
|
|
20
|
+
is_replica_identity: false,
|
|
21
|
+
key_columns: [1],
|
|
22
|
+
column_collations: [null],
|
|
23
|
+
operator_classes: ["default"],
|
|
24
|
+
column_options: [0],
|
|
25
|
+
index_expressions: null,
|
|
26
|
+
partial_predicate: null,
|
|
27
|
+
is_owned_by_constraint: false,
|
|
28
|
+
table_relkind: "r" as const,
|
|
29
|
+
is_partitioned_index: false,
|
|
30
|
+
is_index_partition: false,
|
|
31
|
+
parent_index_name: null,
|
|
32
|
+
comment: null,
|
|
33
|
+
owner: "postgres",
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const mockPool = (rows: unknown[]): Pool =>
|
|
37
|
+
({ query: async () => ({ rows }) }) as unknown as Pool;
|
|
38
|
+
|
|
39
|
+
describe("extractIndexes", () => {
|
|
40
|
+
test("skips rows where pg_get_indexdef returned NULL", async () => {
|
|
41
|
+
const indexes = await extractIndexes(
|
|
42
|
+
mockPool([
|
|
43
|
+
{
|
|
44
|
+
...baseRow,
|
|
45
|
+
name: '"good_idx"',
|
|
46
|
+
definition: "CREATE INDEX good_idx ON users (id)",
|
|
47
|
+
},
|
|
48
|
+
{ ...baseRow, name: '"orphan_idx"', definition: null },
|
|
49
|
+
]),
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
expect(indexes).toHaveLength(1);
|
|
53
|
+
expect(indexes[0]).toBeInstanceOf(Index);
|
|
54
|
+
expect(indexes[0]?.name).toBe('"good_idx"');
|
|
55
|
+
expect(indexes[0]?.definition).toBe("CREATE INDEX good_idx ON users (id)");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("does not throw ZodError when the only row has a null definition", async () => {
|
|
59
|
+
await expect(
|
|
60
|
+
extractIndexes(
|
|
61
|
+
mockPool([{ ...baseRow, name: '"orphan"', definition: null }]),
|
|
62
|
+
),
|
|
63
|
+
).resolves.toEqual([]);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("returns all indexes when every row has a valid definition", async () => {
|
|
67
|
+
const indexes = await extractIndexes(
|
|
68
|
+
mockPool([
|
|
69
|
+
{
|
|
70
|
+
...baseRow,
|
|
71
|
+
name: '"a"',
|
|
72
|
+
definition: "CREATE INDEX a ON users (id)",
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
...baseRow,
|
|
76
|
+
name: '"b"',
|
|
77
|
+
definition: "CREATE INDEX b ON users (id)",
|
|
78
|
+
},
|
|
79
|
+
]),
|
|
80
|
+
);
|
|
81
|
+
expect(indexes.map((i) => i.name)).toEqual(['"a"', '"b"']);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -40,6 +40,16 @@ const indexPropsSchema = z.object({
|
|
|
40
40
|
owner: z.string(),
|
|
41
41
|
});
|
|
42
42
|
|
|
43
|
+
// pg_get_indexdef(oid, colno, pretty) invokes pg_get_indexdef_worker with
|
|
44
|
+
// missing_ok = true, so it can return NULL when any internal system-cache lookup
|
|
45
|
+
// fails (race with concurrent DROP, role visibility edge cases, orphaned index
|
|
46
|
+
// metadata, recovery transients). An unreadable index cannot be diffed, so we
|
|
47
|
+
// accept NULL here and filter the row out with a debug log instead of crashing
|
|
48
|
+
// the whole catalog extraction.
|
|
49
|
+
const indexRowSchema = indexPropsSchema.extend({
|
|
50
|
+
definition: z.string().nullable(),
|
|
51
|
+
});
|
|
52
|
+
|
|
43
53
|
/**
|
|
44
54
|
* All properties exposed by CREATE INDEX statement are included in diff output.
|
|
45
55
|
* https://www.postgresql.org/docs/current/sql-createindex.html
|
|
@@ -362,9 +372,8 @@ export async function extractIndexes(pool: Pool): Promise<Index[]> {
|
|
|
362
372
|
|
|
363
373
|
order by 1, 2
|
|
364
374
|
`);
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
);
|
|
375
|
+
const validatedRows = indexRows
|
|
376
|
+
.map((row: unknown) => indexRowSchema.parse(row))
|
|
377
|
+
.filter((row): row is IndexProps => row.definition !== null);
|
|
369
378
|
return validatedRows.map((row: IndexProps) => new Index(row));
|
|
370
379
|
}
|
|
@@ -23,6 +23,8 @@ describe.concurrent("rls-policy", () => {
|
|
|
23
23
|
with_check_expression: null,
|
|
24
24
|
owner: "owner",
|
|
25
25
|
comment: null,
|
|
26
|
+
referenced_relations: [],
|
|
27
|
+
referenced_procedures: [],
|
|
26
28
|
};
|
|
27
29
|
const policy = new RlsPolicy({
|
|
28
30
|
...props,
|
|
@@ -52,6 +54,8 @@ describe.concurrent("rls-policy", () => {
|
|
|
52
54
|
with_check_expression: null,
|
|
53
55
|
owner: "owner",
|
|
54
56
|
comment: null,
|
|
57
|
+
referenced_relations: [],
|
|
58
|
+
referenced_procedures: [],
|
|
55
59
|
};
|
|
56
60
|
const policy = new RlsPolicy({
|
|
57
61
|
...props,
|
|
@@ -81,6 +85,8 @@ describe.concurrent("rls-policy", () => {
|
|
|
81
85
|
with_check_expression: null,
|
|
82
86
|
owner: "owner",
|
|
83
87
|
comment: null,
|
|
88
|
+
referenced_relations: [],
|
|
89
|
+
referenced_procedures: [],
|
|
84
90
|
};
|
|
85
91
|
const main = new RlsPolicy({
|
|
86
92
|
...props,
|
|
@@ -120,6 +126,8 @@ describe.concurrent("rls-policy", () => {
|
|
|
120
126
|
with_check_expression: null,
|
|
121
127
|
owner: "owner",
|
|
122
128
|
comment: null,
|
|
129
|
+
referenced_relations: [],
|
|
130
|
+
referenced_procedures: [],
|
|
123
131
|
};
|
|
124
132
|
const main = new RlsPolicy({
|
|
125
133
|
...props,
|
|
@@ -159,6 +167,8 @@ describe.concurrent("rls-policy", () => {
|
|
|
159
167
|
with_check_expression: null,
|
|
160
168
|
owner: "test",
|
|
161
169
|
comment: null,
|
|
170
|
+
referenced_relations: [],
|
|
171
|
+
referenced_procedures: [],
|
|
162
172
|
};
|
|
163
173
|
const policy = new RlsPolicy({
|
|
164
174
|
...props,
|
|
@@ -188,6 +198,8 @@ describe.concurrent("rls-policy", () => {
|
|
|
188
198
|
with_check_expression: null,
|
|
189
199
|
owner: "test",
|
|
190
200
|
comment: null,
|
|
201
|
+
referenced_relations: [],
|
|
202
|
+
referenced_procedures: [],
|
|
191
203
|
};
|
|
192
204
|
const policy = new RlsPolicy({
|
|
193
205
|
...props,
|
|
@@ -217,6 +229,8 @@ describe.concurrent("rls-policy", () => {
|
|
|
217
229
|
using_expression: "expr",
|
|
218
230
|
owner: "test",
|
|
219
231
|
comment: null,
|
|
232
|
+
referenced_relations: [],
|
|
233
|
+
referenced_procedures: [],
|
|
220
234
|
};
|
|
221
235
|
const policy = new RlsPolicy({
|
|
222
236
|
...props,
|
|
@@ -246,6 +260,8 @@ describe.concurrent("rls-policy", () => {
|
|
|
246
260
|
using_expression: "expr",
|
|
247
261
|
owner: "test",
|
|
248
262
|
comment: null,
|
|
263
|
+
referenced_relations: [],
|
|
264
|
+
referenced_procedures: [],
|
|
249
265
|
};
|
|
250
266
|
const policy = new RlsPolicy({
|
|
251
267
|
...props,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
import { assertValidSql } from "../../../test-utils/assert-valid-sql.ts";
|
|
3
|
+
import { stableId } from "../../utils.ts";
|
|
3
4
|
import { RlsPolicy } from "../rls-policy.model.ts";
|
|
4
5
|
import { CreateRlsPolicy } from "./rls-policy.create.ts";
|
|
5
6
|
|
|
@@ -16,6 +17,8 @@ describe("rls-policy", () => {
|
|
|
16
17
|
with_check_expression: null,
|
|
17
18
|
owner: "test",
|
|
18
19
|
comment: null,
|
|
20
|
+
referenced_relations: [],
|
|
21
|
+
referenced_procedures: [],
|
|
19
22
|
});
|
|
20
23
|
|
|
21
24
|
const change = new CreateRlsPolicy({
|
|
@@ -41,6 +44,8 @@ describe("rls-policy", () => {
|
|
|
41
44
|
with_check_expression: null,
|
|
42
45
|
owner: "test",
|
|
43
46
|
comment: null,
|
|
47
|
+
referenced_relations: [],
|
|
48
|
+
referenced_procedures: [],
|
|
44
49
|
});
|
|
45
50
|
|
|
46
51
|
const change = new CreateRlsPolicy({
|
|
@@ -66,6 +71,8 @@ describe("rls-policy", () => {
|
|
|
66
71
|
with_check_expression: "expr2",
|
|
67
72
|
owner: "test",
|
|
68
73
|
comment: null,
|
|
74
|
+
referenced_relations: [],
|
|
75
|
+
referenced_procedures: [],
|
|
69
76
|
});
|
|
70
77
|
|
|
71
78
|
const change = new CreateRlsPolicy({
|
|
@@ -78,4 +85,125 @@ describe("rls-policy", () => {
|
|
|
78
85
|
"CREATE POLICY test_policy_all ON public.test_table AS RESTRICTIVE FOR UPDATE TO role1, role2 USING (expr1) WITH CHECK (expr2)",
|
|
79
86
|
);
|
|
80
87
|
});
|
|
88
|
+
|
|
89
|
+
test("requires referenced relations reported by pg_depend", () => {
|
|
90
|
+
const policy = new RlsPolicy({
|
|
91
|
+
schema: "app",
|
|
92
|
+
name: "cross_relation_policy",
|
|
93
|
+
table_name: "accounts",
|
|
94
|
+
command: "r",
|
|
95
|
+
permissive: true,
|
|
96
|
+
roles: ["public"],
|
|
97
|
+
using_expression:
|
|
98
|
+
"(EXISTS (SELECT 1 FROM app.users) AND EXISTS (SELECT 1 FROM app.active_accounts))",
|
|
99
|
+
with_check_expression:
|
|
100
|
+
"(id IN (SELECT account_id FROM app.memberships WHERE active))",
|
|
101
|
+
owner: "test",
|
|
102
|
+
comment: null,
|
|
103
|
+
referenced_relations: [
|
|
104
|
+
{ kind: "table", schema: "app", name: "users" },
|
|
105
|
+
{ kind: "table", schema: "app", name: "memberships" },
|
|
106
|
+
{ kind: "view", schema: "app", name: "active_accounts" },
|
|
107
|
+
{ kind: "materialized_view", schema: "app", name: "account_stats" },
|
|
108
|
+
{ kind: "foreign_table", schema: "app", name: "remote_profiles" },
|
|
109
|
+
],
|
|
110
|
+
referenced_procedures: [],
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
const change = new CreateRlsPolicy({ policy });
|
|
114
|
+
|
|
115
|
+
expect(change.requires).toContain(stableId.table("app", "users"));
|
|
116
|
+
expect(change.requires).toContain(stableId.table("app", "memberships"));
|
|
117
|
+
expect(change.requires).toContain(stableId.view("app", "active_accounts"));
|
|
118
|
+
expect(change.requires).toContain(
|
|
119
|
+
stableId.materializedView("app", "account_stats"),
|
|
120
|
+
);
|
|
121
|
+
expect(change.requires).toContain(
|
|
122
|
+
stableId.foreignTable("app", "remote_profiles"),
|
|
123
|
+
);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("requires referenced procedures reported by pg_depend", () => {
|
|
127
|
+
const policy = new RlsPolicy({
|
|
128
|
+
schema: "app",
|
|
129
|
+
name: "function_guarded_policy",
|
|
130
|
+
table_name: "accounts",
|
|
131
|
+
command: "r",
|
|
132
|
+
permissive: true,
|
|
133
|
+
roles: ["public"],
|
|
134
|
+
using_expression: "public.is_admin()",
|
|
135
|
+
with_check_expression: null,
|
|
136
|
+
owner: "test",
|
|
137
|
+
comment: null,
|
|
138
|
+
referenced_relations: [],
|
|
139
|
+
referenced_procedures: [
|
|
140
|
+
{ schema: "public", name: "is_admin", argument_types: [] },
|
|
141
|
+
{
|
|
142
|
+
schema: "public",
|
|
143
|
+
name: "has_role",
|
|
144
|
+
argument_types: ["text", "integer"],
|
|
145
|
+
},
|
|
146
|
+
],
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
const change = new CreateRlsPolicy({ policy });
|
|
150
|
+
|
|
151
|
+
expect(change.requires).toContain(stableId.procedure("public", "is_admin"));
|
|
152
|
+
expect(change.requires).toContain(
|
|
153
|
+
stableId.procedure("public", "has_role", "text,integer"),
|
|
154
|
+
);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test("does not require additional objects when referenced lists are empty", () => {
|
|
158
|
+
const policy = new RlsPolicy({
|
|
159
|
+
schema: "app",
|
|
160
|
+
name: "simple_policy",
|
|
161
|
+
table_name: "accounts",
|
|
162
|
+
command: "*",
|
|
163
|
+
permissive: true,
|
|
164
|
+
roles: [],
|
|
165
|
+
using_expression: null,
|
|
166
|
+
with_check_expression: null,
|
|
167
|
+
owner: "test",
|
|
168
|
+
comment: null,
|
|
169
|
+
referenced_relations: [],
|
|
170
|
+
referenced_procedures: [],
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
const change = new CreateRlsPolicy({ policy });
|
|
174
|
+
|
|
175
|
+
expect(change.requires).toEqual([
|
|
176
|
+
stableId.schema("app"),
|
|
177
|
+
stableId.table("app", "accounts"),
|
|
178
|
+
stableId.role("test"),
|
|
179
|
+
]);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// Sequences referenced via nextval() are a known gap. pg_depend only
|
|
183
|
+
// records the sequence edge when the argument is written as a regclass
|
|
184
|
+
// literal (e.g. `nextval('app.seq'::regclass)`); bare string literals
|
|
185
|
+
// produce no pg_depend row. Tracked in
|
|
186
|
+
// https://github.com/supabase/pg-toolbelt/issues/220.
|
|
187
|
+
test.skip("requires referenced sequences (follow-up)", () => {
|
|
188
|
+
const policy = new RlsPolicy({
|
|
189
|
+
schema: "app",
|
|
190
|
+
name: "sequence_policy",
|
|
191
|
+
table_name: "accounts",
|
|
192
|
+
command: "r",
|
|
193
|
+
permissive: true,
|
|
194
|
+
roles: ["public"],
|
|
195
|
+
using_expression: "id < nextval('app.next_id'::regclass)",
|
|
196
|
+
with_check_expression: null,
|
|
197
|
+
owner: "test",
|
|
198
|
+
comment: null,
|
|
199
|
+
referenced_relations: [],
|
|
200
|
+
referenced_procedures: [],
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
const change = new CreateRlsPolicy({ policy });
|
|
204
|
+
|
|
205
|
+
// Expected once the gap is closed:
|
|
206
|
+
// expect(change.requires).toContain(stableId.sequence("app", "next_id"));
|
|
207
|
+
expect(change.requires.length).toBeGreaterThan(0);
|
|
208
|
+
});
|
|
81
209
|
});
|
|
@@ -45,6 +45,33 @@ export class CreateRlsPolicy extends CreateRlsPolicyChange {
|
|
|
45
45
|
// Owner dependency
|
|
46
46
|
dependencies.add(stableId.role(this.policy.owner));
|
|
47
47
|
|
|
48
|
+
// Relations and functions referenced inside USING / WITH CHECK
|
|
49
|
+
// expressions must exist before the policy is created. These come from
|
|
50
|
+
// pg_depend (populated by PostgreSQL's recordDependencyOnExpr at policy
|
|
51
|
+
// creation), not from re-parsing the expression text.
|
|
52
|
+
for (const ref of this.policy.referenced_relations) {
|
|
53
|
+
switch (ref.kind) {
|
|
54
|
+
case "table":
|
|
55
|
+
dependencies.add(stableId.table(ref.schema, ref.name));
|
|
56
|
+
break;
|
|
57
|
+
case "view":
|
|
58
|
+
dependencies.add(stableId.view(ref.schema, ref.name));
|
|
59
|
+
break;
|
|
60
|
+
case "materialized_view":
|
|
61
|
+
dependencies.add(stableId.materializedView(ref.schema, ref.name));
|
|
62
|
+
break;
|
|
63
|
+
case "foreign_table":
|
|
64
|
+
dependencies.add(stableId.foreignTable(ref.schema, ref.name));
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
for (const ref of this.policy.referenced_procedures) {
|
|
70
|
+
dependencies.add(
|
|
71
|
+
stableId.procedure(ref.schema, ref.name, ref.argument_types.join(",")),
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
48
75
|
return Array.from(dependencies);
|
|
49
76
|
}
|
|
50
77
|
|
|
@@ -11,6 +11,33 @@ const RlsPolicyCommandSchema = z.enum([
|
|
|
11
11
|
"*", // ALL commands
|
|
12
12
|
]);
|
|
13
13
|
|
|
14
|
+
const RlsPolicyReferencedRelationKindSchema = z.enum([
|
|
15
|
+
"table",
|
|
16
|
+
"view",
|
|
17
|
+
"materialized_view",
|
|
18
|
+
"foreign_table",
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
const rlsPolicyReferencedRelationSchema = z.object({
|
|
22
|
+
kind: RlsPolicyReferencedRelationKindSchema,
|
|
23
|
+
schema: z.string(),
|
|
24
|
+
name: z.string(),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
export type RlsPolicyReferencedRelation = z.infer<
|
|
28
|
+
typeof rlsPolicyReferencedRelationSchema
|
|
29
|
+
>;
|
|
30
|
+
|
|
31
|
+
const rlsPolicyReferencedProcedureSchema = z.object({
|
|
32
|
+
schema: z.string(),
|
|
33
|
+
name: z.string(),
|
|
34
|
+
argument_types: z.array(z.string()),
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
export type RlsPolicyReferencedProcedure = z.infer<
|
|
38
|
+
typeof rlsPolicyReferencedProcedureSchema
|
|
39
|
+
>;
|
|
40
|
+
|
|
14
41
|
const rlsPolicyPropsSchema = z.object({
|
|
15
42
|
schema: z.string(),
|
|
16
43
|
name: z.string(),
|
|
@@ -22,6 +49,8 @@ const rlsPolicyPropsSchema = z.object({
|
|
|
22
49
|
with_check_expression: z.string().nullable(),
|
|
23
50
|
owner: z.string(),
|
|
24
51
|
comment: z.string().nullable(),
|
|
52
|
+
referenced_relations: z.array(rlsPolicyReferencedRelationSchema),
|
|
53
|
+
referenced_procedures: z.array(rlsPolicyReferencedProcedureSchema),
|
|
25
54
|
});
|
|
26
55
|
|
|
27
56
|
export type RlsPolicyProps = z.infer<typeof rlsPolicyPropsSchema>;
|
|
@@ -37,6 +66,23 @@ export class RlsPolicy extends BasePgModel {
|
|
|
37
66
|
public readonly with_check_expression: RlsPolicyProps["with_check_expression"];
|
|
38
67
|
public readonly owner: RlsPolicyProps["owner"];
|
|
39
68
|
public readonly comment: RlsPolicyProps["comment"];
|
|
69
|
+
/**
|
|
70
|
+
* Tables / views / materialized views / foreign tables that
|
|
71
|
+
* `using_expression` / `with_check_expression` reference, sourced from
|
|
72
|
+
* `pg_depend` (`recordDependencyOnExpr` at policy creation). Drives
|
|
73
|
+
* ordering dependencies in `CreateRlsPolicy.requires`. Intentionally
|
|
74
|
+
* excluded from `dataFields` — it's derived from the expression text
|
|
75
|
+
* and changes lockstep with it.
|
|
76
|
+
*/
|
|
77
|
+
public readonly referenced_relations: RlsPolicyProps["referenced_relations"];
|
|
78
|
+
/**
|
|
79
|
+
* Functions / procedures that `using_expression` / `with_check_expression`
|
|
80
|
+
* reference, sourced from `pg_depend` (refclassid = `pg_proc`). The
|
|
81
|
+
* argument-type signature comes straight from `pg_proc.proargtypes` via
|
|
82
|
+
* `format_type`, so it matches the signature the procedure extractor
|
|
83
|
+
* embeds in `stableId.procedure(...)`. Not part of `dataFields`.
|
|
84
|
+
*/
|
|
85
|
+
public readonly referenced_procedures: RlsPolicyProps["referenced_procedures"];
|
|
40
86
|
|
|
41
87
|
constructor(props: RlsPolicyProps) {
|
|
42
88
|
super();
|
|
@@ -54,6 +100,10 @@ export class RlsPolicy extends BasePgModel {
|
|
|
54
100
|
this.with_check_expression = props.with_check_expression;
|
|
55
101
|
this.owner = props.owner;
|
|
56
102
|
this.comment = props.comment;
|
|
103
|
+
|
|
104
|
+
// Derived metadata (not part of equality)
|
|
105
|
+
this.referenced_relations = props.referenced_relations;
|
|
106
|
+
this.referenced_procedures = props.referenced_procedures;
|
|
57
107
|
}
|
|
58
108
|
|
|
59
109
|
get stableId(): `rlsPolicy:${string}` {
|
|
@@ -101,6 +151,59 @@ extension_table_oids as (
|
|
|
101
151
|
d.refclassid = 'pg_extension'::regclass
|
|
102
152
|
and d.classid = 'pg_class'::regclass
|
|
103
153
|
and d.deptype = 'e'
|
|
154
|
+
),
|
|
155
|
+
policy_relation_deps as (
|
|
156
|
+
-- Relations referenced inside polqual / polwithcheck. PostgreSQL records
|
|
157
|
+
-- these via recordDependencyOnExpr(..., DEPENDENCY_NORMAL = 'n') at
|
|
158
|
+
-- CREATE POLICY time, so pg_depend is authoritative and we don't need to
|
|
159
|
+
-- re-parse the expression text. Covers regular tables, partitioned
|
|
160
|
+
-- tables, views, materialized views, and foreign tables — any relation
|
|
161
|
+
-- kind the policy can reference in a subquery.
|
|
162
|
+
select distinct
|
|
163
|
+
d.objid as policy_oid,
|
|
164
|
+
case ref_c.relkind
|
|
165
|
+
when 'r' then 'table'
|
|
166
|
+
when 'p' then 'table'
|
|
167
|
+
when 'v' then 'view'
|
|
168
|
+
when 'm' then 'materialized_view'
|
|
169
|
+
when 'f' then 'foreign_table'
|
|
170
|
+
end as ref_kind,
|
|
171
|
+
ref_ns.nspname as ref_schema,
|
|
172
|
+
ref_c.relname as ref_name
|
|
173
|
+
from
|
|
174
|
+
pg_depend d
|
|
175
|
+
join pg_policy p on p.oid = d.objid
|
|
176
|
+
join pg_class ref_c on ref_c.oid = d.refobjid
|
|
177
|
+
join pg_namespace ref_ns on ref_ns.oid = ref_c.relnamespace
|
|
178
|
+
where
|
|
179
|
+
d.classid = 'pg_policy'::regclass
|
|
180
|
+
and d.refclassid = 'pg_class'::regclass
|
|
181
|
+
and d.deptype = 'n'
|
|
182
|
+
and ref_c.relkind in ('r', 'p', 'v', 'm', 'f')
|
|
183
|
+
and d.refobjid <> p.polrelid
|
|
184
|
+
),
|
|
185
|
+
policy_procedure_deps as (
|
|
186
|
+
-- Functions / procedures referenced inside polqual / polwithcheck. Same
|
|
187
|
+
-- pg_depend mechanism as above, just refclassid = pg_proc. proargtypes
|
|
188
|
+
-- formatted via format_type(oid, null) matches the signature produced by
|
|
189
|
+
-- the procedure extractor (see procedure.model.ts), so stableId.procedure
|
|
190
|
+
-- on both sides of the diff lines up exactly.
|
|
191
|
+
select distinct
|
|
192
|
+
d.objid as policy_oid,
|
|
193
|
+
ref_ns.nspname as ref_schema,
|
|
194
|
+
ref_p.proname as ref_name,
|
|
195
|
+
array(
|
|
196
|
+
select format_type(oid, null)
|
|
197
|
+
from unnest(ref_p.proargtypes) as oid
|
|
198
|
+
) as ref_argument_types
|
|
199
|
+
from
|
|
200
|
+
pg_depend d
|
|
201
|
+
join pg_proc ref_p on ref_p.oid = d.refobjid
|
|
202
|
+
join pg_namespace ref_ns on ref_ns.oid = ref_p.pronamespace
|
|
203
|
+
where
|
|
204
|
+
d.classid = 'pg_policy'::regclass
|
|
205
|
+
and d.refclassid = 'pg_proc'::regclass
|
|
206
|
+
and d.deptype = 'n'
|
|
104
207
|
)
|
|
105
208
|
select
|
|
106
209
|
tc.relnamespace::regnamespace::text as schema,
|
|
@@ -120,7 +223,37 @@ select
|
|
|
120
223
|
pg_get_expr(p.polqual, p.polrelid) as using_expression,
|
|
121
224
|
pg_get_expr(p.polwithcheck, p.polrelid) as with_check_expression,
|
|
122
225
|
tc.relowner::regrole::text as owner,
|
|
123
|
-
obj_description(p.oid, 'pg_policy') as comment
|
|
226
|
+
obj_description(p.oid, 'pg_policy') as comment,
|
|
227
|
+
coalesce(
|
|
228
|
+
(
|
|
229
|
+
select json_agg(
|
|
230
|
+
json_build_object(
|
|
231
|
+
'kind', prd.ref_kind,
|
|
232
|
+
'schema', prd.ref_schema,
|
|
233
|
+
'name', prd.ref_name
|
|
234
|
+
)
|
|
235
|
+
order by prd.ref_schema, prd.ref_name
|
|
236
|
+
)
|
|
237
|
+
from policy_relation_deps prd
|
|
238
|
+
where prd.policy_oid = p.oid
|
|
239
|
+
),
|
|
240
|
+
'[]'
|
|
241
|
+
) as referenced_relations,
|
|
242
|
+
coalesce(
|
|
243
|
+
(
|
|
244
|
+
select json_agg(
|
|
245
|
+
json_build_object(
|
|
246
|
+
'schema', ppd.ref_schema,
|
|
247
|
+
'name', ppd.ref_name,
|
|
248
|
+
'argument_types', ppd.ref_argument_types
|
|
249
|
+
)
|
|
250
|
+
order by ppd.ref_schema, ppd.ref_name, ppd.ref_argument_types
|
|
251
|
+
)
|
|
252
|
+
from policy_procedure_deps ppd
|
|
253
|
+
where ppd.policy_oid = p.oid
|
|
254
|
+
),
|
|
255
|
+
'[]'
|
|
256
|
+
) as referenced_procedures
|
|
124
257
|
from
|
|
125
258
|
pg_catalog.pg_policy p
|
|
126
259
|
inner join pg_catalog.pg_class tc on tc.oid = p.polrelid
|
|
@@ -1000,6 +1000,8 @@ const rlsPolicy = new RlsPolicy({
|
|
|
1000
1000
|
with_check_expression: null,
|
|
1001
1001
|
owner: "owner1",
|
|
1002
1002
|
comment: "rls policy comment",
|
|
1003
|
+
referenced_relations: [],
|
|
1004
|
+
referenced_procedures: [],
|
|
1003
1005
|
});
|
|
1004
1006
|
|
|
1005
1007
|
const rlsPolicyRestrictive = new RlsPolicy({
|
|
@@ -1013,6 +1015,8 @@ const rlsPolicyRestrictive = new RlsPolicy({
|
|
|
1013
1015
|
with_check_expression: "status <> 'locked'",
|
|
1014
1016
|
owner: "owner1",
|
|
1015
1017
|
comment: null,
|
|
1018
|
+
referenced_relations: [],
|
|
1019
|
+
referenced_procedures: [],
|
|
1016
1020
|
});
|
|
1017
1021
|
|
|
1018
1022
|
const index = new Index({
|