@supabase/pg-delta 1.0.0-alpha.24 → 1.0.0-alpha.26

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 (100) hide show
  1. package/dist/core/catalog.model.d.ts +2 -2
  2. package/dist/core/catalog.model.js +28 -21
  3. package/dist/core/expand-replace-dependencies.js +1 -7
  4. package/dist/core/integrations/supabase.js +84 -0
  5. package/dist/core/objects/aggregate/changes/aggregate.privilege.js +21 -9
  6. package/dist/core/objects/foreign-data-wrapper/foreign-data-wrapper/changes/foreign-data-wrapper.alter.js +4 -1
  7. package/dist/core/objects/foreign-data-wrapper/foreign-data-wrapper/changes/foreign-data-wrapper.create.js +6 -3
  8. package/dist/core/objects/foreign-data-wrapper/foreign-data-wrapper/foreign-data-wrapper.model.d.ts +11 -0
  9. package/dist/core/objects/foreign-data-wrapper/foreign-data-wrapper/foreign-data-wrapper.model.js +11 -0
  10. package/dist/core/objects/foreign-data-wrapper/foreign-table/changes/foreign-table.alter.js +4 -1
  11. package/dist/core/objects/foreign-data-wrapper/foreign-table/changes/foreign-table.create.js +6 -3
  12. package/dist/core/objects/foreign-data-wrapper/foreign-table/foreign-table.model.d.ts +11 -0
  13. package/dist/core/objects/foreign-data-wrapper/foreign-table/foreign-table.model.js +11 -0
  14. package/dist/core/objects/foreign-data-wrapper/sensitive-options.d.ts +32 -0
  15. package/dist/core/objects/foreign-data-wrapper/sensitive-options.js +129 -0
  16. package/dist/core/objects/foreign-data-wrapper/server/changes/server.alter.js +4 -1
  17. package/dist/core/objects/foreign-data-wrapper/server/changes/server.create.js +6 -3
  18. package/dist/core/objects/foreign-data-wrapper/server/server.model.d.ts +10 -0
  19. package/dist/core/objects/foreign-data-wrapper/server/server.model.js +10 -0
  20. package/dist/core/objects/foreign-data-wrapper/user-mapping/changes/user-mapping.alter.js +4 -1
  21. package/dist/core/objects/foreign-data-wrapper/user-mapping/changes/user-mapping.create.js +6 -3
  22. package/dist/core/objects/foreign-data-wrapper/user-mapping/user-mapping.model.d.ts +10 -0
  23. package/dist/core/objects/foreign-data-wrapper/user-mapping/user-mapping.model.js +10 -0
  24. package/dist/core/objects/rls-policy/rls-policy.model.d.ts +2 -2
  25. package/dist/core/objects/table/table.diff.js +53 -30
  26. package/dist/core/objects/table/table.model.js +7 -2
  27. package/dist/core/plan/hierarchy.js +4 -4
  28. package/dist/core/postgres-config.d.ts +7 -0
  29. package/dist/core/postgres-config.js +19 -5
  30. package/dist/core/sort/debug-visualization.js +1 -1
  31. package/dist/core/sort/topological-sort.js +2 -2
  32. package/package.json +34 -33
  33. package/src/core/catalog.model.ts +40 -23
  34. package/src/core/catalog.snapshot.test.ts +1 -0
  35. package/src/core/expand-replace-dependencies.test.ts +12 -0
  36. package/src/core/expand-replace-dependencies.ts +1 -12
  37. package/src/core/integrations/supabase.test.ts +198 -0
  38. package/src/core/integrations/supabase.ts +84 -0
  39. package/src/core/objects/aggregate/changes/aggregate.base.ts +1 -1
  40. package/src/core/objects/aggregate/changes/aggregate.privilege.test.ts +79 -0
  41. package/src/core/objects/aggregate/changes/aggregate.privilege.ts +22 -9
  42. package/src/core/objects/collation/changes/collation.base.ts +1 -1
  43. package/src/core/objects/domain/changes/domain.base.ts +1 -1
  44. package/src/core/objects/extension/changes/extension.base.ts +1 -1
  45. package/src/core/objects/foreign-data-wrapper/foreign-data-wrapper/changes/foreign-data-wrapper.alter.test.ts +34 -4
  46. package/src/core/objects/foreign-data-wrapper/foreign-data-wrapper/changes/foreign-data-wrapper.alter.ts +5 -1
  47. package/src/core/objects/foreign-data-wrapper/foreign-data-wrapper/changes/foreign-data-wrapper.base.ts +1 -1
  48. package/src/core/objects/foreign-data-wrapper/foreign-data-wrapper/changes/foreign-data-wrapper.create.test.ts +34 -0
  49. package/src/core/objects/foreign-data-wrapper/foreign-data-wrapper/changes/foreign-data-wrapper.create.ts +7 -5
  50. package/src/core/objects/foreign-data-wrapper/foreign-data-wrapper/foreign-data-wrapper.model.ts +11 -0
  51. package/src/core/objects/foreign-data-wrapper/foreign-table/changes/foreign-table.alter.test.ts +25 -4
  52. package/src/core/objects/foreign-data-wrapper/foreign-table/changes/foreign-table.alter.ts +5 -1
  53. package/src/core/objects/foreign-data-wrapper/foreign-table/changes/foreign-table.base.ts +1 -1
  54. package/src/core/objects/foreign-data-wrapper/foreign-table/changes/foreign-table.create.test.ts +54 -0
  55. package/src/core/objects/foreign-data-wrapper/foreign-table/changes/foreign-table.create.ts +7 -5
  56. package/src/core/objects/foreign-data-wrapper/foreign-table/foreign-table.model.ts +11 -0
  57. package/src/core/objects/foreign-data-wrapper/sensitive-options.test.ts +98 -0
  58. package/src/core/objects/foreign-data-wrapper/sensitive-options.ts +133 -0
  59. package/src/core/objects/foreign-data-wrapper/server/changes/server.alter.test.ts +39 -4
  60. package/src/core/objects/foreign-data-wrapper/server/changes/server.alter.ts +5 -1
  61. package/src/core/objects/foreign-data-wrapper/server/changes/server.base.ts +1 -1
  62. package/src/core/objects/foreign-data-wrapper/server/changes/server.create.test.ts +36 -0
  63. package/src/core/objects/foreign-data-wrapper/server/changes/server.create.ts +7 -5
  64. package/src/core/objects/foreign-data-wrapper/server/server.model.ts +10 -0
  65. package/src/core/objects/foreign-data-wrapper/user-mapping/changes/user-mapping.alter.test.ts +39 -6
  66. package/src/core/objects/foreign-data-wrapper/user-mapping/changes/user-mapping.alter.ts +5 -1
  67. package/src/core/objects/foreign-data-wrapper/user-mapping/changes/user-mapping.base.ts +1 -1
  68. package/src/core/objects/foreign-data-wrapper/user-mapping/changes/user-mapping.create.test.ts +38 -2
  69. package/src/core/objects/foreign-data-wrapper/user-mapping/changes/user-mapping.create.ts +7 -5
  70. package/src/core/objects/foreign-data-wrapper/user-mapping/user-mapping.model.ts +10 -0
  71. package/src/core/objects/index/changes/index.base.ts +1 -1
  72. package/src/core/objects/language/changes/language.base.ts +1 -1
  73. package/src/core/objects/materialized-view/changes/materialized-view.base.ts +1 -1
  74. package/src/core/objects/procedure/changes/procedure.base.ts +1 -1
  75. package/src/core/objects/rls-policy/changes/rls-policy.base.ts +1 -1
  76. package/src/core/objects/role/changes/role.base.ts +1 -1
  77. package/src/core/objects/schema/changes/schema.base.ts +1 -1
  78. package/src/core/objects/sequence/changes/sequence.base.ts +1 -1
  79. package/src/core/objects/table/changes/table.base.ts +1 -1
  80. package/src/core/objects/table/changes/table.comment.ts +2 -8
  81. package/src/core/objects/table/table.diff.test.ts +198 -5
  82. package/src/core/objects/table/table.diff.ts +63 -34
  83. package/src/core/objects/table/table.model.ts +7 -2
  84. package/src/core/objects/trigger/changes/trigger.alter.ts +1 -4
  85. package/src/core/objects/trigger/changes/trigger.base.ts +1 -1
  86. package/src/core/objects/type/composite-type/changes/composite-type.base.ts +1 -1
  87. package/src/core/objects/type/enum/changes/enum.base.ts +1 -1
  88. package/src/core/objects/type/range/changes/range.base.ts +1 -1
  89. package/src/core/objects/view/changes/view.base.ts +1 -1
  90. package/src/core/plan/hierarchy.ts +4 -4
  91. package/src/core/plan/sql-format/format-off.test.ts +4 -4
  92. package/src/core/plan/sql-format/format-pretty-lower-leading.test.ts +4 -4
  93. package/src/core/plan/sql-format/format-pretty-narrow.test.ts +5 -4
  94. package/src/core/plan/sql-format/format-pretty-preserve.test.ts +4 -4
  95. package/src/core/plan/sql-format/format-pretty-upper.test.ts +4 -4
  96. package/src/core/postgres-config.test.ts +39 -1
  97. package/src/core/postgres-config.ts +32 -16
  98. package/src/core/sort/debug-visualization.ts +1 -1
  99. package/src/core/sort/sort-changes.test.ts +1 -0
  100. package/src/core/sort/topological-sort.ts +2 -2
@@ -6,8 +6,8 @@ import { type Collation } from "./objects/collation/collation.model.ts";
6
6
  import type { Domain } from "./objects/domain/domain.model.ts";
7
7
  import { type EventTrigger } from "./objects/event-trigger/event-trigger.model.ts";
8
8
  import { type Extension } from "./objects/extension/extension.model.ts";
9
- import { type ForeignDataWrapper } from "./objects/foreign-data-wrapper/foreign-data-wrapper/foreign-data-wrapper.model.ts";
10
- import { type ForeignTable } from "./objects/foreign-data-wrapper/foreign-table/foreign-table.model.ts";
9
+ import { ForeignDataWrapper } from "./objects/foreign-data-wrapper/foreign-data-wrapper/foreign-data-wrapper.model.ts";
10
+ import { ForeignTable } from "./objects/foreign-data-wrapper/foreign-table/foreign-table.model.ts";
11
11
  import { Server } from "./objects/foreign-data-wrapper/server/server.model.ts";
12
12
  import { UserMapping } from "./objects/foreign-data-wrapper/user-mapping/user-mapping.model.ts";
13
13
  import { type Index } from "./objects/index/index.model.ts";
@@ -5,8 +5,9 @@ import { extractCollations, } from "./objects/collation/collation.model.js";
5
5
  import { extractDomains } from "./objects/domain/domain.model.js";
6
6
  import { extractEventTriggers, } from "./objects/event-trigger/event-trigger.model.js";
7
7
  import { extractExtensions, } from "./objects/extension/extension.model.js";
8
- import { extractForeignDataWrappers, } from "./objects/foreign-data-wrapper/foreign-data-wrapper/foreign-data-wrapper.model.js";
9
- import { extractForeignTables, } from "./objects/foreign-data-wrapper/foreign-table/foreign-table.model.js";
8
+ import { extractForeignDataWrappers, ForeignDataWrapper, } from "./objects/foreign-data-wrapper/foreign-data-wrapper/foreign-data-wrapper.model.js";
9
+ import { extractForeignTables, ForeignTable, } from "./objects/foreign-data-wrapper/foreign-table/foreign-table.model.js";
10
+ import { redactSensitiveOptionPairs } from "./objects/foreign-data-wrapper/sensitive-options.js";
10
11
  import { extractServers, Server, } from "./objects/foreign-data-wrapper/server/server.model.js";
11
12
  import { extractUserMappings, UserMapping, } from "./objects/foreign-data-wrapper/user-mapping/user-mapping.model.js";
12
13
  import { extractIndexes } from "./objects/index/index.model.js";
@@ -146,10 +147,12 @@ async function getPg17Baseline() {
146
147
  export async function createEmptyCatalog(version, currentUser) {
147
148
  if (version >= 170000) {
148
149
  const baseline = await getPg17Baseline();
150
+ // oxlint-disable-next-line typescript/no-misused-spread
149
151
  return new Catalog({ ...baseline, version, currentUser });
150
152
  }
151
153
  if (version >= 150000) {
152
154
  const baseline = await getPg1516Baseline();
155
+ // oxlint-disable-next-line typescript/no-misused-spread
153
156
  return new Catalog({ ...baseline, version, currentUser });
154
157
  }
155
158
  const publicSchema = new Schema({
@@ -264,27 +267,44 @@ function listToRecord(list) {
264
267
  return Object.fromEntries(list.map((item) => [item.stableId, item]));
265
268
  }
266
269
  function normalizeCatalog(catalog) {
270
+ const foreignDataWrappers = mapRecord(catalog.foreignDataWrappers, (fdw) => new ForeignDataWrapper({
271
+ name: fdw.name,
272
+ owner: fdw.owner,
273
+ handler: fdw.handler,
274
+ validator: fdw.validator,
275
+ options: redactSensitiveOptionPairs(fdw.options),
276
+ comment: fdw.comment,
277
+ privileges: fdw.privileges,
278
+ }));
267
279
  const servers = mapRecord(catalog.servers, (server) => {
268
- const maskedOptions = maskOptions(server.options);
269
280
  return new Server({
270
281
  name: server.name,
271
282
  owner: server.owner,
272
283
  foreign_data_wrapper: server.foreign_data_wrapper,
273
284
  type: server.type,
274
285
  version: server.version,
275
- options: maskedOptions,
286
+ options: redactSensitiveOptionPairs(server.options),
276
287
  comment: server.comment,
277
288
  privileges: server.privileges,
278
289
  });
279
290
  });
280
291
  const userMappings = mapRecord(catalog.userMappings, (mapping) => {
281
- const maskedOptions = maskOptions(mapping.options);
282
292
  return new UserMapping({
283
293
  user: mapping.user,
284
294
  server: mapping.server,
285
- options: maskedOptions,
295
+ options: redactSensitiveOptionPairs(mapping.options),
286
296
  });
287
297
  });
298
+ const foreignTables = mapRecord(catalog.foreignTables, (foreignTable) => new ForeignTable({
299
+ schema: foreignTable.schema,
300
+ name: foreignTable.name,
301
+ owner: foreignTable.owner,
302
+ server: foreignTable.server,
303
+ columns: foreignTable.columns,
304
+ options: redactSensitiveOptionPairs(foreignTable.options),
305
+ comment: foreignTable.comment,
306
+ privileges: foreignTable.privileges,
307
+ }));
288
308
  const subscriptions = mapRecord(catalog.subscriptions, (subscription) => {
289
309
  return new Subscription({
290
310
  name: subscription.name,
@@ -330,29 +350,16 @@ function normalizeCatalog(catalog) {
330
350
  rules: catalog.rules,
331
351
  ranges: catalog.ranges,
332
352
  views: catalog.views,
333
- foreignDataWrappers: catalog.foreignDataWrappers,
353
+ foreignDataWrappers,
334
354
  servers,
335
355
  userMappings,
336
- foreignTables: catalog.foreignTables,
356
+ foreignTables,
337
357
  depends: catalog.depends,
338
358
  indexableObjects: catalog.indexableObjects,
339
359
  version: catalog.version,
340
360
  currentUser: catalog.currentUser,
341
361
  });
342
362
  }
343
- function maskOptions(options) {
344
- if (!options || options.length === 0)
345
- return options;
346
- const masked = [];
347
- for (let i = 0; i < options.length; i += 2) {
348
- const key = options[i];
349
- const value = options[i + 1];
350
- if (key === undefined || value === undefined)
351
- continue;
352
- masked.push(key, `__OPTION_${key.toUpperCase()}__`);
353
- }
354
- return masked.length > 0 ? masked : null;
355
- }
356
363
  function mapRecord(record, mapper) {
357
364
  return Object.fromEntries(Object.entries(record).map(([key, value]) => [key, mapper(value)]));
358
365
  }
@@ -6,7 +6,7 @@ import { CreateMaterializedView } from "./objects/materialized-view/changes/mate
6
6
  import { DropMaterializedView } from "./objects/materialized-view/changes/materialized-view.drop.js";
7
7
  import { CreateProcedure } from "./objects/procedure/changes/procedure.create.js";
8
8
  import { DropProcedure } from "./objects/procedure/changes/procedure.drop.js";
9
- import { AlterTableAddConstraint, AlterTableValidateConstraint, } from "./objects/table/changes/table.alter.js";
9
+ import { AlterTableAddConstraint } from "./objects/table/changes/table.alter.js";
10
10
  import { CreateCommentOnConstraint } from "./objects/table/changes/table.comment.js";
11
11
  import { CreateTable } from "./objects/table/changes/table.create.js";
12
12
  import { DropTable } from "./objects/table/changes/table.drop.js";
@@ -312,12 +312,6 @@ function buildReplaceChanges(resolved, options) {
312
312
  constraint,
313
313
  }),
314
314
  ];
315
- if (!constraint.validated) {
316
- items.push(new AlterTableValidateConstraint({
317
- table: resolved.branch,
318
- constraint,
319
- }));
320
- }
321
315
  if (constraint.comment !== null &&
322
316
  constraint.comment !== undefined) {
323
317
  items.push(new CreateCommentOnConstraint({
@@ -94,6 +94,34 @@ export const supabase = {
94
94
  operation: "drop",
95
95
  scope: "object",
96
96
  },
97
+ // Include user-attached triggers on tables in Supabase-managed schemas.
98
+ //
99
+ // Triggers live in the schema of the table they fire on, so a user
100
+ // trigger on `auth.users` reports `trigger/schema = auth` and is
101
+ // otherwise indistinguishable from Supabase's own triggers via the
102
+ // schema-level deny list. Triggers also have no real owner — pg-delta
103
+ // surfaces the parent table's owner as `trigger/owner`, which for
104
+ // `auth.users` and `storage.objects` is always a Supabase system role,
105
+ // so the owner-level deny list catches them too.
106
+ //
107
+ // The trigger function, however, is genuinely user-owned: a customer
108
+ // who wants to run code on an auth event creates a function in
109
+ // `public` (or any non-managed schema) and points the trigger at it.
110
+ // Supabase's own auth/storage triggers either come from extensions
111
+ // (already filtered out at extract time via `pg_depend`) or call
112
+ // functions inside the same managed schema, so `function_schema`
113
+ // outside the managed list is a reliable user-defined marker.
114
+ {
115
+ and: [
116
+ { objectType: "trigger" },
117
+ { "trigger/schema": [...SUPABASE_SYSTEM_SCHEMAS] },
118
+ {
119
+ not: {
120
+ "trigger/function_schema": [...SUPABASE_SYSTEM_SCHEMAS],
121
+ },
122
+ },
123
+ ],
124
+ },
97
125
  // Exclude system objects
98
126
  {
99
127
  not: {
@@ -126,6 +154,62 @@ export const supabase = {
126
154
  },
127
155
  ],
128
156
  },
157
+ // Platform-managed foreign data wrapper ACL.
158
+ // `GRANT`/`REVOKE ... ON FOREIGN DATA WRAPPER` requires
159
+ // superuser. On Supabase Cloud `postgres` has the elevated
160
+ // rights to make this work, but the local Docker image does
161
+ // not, so `supabase db reset` aborts with
162
+ // `permission denied for foreign-data wrapper`. The
163
+ // `*/owner` rule above already covers wrappers owned by
164
+ // `supabase_admin`, but `pg_dump` rewrites OWNER TO clauses
165
+ // to whoever the dump runs under, so after a restore the
166
+ // FDW typically ends up owned by `postgres` and slips past
167
+ // the owner gate. A non-superuser `postgres` still can't
168
+ // grant on a FDW (this is true regardless of who owns the
169
+ // wrapper locally), so the ACL diff is not user-replayable.
170
+ // We don't apply the same blanket rule to `FOREIGN SERVER`:
171
+ // server GRANT/REVOKE doesn't require superuser, and
172
+ // user-created servers (e.g. a `dblink` server pointing to
173
+ // a peer DB) carry legitimate user ACL that should
174
+ // roundtrip — the existing `*/owner` rule already drops
175
+ // platform-managed servers.
176
+ {
177
+ and: [
178
+ { objectType: "foreign_data_wrapper" },
179
+ { scope: "privilege" },
180
+ ],
181
+ },
182
+ // Platform-managed foreign data wrappers — Wasm-based FDWs
183
+ // (e.g. `clerk`, `clerk_oauth`) whose handler/validator live in
184
+ // the `extensions` schema. `CREATE FOREIGN DATA WRAPPER`
185
+ // requires superuser, and Supabase Cloud provisions these via
186
+ // `supabase_admin` at project creation; replaying the DDL
187
+ // against a local image fails because the local environment
188
+ // has no equivalent pre-step. We can't rely on the FDW owner
189
+ // alone — after a dump/restore the owner is often rewritten
190
+ // away from `supabase_admin` — so match on the function
191
+ // reference instead.
192
+ {
193
+ and: [
194
+ { objectType: "foreign_data_wrapper" },
195
+ {
196
+ or: [
197
+ {
198
+ "foreign_data_wrapper/handler": {
199
+ op: "regex",
200
+ value: "^extensions\\.",
201
+ },
202
+ },
203
+ {
204
+ "foreign_data_wrapper/validator": {
205
+ op: "regex",
206
+ value: "^extensions\\.",
207
+ },
208
+ },
209
+ ],
210
+ },
211
+ ],
212
+ },
129
213
  ],
130
214
  },
131
215
  },
@@ -1,6 +1,24 @@
1
1
  import { formatObjectPrivilegeList, getObjectKindPrefix, } from "../../base.privilege.js";
2
2
  import { stableId } from "../../utils.js";
3
3
  import { AlterAggregateChange } from "./aggregate.base.js";
4
+ /**
5
+ * Build the signature `<schema>.<name>(<argtypes>)` for use inside
6
+ * `GRANT`/`REVOKE ... ON FUNCTION (...)`.
7
+ *
8
+ * The aggregate's `identityArguments` (from
9
+ * `pg_get_function_identity_arguments`) embeds `ORDER BY` for ordered-set
10
+ * and hypothetical-set aggregates (`aggkind` of `o`/`h`) and `VARIADIC`
11
+ * for variadic aggregates — both of which the GRANT parser rejects with
12
+ * a syntax error. PostgreSQL resolves the aggregate from the positional
13
+ * argument types alone, so use `argument_types` here regardless of
14
+ * `aggkind`. Other aggregate DDL (`ALTER AGGREGATE`, `COMMENT ON
15
+ * AGGREGATE`, `SECURITY LABEL ON AGGREGATE`, `DROP AGGREGATE`) accepts
16
+ * the identity form and keeps using it.
17
+ */
18
+ function aggregateGrantSignature(aggregate) {
19
+ const args = (aggregate.argument_types ?? []).join(", ");
20
+ return `${aggregate.schema}.${aggregate.name}(${args})`;
21
+ }
4
22
  export class GrantAggregatePrivileges extends AlterAggregateChange {
5
23
  aggregate;
6
24
  grantee;
@@ -30,9 +48,7 @@ export class GrantAggregatePrivileges extends AlterAggregateChange {
30
48
  const kindPrefix = getObjectKindPrefix("FUNCTION");
31
49
  const list = this.privileges.map((p) => p.privilege);
32
50
  const privSql = formatObjectPrivilegeList("FUNCTION", list, this.version);
33
- const aggregateName = `${this.aggregate.schema}.${this.aggregate.name}`;
34
- const signature = this.aggregate.identityArguments;
35
- const qualified = `${aggregateName}(${signature})`;
51
+ const qualified = aggregateGrantSignature(this.aggregate);
36
52
  return `GRANT ${privSql} ${kindPrefix} ${qualified} TO ${this.grantee}${withGrant}`;
37
53
  }
38
54
  }
@@ -65,9 +81,7 @@ export class RevokeAggregatePrivileges extends AlterAggregateChange {
65
81
  const kindPrefix = getObjectKindPrefix("FUNCTION");
66
82
  const list = this.privileges.map((p) => p.privilege);
67
83
  const privSql = formatObjectPrivilegeList("FUNCTION", list, this.version);
68
- const aggregateName = `${this.aggregate.schema}.${this.aggregate.name}`;
69
- const signature = this.aggregate.identityArguments;
70
- const qualified = `${aggregateName}(${signature})`;
84
+ const qualified = aggregateGrantSignature(this.aggregate);
71
85
  return `REVOKE ${privSql} ${kindPrefix} ${qualified} FROM ${this.grantee}`;
72
86
  }
73
87
  }
@@ -94,9 +108,7 @@ export class RevokeGrantOptionAggregatePrivileges extends AlterAggregateChange {
94
108
  serialize(_options) {
95
109
  const kindPrefix = getObjectKindPrefix("FUNCTION");
96
110
  const privSql = formatObjectPrivilegeList("FUNCTION", this.privilegeNames, this.version);
97
- const aggregateName = `${this.aggregate.schema}.${this.aggregate.name}`;
98
- const signature = this.aggregate.identityArguments;
99
- const qualified = `${aggregateName}(${signature})`;
111
+ const qualified = aggregateGrantSignature(this.aggregate);
100
112
  return `REVOKE GRANT OPTION FOR ${privSql} ${kindPrefix} ${qualified} FROM ${this.grantee}`;
101
113
  }
102
114
  }
@@ -1,5 +1,6 @@
1
1
  import { quoteLiteral } from "../../../base.change.js";
2
2
  import { stableId } from "../../../utils.js";
3
+ import { redactOptionValue } from "../../sensitive-options.js";
3
4
  import { AlterForeignDataWrapperChange } from "./foreign-data-wrapper.base.js";
4
5
  /**
5
6
  * ALTER FOREIGN DATA WRAPPER ... OWNER TO ...
@@ -47,7 +48,9 @@ export class AlterForeignDataWrapperSetOptions extends AlterForeignDataWrapperCh
47
48
  optionParts.push(`DROP ${opt.option}`);
48
49
  }
49
50
  else {
50
- const value = opt.value !== undefined ? quoteLiteral(opt.value) : "''";
51
+ const value = opt.value !== undefined
52
+ ? quoteLiteral(redactOptionValue(opt.option, opt.value))
53
+ : "''";
51
54
  optionParts.push(`${opt.action} ${opt.option} ${value}`);
52
55
  }
53
56
  }
@@ -1,5 +1,6 @@
1
1
  import { quoteLiteral } from "../../../base.change.js";
2
2
  import { stableId } from "../../../utils.js";
3
+ import { redactOptionValue } from "../../sensitive-options.js";
3
4
  import { CreateForeignDataWrapperChange } from "./foreign-data-wrapper.base.js";
4
5
  /**
5
6
  * Create a foreign data wrapper.
@@ -65,9 +66,11 @@ export class CreateForeignDataWrapper extends CreateForeignDataWrapperChange {
65
66
  this.foreignDataWrapper.options.length > 0) {
66
67
  const optionPairs = [];
67
68
  for (let i = 0; i < this.foreignDataWrapper.options.length; i += 2) {
68
- if (i + 1 < this.foreignDataWrapper.options.length) {
69
- optionPairs.push(`${this.foreignDataWrapper.options[i]} ${quoteLiteral(this.foreignDataWrapper.options[i + 1])}`);
70
- }
69
+ const key = this.foreignDataWrapper.options[i];
70
+ const value = this.foreignDataWrapper.options[i + 1];
71
+ if (key === undefined || value === undefined)
72
+ continue;
73
+ optionPairs.push(`${key} ${quoteLiteral(redactOptionValue(key, value))}`);
71
74
  }
72
75
  if (optionPairs.length > 0) {
73
76
  parts.push(`OPTIONS (${optionPairs.join(", ")})`);
@@ -55,5 +55,16 @@ export declare class ForeignDataWrapper extends BasePgModel {
55
55
  }[];
56
56
  };
57
57
  }
58
+ /**
59
+ * Extract `pg_foreign_data_wrapper` rows into `ForeignDataWrapper` models.
60
+ *
61
+ * The returned models carry option values **verbatim** from
62
+ * `pg_foreign_data_wrapper.fdwoptions`, which means a wrapper that ships
63
+ * shared credentials (`password`, `api_key`, …) would expose them
64
+ * cleartext in memory. Always route through `extractCatalog` (which
65
+ * calls `normalizeCatalog`) before emitting options to any output
66
+ * channel — see CLI-1467 and
67
+ * `packages/pg-delta/src/core/objects/foreign-data-wrapper/sensitive-options.ts`.
68
+ */
58
69
  export declare function extractForeignDataWrappers(pool: Pool): Promise<ForeignDataWrapper[]>;
59
70
  export {};
@@ -60,6 +60,17 @@ export class ForeignDataWrapper extends BasePgModel {
60
60
  };
61
61
  }
62
62
  }
63
+ /**
64
+ * Extract `pg_foreign_data_wrapper` rows into `ForeignDataWrapper` models.
65
+ *
66
+ * The returned models carry option values **verbatim** from
67
+ * `pg_foreign_data_wrapper.fdwoptions`, which means a wrapper that ships
68
+ * shared credentials (`password`, `api_key`, …) would expose them
69
+ * cleartext in memory. Always route through `extractCatalog` (which
70
+ * calls `normalizeCatalog`) before emitting options to any output
71
+ * channel — see CLI-1467 and
72
+ * `packages/pg-delta/src/core/objects/foreign-data-wrapper/sensitive-options.ts`.
73
+ */
63
74
  export async function extractForeignDataWrappers(pool) {
64
75
  const { rows: fdwRows } = await pool.query(sql `
65
76
  with extension_oids as (
@@ -1,5 +1,6 @@
1
1
  import { quoteLiteral } from "../../../base.change.js";
2
2
  import { stableId } from "../../../utils.js";
3
+ import { redactOptionValue } from "../../sensitive-options.js";
3
4
  import { AlterForeignTableChange } from "./foreign-table.base.js";
4
5
  /**
5
6
  * ALTER FOREIGN TABLE ... OWNER TO ...
@@ -234,7 +235,9 @@ export class AlterForeignTableSetOptions extends AlterForeignTableChange {
234
235
  optionParts.push(`DROP ${opt.option}`);
235
236
  }
236
237
  else {
237
- const value = opt.value !== undefined ? quoteLiteral(opt.value) : "''";
238
+ const value = opt.value !== undefined
239
+ ? quoteLiteral(redactOptionValue(opt.option, opt.value))
240
+ : "''";
238
241
  optionParts.push(`${opt.action} ${opt.option} ${value}`);
239
242
  }
240
243
  }
@@ -1,5 +1,6 @@
1
1
  import { quoteLiteral } from "../../../base.change.js";
2
2
  import { stableId } from "../../../utils.js";
3
+ import { redactOptionValue } from "../../sensitive-options.js";
3
4
  import { CreateForeignTableChange } from "./foreign-table.base.js";
4
5
  /**
5
6
  * Create a foreign table.
@@ -51,9 +52,11 @@ export class CreateForeignTable extends CreateForeignTableChange {
51
52
  if (this.foreignTable.options && this.foreignTable.options.length > 0) {
52
53
  const optionPairs = [];
53
54
  for (let i = 0; i < this.foreignTable.options.length; i += 2) {
54
- if (i + 1 < this.foreignTable.options.length) {
55
- optionPairs.push(`${this.foreignTable.options[i]} ${quoteLiteral(this.foreignTable.options[i + 1])}`);
56
- }
55
+ const key = this.foreignTable.options[i];
56
+ const value = this.foreignTable.options[i + 1];
57
+ if (key === undefined || value === undefined)
58
+ continue;
59
+ optionPairs.push(`${key} ${quoteLiteral(redactOptionValue(key, value))}`);
57
60
  }
58
61
  if (optionPairs.length > 0) {
59
62
  parts.push(`OPTIONS (${optionPairs.join(", ")})`);
@@ -135,5 +135,16 @@ export declare class ForeignTable extends BasePgModel implements TableLikeObject
135
135
  };
136
136
  };
137
137
  }
138
+ /**
139
+ * Extract `pg_foreign_table` rows into `ForeignTable` models.
140
+ *
141
+ * The returned models carry option values **verbatim** from
142
+ * `pg_foreign_table.ftoptions`, which means a wrapper that puts
143
+ * credentials at the table level (uncommon but possible) would expose
144
+ * them cleartext in memory. Always route through `extractCatalog`
145
+ * (which calls `normalizeCatalog`) before emitting options to any
146
+ * output channel — see CLI-1467 and
147
+ * `packages/pg-delta/src/core/objects/foreign-data-wrapper/sensitive-options.ts`.
148
+ */
138
149
  export declare function extractForeignTables(pool: Pool): Promise<ForeignTable[]>;
139
150
  export {};
@@ -89,6 +89,17 @@ export class ForeignTable extends BasePgModel {
89
89
  };
90
90
  }
91
91
  }
92
+ /**
93
+ * Extract `pg_foreign_table` rows into `ForeignTable` models.
94
+ *
95
+ * The returned models carry option values **verbatim** from
96
+ * `pg_foreign_table.ftoptions`, which means a wrapper that puts
97
+ * credentials at the table level (uncommon but possible) would expose
98
+ * them cleartext in memory. Always route through `extractCatalog`
99
+ * (which calls `normalizeCatalog`) before emitting options to any
100
+ * output channel — see CLI-1467 and
101
+ * `packages/pg-delta/src/core/objects/foreign-data-wrapper/sensitive-options.ts`.
102
+ */
92
103
  export async function extractForeignTables(pool) {
93
104
  const { rows: tableRows } = await pool.query(sql `
94
105
  with extension_oids as (
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Sensitive-option redaction for foreign-data-wrapper objects.
3
+ *
4
+ * Foreign servers (`pg_foreign_server.srvoptions`) and user mappings
5
+ * (`pg_user_mapping.umoptions`) store libpq/FDW credentials in cleartext.
6
+ * Any code path that emits these option values verbatim — plan SQL, catalog
7
+ * snapshots, declarative export, fingerprints — leaks the credentials to
8
+ * disk, stdout, CI logs, and version control.
9
+ *
10
+ * The redaction policy is **allowlist-based**: replace every option value
11
+ * with `__OPTION_<KEY>__` unless the option key appears in
12
+ * {@link SAFE_OPTION_KEYS}. Failure mode of a missing entry is "the plan
13
+ * shows the placeholder instead of the real value" — annoying, but safe;
14
+ * a denylist's failure mode was secrets leaking, which is the bug we are
15
+ * fixing (CLI-1467).
16
+ *
17
+ * Match is case-insensitive but exact — substrings do not match, so an
18
+ * option key like `password_validator_extension` will be redacted unless
19
+ * explicitly allowlisted. When a new wrapper introduces a non-credential
20
+ * key we want to surface in plans, add it here.
21
+ */
22
+ export declare function redactOptionValue(key: string, value: string): string;
23
+ /**
24
+ * Redact non-allowlisted values in a flat `[key, value, key, value, ...]`
25
+ * options array — the shape used by the {@link Server},
26
+ * {@link UserMapping}, {@link ForeignDataWrapper}, and {@link ForeignTable}
27
+ * models.
28
+ *
29
+ * Returns `null` for `null` input, and otherwise returns an array of the
30
+ * same length as the input with sensitive values replaced.
31
+ */
32
+ export declare function redactSensitiveOptionPairs(options: readonly string[] | null): string[] | null;
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Sensitive-option redaction for foreign-data-wrapper objects.
3
+ *
4
+ * Foreign servers (`pg_foreign_server.srvoptions`) and user mappings
5
+ * (`pg_user_mapping.umoptions`) store libpq/FDW credentials in cleartext.
6
+ * Any code path that emits these option values verbatim — plan SQL, catalog
7
+ * snapshots, declarative export, fingerprints — leaks the credentials to
8
+ * disk, stdout, CI logs, and version control.
9
+ *
10
+ * The redaction policy is **allowlist-based**: replace every option value
11
+ * with `__OPTION_<KEY>__` unless the option key appears in
12
+ * {@link SAFE_OPTION_KEYS}. Failure mode of a missing entry is "the plan
13
+ * shows the placeholder instead of the real value" — annoying, but safe;
14
+ * a denylist's failure mode was secrets leaking, which is the bug we are
15
+ * fixing (CLI-1467).
16
+ *
17
+ * Match is case-insensitive but exact — substrings do not match, so an
18
+ * option key like `password_validator_extension` will be redacted unless
19
+ * explicitly allowlisted. When a new wrapper introduces a non-credential
20
+ * key we want to surface in plans, add it here.
21
+ */
22
+ const SAFE_OPTION_KEYS = new Set([
23
+ // libpq connection params (non-credential subset).
24
+ // https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-PARAMKEYWORDS
25
+ "host",
26
+ "hostaddr",
27
+ "port",
28
+ "dbname",
29
+ "user",
30
+ "sslmode",
31
+ "sslcompression",
32
+ "sslcert",
33
+ "sslkey",
34
+ "sslrootcert",
35
+ "sslcrl",
36
+ "sslcrldir",
37
+ "sslsni",
38
+ "requirepeer",
39
+ "krbsrvname",
40
+ "gsslib",
41
+ "sspi",
42
+ "gssencmode",
43
+ "gssdelegation",
44
+ "channel_binding",
45
+ "target_session_attrs",
46
+ "application_name",
47
+ "fallback_application_name",
48
+ "connect_timeout",
49
+ "client_encoding",
50
+ "options",
51
+ "keepalives",
52
+ "keepalives_idle",
53
+ "keepalives_interval",
54
+ "keepalives_count",
55
+ "tcp_user_timeout",
56
+ "replication",
57
+ "load_balance_hosts",
58
+ // postgres_fdw behavior tuning.
59
+ // https://www.postgresql.org/docs/current/postgres-fdw.html#POSTGRES-FDW-OPTIONS-CONNECTION
60
+ "use_remote_estimate",
61
+ "fdw_startup_cost",
62
+ "fdw_tuple_cost",
63
+ "fetch_size",
64
+ "batch_size",
65
+ "async_capable",
66
+ "analyze_sampling",
67
+ "parallel_commit",
68
+ "parallel_abort",
69
+ "extensions",
70
+ "updatable",
71
+ "truncatable",
72
+ "schema_name",
73
+ "table_name",
74
+ "column_name",
75
+ // Common shape for table-like FDWs (file_fdw, cloud-storage wrappers).
76
+ "schema",
77
+ "database",
78
+ "table",
79
+ "format",
80
+ "header",
81
+ "delimiter",
82
+ "quote",
83
+ "escape",
84
+ "encoding",
85
+ "compression",
86
+ // Cloud / Supabase Wrappers non-credential shape.
87
+ // https://github.com/supabase/wrappers
88
+ "region",
89
+ "endpoint",
90
+ "bucket",
91
+ "prefix",
92
+ "location",
93
+ "project_id",
94
+ "dataset_id",
95
+ "dataset",
96
+ "workspace",
97
+ "organization",
98
+ "api_version",
99
+ ]);
100
+ function redactedOptionPlaceholder(key) {
101
+ return `__OPTION_${key.toUpperCase()}__`;
102
+ }
103
+ export function redactOptionValue(key, value) {
104
+ return SAFE_OPTION_KEYS.has(key.toLowerCase())
105
+ ? value
106
+ : redactedOptionPlaceholder(key);
107
+ }
108
+ /**
109
+ * Redact non-allowlisted values in a flat `[key, value, key, value, ...]`
110
+ * options array — the shape used by the {@link Server},
111
+ * {@link UserMapping}, {@link ForeignDataWrapper}, and {@link ForeignTable}
112
+ * models.
113
+ *
114
+ * Returns `null` for `null` input, and otherwise returns an array of the
115
+ * same length as the input with sensitive values replaced.
116
+ */
117
+ export function redactSensitiveOptionPairs(options) {
118
+ if (options === null)
119
+ return null;
120
+ const result = [];
121
+ for (let i = 0; i < options.length; i += 2) {
122
+ const key = options[i];
123
+ const value = options[i + 1];
124
+ if (key === undefined || value === undefined)
125
+ continue;
126
+ result.push(key, redactOptionValue(key, value));
127
+ }
128
+ return result;
129
+ }
@@ -1,5 +1,6 @@
1
1
  import { quoteLiteral } from "../../../base.change.js";
2
2
  import { stableId } from "../../../utils.js";
3
+ import { redactOptionValue } from "../../sensitive-options.js";
3
4
  import { AlterServerChange } from "./server.base.js";
4
5
  /**
5
6
  * ALTER SERVER ... OWNER TO ...
@@ -70,7 +71,9 @@ export class AlterServerSetOptions extends AlterServerChange {
70
71
  optionParts.push(`DROP ${opt.option}`);
71
72
  }
72
73
  else {
73
- const value = opt.value !== undefined ? quoteLiteral(opt.value) : "''";
74
+ const value = opt.value !== undefined
75
+ ? quoteLiteral(redactOptionValue(opt.option, opt.value))
76
+ : "''";
74
77
  optionParts.push(`${opt.action} ${opt.option} ${value}`);
75
78
  }
76
79
  }
@@ -1,5 +1,6 @@
1
1
  import { quoteLiteral } from "../../../base.change.js";
2
2
  import { stableId } from "../../../utils.js";
3
+ import { redactOptionValue } from "../../sensitive-options.js";
3
4
  import { CreateServerChange } from "./server.base.js";
4
5
  /**
5
6
  * Create a server.
@@ -49,9 +50,11 @@ export class CreateServer extends CreateServerChange {
49
50
  if (this.server.options && this.server.options.length > 0) {
50
51
  const optionPairs = [];
51
52
  for (let i = 0; i < this.server.options.length; i += 2) {
52
- if (i + 1 < this.server.options.length) {
53
- optionPairs.push(`${this.server.options[i]} ${quoteLiteral(this.server.options[i + 1])}`);
54
- }
53
+ const key = this.server.options[i];
54
+ const value = this.server.options[i + 1];
55
+ if (key === undefined || value === undefined)
56
+ continue;
57
+ optionPairs.push(`${key} ${quoteLiteral(redactOptionValue(key, value))}`);
55
58
  }
56
59
  if (optionPairs.length > 0) {
57
60
  parts.push(`OPTIONS (${optionPairs.join(", ")})`);