@supabase/pg-delta 1.0.0-alpha.26 → 1.0.0-alpha.28
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/cli/commands/catalog-export.js +22 -1
- package/dist/core/catalog.diff.js +22 -3
- package/dist/core/catalog.filter.d.ts +17 -0
- package/dist/core/catalog.filter.js +75 -0
- package/dist/core/catalog.model.js +7 -1
- package/dist/core/expand-replace-dependencies.d.ts +3 -1
- package/dist/core/expand-replace-dependencies.js +117 -7
- package/dist/core/integrations/supabase.js +102 -11
- package/dist/core/objects/base.change.d.ts +12 -0
- package/dist/core/objects/base.change.js +14 -0
- package/dist/core/objects/foreign-data-wrapper/foreign-table/foreign-table.model.d.ts +4 -0
- package/dist/core/objects/foreign-data-wrapper/foreign-table/foreign-table.model.js +28 -2
- package/dist/core/objects/foreign-data-wrapper/server/server.model.d.ts +4 -0
- package/dist/core/objects/foreign-data-wrapper/server/server.model.js +18 -1
- package/dist/core/objects/foreign-data-wrapper/user-mapping/user-mapping.model.d.ts +4 -0
- package/dist/core/objects/foreign-data-wrapper/user-mapping/user-mapping.model.js +18 -1
- package/dist/core/objects/materialized-view/materialized-view.diff.d.ts +1 -0
- package/dist/core/objects/materialized-view/materialized-view.diff.js +59 -59
- package/dist/core/objects/table/changes/table.alter.d.ts +1 -0
- package/dist/core/objects/table/changes/table.alter.js +8 -0
- package/dist/core/objects/view/view.diff.d.ts +1 -0
- package/dist/core/objects/view/view.diff.js +35 -34
- package/dist/core/sort/graph-builder.js +6 -0
- package/package.json +1 -1
- package/src/cli/commands/catalog-export.ts +26 -1
- package/src/core/catalog.diff.test.ts +173 -0
- package/src/core/catalog.diff.ts +24 -3
- package/src/core/catalog.filter.ts +96 -0
- package/src/core/catalog.model.ts +10 -2
- package/src/core/expand-replace-dependencies.test.ts +282 -0
- package/src/core/expand-replace-dependencies.ts +165 -7
- package/src/core/integrations/supabase.test.ts +335 -0
- package/src/core/integrations/supabase.ts +102 -11
- package/src/core/objects/base.change.ts +15 -0
- package/src/core/objects/foreign-data-wrapper/foreign-table/foreign-table.model.ts +28 -2
- package/src/core/objects/foreign-data-wrapper/server/server.model.ts +18 -1
- package/src/core/objects/foreign-data-wrapper/user-mapping/user-mapping.model.ts +18 -1
- package/src/core/objects/materialized-view/materialized-view.diff.test.ts +3 -2
- package/src/core/objects/materialized-view/materialized-view.diff.ts +99 -92
- package/src/core/objects/table/changes/table.alter.ts +9 -0
- package/src/core/objects/view/view.diff.ts +67 -60
- package/src/core/sort/graph-builder.ts +6 -0
- package/src/core/sort/sort-changes.test.ts +73 -1
|
@@ -125,6 +125,30 @@ export const supabase: IntegrationDSL = {
|
|
|
125
125
|
"trigger/function_schema": [...SUPABASE_SYSTEM_SCHEMAS],
|
|
126
126
|
},
|
|
127
127
|
},
|
|
128
|
+
// Defensive fallback for dynamically-created pgmq queue /
|
|
129
|
+
// archive tables. `pgmq.q_<name>` and `pgmq.a_<name>` are
|
|
130
|
+
// materialized by `select pgmq.create('<name>')`, NOT by
|
|
131
|
+
// `CREATE EXTENSION pgmq`, so emitting a user trigger against
|
|
132
|
+
// them fails locally with
|
|
133
|
+
// `relation "pgmq.q_<name>" does not exist`. On a healthy
|
|
134
|
+
// install the trigger extractor's `extension_table_oids` join
|
|
135
|
+
// (packages/pg-delta/src/core/objects/trigger/trigger.model.ts)
|
|
136
|
+
// already drops these via the `pg_depend deptype='e'` row pgmq
|
|
137
|
+
// records during `pgmq.create()`; this rule covers projects
|
|
138
|
+
// where that row is missing (older pgmq, manual table
|
|
139
|
+
// rewrites, `pg_dump`/restore that loses extension deps, ...).
|
|
140
|
+
// pgmq 1.4.4 — the version Supabase Cloud currently ships —
|
|
141
|
+
// does not record the dependency at all.
|
|
142
|
+
{
|
|
143
|
+
not: {
|
|
144
|
+
and: [
|
|
145
|
+
{ "trigger/schema": "pgmq" },
|
|
146
|
+
{
|
|
147
|
+
"trigger/table_name": { op: "regex", value: "^[qa]_" },
|
|
148
|
+
},
|
|
149
|
+
],
|
|
150
|
+
},
|
|
151
|
+
},
|
|
128
152
|
],
|
|
129
153
|
},
|
|
130
154
|
// Exclude system objects
|
|
@@ -185,15 +209,25 @@ export const supabase: IntegrationDSL = {
|
|
|
185
209
|
],
|
|
186
210
|
},
|
|
187
211
|
// Platform-managed foreign data wrappers — Wasm-based FDWs
|
|
188
|
-
// (e.g. `clerk`, `clerk_oauth`)
|
|
189
|
-
//
|
|
190
|
-
//
|
|
191
|
-
//
|
|
192
|
-
//
|
|
193
|
-
//
|
|
194
|
-
//
|
|
195
|
-
//
|
|
196
|
-
//
|
|
212
|
+
// (e.g. `clerk`, `clerk_oauth`) provisioned via the `wrappers`
|
|
213
|
+
// extension. Supabase Cloud creates these as
|
|
214
|
+
// `CREATE FOREIGN DATA WRAPPER clerk_oauth HANDLER
|
|
215
|
+
// extensions.wasm_fdw_handler VALIDATOR
|
|
216
|
+
// extensions.wasm_fdw_validator` at project creation; replaying
|
|
217
|
+
// the DDL against a local image fails because the local
|
|
218
|
+
// environment has no equivalent pre-step. We can't rely on the
|
|
219
|
+
// FDW owner alone — after a dump/restore the owner is often
|
|
220
|
+
// rewritten away from `supabase_admin` — so match on the shared
|
|
221
|
+
// Wasm handler/validator (`extensions.wasm_fdw_handler` /
|
|
222
|
+
// `extensions.wasm_fdw_validator`) instead.
|
|
223
|
+
//
|
|
224
|
+
// Matching the bare `extensions.*` namespace would be too broad:
|
|
225
|
+
// contrib FDWs like `postgres_fdw` also install their
|
|
226
|
+
// handler/validator into `extensions` on Supabase, and those ARE
|
|
227
|
+
// available in the local image, so a user-created `postgres_fdw`
|
|
228
|
+
// wrapper (and its servers/foreign tables/user mappings) must
|
|
229
|
+
// still roundtrip. Keying on the `wasm_fdw_*` function names
|
|
230
|
+
// targets only the platform Wasm wrappers.
|
|
197
231
|
{
|
|
198
232
|
and: [
|
|
199
233
|
{ objectType: "foreign_data_wrapper" },
|
|
@@ -202,13 +236,70 @@ export const supabase: IntegrationDSL = {
|
|
|
202
236
|
{
|
|
203
237
|
"foreign_data_wrapper/handler": {
|
|
204
238
|
op: "regex",
|
|
205
|
-
value: "^extensions\\.",
|
|
239
|
+
value: "^extensions\\.wasm_fdw_handler$",
|
|
206
240
|
},
|
|
207
241
|
},
|
|
208
242
|
{
|
|
209
243
|
"foreign_data_wrapper/validator": {
|
|
210
244
|
op: "regex",
|
|
211
|
-
value: "^extensions\\.",
|
|
245
|
+
value: "^extensions\\.wasm_fdw_validator$",
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
],
|
|
249
|
+
},
|
|
250
|
+
],
|
|
251
|
+
},
|
|
252
|
+
// Platform-managed Wasm FDW dependents (CLI-1470 follow-up).
|
|
253
|
+
// Suppressing the wrapper DDL alone leaves `CREATE SERVER` /
|
|
254
|
+
// `CREATE FOREIGN TABLE` / `CREATE USER MAPPING` that reference
|
|
255
|
+
// a wrapper local Docker never provisions (`clerk_oauth`, etc.).
|
|
256
|
+
// Match on the parent wrapper's Wasm handler/validator
|
|
257
|
+
// (`extensions.wasm_fdw_handler` / `extensions.wasm_fdw_validator`,
|
|
258
|
+
// joined at extract time) — the same discriminator used for the
|
|
259
|
+
// wrapper itself above. A bare `extensions.*` match would also
|
|
260
|
+
// drop user-created `postgres_fdw` servers/foreign tables/user
|
|
261
|
+
// mappings (whose handler installs into `extensions` but which
|
|
262
|
+
// the local image CAN provision), so keep it scoped to the Wasm
|
|
263
|
+
// function names. Server _privilege_ scope is excluded here —
|
|
264
|
+
// `GRANT/REVOKE ON SERVER` does not require superuser and remains
|
|
265
|
+
// user-declarative state (see CLI-1469 companion test).
|
|
266
|
+
{
|
|
267
|
+
and: [
|
|
268
|
+
{ objectType: "server" },
|
|
269
|
+
{ not: { scope: "privilege" } },
|
|
270
|
+
{
|
|
271
|
+
or: [
|
|
272
|
+
{
|
|
273
|
+
"{server,foreign_table,user_mapping}/wrapper_handler": {
|
|
274
|
+
op: "regex",
|
|
275
|
+
value: "^extensions\\.wasm_fdw_handler$",
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
"{server,foreign_table,user_mapping}/wrapper_validator": {
|
|
280
|
+
op: "regex",
|
|
281
|
+
value: "^extensions\\.wasm_fdw_validator$",
|
|
282
|
+
},
|
|
283
|
+
},
|
|
284
|
+
],
|
|
285
|
+
},
|
|
286
|
+
],
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
and: [
|
|
290
|
+
{ objectType: ["foreign_table", "user_mapping"] },
|
|
291
|
+
{
|
|
292
|
+
or: [
|
|
293
|
+
{
|
|
294
|
+
"{server,foreign_table,user_mapping}/wrapper_handler": {
|
|
295
|
+
op: "regex",
|
|
296
|
+
value: "^extensions\\.wasm_fdw_handler$",
|
|
297
|
+
},
|
|
298
|
+
},
|
|
299
|
+
{
|
|
300
|
+
"{server,foreign_table,user_mapping}/wrapper_validator": {
|
|
301
|
+
op: "regex",
|
|
302
|
+
value: "^extensions\\.wasm_fdw_validator$",
|
|
212
303
|
},
|
|
213
304
|
},
|
|
214
305
|
],
|
|
@@ -51,6 +51,21 @@ export abstract class BaseChange {
|
|
|
51
51
|
return [];
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Stable identifiers this change invalidates in place.
|
|
56
|
+
*
|
|
57
|
+
* Unlike `drops`, the object keeps its identity. This is an ordering-only
|
|
58
|
+
* signal for mutations that rewrite an existing object in a way that requires
|
|
59
|
+
* dependents bound to the old definition to be dropped before the mutation
|
|
60
|
+
* and rebuilt afterward.
|
|
61
|
+
*
|
|
62
|
+
* Defaults to an empty array. Override in subclasses that invalidate
|
|
63
|
+
* dependents without dropping the object.
|
|
64
|
+
*/
|
|
65
|
+
get invalidates(): string[] {
|
|
66
|
+
return [];
|
|
67
|
+
}
|
|
68
|
+
|
|
54
69
|
/**
|
|
55
70
|
* Stable identifiers this change requires to exist beforehand.
|
|
56
71
|
*
|
|
@@ -36,6 +36,9 @@ const foreignTablePropsSchema = z.object({
|
|
|
36
36
|
columns: z.array(columnPropsSchema),
|
|
37
37
|
privileges: z.array(privilegePropsSchema),
|
|
38
38
|
security_labels: z.array(securityLabelPropsSchema).default([]).optional(),
|
|
39
|
+
// Parent FDW handler/validator — filter metadata only, not in dataFields.
|
|
40
|
+
wrapper_handler: z.string().nullable().optional(),
|
|
41
|
+
wrapper_validator: z.string().nullable().optional(),
|
|
39
42
|
});
|
|
40
43
|
|
|
41
44
|
type ForeignTablePrivilegeProps = PrivilegeProps;
|
|
@@ -51,6 +54,8 @@ export class ForeignTable extends BasePgModel implements TableLikeObject {
|
|
|
51
54
|
public readonly columns: ForeignTableProps["columns"];
|
|
52
55
|
public readonly privileges: ForeignTablePrivilegeProps[];
|
|
53
56
|
public readonly security_labels: SecurityLabelProps[];
|
|
57
|
+
public readonly wrapper_handler: ForeignTableProps["wrapper_handler"];
|
|
58
|
+
public readonly wrapper_validator: ForeignTableProps["wrapper_validator"];
|
|
54
59
|
|
|
55
60
|
constructor(props: ForeignTableProps) {
|
|
56
61
|
super();
|
|
@@ -67,6 +72,8 @@ export class ForeignTable extends BasePgModel implements TableLikeObject {
|
|
|
67
72
|
this.columns = props.columns;
|
|
68
73
|
this.privileges = props.privileges;
|
|
69
74
|
this.security_labels = props.security_labels ?? [];
|
|
75
|
+
this.wrapper_handler = props.wrapper_handler ?? null;
|
|
76
|
+
this.wrapper_validator = props.wrapper_validator ?? null;
|
|
70
77
|
}
|
|
71
78
|
|
|
72
79
|
get stableId(): `foreignTable:${string}` {
|
|
@@ -146,12 +153,22 @@ export async function extractForeignTables(
|
|
|
146
153
|
c.relowner::regrole::text as owner,
|
|
147
154
|
quote_ident(srv.srvname) as server,
|
|
148
155
|
coalesce(ft.ftoptions, array[]::text[]) as options,
|
|
149
|
-
c.oid as oid
|
|
156
|
+
c.oid as oid,
|
|
157
|
+
case
|
|
158
|
+
when fdw.fdwhandler = 0 then null
|
|
159
|
+
else p_handler.pronamespace::regnamespace::text || '.' || quote_ident(p_handler.proname)
|
|
160
|
+
end as wrapper_handler,
|
|
161
|
+
case
|
|
162
|
+
when fdw.fdwvalidator = 0 then null
|
|
163
|
+
else p_validator.pronamespace::regnamespace::text || '.' || quote_ident(p_validator.proname)
|
|
164
|
+
end as wrapper_validator
|
|
150
165
|
from
|
|
151
166
|
pg_class c
|
|
152
167
|
inner join pg_foreign_table ft on ft.ftrelid = c.oid
|
|
153
168
|
inner join pg_foreign_server srv on srv.oid = ft.ftserver
|
|
154
169
|
inner join pg_foreign_data_wrapper fdw on fdw.oid = srv.srvfdw
|
|
170
|
+
left join pg_catalog.pg_proc p_handler on p_handler.oid = fdw.fdwhandler
|
|
171
|
+
left join pg_catalog.pg_proc p_validator on p_validator.oid = fdw.fdwvalidator
|
|
155
172
|
left outer join extension_oids e1 on c.oid = e1.objid
|
|
156
173
|
where
|
|
157
174
|
c.relkind = 'f'
|
|
@@ -165,6 +182,8 @@ export async function extractForeignTables(
|
|
|
165
182
|
ft.owner,
|
|
166
183
|
ft.server,
|
|
167
184
|
ft.options,
|
|
185
|
+
ft.wrapper_handler,
|
|
186
|
+
ft.wrapper_validator,
|
|
168
187
|
obj_description(ft.oid, 'pg_class') as comment,
|
|
169
188
|
coalesce(json_agg(
|
|
170
189
|
case when a.attname is not null then
|
|
@@ -250,7 +269,14 @@ export async function extractForeignTables(
|
|
|
250
269
|
left join pg_attrdef ad on a.attrelid = ad.adrelid and a.attnum = ad.adnum
|
|
251
270
|
left join pg_type ty on ty.oid = a.atttypid
|
|
252
271
|
group by
|
|
253
|
-
ft.oid,
|
|
272
|
+
ft.oid,
|
|
273
|
+
ft.schema,
|
|
274
|
+
ft.name,
|
|
275
|
+
ft.owner,
|
|
276
|
+
ft.server,
|
|
277
|
+
ft.options,
|
|
278
|
+
ft.wrapper_handler,
|
|
279
|
+
ft.wrapper_validator
|
|
254
280
|
order by
|
|
255
281
|
ft.schema, ft.name
|
|
256
282
|
`);
|
|
@@ -26,6 +26,9 @@ const serverPropsSchema = z.object({
|
|
|
26
26
|
options: z.array(z.string()).nullable(),
|
|
27
27
|
comment: z.string().nullable(),
|
|
28
28
|
privileges: z.array(privilegePropsSchema),
|
|
29
|
+
// Parent FDW handler/validator — filter metadata only, not in dataFields.
|
|
30
|
+
wrapper_handler: z.string().nullable().optional(),
|
|
31
|
+
wrapper_validator: z.string().nullable().optional(),
|
|
29
32
|
});
|
|
30
33
|
|
|
31
34
|
type ServerPrivilegeProps = PrivilegeProps;
|
|
@@ -40,6 +43,8 @@ export class Server extends BasePgModel {
|
|
|
40
43
|
public readonly options: ServerProps["options"];
|
|
41
44
|
public readonly comment: ServerProps["comment"];
|
|
42
45
|
public readonly privileges: ServerPrivilegeProps[];
|
|
46
|
+
public readonly wrapper_handler: ServerProps["wrapper_handler"];
|
|
47
|
+
public readonly wrapper_validator: ServerProps["wrapper_validator"];
|
|
43
48
|
|
|
44
49
|
constructor(props: ServerProps) {
|
|
45
50
|
super();
|
|
@@ -55,6 +60,8 @@ export class Server extends BasePgModel {
|
|
|
55
60
|
this.options = props.options;
|
|
56
61
|
this.comment = props.comment;
|
|
57
62
|
this.privileges = props.privileges;
|
|
63
|
+
this.wrapper_handler = props.wrapper_handler ?? null;
|
|
64
|
+
this.wrapper_validator = props.wrapper_validator ?? null;
|
|
58
65
|
}
|
|
59
66
|
|
|
60
67
|
get stableId(): `server:${string}` {
|
|
@@ -112,10 +119,20 @@ export async function extractServers(pool: Pool): Promise<Server[]> {
|
|
|
112
119
|
)
|
|
113
120
|
from lateral aclexplode(srv.srvacl) as x(grantor, grantee, privilege_type, is_grantable)
|
|
114
121
|
), '[]'
|
|
115
|
-
) as privileges
|
|
122
|
+
) as privileges,
|
|
123
|
+
case
|
|
124
|
+
when fdw.fdwhandler = 0 then null
|
|
125
|
+
else p_handler.pronamespace::regnamespace::text || '.' || quote_ident(p_handler.proname)
|
|
126
|
+
end as wrapper_handler,
|
|
127
|
+
case
|
|
128
|
+
when fdw.fdwvalidator = 0 then null
|
|
129
|
+
else p_validator.pronamespace::regnamespace::text || '.' || quote_ident(p_validator.proname)
|
|
130
|
+
end as wrapper_validator
|
|
116
131
|
from
|
|
117
132
|
pg_catalog.pg_foreign_server srv
|
|
118
133
|
inner join pg_catalog.pg_foreign_data_wrapper fdw on fdw.oid = srv.srvfdw
|
|
134
|
+
left join pg_catalog.pg_proc p_handler on p_handler.oid = fdw.fdwhandler
|
|
135
|
+
left join pg_catalog.pg_proc p_validator on p_validator.oid = fdw.fdwvalidator
|
|
119
136
|
where
|
|
120
137
|
not fdw.fdwname like any(array['pg\\_%'])
|
|
121
138
|
order by
|
|
@@ -18,6 +18,9 @@ const userMappingPropsSchema = z.object({
|
|
|
18
18
|
user: z.string(),
|
|
19
19
|
server: z.string(),
|
|
20
20
|
options: z.array(z.string()).nullable(),
|
|
21
|
+
// Parent FDW handler/validator — filter metadata only, not in dataFields.
|
|
22
|
+
wrapper_handler: z.string().nullable().optional(),
|
|
23
|
+
wrapper_validator: z.string().nullable().optional(),
|
|
21
24
|
});
|
|
22
25
|
|
|
23
26
|
export type UserMappingProps = z.infer<typeof userMappingPropsSchema>;
|
|
@@ -26,6 +29,8 @@ export class UserMapping extends BasePgModel {
|
|
|
26
29
|
public readonly user: UserMappingProps["user"];
|
|
27
30
|
public readonly server: UserMappingProps["server"];
|
|
28
31
|
public readonly options: UserMappingProps["options"];
|
|
32
|
+
public readonly wrapper_handler: UserMappingProps["wrapper_handler"];
|
|
33
|
+
public readonly wrapper_validator: UserMappingProps["wrapper_validator"];
|
|
29
34
|
|
|
30
35
|
constructor(props: UserMappingProps) {
|
|
31
36
|
super();
|
|
@@ -36,6 +41,8 @@ export class UserMapping extends BasePgModel {
|
|
|
36
41
|
|
|
37
42
|
// Data fields
|
|
38
43
|
this.options = props.options;
|
|
44
|
+
this.wrapper_handler = props.wrapper_handler ?? null;
|
|
45
|
+
this.wrapper_validator = props.wrapper_validator ?? null;
|
|
39
46
|
}
|
|
40
47
|
|
|
41
48
|
get stableId(): `userMapping:${string}:${string}` {
|
|
@@ -74,11 +81,21 @@ export async function extractUserMappings(pool: Pool): Promise<UserMapping[]> {
|
|
|
74
81
|
else um.umuser::regrole::text
|
|
75
82
|
end as user,
|
|
76
83
|
quote_ident(srv.srvname) as server,
|
|
77
|
-
coalesce(um.umoptions, array[]::text[]) as options
|
|
84
|
+
coalesce(um.umoptions, array[]::text[]) as options,
|
|
85
|
+
case
|
|
86
|
+
when fdw.fdwhandler = 0 then null
|
|
87
|
+
else p_handler.pronamespace::regnamespace::text || '.' || quote_ident(p_handler.proname)
|
|
88
|
+
end as wrapper_handler,
|
|
89
|
+
case
|
|
90
|
+
when fdw.fdwvalidator = 0 then null
|
|
91
|
+
else p_validator.pronamespace::regnamespace::text || '.' || quote_ident(p_validator.proname)
|
|
92
|
+
end as wrapper_validator
|
|
78
93
|
from
|
|
79
94
|
pg_catalog.pg_user_mapping um
|
|
80
95
|
inner join pg_catalog.pg_foreign_server srv on srv.oid = um.umserver
|
|
81
96
|
inner join pg_catalog.pg_foreign_data_wrapper fdw on fdw.oid = srv.srvfdw
|
|
97
|
+
left join pg_catalog.pg_proc p_handler on p_handler.oid = fdw.fdwhandler
|
|
98
|
+
left join pg_catalog.pg_proc p_validator on p_validator.oid = fdw.fdwvalidator
|
|
82
99
|
where
|
|
83
100
|
not fdw.fdwname like any(array['pg\\_%'])
|
|
84
101
|
order by
|
|
@@ -106,7 +106,7 @@ describe.concurrent("materialized-view.diff", () => {
|
|
|
106
106
|
expect(changes[0]).toBeInstanceOf(AlterMaterializedViewChangeOwner);
|
|
107
107
|
});
|
|
108
108
|
|
|
109
|
-
test("drop + create on non-alterable change", () => {
|
|
109
|
+
test("drop + create with metadata on non-alterable change", () => {
|
|
110
110
|
const main = new MaterializedView(base);
|
|
111
111
|
const branch = new MaterializedView({ ...base, definition: "select 2" });
|
|
112
112
|
const changes = diffMaterializedViews(
|
|
@@ -114,9 +114,10 @@ describe.concurrent("materialized-view.diff", () => {
|
|
|
114
114
|
{ [main.stableId]: main },
|
|
115
115
|
{ [branch.stableId]: branch },
|
|
116
116
|
);
|
|
117
|
-
expect(changes).toHaveLength(
|
|
117
|
+
expect(changes).toHaveLength(3);
|
|
118
118
|
expect(changes[0]).toBeInstanceOf(DropMaterializedView);
|
|
119
119
|
expect(changes[1]).toBeInstanceOf(CreateMaterializedView);
|
|
120
|
+
expect(changes[2]).toBeInstanceOf(AlterMaterializedViewChangeOwner);
|
|
120
121
|
});
|
|
121
122
|
|
|
122
123
|
test("alter storage parameters: set and reset", () => {
|
|
@@ -30,117 +30,126 @@ import {
|
|
|
30
30
|
import type { MaterializedViewChange } from "./changes/materialized-view.types.ts";
|
|
31
31
|
import type { MaterializedView } from "./materialized-view.model.ts";
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
* Diff two sets of materialized views from main and branch catalogs.
|
|
35
|
-
*
|
|
36
|
-
* @param ctx - Context containing version, currentUser, and defaultPrivilegeState
|
|
37
|
-
* @param main - The materialized views in the main catalog.
|
|
38
|
-
* @param branch - The materialized views in the branch catalog.
|
|
39
|
-
* @returns A list of changes to apply to main to make it match branch.
|
|
40
|
-
*/
|
|
41
|
-
export function diffMaterializedViews(
|
|
33
|
+
export function buildCreateMaterializedViewChanges(
|
|
42
34
|
ctx: Pick<
|
|
43
35
|
ObjectDiffContext,
|
|
44
36
|
"version" | "currentUser" | "defaultPrivilegeState"
|
|
45
37
|
>,
|
|
46
|
-
|
|
47
|
-
branch: Record<string, MaterializedView>,
|
|
38
|
+
mv: MaterializedView,
|
|
48
39
|
): MaterializedViewChange[] {
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
40
|
+
const changes: MaterializedViewChange[] = [
|
|
41
|
+
new CreateMaterializedView({
|
|
42
|
+
materializedView: mv,
|
|
43
|
+
}),
|
|
44
|
+
];
|
|
52
45
|
|
|
53
|
-
|
|
54
|
-
|
|
46
|
+
// OWNER: If the materialized view should be owned by someone other than the current user,
|
|
47
|
+
// emit ALTER MATERIALIZED VIEW ... OWNER TO after creation
|
|
48
|
+
if (mv.owner !== ctx.currentUser) {
|
|
55
49
|
changes.push(
|
|
56
|
-
new
|
|
50
|
+
new AlterMaterializedViewChangeOwner({
|
|
57
51
|
materializedView: mv,
|
|
52
|
+
owner: mv.owner,
|
|
58
53
|
}),
|
|
59
54
|
);
|
|
55
|
+
}
|
|
60
56
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
57
|
+
// Materialized view comment on creation
|
|
58
|
+
if (mv.comment !== null) {
|
|
59
|
+
changes.push(
|
|
60
|
+
new CreateCommentOnMaterializedView({
|
|
61
|
+
materializedView: mv,
|
|
62
|
+
}),
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
// Column comments on creation
|
|
66
|
+
for (const col of mv.columns) {
|
|
67
|
+
if (col.comment !== null) {
|
|
64
68
|
changes.push(
|
|
65
|
-
new
|
|
69
|
+
new CreateCommentOnMaterializedViewColumn({
|
|
66
70
|
materializedView: mv,
|
|
67
|
-
|
|
71
|
+
column: col,
|
|
68
72
|
}),
|
|
69
73
|
);
|
|
70
74
|
}
|
|
75
|
+
}
|
|
71
76
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
77
|
+
// Security labels on the matview itself (columns of matviews are not
|
|
78
|
+
// supported targets of SECURITY LABEL, so we only label the relation).
|
|
79
|
+
for (const label of mv.security_labels) {
|
|
80
|
+
changes.push(
|
|
81
|
+
new CreateSecurityLabelOnMaterializedView({
|
|
82
|
+
materializedView: mv,
|
|
83
|
+
securityLabel: label,
|
|
84
|
+
}),
|
|
85
|
+
);
|
|
86
|
+
}
|
|
75
87
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
88
|
+
// PRIVILEGES: For created objects, compare against default privileges state
|
|
89
|
+
// The migration script will run ALTER DEFAULT PRIVILEGES before CREATE (via constraint spec),
|
|
90
|
+
// so objects are created with the default privileges state in effect.
|
|
91
|
+
// We compare default privileges against desired privileges to generate REVOKE/GRANT statements
|
|
92
|
+
// needed to reach the final desired state.
|
|
93
|
+
const effectiveDefaults = ctx.defaultPrivilegeState.getEffectiveDefaults(
|
|
94
|
+
ctx.currentUser,
|
|
95
|
+
"materialized_view",
|
|
96
|
+
mv.schema ?? "",
|
|
97
|
+
);
|
|
98
|
+
const creatorFilteredDefaults =
|
|
99
|
+
mv.owner !== ctx.currentUser
|
|
100
|
+
? effectiveDefaults.filter((p) => p.grantee !== ctx.currentUser)
|
|
101
|
+
: effectiveDefaults;
|
|
102
|
+
const desiredPrivileges = mv.privileges;
|
|
103
|
+
// Filter out owner privileges - owner always has ALL privileges implicitly
|
|
104
|
+
// and shouldn't be compared. Use the materialized view owner as the reference.
|
|
105
|
+
const privilegeResults = diffPrivileges(
|
|
106
|
+
creatorFilteredDefaults,
|
|
107
|
+
desiredPrivileges,
|
|
108
|
+
mv.owner,
|
|
109
|
+
);
|
|
95
110
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
111
|
+
changes.push(
|
|
112
|
+
...(emitColumnPrivilegeChanges(
|
|
113
|
+
privilegeResults,
|
|
114
|
+
mv,
|
|
115
|
+
mv,
|
|
116
|
+
"materializedView",
|
|
117
|
+
{
|
|
118
|
+
Grant: GrantMaterializedViewPrivileges,
|
|
119
|
+
Revoke: RevokeMaterializedViewPrivileges,
|
|
120
|
+
RevokeGrantOption: RevokeGrantOptionMaterializedViewPrivileges,
|
|
121
|
+
},
|
|
122
|
+
effectiveDefaults,
|
|
123
|
+
ctx.version,
|
|
124
|
+
) as MaterializedViewChange[]),
|
|
125
|
+
);
|
|
106
126
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
mv.owner,
|
|
128
|
-
);
|
|
127
|
+
return changes;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Diff two sets of materialized views from main and branch catalogs.
|
|
132
|
+
*
|
|
133
|
+
* @param ctx - Context containing version, currentUser, and defaultPrivilegeState
|
|
134
|
+
* @param main - The materialized views in the main catalog.
|
|
135
|
+
* @param branch - The materialized views in the branch catalog.
|
|
136
|
+
* @returns A list of changes to apply to main to make it match branch.
|
|
137
|
+
*/
|
|
138
|
+
export function diffMaterializedViews(
|
|
139
|
+
ctx: Pick<
|
|
140
|
+
ObjectDiffContext,
|
|
141
|
+
"version" | "currentUser" | "defaultPrivilegeState"
|
|
142
|
+
>,
|
|
143
|
+
main: Record<string, MaterializedView>,
|
|
144
|
+
branch: Record<string, MaterializedView>,
|
|
145
|
+
): MaterializedViewChange[] {
|
|
146
|
+
const { created, dropped, altered } = diffObjects(main, branch);
|
|
129
147
|
|
|
148
|
+
const changes: MaterializedViewChange[] = [];
|
|
149
|
+
|
|
150
|
+
for (const materializedViewId of created) {
|
|
130
151
|
changes.push(
|
|
131
|
-
...(
|
|
132
|
-
privilegeResults,
|
|
133
|
-
mv,
|
|
134
|
-
mv,
|
|
135
|
-
"materializedView",
|
|
136
|
-
{
|
|
137
|
-
Grant: GrantMaterializedViewPrivileges,
|
|
138
|
-
Revoke: RevokeMaterializedViewPrivileges,
|
|
139
|
-
RevokeGrantOption: RevokeGrantOptionMaterializedViewPrivileges,
|
|
140
|
-
},
|
|
141
|
-
effectiveDefaults,
|
|
142
|
-
ctx.version,
|
|
143
|
-
) as MaterializedViewChange[]),
|
|
152
|
+
...buildCreateMaterializedViewChanges(ctx, branch[materializedViewId]),
|
|
144
153
|
);
|
|
145
154
|
}
|
|
146
155
|
|
|
@@ -180,9 +189,7 @@ export function diffMaterializedViews(
|
|
|
180
189
|
// Replace the entire materialized view (drop + create)
|
|
181
190
|
changes.push(
|
|
182
191
|
new DropMaterializedView({ materializedView: mainMaterializedView }),
|
|
183
|
-
|
|
184
|
-
materializedView: branchMaterializedView,
|
|
185
|
-
}),
|
|
192
|
+
...buildCreateMaterializedViewChanges(ctx, branchMaterializedView),
|
|
186
193
|
);
|
|
187
194
|
} else {
|
|
188
195
|
// Only alterable properties changed - check each one
|
|
@@ -651,6 +651,15 @@ export class AlterTableAlterColumnType extends AlterTableChange {
|
|
|
651
651
|
];
|
|
652
652
|
}
|
|
653
653
|
|
|
654
|
+
get invalidates() {
|
|
655
|
+
// ALTER COLUMN ... TYPE rewrites the column in place. The column keeps its
|
|
656
|
+
// identity, but anything bound to its old type (views, rules, etc.) must be
|
|
657
|
+
// dropped before the rewrite and rebuilt after, so report it as invalidated.
|
|
658
|
+
return [
|
|
659
|
+
stableId.column(this.table.schema, this.table.name, this.column.name),
|
|
660
|
+
];
|
|
661
|
+
}
|
|
662
|
+
|
|
654
663
|
serialize(_options?: SerializeOptions): string {
|
|
655
664
|
// previousColumn is optional so direct serializer tests/fixtures can keep
|
|
656
665
|
// emitting canonical ALTER TYPE SQL without forcing a USING expression.
|