@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.
Files changed (43) hide show
  1. package/dist/cli/commands/catalog-export.js +22 -1
  2. package/dist/core/catalog.diff.js +22 -3
  3. package/dist/core/catalog.filter.d.ts +17 -0
  4. package/dist/core/catalog.filter.js +75 -0
  5. package/dist/core/catalog.model.js +7 -1
  6. package/dist/core/expand-replace-dependencies.d.ts +3 -1
  7. package/dist/core/expand-replace-dependencies.js +117 -7
  8. package/dist/core/integrations/supabase.js +102 -11
  9. package/dist/core/objects/base.change.d.ts +12 -0
  10. package/dist/core/objects/base.change.js +14 -0
  11. package/dist/core/objects/foreign-data-wrapper/foreign-table/foreign-table.model.d.ts +4 -0
  12. package/dist/core/objects/foreign-data-wrapper/foreign-table/foreign-table.model.js +28 -2
  13. package/dist/core/objects/foreign-data-wrapper/server/server.model.d.ts +4 -0
  14. package/dist/core/objects/foreign-data-wrapper/server/server.model.js +18 -1
  15. package/dist/core/objects/foreign-data-wrapper/user-mapping/user-mapping.model.d.ts +4 -0
  16. package/dist/core/objects/foreign-data-wrapper/user-mapping/user-mapping.model.js +18 -1
  17. package/dist/core/objects/materialized-view/materialized-view.diff.d.ts +1 -0
  18. package/dist/core/objects/materialized-view/materialized-view.diff.js +59 -59
  19. package/dist/core/objects/table/changes/table.alter.d.ts +1 -0
  20. package/dist/core/objects/table/changes/table.alter.js +8 -0
  21. package/dist/core/objects/view/view.diff.d.ts +1 -0
  22. package/dist/core/objects/view/view.diff.js +35 -34
  23. package/dist/core/sort/graph-builder.js +6 -0
  24. package/package.json +1 -1
  25. package/src/cli/commands/catalog-export.ts +26 -1
  26. package/src/core/catalog.diff.test.ts +173 -0
  27. package/src/core/catalog.diff.ts +24 -3
  28. package/src/core/catalog.filter.ts +96 -0
  29. package/src/core/catalog.model.ts +10 -2
  30. package/src/core/expand-replace-dependencies.test.ts +282 -0
  31. package/src/core/expand-replace-dependencies.ts +165 -7
  32. package/src/core/integrations/supabase.test.ts +335 -0
  33. package/src/core/integrations/supabase.ts +102 -11
  34. package/src/core/objects/base.change.ts +15 -0
  35. package/src/core/objects/foreign-data-wrapper/foreign-table/foreign-table.model.ts +28 -2
  36. package/src/core/objects/foreign-data-wrapper/server/server.model.ts +18 -1
  37. package/src/core/objects/foreign-data-wrapper/user-mapping/user-mapping.model.ts +18 -1
  38. package/src/core/objects/materialized-view/materialized-view.diff.test.ts +3 -2
  39. package/src/core/objects/materialized-view/materialized-view.diff.ts +99 -92
  40. package/src/core/objects/table/changes/table.alter.ts +9 -0
  41. package/src/core/objects/view/view.diff.ts +67 -60
  42. package/src/core/sort/graph-builder.ts +6 -0
  43. package/src/core/sort/sort-changes.test.ts +73 -1
@@ -52,6 +52,8 @@ declare const foreignTablePropsSchema: z.ZodObject<{
52
52
  provider: z.ZodString;
53
53
  label: z.ZodString;
54
54
  }, z.z.core.$strip>>>>;
55
+ wrapper_handler: z.ZodOptional<z.ZodNullable<z.ZodString>>;
56
+ wrapper_validator: z.ZodOptional<z.ZodNullable<z.ZodString>>;
55
57
  }, z.z.core.$strip>;
56
58
  type ForeignTablePrivilegeProps = PrivilegeProps;
57
59
  export type ForeignTableProps = z.infer<typeof foreignTablePropsSchema>;
@@ -65,6 +67,8 @@ export declare class ForeignTable extends BasePgModel implements TableLikeObject
65
67
  readonly columns: ForeignTableProps["columns"];
66
68
  readonly privileges: ForeignTablePrivilegeProps[];
67
69
  readonly security_labels: SecurityLabelProps[];
70
+ readonly wrapper_handler: ForeignTableProps["wrapper_handler"];
71
+ readonly wrapper_validator: ForeignTableProps["wrapper_validator"];
68
72
  constructor(props: ForeignTableProps);
69
73
  get stableId(): `foreignTable:${string}`;
70
74
  get identityFields(): {
@@ -23,6 +23,9 @@ const foreignTablePropsSchema = z.object({
23
23
  columns: z.array(columnPropsSchema),
24
24
  privileges: z.array(privilegePropsSchema),
25
25
  security_labels: z.array(securityLabelPropsSchema).default([]).optional(),
26
+ // Parent FDW handler/validator — filter metadata only, not in dataFields.
27
+ wrapper_handler: z.string().nullable().optional(),
28
+ wrapper_validator: z.string().nullable().optional(),
26
29
  });
27
30
  export class ForeignTable extends BasePgModel {
28
31
  schema;
@@ -34,6 +37,8 @@ export class ForeignTable extends BasePgModel {
34
37
  columns;
35
38
  privileges;
36
39
  security_labels;
40
+ wrapper_handler;
41
+ wrapper_validator;
37
42
  constructor(props) {
38
43
  super();
39
44
  // Identity fields
@@ -47,6 +52,8 @@ export class ForeignTable extends BasePgModel {
47
52
  this.columns = props.columns;
48
53
  this.privileges = props.privileges;
49
54
  this.security_labels = props.security_labels ?? [];
55
+ this.wrapper_handler = props.wrapper_handler ?? null;
56
+ this.wrapper_validator = props.wrapper_validator ?? null;
50
57
  }
51
58
  get stableId() {
52
59
  return `foreignTable:${this.schema}.${this.name}`;
@@ -114,12 +121,22 @@ export async function extractForeignTables(pool) {
114
121
  c.relowner::regrole::text as owner,
115
122
  quote_ident(srv.srvname) as server,
116
123
  coalesce(ft.ftoptions, array[]::text[]) as options,
117
- c.oid as oid
124
+ c.oid as oid,
125
+ case
126
+ when fdw.fdwhandler = 0 then null
127
+ else p_handler.pronamespace::regnamespace::text || '.' || quote_ident(p_handler.proname)
128
+ end as wrapper_handler,
129
+ case
130
+ when fdw.fdwvalidator = 0 then null
131
+ else p_validator.pronamespace::regnamespace::text || '.' || quote_ident(p_validator.proname)
132
+ end as wrapper_validator
118
133
  from
119
134
  pg_class c
120
135
  inner join pg_foreign_table ft on ft.ftrelid = c.oid
121
136
  inner join pg_foreign_server srv on srv.oid = ft.ftserver
122
137
  inner join pg_foreign_data_wrapper fdw on fdw.oid = srv.srvfdw
138
+ left join pg_catalog.pg_proc p_handler on p_handler.oid = fdw.fdwhandler
139
+ left join pg_catalog.pg_proc p_validator on p_validator.oid = fdw.fdwvalidator
123
140
  left outer join extension_oids e1 on c.oid = e1.objid
124
141
  where
125
142
  c.relkind = 'f'
@@ -133,6 +150,8 @@ export async function extractForeignTables(pool) {
133
150
  ft.owner,
134
151
  ft.server,
135
152
  ft.options,
153
+ ft.wrapper_handler,
154
+ ft.wrapper_validator,
136
155
  obj_description(ft.oid, 'pg_class') as comment,
137
156
  coalesce(json_agg(
138
157
  case when a.attname is not null then
@@ -218,7 +237,14 @@ export async function extractForeignTables(pool) {
218
237
  left join pg_attrdef ad on a.attrelid = ad.adrelid and a.attnum = ad.adnum
219
238
  left join pg_type ty on ty.oid = a.atttypid
220
239
  group by
221
- ft.oid, ft.schema, ft.name, ft.owner, ft.server, ft.options
240
+ ft.oid,
241
+ ft.schema,
242
+ ft.name,
243
+ ft.owner,
244
+ ft.server,
245
+ ft.options,
246
+ ft.wrapper_handler,
247
+ ft.wrapper_validator
222
248
  order by
223
249
  ft.schema, ft.name
224
250
  `);
@@ -26,6 +26,8 @@ declare const serverPropsSchema: z.ZodObject<{
26
26
  grantable: z.ZodBoolean;
27
27
  columns: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodString>>>;
28
28
  }, z.z.core.$strip>>;
29
+ wrapper_handler: z.ZodOptional<z.ZodNullable<z.ZodString>>;
30
+ wrapper_validator: z.ZodOptional<z.ZodNullable<z.ZodString>>;
29
31
  }, z.z.core.$strip>;
30
32
  type ServerPrivilegeProps = PrivilegeProps;
31
33
  export type ServerProps = z.infer<typeof serverPropsSchema>;
@@ -38,6 +40,8 @@ export declare class Server extends BasePgModel {
38
40
  readonly options: ServerProps["options"];
39
41
  readonly comment: ServerProps["comment"];
40
42
  readonly privileges: ServerPrivilegeProps[];
43
+ readonly wrapper_handler: ServerProps["wrapper_handler"];
44
+ readonly wrapper_validator: ServerProps["wrapper_validator"];
41
45
  constructor(props: ServerProps);
42
46
  get stableId(): `server:${string}`;
43
47
  get identityFields(): {
@@ -21,6 +21,9 @@ const serverPropsSchema = z.object({
21
21
  options: z.array(z.string()).nullable(),
22
22
  comment: z.string().nullable(),
23
23
  privileges: z.array(privilegePropsSchema),
24
+ // Parent FDW handler/validator — filter metadata only, not in dataFields.
25
+ wrapper_handler: z.string().nullable().optional(),
26
+ wrapper_validator: z.string().nullable().optional(),
24
27
  });
25
28
  export class Server extends BasePgModel {
26
29
  name;
@@ -31,6 +34,8 @@ export class Server extends BasePgModel {
31
34
  options;
32
35
  comment;
33
36
  privileges;
37
+ wrapper_handler;
38
+ wrapper_validator;
34
39
  constructor(props) {
35
40
  super();
36
41
  // Identity fields
@@ -43,6 +48,8 @@ export class Server extends BasePgModel {
43
48
  this.options = props.options;
44
49
  this.comment = props.comment;
45
50
  this.privileges = props.privileges;
51
+ this.wrapper_handler = props.wrapper_handler ?? null;
52
+ this.wrapper_validator = props.wrapper_validator ?? null;
46
53
  }
47
54
  get stableId() {
48
55
  return `server:${this.name}`;
@@ -96,10 +103,20 @@ export async function extractServers(pool) {
96
103
  )
97
104
  from lateral aclexplode(srv.srvacl) as x(grantor, grantee, privilege_type, is_grantable)
98
105
  ), '[]'
99
- ) as privileges
106
+ ) as privileges,
107
+ case
108
+ when fdw.fdwhandler = 0 then null
109
+ else p_handler.pronamespace::regnamespace::text || '.' || quote_ident(p_handler.proname)
110
+ end as wrapper_handler,
111
+ case
112
+ when fdw.fdwvalidator = 0 then null
113
+ else p_validator.pronamespace::regnamespace::text || '.' || quote_ident(p_validator.proname)
114
+ end as wrapper_validator
100
115
  from
101
116
  pg_catalog.pg_foreign_server srv
102
117
  inner join pg_catalog.pg_foreign_data_wrapper fdw on fdw.oid = srv.srvfdw
118
+ left join pg_catalog.pg_proc p_handler on p_handler.oid = fdw.fdwhandler
119
+ left join pg_catalog.pg_proc p_validator on p_validator.oid = fdw.fdwvalidator
103
120
  where
104
121
  not fdw.fdwname like any(array['pg\\_%'])
105
122
  order by
@@ -16,12 +16,16 @@ declare const userMappingPropsSchema: z.ZodObject<{
16
16
  user: z.ZodString;
17
17
  server: z.ZodString;
18
18
  options: z.ZodNullable<z.ZodArray<z.ZodString>>;
19
+ wrapper_handler: z.ZodOptional<z.ZodNullable<z.ZodString>>;
20
+ wrapper_validator: z.ZodOptional<z.ZodNullable<z.ZodString>>;
19
21
  }, z.z.core.$strip>;
20
22
  export type UserMappingProps = z.infer<typeof userMappingPropsSchema>;
21
23
  export declare class UserMapping extends BasePgModel {
22
24
  readonly user: UserMappingProps["user"];
23
25
  readonly server: UserMappingProps["server"];
24
26
  readonly options: UserMappingProps["options"];
27
+ readonly wrapper_handler: UserMappingProps["wrapper_handler"];
28
+ readonly wrapper_validator: UserMappingProps["wrapper_validator"];
25
29
  constructor(props: UserMappingProps);
26
30
  get stableId(): `userMapping:${string}:${string}`;
27
31
  get identityFields(): {
@@ -16,11 +16,16 @@ const userMappingPropsSchema = z.object({
16
16
  user: z.string(),
17
17
  server: z.string(),
18
18
  options: z.array(z.string()).nullable(),
19
+ // Parent FDW handler/validator — filter metadata only, not in dataFields.
20
+ wrapper_handler: z.string().nullable().optional(),
21
+ wrapper_validator: z.string().nullable().optional(),
19
22
  });
20
23
  export class UserMapping extends BasePgModel {
21
24
  user;
22
25
  server;
23
26
  options;
27
+ wrapper_handler;
28
+ wrapper_validator;
24
29
  constructor(props) {
25
30
  super();
26
31
  // Identity fields
@@ -28,6 +33,8 @@ export class UserMapping extends BasePgModel {
28
33
  this.server = props.server;
29
34
  // Data fields
30
35
  this.options = props.options;
36
+ this.wrapper_handler = props.wrapper_handler ?? null;
37
+ this.wrapper_validator = props.wrapper_validator ?? null;
31
38
  }
32
39
  get stableId() {
33
40
  return `userMapping:${this.server}:${this.user}`;
@@ -62,11 +69,21 @@ export async function extractUserMappings(pool) {
62
69
  else um.umuser::regrole::text
63
70
  end as user,
64
71
  quote_ident(srv.srvname) as server,
65
- coalesce(um.umoptions, array[]::text[]) as options
72
+ coalesce(um.umoptions, array[]::text[]) as options,
73
+ case
74
+ when fdw.fdwhandler = 0 then null
75
+ else p_handler.pronamespace::regnamespace::text || '.' || quote_ident(p_handler.proname)
76
+ end as wrapper_handler,
77
+ case
78
+ when fdw.fdwvalidator = 0 then null
79
+ else p_validator.pronamespace::regnamespace::text || '.' || quote_ident(p_validator.proname)
80
+ end as wrapper_validator
66
81
  from
67
82
  pg_catalog.pg_user_mapping um
68
83
  inner join pg_catalog.pg_foreign_server srv on srv.oid = um.umserver
69
84
  inner join pg_catalog.pg_foreign_data_wrapper fdw on fdw.oid = srv.srvfdw
85
+ left join pg_catalog.pg_proc p_handler on p_handler.oid = fdw.fdwhandler
86
+ left join pg_catalog.pg_proc p_validator on p_validator.oid = fdw.fdwvalidator
70
87
  where
71
88
  not fdw.fdwname like any(array['pg\\_%'])
72
89
  order by
@@ -1,6 +1,7 @@
1
1
  import type { ObjectDiffContext } from "../diff-context.ts";
2
2
  import type { MaterializedViewChange } from "./changes/materialized-view.types.ts";
3
3
  import type { MaterializedView } from "./materialized-view.model.ts";
4
+ export declare function buildCreateMaterializedViewChanges(ctx: Pick<ObjectDiffContext, "version" | "currentUser" | "defaultPrivilegeState">, mv: MaterializedView): MaterializedViewChange[];
4
5
  /**
5
6
  * Diff two sets of materialized views from main and branch catalogs.
6
7
  *
@@ -8,6 +8,63 @@ import { CreateMaterializedView } from "./changes/materialized-view.create.js";
8
8
  import { DropMaterializedView } from "./changes/materialized-view.drop.js";
9
9
  import { GrantMaterializedViewPrivileges, RevokeGrantOptionMaterializedViewPrivileges, RevokeMaterializedViewPrivileges, } from "./changes/materialized-view.privilege.js";
10
10
  import { CreateSecurityLabelOnMaterializedView, DropSecurityLabelOnMaterializedView, } from "./changes/materialized-view.security-label.js";
11
+ export function buildCreateMaterializedViewChanges(ctx, mv) {
12
+ const changes = [
13
+ new CreateMaterializedView({
14
+ materializedView: mv,
15
+ }),
16
+ ];
17
+ // OWNER: If the materialized view should be owned by someone other than the current user,
18
+ // emit ALTER MATERIALIZED VIEW ... OWNER TO after creation
19
+ if (mv.owner !== ctx.currentUser) {
20
+ changes.push(new AlterMaterializedViewChangeOwner({
21
+ materializedView: mv,
22
+ owner: mv.owner,
23
+ }));
24
+ }
25
+ // Materialized view comment on creation
26
+ if (mv.comment !== null) {
27
+ changes.push(new CreateCommentOnMaterializedView({
28
+ materializedView: mv,
29
+ }));
30
+ }
31
+ // Column comments on creation
32
+ for (const col of mv.columns) {
33
+ if (col.comment !== null) {
34
+ changes.push(new CreateCommentOnMaterializedViewColumn({
35
+ materializedView: mv,
36
+ column: col,
37
+ }));
38
+ }
39
+ }
40
+ // Security labels on the matview itself (columns of matviews are not
41
+ // supported targets of SECURITY LABEL, so we only label the relation).
42
+ for (const label of mv.security_labels) {
43
+ changes.push(new CreateSecurityLabelOnMaterializedView({
44
+ materializedView: mv,
45
+ securityLabel: label,
46
+ }));
47
+ }
48
+ // PRIVILEGES: For created objects, compare against default privileges state
49
+ // The migration script will run ALTER DEFAULT PRIVILEGES before CREATE (via constraint spec),
50
+ // so objects are created with the default privileges state in effect.
51
+ // We compare default privileges against desired privileges to generate REVOKE/GRANT statements
52
+ // needed to reach the final desired state.
53
+ const effectiveDefaults = ctx.defaultPrivilegeState.getEffectiveDefaults(ctx.currentUser, "materialized_view", mv.schema ?? "");
54
+ const creatorFilteredDefaults = mv.owner !== ctx.currentUser
55
+ ? effectiveDefaults.filter((p) => p.grantee !== ctx.currentUser)
56
+ : effectiveDefaults;
57
+ const desiredPrivileges = mv.privileges;
58
+ // Filter out owner privileges - owner always has ALL privileges implicitly
59
+ // and shouldn't be compared. Use the materialized view owner as the reference.
60
+ const privilegeResults = diffPrivileges(creatorFilteredDefaults, desiredPrivileges, mv.owner);
61
+ changes.push(...emitColumnPrivilegeChanges(privilegeResults, mv, mv, "materializedView", {
62
+ Grant: GrantMaterializedViewPrivileges,
63
+ Revoke: RevokeMaterializedViewPrivileges,
64
+ RevokeGrantOption: RevokeGrantOptionMaterializedViewPrivileges,
65
+ }, effectiveDefaults, ctx.version));
66
+ return changes;
67
+ }
11
68
  /**
12
69
  * Diff two sets of materialized views from main and branch catalogs.
13
70
  *
@@ -20,62 +77,7 @@ export function diffMaterializedViews(ctx, main, branch) {
20
77
  const { created, dropped, altered } = diffObjects(main, branch);
21
78
  const changes = [];
22
79
  for (const materializedViewId of created) {
23
- const mv = branch[materializedViewId];
24
- changes.push(new CreateMaterializedView({
25
- materializedView: mv,
26
- }));
27
- // OWNER: If the materialized view should be owned by someone other than the current user,
28
- // emit ALTER MATERIALIZED VIEW ... OWNER TO after creation
29
- if (mv.owner !== ctx.currentUser) {
30
- changes.push(new AlterMaterializedViewChangeOwner({
31
- materializedView: mv,
32
- owner: mv.owner,
33
- }));
34
- }
35
- // Note: RLS (row_security, force_row_security) is a non-alterable property for materialized views.
36
- // If RLS needs to be enabled, the materialized view must be dropped and recreated, which is
37
- // handled in the "altered" section when non-alterable properties change.
38
- // Materialized view comment on creation
39
- if (mv.comment !== null) {
40
- changes.push(new CreateCommentOnMaterializedView({
41
- materializedView: mv,
42
- }));
43
- }
44
- // Column comments on creation
45
- for (const col of mv.columns) {
46
- if (col.comment !== null) {
47
- changes.push(new CreateCommentOnMaterializedViewColumn({
48
- materializedView: mv,
49
- column: col,
50
- }));
51
- }
52
- }
53
- // Security labels on the matview itself (columns of matviews are not
54
- // supported targets of SECURITY LABEL, so we only label the relation).
55
- for (const label of mv.security_labels) {
56
- changes.push(new CreateSecurityLabelOnMaterializedView({
57
- materializedView: mv,
58
- securityLabel: label,
59
- }));
60
- }
61
- // PRIVILEGES: For created objects, compare against default privileges state
62
- // The migration script will run ALTER DEFAULT PRIVILEGES before CREATE (via constraint spec),
63
- // so objects are created with the default privileges state in effect.
64
- // We compare default privileges against desired privileges to generate REVOKE/GRANT statements
65
- // needed to reach the final desired state.
66
- const effectiveDefaults = ctx.defaultPrivilegeState.getEffectiveDefaults(ctx.currentUser, "materialized_view", mv.schema ?? "");
67
- const creatorFilteredDefaults = mv.owner !== ctx.currentUser
68
- ? effectiveDefaults.filter((p) => p.grantee !== ctx.currentUser)
69
- : effectiveDefaults;
70
- const desiredPrivileges = mv.privileges;
71
- // Filter out owner privileges - owner always has ALL privileges implicitly
72
- // and shouldn't be compared. Use the materialized view owner as the reference.
73
- const privilegeResults = diffPrivileges(creatorFilteredDefaults, desiredPrivileges, mv.owner);
74
- changes.push(...emitColumnPrivilegeChanges(privilegeResults, mv, mv, "materializedView", {
75
- Grant: GrantMaterializedViewPrivileges,
76
- Revoke: RevokeMaterializedViewPrivileges,
77
- RevokeGrantOption: RevokeGrantOptionMaterializedViewPrivileges,
78
- }, effectiveDefaults, ctx.version));
80
+ changes.push(...buildCreateMaterializedViewChanges(ctx, branch[materializedViewId]));
79
81
  }
80
82
  for (const materializedViewId of dropped) {
81
83
  changes.push(new DropMaterializedView({ materializedView: main[materializedViewId] }));
@@ -101,9 +103,7 @@ export function diffMaterializedViews(ctx, main, branch) {
101
103
  const nonAlterablePropsChanged = hasNonAlterableChanges(mainMaterializedView, branchMaterializedView, NON_ALTERABLE_FIELDS, { options: deepEqual });
102
104
  if (nonAlterablePropsChanged) {
103
105
  // Replace the entire materialized view (drop + create)
104
- changes.push(new DropMaterializedView({ materializedView: mainMaterializedView }), new CreateMaterializedView({
105
- materializedView: branchMaterializedView,
106
- }));
106
+ changes.push(new DropMaterializedView({ materializedView: mainMaterializedView }), ...buildCreateMaterializedViewChanges(ctx, branchMaterializedView));
107
107
  }
108
108
  else {
109
109
  // Only alterable properties changed - check each one
@@ -285,6 +285,7 @@ export declare class AlterTableAlterColumnType extends AlterTableChange {
285
285
  previousColumn?: ColumnProps;
286
286
  });
287
287
  get requires(): `column:${string}.${string}.${string}`[];
288
+ get invalidates(): `column:${string}.${string}.${string}`[];
288
289
  serialize(_options?: SerializeOptions): string;
289
290
  }
290
291
  /**
@@ -452,6 +452,14 @@ export class AlterTableAlterColumnType extends AlterTableChange {
452
452
  stableId.column(this.table.schema, this.table.name, this.column.name),
453
453
  ];
454
454
  }
455
+ get invalidates() {
456
+ // ALTER COLUMN ... TYPE rewrites the column in place. The column keeps its
457
+ // identity, but anything bound to its old type (views, rules, etc.) must be
458
+ // dropped before the rewrite and rebuilt after, so report it as invalidated.
459
+ return [
460
+ stableId.column(this.table.schema, this.table.name, this.column.name),
461
+ ];
462
+ }
455
463
  serialize(_options) {
456
464
  // previousColumn is optional so direct serializer tests/fixtures can keep
457
465
  // emitting canonical ALTER TYPE SQL without forcing a USING expression.
@@ -1,6 +1,7 @@
1
1
  import type { ObjectDiffContext } from "../diff-context.ts";
2
2
  import type { ViewChange } from "./changes/view.types.ts";
3
3
  import type { View } from "./view.model.ts";
4
+ export declare function buildCreateViewChanges(ctx: Pick<ObjectDiffContext, "version" | "currentUser" | "defaultPrivilegeState">, view: View): ViewChange[];
4
5
  /**
5
6
  * Diff two sets of views from main and branch catalogs.
6
7
  *
@@ -9,6 +9,39 @@ import { CreateView } from "./changes/view.create.js";
9
9
  import { DropView } from "./changes/view.drop.js";
10
10
  import { GrantViewPrivileges, RevokeGrantOptionViewPrivileges, RevokeViewPrivileges, } from "./changes/view.privilege.js";
11
11
  import { CreateSecurityLabelOnView, DropSecurityLabelOnView, } from "./changes/view.security-label.js";
12
+ export function buildCreateViewChanges(ctx, view) {
13
+ const changes = [new CreateView({ view })];
14
+ // OWNER: If the view should be owned by someone other than the current user,
15
+ // emit ALTER VIEW ... OWNER TO after creation
16
+ if (view.owner !== ctx.currentUser) {
17
+ changes.push(new AlterViewChangeOwner({ view, owner: view.owner }));
18
+ }
19
+ if (view.comment !== null) {
20
+ changes.push(new CreateCommentOnView({ view }));
21
+ }
22
+ for (const label of view.security_labels) {
23
+ changes.push(new CreateSecurityLabelOnView({ view, securityLabel: label }));
24
+ }
25
+ // PRIVILEGES: For created objects, compare against default privileges state
26
+ // The migration script will run ALTER DEFAULT PRIVILEGES before CREATE (via constraint spec),
27
+ // so objects are created with the default privileges state in effect.
28
+ // We compare default privileges against desired privileges to generate REVOKE/GRANT statements
29
+ // needed to reach the final desired state.
30
+ const effectiveDefaults = ctx.defaultPrivilegeState.getEffectiveDefaults(ctx.currentUser, "view", view.schema ?? "");
31
+ const creatorFilteredDefaults = view.owner !== ctx.currentUser
32
+ ? effectiveDefaults.filter((p) => p.grantee !== ctx.currentUser)
33
+ : effectiveDefaults;
34
+ const desiredPrivileges = view.privileges;
35
+ // Filter out owner privileges - owner always has ALL privileges implicitly
36
+ // and shouldn't be compared. Use the view owner as the reference.
37
+ const privilegeResults = diffPrivileges(creatorFilteredDefaults, desiredPrivileges, view.owner);
38
+ changes.push(...emitColumnPrivilegeChanges(privilegeResults, view, view, "view", {
39
+ Grant: GrantViewPrivileges,
40
+ Revoke: RevokeViewPrivileges,
41
+ RevokeGrantOption: RevokeGrantOptionViewPrivileges,
42
+ }, effectiveDefaults, ctx.version));
43
+ return changes;
44
+ }
12
45
  /**
13
46
  * Diff two sets of views from main and branch catalogs.
14
47
  *
@@ -20,40 +53,8 @@ import { CreateSecurityLabelOnView, DropSecurityLabelOnView, } from "./changes/v
20
53
  export function diffViews(ctx, main, branch) {
21
54
  const { created, dropped, altered } = diffObjects(main, branch);
22
55
  const changes = [];
23
- const appendCreateViewChanges = (view) => {
24
- changes.push(new CreateView({ view }));
25
- // OWNER: If the view should be owned by someone other than the current user,
26
- // emit ALTER VIEW ... OWNER TO after creation
27
- if (view.owner !== ctx.currentUser) {
28
- changes.push(new AlterViewChangeOwner({ view, owner: view.owner }));
29
- }
30
- if (view.comment !== null) {
31
- changes.push(new CreateCommentOnView({ view }));
32
- }
33
- for (const label of view.security_labels) {
34
- changes.push(new CreateSecurityLabelOnView({ view, securityLabel: label }));
35
- }
36
- // PRIVILEGES: For created objects, compare against default privileges state
37
- // The migration script will run ALTER DEFAULT PRIVILEGES before CREATE (via constraint spec),
38
- // so objects are created with the default privileges state in effect.
39
- // We compare default privileges against desired privileges to generate REVOKE/GRANT statements
40
- // needed to reach the final desired state.
41
- const effectiveDefaults = ctx.defaultPrivilegeState.getEffectiveDefaults(ctx.currentUser, "view", view.schema ?? "");
42
- const creatorFilteredDefaults = view.owner !== ctx.currentUser
43
- ? effectiveDefaults.filter((p) => p.grantee !== ctx.currentUser)
44
- : effectiveDefaults;
45
- const desiredPrivileges = view.privileges;
46
- // Filter out owner privileges - owner always has ALL privileges implicitly
47
- // and shouldn't be compared. Use the view owner as the reference.
48
- const privilegeResults = diffPrivileges(creatorFilteredDefaults, desiredPrivileges, view.owner);
49
- changes.push(...emitColumnPrivilegeChanges(privilegeResults, view, view, "view", {
50
- Grant: GrantViewPrivileges,
51
- Revoke: RevokeViewPrivileges,
52
- RevokeGrantOption: RevokeGrantOptionViewPrivileges,
53
- }, effectiveDefaults, ctx.version));
54
- };
55
56
  for (const viewId of created) {
56
- appendCreateViewChanges(branch[viewId]);
57
+ changes.push(...buildCreateViewChanges(ctx, branch[viewId]));
57
58
  }
58
59
  for (const viewId of dropped) {
59
60
  changes.push(new DropView({ view: main[viewId] }));
@@ -83,7 +84,7 @@ export function diffViews(ctx, main, branch) {
83
84
  // NON_ALTERABLE_FIELDS - a position change always implies a definition change.
84
85
  if (!deepEqual(normalizeColumns(mainView.columns), normalizeColumns(branchView.columns))) {
85
86
  changes.push(new DropView({ view: mainView }));
86
- appendCreateViewChanges(branchView);
87
+ changes.push(...buildCreateViewChanges(ctx, branchView));
87
88
  }
88
89
  else if (nonAlterablePropsChanged) {
89
90
  // Replace the entire view using CREATE OR REPLACE to avoid drop when possible
@@ -119,6 +119,12 @@ export function buildGraphData(phaseChanges, options) {
119
119
  for (const droppedId of changeItem.drops ?? []) {
120
120
  createdIds.add(droppedId);
121
121
  }
122
+ // In-place mutations keep the object identity but invalidate
123
+ // dependents, so for drop-phase ordering they behave like producers of
124
+ // the invalidated ids without changing Change.drops.
125
+ for (const invalidatedId of changeItem.invalidates) {
126
+ createdIds.add(invalidatedId);
127
+ }
122
128
  }
123
129
  return createdIds;
124
130
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supabase/pg-delta",
3
- "version": "1.0.0-alpha.26",
3
+ "version": "1.0.0-alpha.28",
4
4
  "description": "PostgreSQL migrations made easy",
5
5
  "keywords": [
6
6
  "diff",
@@ -4,11 +4,13 @@
4
4
 
5
5
  import { writeFile } from "node:fs/promises";
6
6
  import { buildCommand, type CommandContext } from "@stricli/core";
7
+ import { filterCatalog } from "../../core/catalog.filter.ts";
7
8
  import { extractCatalog } from "../../core/catalog.model.ts";
8
9
  import {
9
10
  serializeCatalog,
10
11
  stringifyCatalogSnapshot,
11
12
  } from "../../core/catalog.snapshot.ts";
13
+ import type { FilterDSL } from "../../core/integrations/filter/dsl.ts";
12
14
  import { createManagedPool } from "../../core/postgres-config.ts";
13
15
 
14
16
  export const catalogExportCommand = buildCommand({
@@ -30,6 +32,21 @@ export const catalogExportCommand = buildCommand({
30
32
  parse: String,
31
33
  optional: true,
32
34
  },
35
+ filter: {
36
+ kind: "parsed",
37
+ brief:
38
+ 'Filter DSL as inline JSON to filter changes (e.g., \'{"*/schema": "app"}\').',
39
+ parse: (value: string): FilterDSL => {
40
+ try {
41
+ return JSON.parse(value) as FilterDSL;
42
+ } catch (error) {
43
+ throw new Error(
44
+ `Invalid filter JSON: ${error instanceof Error ? error.message : String(error)}`,
45
+ );
46
+ }
47
+ },
48
+ optional: true,
49
+ },
33
50
  },
34
51
  aliases: {
35
52
  t: "target",
@@ -48,6 +65,10 @@ Use cases:
48
65
  - Snapshot template1 for use as an empty-database baseline
49
66
  - Snapshot a production database to generate revert migrations
50
67
  - Snapshot any state for reproducible offline diffs
68
+
69
+ Pass --filter to scope the snapshot to a subset of the catalog (same
70
+ Filter DSL accepted by plan/sync). Useful when committing a baseline
71
+ snapshot to a repo and only one schema's drift is interesting.
51
72
  `.trim(),
52
73
  },
53
74
  async func(
@@ -56,6 +77,7 @@ Use cases:
56
77
  target: string;
57
78
  output: string;
58
79
  role?: string;
80
+ filter?: FilterDSL;
59
81
  },
60
82
  ) {
61
83
  const { pool, close } = await createManagedPool(flags.target, {
@@ -65,7 +87,10 @@ Use cases:
65
87
 
66
88
  try {
67
89
  const catalog = await extractCatalog(pool);
68
- const snapshot = serializeCatalog(catalog);
90
+ const scoped = flags.filter
91
+ ? await filterCatalog(catalog, flags.filter)
92
+ : catalog;
93
+ const snapshot = serializeCatalog(scoped);
69
94
  const json = stringifyCatalogSnapshot(snapshot);
70
95
  await writeFile(flags.output, json, "utf-8");
71
96
  this.process.stdout.write(