@supabase/pg-delta 1.0.0-alpha.13 → 1.0.0-alpha.15

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 (37) hide show
  1. package/README.md +7 -1
  2. package/dist/core/catalog.diff.js +7 -1
  3. package/dist/core/connection-url.d.ts +32 -0
  4. package/dist/core/connection-url.js +77 -0
  5. package/dist/core/expand-replace-dependencies.d.ts +8 -2
  6. package/dist/core/expand-replace-dependencies.js +24 -10
  7. package/dist/core/integrations/supabase.js +1 -0
  8. package/dist/core/objects/procedure/procedure.diff.js +8 -0
  9. package/dist/core/objects/sequence/sequence.diff.js +14 -6
  10. package/dist/core/objects/table/changes/table.alter.js +4 -1
  11. package/dist/core/objects/table/changes/table.drop.d.ts +12 -0
  12. package/dist/core/objects/table/changes/table.drop.js +20 -3
  13. package/dist/core/objects/table/table.diff.js +7 -2
  14. package/dist/core/post-diff-cycle-breaking.d.ts +22 -0
  15. package/dist/core/post-diff-cycle-breaking.js +143 -0
  16. package/dist/core/postgres-config.d.ts +27 -0
  17. package/dist/core/postgres-config.js +99 -7
  18. package/package.json +2 -1
  19. package/src/core/catalog.diff.ts +7 -1
  20. package/src/core/connection-url.test.ts +142 -0
  21. package/src/core/connection-url.ts +82 -0
  22. package/src/core/expand-replace-dependencies.test.ts +247 -8
  23. package/src/core/expand-replace-dependencies.ts +33 -5
  24. package/src/core/integrations/supabase.ts +1 -0
  25. package/src/core/objects/procedure/procedure.diff.test.ts +25 -0
  26. package/src/core/objects/procedure/procedure.diff.ts +12 -0
  27. package/src/core/objects/sequence/sequence.diff.test.ts +110 -8
  28. package/src/core/objects/sequence/sequence.diff.ts +16 -6
  29. package/src/core/objects/table/changes/table.alter.test.ts +14 -0
  30. package/src/core/objects/table/changes/table.alter.ts +4 -1
  31. package/src/core/objects/table/changes/table.drop.ts +27 -4
  32. package/src/core/objects/table/table.diff.test.ts +55 -0
  33. package/src/core/objects/table/table.diff.ts +10 -2
  34. package/src/core/post-diff-cycle-breaking.test.ts +317 -0
  35. package/src/core/post-diff-cycle-breaking.ts +236 -0
  36. package/src/core/postgres-config.test.ts +241 -0
  37. package/src/core/postgres-config.ts +127 -16
@@ -7,10 +7,21 @@ import { CreateSequence } from "./objects/sequence/changes/sequence.create.ts";
7
7
  import { DropSequence } from "./objects/sequence/changes/sequence.drop.ts";
8
8
  import { diffSequences } from "./objects/sequence/sequence.diff.ts";
9
9
  import { Sequence } from "./objects/sequence/sequence.model.ts";
10
- import { AlterTableAlterColumnSetDefault } from "./objects/table/changes/table.alter.ts";
10
+ import {
11
+ AlterTableAlterColumnSetDefault,
12
+ AlterTableChangeOwner,
13
+ AlterTableDropColumn,
14
+ AlterTableDropConstraint,
15
+ AlterTableEnableRowLevelSecurity,
16
+ AlterTableSetReplicaIdentity,
17
+ } from "./objects/table/changes/table.alter.ts";
11
18
  import { CreateTable } from "./objects/table/changes/table.create.ts";
12
19
  import { DropTable } from "./objects/table/changes/table.drop.ts";
20
+ import { GrantTablePrivileges } from "./objects/table/changes/table.privilege.ts";
13
21
  import { Table } from "./objects/table/table.model.ts";
22
+ import { CreateEnum } from "./objects/type/enum/changes/enum.create.ts";
23
+ import { DropEnum } from "./objects/type/enum/changes/enum.drop.ts";
24
+ import { Enum } from "./objects/type/enum/enum.model.ts";
14
25
 
15
26
  function mockChange(overrides: {
16
27
  creates?: string[];
@@ -43,8 +54,9 @@ describe("expandReplaceDependencies", () => {
43
54
  mainCatalog: catalog,
44
55
  branchCatalog: catalog,
45
56
  });
46
- expect(result).toHaveLength(1);
47
- expect(result).toBe(changes);
57
+ expect(result.changes).toHaveLength(1);
58
+ expect(result.changes).toBe(changes);
59
+ expect(result.replacedTableIds.size).toBe(0);
48
60
  });
49
61
 
50
62
  test("returns changes unchanged when replace roots have no dependents in catalog", async () => {
@@ -60,8 +72,9 @@ describe("expandReplaceDependencies", () => {
60
72
  mainCatalog: catalog,
61
73
  branchCatalog: catalog,
62
74
  });
63
- expect(result).toHaveLength(1);
64
- expect(result[0]).toBe(changes[0]);
75
+ expect(result.changes).toHaveLength(1);
76
+ expect(result.changes[0]).toBe(changes[0]);
77
+ expect(result.replacedTableIds.size).toBe(0);
65
78
  });
66
79
 
67
80
  test("returns same array reference when replaceRoots.size is 0", async () => {
@@ -74,7 +87,8 @@ describe("expandReplaceDependencies", () => {
74
87
  mainCatalog: catalog,
75
88
  branchCatalog: catalog,
76
89
  });
77
- expect(result).toBe(changes);
90
+ expect(result.changes).toBe(changes);
91
+ expect(result.replacedTableIds.size).toBe(0);
78
92
  });
79
93
 
80
94
  test("does not replace the owning table for an owned sequence recreation", async () => {
@@ -187,9 +201,234 @@ describe("expandReplaceDependencies", () => {
187
201
  expect(changes[0]).toBeInstanceOf(DropSequence);
188
202
  expect(changes[1]).toBeInstanceOf(CreateSequence);
189
203
  expect(changes[3]).toBeInstanceOf(AlterTableAlterColumnSetDefault);
190
- expect(expanded.some((change) => change instanceof DropTable)).toBe(false);
191
- expect(expanded.some((change) => change instanceof CreateTable)).toBe(
204
+ expect(expanded.changes.some((change) => change instanceof DropTable)).toBe(
192
205
  false,
193
206
  );
207
+ expect(
208
+ expanded.changes.some((change) => change instanceof CreateTable),
209
+ ).toBe(false);
210
+ expect(expanded.replacedTableIds.size).toBe(0);
211
+ });
212
+
213
+ test("reports replaced tables for downstream post-diff normalization", async () => {
214
+ // Reproduction guard for the enum-replacement expansion case: the expander
215
+ // must report which dependent tables it promoted to DropTable+CreateTable,
216
+ // but the pruning of same-table AlterTableDropColumn/DropConstraint belongs
217
+ // to the later post-diff normalization pass, not this expansion step.
218
+ const baseline = await createEmptyCatalog(170000, "postgres");
219
+ const mainEnum = new Enum({
220
+ schema: "public",
221
+ name: "item_status",
222
+ owner: "postgres",
223
+ labels: [
224
+ { sort_order: 1, label: "draft" },
225
+ { sort_order: 2, label: "published" },
226
+ { sort_order: 3, label: "archived" },
227
+ ],
228
+ comment: null,
229
+ privileges: [],
230
+ });
231
+ const branchEnum = new Enum({
232
+ ...mainEnum,
233
+ labels: [
234
+ { sort_order: 1, label: "draft" },
235
+ { sort_order: 2, label: "published" },
236
+ ],
237
+ });
238
+ const columnTemplate = {
239
+ data_type: "integer" as const,
240
+ data_type_str: "integer",
241
+ is_custom_type: false as const,
242
+ custom_type_type: null,
243
+ custom_type_category: null,
244
+ custom_type_schema: null,
245
+ custom_type_name: null,
246
+ not_null: false,
247
+ is_identity: false,
248
+ is_identity_always: false,
249
+ is_generated: false,
250
+ collation: null,
251
+ default: null,
252
+ comment: null,
253
+ };
254
+ const mainChildren = new Table({
255
+ schema: "public",
256
+ name: "children",
257
+ persistence: "p",
258
+ row_security: false,
259
+ force_row_security: false,
260
+ has_indexes: false,
261
+ has_rules: false,
262
+ has_triggers: false,
263
+ has_subclasses: false,
264
+ is_populated: true,
265
+ replica_identity: "d",
266
+ is_partition: false,
267
+ options: null,
268
+ partition_bound: null,
269
+ partition_by: null,
270
+ owner: "postgres",
271
+ comment: null,
272
+ parent_schema: null,
273
+ parent_name: null,
274
+ columns: [
275
+ { ...columnTemplate, name: "id", position: 1, not_null: true },
276
+ { ...columnTemplate, name: "parent_ref", position: 2 },
277
+ {
278
+ ...columnTemplate,
279
+ name: "status",
280
+ position: 3,
281
+ data_type: "item_status",
282
+ data_type_str: "public.item_status",
283
+ is_custom_type: true,
284
+ custom_type_type: "e",
285
+ custom_type_category: "E",
286
+ custom_type_schema: "public",
287
+ custom_type_name: "item_status",
288
+ },
289
+ ],
290
+ privileges: [],
291
+ });
292
+ const branchChildren = new Table({
293
+ ...mainChildren,
294
+ columns: [
295
+ { ...columnTemplate, name: "id", position: 1, not_null: true },
296
+ {
297
+ ...columnTemplate,
298
+ name: "status",
299
+ position: 2,
300
+ data_type: "item_status",
301
+ data_type_str: "public.item_status",
302
+ is_custom_type: true,
303
+ custom_type_type: "e",
304
+ custom_type_category: "E",
305
+ custom_type_schema: "public",
306
+ custom_type_name: "item_status",
307
+ },
308
+ ],
309
+ });
310
+
311
+ // Pre-existing planner output: the enum replacement from diffEnums plus
312
+ // targeted ALTER TABLE statements from diffTables. The two cycle-forming
313
+ // ALTERs (drop-column, drop-constraint) must be elided. The privilege
314
+ // ALTER and the owner / RLS / replica-identity ALTERs must all survive.
315
+ const droppedColumn = mainChildren.columns.find(
316
+ (c) => c.name === "parent_ref",
317
+ );
318
+ if (!droppedColumn) throw new Error("test setup: parent_ref missing");
319
+ const preExistingDropColumn = new AlterTableDropColumn({
320
+ table: mainChildren,
321
+ column: droppedColumn,
322
+ });
323
+ const preExistingDropConstraint = new AlterTableDropConstraint({
324
+ table: mainChildren,
325
+ constraint: {
326
+ name: "children_parent_ref_fkey",
327
+ constraint_type: "f",
328
+ deferrable: false,
329
+ initially_deferred: false,
330
+ validated: true,
331
+ is_local: true,
332
+ no_inherit: false,
333
+ is_partition_clone: false,
334
+ parent_constraint_schema: null,
335
+ parent_constraint_name: null,
336
+ parent_table_schema: null,
337
+ parent_table_name: null,
338
+ key_columns: ["parent_ref"],
339
+ foreign_key_columns: ["id"],
340
+ foreign_key_table: "parents",
341
+ foreign_key_schema: "public",
342
+ foreign_key_table_is_partition: false,
343
+ foreign_key_parent_schema: null,
344
+ foreign_key_parent_table: null,
345
+ foreign_key_effective_schema: "public",
346
+ foreign_key_effective_table: "parents",
347
+ on_update: "a",
348
+ on_delete: "a",
349
+ match_type: "s",
350
+ check_expression: null,
351
+ owner: "postgres",
352
+ definition: "FOREIGN KEY (parent_ref) REFERENCES public.parents(id)",
353
+ comment: null,
354
+ },
355
+ });
356
+ const preExistingChangeOwner = new AlterTableChangeOwner({
357
+ table: branchChildren,
358
+ owner: "new_owner",
359
+ });
360
+ const preExistingEnableRls = new AlterTableEnableRowLevelSecurity({
361
+ table: branchChildren,
362
+ });
363
+ const preExistingReplicaIdentity = new AlterTableSetReplicaIdentity({
364
+ table: branchChildren,
365
+ mode: "f",
366
+ });
367
+ const preExistingGrant = new GrantTablePrivileges({
368
+ table: branchChildren,
369
+ grantee: "reader",
370
+ privileges: [{ privilege: "SELECT", grantable: false }],
371
+ });
372
+ const changes: Change[] = [
373
+ new DropEnum({ enum: mainEnum }),
374
+ new CreateEnum({ enum: branchEnum }),
375
+ preExistingDropColumn,
376
+ preExistingDropConstraint,
377
+ preExistingChangeOwner,
378
+ preExistingEnableRls,
379
+ preExistingReplicaIdentity,
380
+ preExistingGrant,
381
+ ];
382
+
383
+ const mainCatalog = new Catalog({
384
+ ...baseline,
385
+ enums: { [mainEnum.stableId]: mainEnum },
386
+ tables: { [mainChildren.stableId]: mainChildren },
387
+ // pg_depend: column children.status depends on type item_status.
388
+ depends: [
389
+ {
390
+ dependent_stable_id: "column:public.children.status",
391
+ referenced_stable_id: mainEnum.stableId,
392
+ deptype: "n",
393
+ },
394
+ ],
395
+ });
396
+ const branchCatalog = new Catalog({
397
+ ...baseline,
398
+ enums: { [branchEnum.stableId]: branchEnum },
399
+ tables: { [branchChildren.stableId]: branchChildren },
400
+ depends: [],
401
+ });
402
+
403
+ const expanded = expandReplaceDependencies({
404
+ changes,
405
+ mainCatalog,
406
+ branchCatalog,
407
+ });
408
+
409
+ // The replace-table pair was added.
410
+ expect(expanded.changes.some((c) => c instanceof DropTable)).toBe(true);
411
+ expect(expanded.changes.some((c) => c instanceof CreateTable)).toBe(true);
412
+ expect(expanded.replacedTableIds.has(mainChildren.stableId)).toBe(true);
413
+ // Expansion itself keeps the pre-existing ALTERs; the post-diff cycle pass
414
+ // decides which of them are superseded by the replacement.
415
+ expect(expanded.changes).toContain(preExistingDropColumn);
416
+ expect(expanded.changes).toContain(preExistingDropConstraint);
417
+ expect(
418
+ expanded.changes.some((c) => c instanceof AlterTableDropColumn),
419
+ ).toBe(true);
420
+ expect(
421
+ expanded.changes.some((c) => c instanceof AlterTableDropConstraint),
422
+ ).toBe(true);
423
+ // The enum replace roots are still present.
424
+ expect(expanded.changes.some((c) => c instanceof DropEnum)).toBe(true);
425
+ expect(expanded.changes.some((c) => c instanceof CreateEnum)).toBe(true);
426
+ // Non-cycle object-scope ALTERs are carried through untouched.
427
+ expect(expanded.changes).toContain(preExistingChangeOwner);
428
+ expect(expanded.changes).toContain(preExistingEnableRls);
429
+ expect(expanded.changes).toContain(preExistingReplicaIdentity);
430
+ // Privilege-scope ALTER on the recreated table survives.
431
+ expect(expanded.changes).toContain(preExistingGrant);
432
+ expect(expanded.replacedTableIds.has("table:public.parents")).toBe(false);
194
433
  });
195
434
  });
@@ -70,8 +70,14 @@ type ResolvedObject =
70
70
  * replaced so that destructive drops succeed. Uses dependency edges from pg_depend
71
71
  * (already captured in Catalog.depends) plus change metadata (creates/drops/requires).
72
72
  *
73
- * New changes are appended; ordering is handled later by the sorter.
73
+ * New changes are appended; ordering and any multi-statement cycle normalization
74
+ * are handled later by post-diff helpers and the sorter.
74
75
  */
76
+ interface ExpandReplaceDependenciesResult {
77
+ changes: Change[];
78
+ replacedTableIds: ReadonlySet<string>;
79
+ }
80
+
75
81
  export function expandReplaceDependencies({
76
82
  changes,
77
83
  mainCatalog,
@@ -80,7 +86,7 @@ export function expandReplaceDependencies({
80
86
  changes: Change[];
81
87
  mainCatalog: Catalog;
82
88
  branchCatalog: Catalog;
83
- }): Change[] {
89
+ }): ExpandReplaceDependenciesResult {
84
90
  const createdIds = new Set<string>();
85
91
  const droppedIds = new Set<string>();
86
92
 
@@ -97,7 +103,10 @@ export function expandReplaceDependencies({
97
103
  }
98
104
 
99
105
  if (replaceRoots.size === 0) {
100
- return changes;
106
+ return {
107
+ changes,
108
+ replacedTableIds: new Set<string>(),
109
+ };
101
110
  }
102
111
 
103
112
  // Build referenced -> dependents adjacency from main catalog dependencies.
@@ -115,6 +124,12 @@ export function expandReplaceDependencies({
115
124
  const visitedTargets = new Set<string>();
116
125
  const visitedRefs = new Set<string>(replaceRoots);
117
126
  const queue: string[] = [...replaceRoots];
127
+ // Tables being replaced by an expansion-added DropTable+CreateTable pair.
128
+ // Any pre-existing targeted AlterTable*(T) object-scope change is superseded
129
+ // by the replacement and must be removed to avoid contradictions (e.g. an
130
+ // AlterTableDropColumn on a table that is about to be dropped) and the
131
+ // associated drop-phase cycle with the catalog constraint→column edge.
132
+ const tablesReplacedByExpansion = new Set<string>();
118
133
 
119
134
  while (queue.length > 0) {
120
135
  const refId = queue.shift() as string;
@@ -178,6 +193,13 @@ export function expandReplaceDependencies({
178
193
 
179
194
  additions.push(...replacementChanges);
180
195
 
196
+ // If we added a DropTable(T) for an existing table, mark T so any
197
+ // pre-existing object-scope AlterTable*(T) changes get dropped below —
198
+ // the DropTable+CreateTable pair supersedes all structural alterations.
199
+ if (resolved.kind === "table" && addDrop) {
200
+ tablesReplacedByExpansion.add(targetId);
201
+ }
202
+
181
203
  // Track new creates/drops so we don't duplicate work for downstream dependents.
182
204
  for (const change of replacementChanges) {
183
205
  for (const id of change.creates ?? []) createdIds.add(id);
@@ -187,10 +209,16 @@ export function expandReplaceDependencies({
187
209
  }
188
210
 
189
211
  if (additions.length === 0) {
190
- return changes;
212
+ return {
213
+ changes,
214
+ replacedTableIds: tablesReplacedByExpansion,
215
+ };
191
216
  }
192
217
 
193
- return [...changes, ...additions];
218
+ return {
219
+ changes: [...changes, ...additions],
220
+ replacedTableIds: tablesReplacedByExpansion,
221
+ };
194
222
  }
195
223
 
196
224
  function isOwnedSequenceColumnDependency(
@@ -16,6 +16,7 @@ const SUPABASE_SYSTEM_SCHEMAS = [
16
16
  "_supavisor",
17
17
  "auth",
18
18
  "cron",
19
+ "etl",
19
20
  "extensions",
20
21
  "graphql",
21
22
  "graphql_public",
@@ -9,6 +9,7 @@ import {
9
9
  AlterProcedureSetStrictness,
10
10
  AlterProcedureSetVolatility,
11
11
  } from "./changes/procedure.alter.ts";
12
+ import { CreateCommentOnProcedure } from "./changes/procedure.comment.ts";
12
13
  import { CreateProcedure } from "./changes/procedure.create.ts";
13
14
  import { DropProcedure } from "./changes/procedure.drop.ts";
14
15
  import { diffProcedures } from "./procedure.diff.ts";
@@ -158,4 +159,28 @@ describe.concurrent("procedure.diff", () => {
158
159
  expect(changes).toHaveLength(1);
159
160
  expect(changes[0]).toBeInstanceOf(CreateProcedure);
160
161
  });
162
+
163
+ test("create or replace also emits a procedure comment when the comment changes", () => {
164
+ const main = new Procedure(base);
165
+ const branch = new Procedure({
166
+ ...base,
167
+ definition:
168
+ "CREATE FUNCTION public.fn1() RETURNS int4 LANGUAGE sql AS $$SELECT 42::int4$$",
169
+ source_code: "SELECT 42::int4",
170
+ comment: "updated comment",
171
+ });
172
+
173
+ const changes = diffProcedures(
174
+ testContext,
175
+ { [main.stableId]: main },
176
+ { [branch.stableId]: branch },
177
+ );
178
+
179
+ expect(changes.some((change) => change instanceof CreateProcedure)).toBe(
180
+ true,
181
+ );
182
+ expect(
183
+ changes.some((change) => change instanceof CreateCommentOnProcedure),
184
+ ).toBe(true);
185
+ });
161
186
  });
@@ -169,6 +169,18 @@ export function diffProcedures(
169
169
  changes.push(
170
170
  new CreateProcedure({ procedure: branchProcedure, orReplace: true }),
171
171
  );
172
+
173
+ if (mainProcedure.comment !== branchProcedure.comment) {
174
+ if (branchProcedure.comment === null) {
175
+ changes.push(
176
+ new DropCommentOnProcedure({ procedure: mainProcedure }),
177
+ );
178
+ } else {
179
+ changes.push(
180
+ new CreateCommentOnProcedure({ procedure: branchProcedure }),
181
+ );
182
+ }
183
+ }
172
184
  } else {
173
185
  // Only alterable properties changed - check each one
174
186
 
@@ -198,28 +198,130 @@ describe.concurrent("sequence.diff", () => {
198
198
  expect(changes).toHaveLength(0);
199
199
  });
200
200
 
201
- test("generate DROP SEQUENCE when owned by table that still exists", () => {
201
+ test("generate DROP SEQUENCE when owned by table/column that still exists", () => {
202
202
  const ownedSequence = new Sequence({
203
203
  ...base,
204
204
  owned_by_schema: "public",
205
205
  owned_by_table: "users",
206
206
  owned_by_column: "id",
207
207
  });
208
- // When the owning table still exists in branch catalog,
209
- // DROP SEQUENCE should be generated
208
+ const branchTable = new Table({
209
+ schema: "public",
210
+ name: "users",
211
+ persistence: "p",
212
+ row_security: false,
213
+ force_row_security: false,
214
+ has_indexes: false,
215
+ has_rules: false,
216
+ has_triggers: false,
217
+ has_subclasses: false,
218
+ is_populated: true,
219
+ replica_identity: "d",
220
+ is_partition: false,
221
+ options: null,
222
+ partition_bound: null,
223
+ partition_by: null,
224
+ owner: "test",
225
+ comment: null,
226
+ parent_schema: null,
227
+ parent_name: null,
228
+ columns: [
229
+ {
230
+ name: "id",
231
+ position: 1,
232
+ data_type: "bigint",
233
+ data_type_str: "bigint",
234
+ is_custom_type: false,
235
+ custom_type_type: null,
236
+ custom_type_category: null,
237
+ custom_type_schema: null,
238
+ custom_type_name: null,
239
+ not_null: true,
240
+ is_identity: false,
241
+ is_identity_always: false,
242
+ is_generated: false,
243
+ collation: null,
244
+ default: null,
245
+ comment: null,
246
+ },
247
+ ],
248
+ privileges: [],
249
+ });
250
+ // When the owning table AND the owning column still exist in branch catalog,
251
+ // DROP SEQUENCE should be generated (sequence is orphaned and needs explicit drop).
210
252
  const changes = diffSequences(
211
253
  testContext,
212
254
  { [ownedSequence.stableId]: ownedSequence },
213
- {}, // branch has no sequences
214
- {
215
- "table:public.users": {} as Table, // table still exists
216
- },
255
+ {},
256
+ { [branchTable.stableId]: branchTable },
217
257
  );
218
- // Should generate DROP SEQUENCE since table still exists
219
258
  expect(changes).toHaveLength(1);
220
259
  expect(changes[0]).toBeInstanceOf(DropSequence);
221
260
  });
222
261
 
262
+ test("skip DROP SEQUENCE when owning column is dropped but table survives", () => {
263
+ // Reproduction guard for the DropSequence ↔ AlterTableDropColumn cycle:
264
+ // dropping a SERIAL column on a surviving table leaves PG to cascade-drop
265
+ // the owned sequence. Emitting DROP SEQUENCE here would both fail at apply
266
+ // time AND form an unbreakable drop-phase cycle with AlterTableDropColumn.
267
+ const ownedSequence = new Sequence({
268
+ ...base,
269
+ owned_by_schema: "public",
270
+ owned_by_table: "widgets",
271
+ owned_by_column: "id",
272
+ });
273
+ const branchTable = new Table({
274
+ schema: "public",
275
+ name: "widgets",
276
+ persistence: "p",
277
+ row_security: false,
278
+ force_row_security: false,
279
+ has_indexes: false,
280
+ has_rules: false,
281
+ has_triggers: false,
282
+ has_subclasses: false,
283
+ is_populated: true,
284
+ replica_identity: "d",
285
+ is_partition: false,
286
+ options: null,
287
+ partition_bound: null,
288
+ partition_by: null,
289
+ owner: "test",
290
+ comment: null,
291
+ parent_schema: null,
292
+ parent_name: null,
293
+ // `id` column has been dropped in branch; only `label` remains.
294
+ columns: [
295
+ {
296
+ name: "label",
297
+ position: 1,
298
+ data_type: "text",
299
+ data_type_str: "text",
300
+ is_custom_type: false,
301
+ custom_type_type: null,
302
+ custom_type_category: null,
303
+ custom_type_schema: null,
304
+ custom_type_name: null,
305
+ not_null: false,
306
+ is_identity: false,
307
+ is_identity_always: false,
308
+ is_generated: false,
309
+ collation: null,
310
+ default: null,
311
+ comment: null,
312
+ },
313
+ ],
314
+ privileges: [],
315
+ });
316
+ const changes = diffSequences(
317
+ testContext,
318
+ { [ownedSequence.stableId]: ownedSequence },
319
+ {},
320
+ { [branchTable.stableId]: branchTable },
321
+ );
322
+ expect(changes).toHaveLength(0);
323
+ });
324
+
223
325
  test("create with comment emits CreateCommentOnSequence", () => {
224
326
  const s = new Sequence({ ...base, comment: "my seq" });
225
327
  const changes = diffSequences(testContext, {}, { [s.stableId]: s });
@@ -116,18 +116,28 @@ export function diffSequences(
116
116
 
117
117
  for (const sequenceId of dropped) {
118
118
  const sequence = main[sequenceId];
119
- // Skip generating DROP SEQUENCE if the sequence is owned by a table that's being dropped.
120
- // PostgreSQL automatically drops sequences owned by tables when the table is dropped,
121
- // so generating DROP SEQUENCE would cause an error (sequence doesn't exist).
119
+ // Skip generating DROP SEQUENCE if the sequence is owned by a table/column that's being dropped.
120
+ // PostgreSQL automatically cascades owned sequences when the owning table OR the owning
121
+ // column is dropped (via OWNED BY). Emitting DROP SEQUENCE in those cases would either
122
+ // fail at apply time (sequence already gone) or — in the column-drop case — create an
123
+ // unbreakable DropSequence ↔ AlterTableDropColumn cycle in the drop-phase sort graph.
122
124
  if (
123
125
  sequence.owned_by_schema &&
124
126
  sequence.owned_by_table &&
125
127
  sequence.owned_by_column
126
128
  ) {
127
129
  const ownedByTableId = `table:${sequence.owned_by_schema}.${sequence.owned_by_table}`;
128
- // If the owning table doesn't exist in branch catalog, it's being dropped
129
- // and will auto-drop this sequence, so skip generating DROP SEQUENCE
130
- if (!(ownedByTableId in branchTables)) {
130
+ const ownedByTable = branchTables[ownedByTableId];
131
+ // Owning table is dropped PG auto-drops the owned sequence.
132
+ if (!ownedByTable) {
133
+ continue;
134
+ }
135
+ // Owning column is dropped (table survives) → PG still auto-drops the owned
136
+ // sequence as part of the column drop, so we must not emit DROP SEQUENCE.
137
+ const ownedByColumnExists = ownedByTable.columns?.some(
138
+ (col) => col.name === sequence.owned_by_column,
139
+ );
140
+ if (!ownedByColumnExists) {
131
141
  continue;
132
142
  }
133
143
  }
@@ -492,6 +492,20 @@ describe.concurrent("table", () => {
492
492
  "ALTER TABLE public.test_table ALTER COLUMN a SET DEFAULT 0",
493
493
  );
494
494
 
495
+ const changeSetGeneratedExpression = new AlterTableAlterColumnSetDefault({
496
+ table: withCols,
497
+ column: {
498
+ ...colText,
499
+ name: "computed_name",
500
+ is_generated: true,
501
+ default: "lower((b))",
502
+ },
503
+ });
504
+ await assertValidSql(changeSetGeneratedExpression.serialize());
505
+ expect(changeSetGeneratedExpression.serialize()).toBe(
506
+ "ALTER TABLE public.test_table ALTER COLUMN computed_name SET EXPRESSION AS (lower((b)))",
507
+ );
508
+
495
509
  const changeDropDefault = new AlterTableAlterColumnDropDefault({
496
510
  table: withCols,
497
511
  column: { ...colInt, default: null },
@@ -644,6 +644,9 @@ export class AlterTableAlterColumnSetDefault extends AlterTableChange {
644
644
 
645
645
  serialize(_options?: SerializeOptions): string {
646
646
  const set = this.column.is_generated ? "SET EXPRESSION AS" : "SET DEFAULT";
647
+ const value = this.column.is_generated
648
+ ? `(${this.column.default ?? "NULL"})`
649
+ : (this.column.default ?? "NULL");
647
650
 
648
651
  return [
649
652
  "ALTER TABLE",
@@ -651,7 +654,7 @@ export class AlterTableAlterColumnSetDefault extends AlterTableChange {
651
654
  "ALTER COLUMN",
652
655
  this.column.name,
653
656
  set,
654
- this.column.default ?? "NULL",
657
+ value,
655
658
  ].join(" ");
656
659
  }
657
660
  }