@supabase/pg-delta 1.0.0-alpha.20 → 1.0.0-alpha.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/catalog.diff.js +4 -4
- package/dist/core/catalog.model.d.ts +8 -1
- package/dist/core/catalog.model.js +9 -8
- package/dist/core/expand-replace-dependencies.js +23 -0
- package/dist/core/objects/extract-with-retry.d.ts +36 -0
- package/dist/core/objects/extract-with-retry.js +51 -0
- package/dist/core/objects/index/index.diff.js +0 -1
- package/dist/core/objects/index/index.model.d.ts +2 -3
- package/dist/core/objects/index/index.model.js +17 -6
- package/dist/core/objects/materialized-view/materialized-view.model.d.ts +2 -1
- package/dist/core/objects/materialized-view/materialized-view.model.js +20 -4
- package/dist/core/objects/procedure/procedure.model.d.ts +2 -1
- package/dist/core/objects/procedure/procedure.model.js +20 -4
- package/dist/core/objects/publication/changes/publication.alter.d.ts +1 -1
- package/dist/core/objects/rls-policy/rls-policy.diff.js +13 -1
- package/dist/core/objects/rule/rule.model.d.ts +2 -1
- package/dist/core/objects/rule/rule.model.js +20 -3
- package/dist/core/objects/sequence/sequence.diff.d.ts +2 -1
- package/dist/core/objects/sequence/sequence.diff.js +41 -9
- package/dist/core/objects/table/changes/table.alter.d.ts +16 -1
- package/dist/core/objects/table/changes/table.alter.js +39 -6
- package/dist/core/objects/table/table.diff.js +40 -17
- package/dist/core/objects/table/table.model.d.ts +6 -1
- package/dist/core/objects/table/table.model.js +50 -12
- package/dist/core/objects/trigger/trigger.model.d.ts +2 -1
- package/dist/core/objects/trigger/trigger.model.js +20 -4
- package/dist/core/objects/utils.d.ts +1 -0
- package/dist/core/objects/utils.js +3 -0
- package/dist/core/objects/view/view.model.d.ts +2 -1
- package/dist/core/objects/view/view.model.js +20 -4
- package/dist/core/plan/create.js +3 -1
- package/dist/core/plan/types.d.ts +8 -0
- package/dist/core/post-diff-normalization.d.ts +36 -0
- package/dist/core/post-diff-normalization.js +202 -0
- package/dist/core/sort/cycle-breakers.d.ts +15 -0
- package/dist/core/sort/cycle-breakers.js +269 -0
- package/dist/core/sort/sort-changes.js +97 -43
- package/dist/core/sort/utils.d.ts +10 -0
- package/dist/core/sort/utils.js +28 -0
- package/package.json +1 -1
- package/src/core/catalog.diff.ts +4 -3
- package/src/core/catalog.model.ts +20 -8
- package/src/core/expand-replace-dependencies.test.ts +139 -5
- package/src/core/expand-replace-dependencies.ts +24 -0
- package/src/core/objects/extract-with-retry.test.ts +143 -0
- package/src/core/objects/extract-with-retry.ts +87 -0
- package/src/core/objects/index/index.diff.ts +0 -1
- package/src/core/objects/index/index.model.test.ts +37 -1
- package/src/core/objects/index/index.model.ts +25 -6
- package/src/core/objects/materialized-view/materialized-view.model.test.ts +93 -0
- package/src/core/objects/materialized-view/materialized-view.model.ts +27 -4
- package/src/core/objects/procedure/procedure.model.test.ts +117 -0
- package/src/core/objects/procedure/procedure.model.ts +28 -5
- package/src/core/objects/publication/changes/publication.alter.ts +1 -1
- package/src/core/objects/rls-policy/rls-policy.diff.ts +19 -1
- package/src/core/objects/rule/rule.model.test.ts +99 -0
- package/src/core/objects/rule/rule.model.ts +28 -4
- package/src/core/objects/sequence/sequence.diff.test.ts +93 -1
- package/src/core/objects/sequence/sequence.diff.ts +43 -10
- package/src/core/objects/table/changes/table.alter.test.ts +26 -23
- package/src/core/objects/table/changes/table.alter.ts +66 -10
- package/src/core/objects/table/table.diff.test.ts +43 -0
- package/src/core/objects/table/table.diff.ts +52 -23
- package/src/core/objects/table/table.model.test.ts +209 -0
- package/src/core/objects/table/table.model.ts +62 -14
- package/src/core/objects/trigger/trigger.model.test.ts +113 -0
- package/src/core/objects/trigger/trigger.model.ts +28 -5
- package/src/core/objects/utils.ts +3 -0
- package/src/core/objects/view/view.model.test.ts +90 -0
- package/src/core/objects/view/view.model.ts +28 -5
- package/src/core/plan/create.ts +3 -1
- package/src/core/plan/types.ts +8 -0
- package/src/core/{post-diff-cycle-breaking.test.ts → post-diff-normalization.test.ts} +168 -160
- package/src/core/post-diff-normalization.ts +260 -0
- package/src/core/sort/cycle-breakers.test.ts +476 -0
- package/src/core/sort/cycle-breakers.ts +311 -0
- package/src/core/sort/sort-changes.ts +135 -50
- package/src/core/sort/utils.ts +38 -0
- package/dist/core/post-diff-cycle-breaking.d.ts +0 -29
- package/dist/core/post-diff-cycle-breaking.js +0 -209
- package/src/core/post-diff-cycle-breaking.ts +0 -317
|
@@ -109,15 +109,20 @@ describe.concurrent("sequence.diff", () => {
|
|
|
109
109
|
});
|
|
110
110
|
|
|
111
111
|
test("replacing an owned sequence re-emits the owning column default", () => {
|
|
112
|
+
// Use `persistence` (UNLOGGED → LOGGED) to trigger the
|
|
113
|
+
// non-alterable replace path: it's the only field still in
|
|
114
|
+
// NON_ALTERABLE_FIELDS. `data_type` was previously in that list
|
|
115
|
+
// but is now alterable in place via ALTER SEQUENCE ... AS <type>.
|
|
112
116
|
const main = new Sequence({
|
|
113
117
|
...base,
|
|
114
|
-
|
|
118
|
+
persistence: "u",
|
|
115
119
|
owned_by_schema: "public",
|
|
116
120
|
owned_by_table: "users",
|
|
117
121
|
owned_by_column: "id",
|
|
118
122
|
});
|
|
119
123
|
const branch = new Sequence({
|
|
120
124
|
...base,
|
|
125
|
+
persistence: "p",
|
|
121
126
|
owned_by_schema: "public",
|
|
122
127
|
owned_by_table: "users",
|
|
123
128
|
owned_by_column: "id",
|
|
@@ -322,6 +327,93 @@ describe.concurrent("sequence.diff", () => {
|
|
|
322
327
|
expect(changes).toHaveLength(0);
|
|
323
328
|
});
|
|
324
329
|
|
|
330
|
+
test("recreate same-name sequence when owning table is renamed away", () => {
|
|
331
|
+
// Reproduces issue #228 case 1: a SERIAL column's table is renamed
|
|
332
|
+
// (`old_table` → `new_table`). The sequence keeps the same name
|
|
333
|
+
// (`old_table_id_seq`) but its OWNED BY now points at `new_table.id`.
|
|
334
|
+
// PostgreSQL cascade-drops the sequence with the old table, so a later
|
|
335
|
+
// CREATE TABLE that references `old_table_id_seq` fails. The diff must
|
|
336
|
+
// emit CreateSequence (and skip the explicit DropSequence to avoid an
|
|
337
|
+
// unbreakable cycle with the DropTable).
|
|
338
|
+
const tableColumn = {
|
|
339
|
+
name: "id",
|
|
340
|
+
position: 1,
|
|
341
|
+
data_type: "integer",
|
|
342
|
+
data_type_str: "integer",
|
|
343
|
+
is_custom_type: false,
|
|
344
|
+
custom_type_type: null,
|
|
345
|
+
custom_type_category: null,
|
|
346
|
+
custom_type_schema: null,
|
|
347
|
+
custom_type_name: null,
|
|
348
|
+
not_null: true,
|
|
349
|
+
is_identity: false,
|
|
350
|
+
is_identity_always: false,
|
|
351
|
+
is_generated: false,
|
|
352
|
+
collation: null,
|
|
353
|
+
default: "nextval('public.old_table_id_seq'::regclass)",
|
|
354
|
+
comment: null,
|
|
355
|
+
};
|
|
356
|
+
const tableBaseProps = {
|
|
357
|
+
schema: "public",
|
|
358
|
+
persistence: "p" as const,
|
|
359
|
+
row_security: false,
|
|
360
|
+
force_row_security: false,
|
|
361
|
+
has_indexes: false,
|
|
362
|
+
has_rules: false,
|
|
363
|
+
has_triggers: false,
|
|
364
|
+
has_subclasses: false,
|
|
365
|
+
is_populated: true,
|
|
366
|
+
replica_identity: "d" as const,
|
|
367
|
+
is_partition: false,
|
|
368
|
+
options: null,
|
|
369
|
+
partition_bound: null,
|
|
370
|
+
partition_by: null,
|
|
371
|
+
owner: "test",
|
|
372
|
+
comment: null,
|
|
373
|
+
parent_schema: null,
|
|
374
|
+
parent_name: null,
|
|
375
|
+
privileges: [],
|
|
376
|
+
};
|
|
377
|
+
const oldTable = new Table({
|
|
378
|
+
...tableBaseProps,
|
|
379
|
+
name: "old_table",
|
|
380
|
+
columns: [tableColumn],
|
|
381
|
+
});
|
|
382
|
+
const newTable = new Table({
|
|
383
|
+
...tableBaseProps,
|
|
384
|
+
name: "new_table",
|
|
385
|
+
columns: [tableColumn],
|
|
386
|
+
});
|
|
387
|
+
const mainSequence = new Sequence({
|
|
388
|
+
...base,
|
|
389
|
+
name: "old_table_id_seq",
|
|
390
|
+
owned_by_schema: "public",
|
|
391
|
+
owned_by_table: "old_table",
|
|
392
|
+
owned_by_column: "id",
|
|
393
|
+
});
|
|
394
|
+
const branchSequence = new Sequence({
|
|
395
|
+
...base,
|
|
396
|
+
name: "old_table_id_seq",
|
|
397
|
+
owned_by_schema: "public",
|
|
398
|
+
owned_by_table: "new_table",
|
|
399
|
+
owned_by_column: "id",
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
const changes = diffSequences(
|
|
403
|
+
testContext,
|
|
404
|
+
{ [mainSequence.stableId]: mainSequence },
|
|
405
|
+
{ [branchSequence.stableId]: branchSequence },
|
|
406
|
+
{ [newTable.stableId]: newTable },
|
|
407
|
+
{ [oldTable.stableId]: oldTable },
|
|
408
|
+
);
|
|
409
|
+
|
|
410
|
+
expect(changes.some((c) => c instanceof DropSequence)).toBe(false);
|
|
411
|
+
expect(changes.some((c) => c instanceof CreateSequence)).toBe(true);
|
|
412
|
+
expect(changes.some((c) => c instanceof AlterSequenceSetOwnedBy)).toBe(
|
|
413
|
+
true,
|
|
414
|
+
);
|
|
415
|
+
});
|
|
416
|
+
|
|
325
417
|
test("create with comment emits CreateCommentOnSequence", () => {
|
|
326
418
|
const s = new Sequence({ ...base, comment: "my seq" });
|
|
327
419
|
const changes = diffSequences(testContext, {}, { [s.stableId]: s });
|
|
@@ -36,6 +36,7 @@ type SequenceOrColumnSetDefaultChange =
|
|
|
36
36
|
* @param main - The sequences in the main catalog.
|
|
37
37
|
* @param branch - The sequences in the branch catalog.
|
|
38
38
|
* @param branchTables - The tables in the branch catalog (used to check if owning tables are being dropped).
|
|
39
|
+
* @param mainTables - The tables in the main catalog (used to detect when a same-name sequence will be cascade-dropped because its main-side owning table is going away).
|
|
39
40
|
* @returns A list of changes to apply to main to make it match branch.
|
|
40
41
|
*/
|
|
41
42
|
export function diffSequences(
|
|
@@ -46,6 +47,7 @@ export function diffSequences(
|
|
|
46
47
|
main: Record<string, Sequence>,
|
|
47
48
|
branch: Record<string, Sequence>,
|
|
48
49
|
branchTables: Record<string, Table> = {},
|
|
50
|
+
mainTables: Record<string, Table> = {},
|
|
49
51
|
): SequenceOrColumnSetDefaultChange[] {
|
|
50
52
|
const { created, dropped, altered } = diffObjects(main, branch);
|
|
51
53
|
|
|
@@ -150,22 +152,42 @@ export function diffSequences(
|
|
|
150
152
|
|
|
151
153
|
// Check if non-alterable properties have changed
|
|
152
154
|
// These require dropping and recreating the sequence
|
|
153
|
-
const NON_ALTERABLE_FIELDS: Array<keyof Sequence> = [
|
|
154
|
-
"data_type",
|
|
155
|
-
"persistence",
|
|
156
|
-
];
|
|
155
|
+
const NON_ALTERABLE_FIELDS: Array<keyof Sequence> = ["persistence"];
|
|
157
156
|
const nonAlterablePropsChanged = hasNonAlterableChanges(
|
|
158
157
|
mainSequence,
|
|
159
158
|
branchSequence,
|
|
160
159
|
NON_ALTERABLE_FIELDS,
|
|
161
160
|
);
|
|
162
161
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
162
|
+
// A sequence kept the same name (so it's "altered" in catalog terms),
|
|
163
|
+
// but its main-side owning table is going away from the plan (renamed
|
|
164
|
+
// away or simply dropped). PostgreSQL will cascade-drop the sequence
|
|
165
|
+
// alongside the table, leaving any later CREATE TABLE / column-default
|
|
166
|
+
// that depends on the sequence name pointing at nothing. Treat this
|
|
167
|
+
// like a non-alterable change so we recreate the sequence after the
|
|
168
|
+
// owning table is dropped.
|
|
169
|
+
const mainOwnedByTableId =
|
|
170
|
+
mainSequence.owned_by_schema && mainSequence.owned_by_table
|
|
171
|
+
? `table:${mainSequence.owned_by_schema}.${mainSequence.owned_by_table}`
|
|
172
|
+
: null;
|
|
173
|
+
const cascadeOrphanedByOwningTable =
|
|
174
|
+
mainOwnedByTableId !== null &&
|
|
175
|
+
mainTables[mainOwnedByTableId] !== undefined &&
|
|
176
|
+
branchTables[mainOwnedByTableId] === undefined;
|
|
177
|
+
|
|
178
|
+
if (nonAlterablePropsChanged || cascadeOrphanedByOwningTable) {
|
|
179
|
+
// When the owning table is going away in this plan, PostgreSQL will
|
|
180
|
+
// cascade-drop the sequence as part of the DROP TABLE. Emitting an
|
|
181
|
+
// explicit DROP SEQUENCE here would (a) introduce an unbreakable
|
|
182
|
+
// DropSequence ↔ DropTable cycle on the catalog edges between the
|
|
183
|
+
// sequence and the dropped column, and (b) be redundant with the
|
|
184
|
+
// cascade. The CreateSequence below restores the sequence under its
|
|
185
|
+
// original name so any same-name reference in a later CREATE TABLE
|
|
186
|
+
// resolves correctly.
|
|
187
|
+
if (!cascadeOrphanedByOwningTable) {
|
|
188
|
+
changes.push(new DropSequence({ sequence: mainSequence }));
|
|
189
|
+
}
|
|
190
|
+
changes.push(new CreateSequence({ sequence: branchSequence }));
|
|
169
191
|
// Re-apply OWNED BY if present on branch
|
|
170
192
|
if (
|
|
171
193
|
branchSequence.owned_by_schema !== null &&
|
|
@@ -215,6 +237,7 @@ export function diffSequences(
|
|
|
215
237
|
} else {
|
|
216
238
|
// Only alterable properties changed - emit ALTER for options/owner
|
|
217
239
|
const optionsChanged =
|
|
240
|
+
mainSequence.data_type !== branchSequence.data_type ||
|
|
218
241
|
mainSequence.increment !== branchSequence.increment ||
|
|
219
242
|
mainSequence.minimum_value !== branchSequence.minimum_value ||
|
|
220
243
|
mainSequence.maximum_value !== branchSequence.maximum_value ||
|
|
@@ -224,6 +247,16 @@ export function diffSequences(
|
|
|
224
247
|
|
|
225
248
|
if (optionsChanged) {
|
|
226
249
|
const options: string[] = [];
|
|
250
|
+
// `AS <type>` must come before any MIN/MAX/RESTART clauses per the
|
|
251
|
+
// PG ALTER SEQUENCE grammar. Valid types are smallint, integer,
|
|
252
|
+
// bigint — the same set CREATE SEQUENCE accepts — so the universe
|
|
253
|
+
// of legal transitions is closed. PG enforces last_value range at
|
|
254
|
+
// apply time when shrinking; that's the desired behavior because
|
|
255
|
+
// the previous Drop+Create path silently reset last_value to 1
|
|
256
|
+
// (data-loss bug, see Sentry SUPABASE-API-7RS).
|
|
257
|
+
if (mainSequence.data_type !== branchSequence.data_type) {
|
|
258
|
+
options.push("AS", branchSequence.data_type);
|
|
259
|
+
}
|
|
227
260
|
if (mainSequence.increment !== branchSequence.increment) {
|
|
228
261
|
options.push("INCREMENT BY", String(branchSequence.increment));
|
|
229
262
|
}
|
|
@@ -343,7 +343,7 @@ describe.concurrent("table", () => {
|
|
|
343
343
|
).toBe("ALTER TABLE public.test_table REPLICA IDENTITY FULL");
|
|
344
344
|
});
|
|
345
345
|
|
|
346
|
-
test("replica identity DEFAULT and INDEX
|
|
346
|
+
test("replica identity DEFAULT and USING INDEX", async () => {
|
|
347
347
|
const baseProps: Omit<
|
|
348
348
|
TableProps,
|
|
349
349
|
"owner" | "options" | "replica_identity"
|
|
@@ -372,31 +372,23 @@ describe.concurrent("table", () => {
|
|
|
372
372
|
options: null,
|
|
373
373
|
replica_identity: "n",
|
|
374
374
|
});
|
|
375
|
-
const toDefault = new Table({
|
|
376
|
-
...baseProps,
|
|
377
|
-
owner: "o1",
|
|
378
|
-
options: null,
|
|
379
|
-
replica_identity: "d",
|
|
380
|
-
});
|
|
381
|
-
const toIndex = new Table({
|
|
382
|
-
...baseProps,
|
|
383
|
-
owner: "o1",
|
|
384
|
-
options: null,
|
|
385
|
-
replica_identity: "i",
|
|
386
|
-
});
|
|
387
|
-
expect(
|
|
388
|
-
new AlterTableSetReplicaIdentity({
|
|
389
|
-
table,
|
|
390
|
-
mode: toDefault.replica_identity,
|
|
391
|
-
}).serialize(),
|
|
392
|
-
).toBe("ALTER TABLE public.test_table REPLICA IDENTITY DEFAULT");
|
|
393
|
-
// AlterTableSetReplicaIdentity of type "i" will not be emitted in diff, it is handled by index changes, we fallback to DEFAULT here
|
|
394
375
|
expect(
|
|
395
376
|
new AlterTableSetReplicaIdentity({
|
|
396
377
|
table,
|
|
397
|
-
mode:
|
|
378
|
+
mode: "d",
|
|
398
379
|
}).serialize(),
|
|
399
380
|
).toBe("ALTER TABLE public.test_table REPLICA IDENTITY DEFAULT");
|
|
381
|
+
const usingIndex = new AlterTableSetReplicaIdentity({
|
|
382
|
+
table,
|
|
383
|
+
mode: "i",
|
|
384
|
+
indexName: "test_table_pkey",
|
|
385
|
+
});
|
|
386
|
+
expect(usingIndex.serialize()).toBe(
|
|
387
|
+
"ALTER TABLE public.test_table REPLICA IDENTITY USING INDEX test_table_pkey",
|
|
388
|
+
);
|
|
389
|
+
expect(usingIndex.requires).toContain(
|
|
390
|
+
"index:public.test_table.test_table_pkey",
|
|
391
|
+
);
|
|
400
392
|
});
|
|
401
393
|
|
|
402
394
|
test("columns add/drop/alter", async () => {
|
|
@@ -444,6 +436,11 @@ describe.concurrent("table", () => {
|
|
|
444
436
|
data_type: "text",
|
|
445
437
|
data_type_str: "text",
|
|
446
438
|
};
|
|
439
|
+
const colTextBefore: ColumnProps = {
|
|
440
|
+
...colText,
|
|
441
|
+
data_type: "integer",
|
|
442
|
+
data_type_str: "integer",
|
|
443
|
+
};
|
|
447
444
|
const withCols = new Table({
|
|
448
445
|
...tableProps,
|
|
449
446
|
owner: "o1",
|
|
@@ -477,10 +474,11 @@ describe.concurrent("table", () => {
|
|
|
477
474
|
const changeType = new AlterTableAlterColumnType({
|
|
478
475
|
table: withCols,
|
|
479
476
|
column: colText,
|
|
477
|
+
previousColumn: colTextBefore,
|
|
480
478
|
});
|
|
481
479
|
await assertValidSql(changeType.serialize());
|
|
482
480
|
expect(changeType.serialize()).toBe(
|
|
483
|
-
"ALTER TABLE public.test_table ALTER COLUMN b TYPE text",
|
|
481
|
+
"ALTER TABLE public.test_table ALTER COLUMN b TYPE text USING b::text",
|
|
484
482
|
);
|
|
485
483
|
|
|
486
484
|
const changeSetDefault = new AlterTableAlterColumnSetDefault({
|
|
@@ -659,10 +657,15 @@ describe.concurrent("table", () => {
|
|
|
659
657
|
const change = new AlterTableAlterColumnType({
|
|
660
658
|
table: withCols,
|
|
661
659
|
column: col,
|
|
660
|
+
previousColumn: {
|
|
661
|
+
...col,
|
|
662
|
+
data_type: "integer",
|
|
663
|
+
data_type_str: "integer",
|
|
664
|
+
},
|
|
662
665
|
});
|
|
663
666
|
await assertValidSql(change.serialize());
|
|
664
667
|
expect(change.serialize()).toBe(
|
|
665
|
-
"ALTER TABLE public.test_table ALTER COLUMN b TYPE text COLLATE mycoll",
|
|
668
|
+
"ALTER TABLE public.test_table ALTER COLUMN b TYPE text COLLATE mycoll USING b::text",
|
|
666
669
|
);
|
|
667
670
|
});
|
|
668
671
|
|
|
@@ -462,20 +462,46 @@ export class AlterTableValidateConstraint extends AlterTableChange {
|
|
|
462
462
|
|
|
463
463
|
/**
|
|
464
464
|
* ALTER TABLE ... REPLICA IDENTITY ...
|
|
465
|
+
*
|
|
466
|
+
* When `mode === "i"` (USING INDEX), `indexName` is the name of the index to
|
|
467
|
+
* use. The extractor populates `Table.replica_identity_index` from
|
|
468
|
+
* `pg_index.indisreplident` whenever `Table.replica_identity` is `'i'`, so
|
|
469
|
+
* callers that source their props from a `Table` instance can rely on the
|
|
470
|
+
* pair being consistent. The non-null assertions in `requires` / `serialize`
|
|
471
|
+
* below are justified by that data invariant — the same pattern the FK
|
|
472
|
+
* branch of `AlterTableAddConstraint` uses for `foreign_key_columns!` /
|
|
473
|
+
* `foreign_key_table!` / `foreign_key_schema!`.
|
|
465
474
|
*/
|
|
466
475
|
export class AlterTableSetReplicaIdentity extends AlterTableChange {
|
|
467
476
|
public readonly table: Table;
|
|
468
477
|
public readonly mode: "d" | "n" | "f" | "i";
|
|
478
|
+
public readonly indexName: string | null;
|
|
469
479
|
public readonly scope = "object" as const;
|
|
470
480
|
|
|
471
|
-
constructor(props: {
|
|
481
|
+
constructor(props: {
|
|
482
|
+
table: Table;
|
|
483
|
+
mode: "d" | "n" | "f" | "i";
|
|
484
|
+
indexName?: string | null;
|
|
485
|
+
}) {
|
|
472
486
|
super();
|
|
473
487
|
this.table = props.table;
|
|
474
488
|
this.mode = props.mode;
|
|
489
|
+
this.indexName = props.indexName ?? null;
|
|
475
490
|
}
|
|
476
491
|
|
|
477
492
|
get requires() {
|
|
478
|
-
|
|
493
|
+
const reqs: string[] = [this.table.stableId];
|
|
494
|
+
if (this.mode === "i") {
|
|
495
|
+
reqs.push(
|
|
496
|
+
stableId.index(
|
|
497
|
+
this.table.schema,
|
|
498
|
+
this.table.name,
|
|
499
|
+
// biome-ignore lint/style/noNonNullAssertion: mode 'i' implies the extractor populated replica_identity_index
|
|
500
|
+
this.indexName!,
|
|
501
|
+
),
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
return reqs;
|
|
479
505
|
}
|
|
480
506
|
|
|
481
507
|
serialize(_options?: SerializeOptions): string {
|
|
@@ -486,7 +512,8 @@ export class AlterTableSetReplicaIdentity extends AlterTableChange {
|
|
|
486
512
|
? "NOTHING"
|
|
487
513
|
: this.mode === "f"
|
|
488
514
|
? "FULL"
|
|
489
|
-
:
|
|
515
|
+
: // biome-ignore lint/style/noNonNullAssertion: mode 'i' implies the extractor populated replica_identity_index
|
|
516
|
+
`USING INDEX ${this.indexName!}`;
|
|
490
517
|
return [
|
|
491
518
|
"ALTER TABLE",
|
|
492
519
|
`${this.table.schema}.${this.table.name}`,
|
|
@@ -556,11 +583,21 @@ export class AlterTableDropColumn extends AlterTableChange {
|
|
|
556
583
|
public readonly table: Table;
|
|
557
584
|
public readonly column: ColumnProps;
|
|
558
585
|
public readonly scope = "object" as const;
|
|
559
|
-
|
|
560
|
-
|
|
586
|
+
// Drop the implicit `requires(table)` edge. Only set by the lazy
|
|
587
|
+
// cycle-breaker for the publication↔column case, where the table survives
|
|
588
|
+
// the migration and the edge is therefore artificial. See
|
|
589
|
+
// `sort/cycle-breakers.ts` for the full justification.
|
|
590
|
+
public readonly omitTableRequirement: boolean;
|
|
591
|
+
|
|
592
|
+
constructor(props: {
|
|
593
|
+
table: Table;
|
|
594
|
+
column: ColumnProps;
|
|
595
|
+
omitTableRequirement?: boolean;
|
|
596
|
+
}) {
|
|
561
597
|
super();
|
|
562
598
|
this.table = props.table;
|
|
563
599
|
this.column = props.column;
|
|
600
|
+
this.omitTableRequirement = props.omitTableRequirement ?? false;
|
|
564
601
|
}
|
|
565
602
|
|
|
566
603
|
get drops() {
|
|
@@ -570,10 +607,12 @@ export class AlterTableDropColumn extends AlterTableChange {
|
|
|
570
607
|
}
|
|
571
608
|
|
|
572
609
|
get requires() {
|
|
573
|
-
|
|
574
|
-
this.table.
|
|
575
|
-
|
|
576
|
-
|
|
610
|
+
const colId = stableId.column(
|
|
611
|
+
this.table.schema,
|
|
612
|
+
this.table.name,
|
|
613
|
+
this.column.name,
|
|
614
|
+
);
|
|
615
|
+
return this.omitTableRequirement ? [colId] : [this.table.stableId, colId];
|
|
577
616
|
}
|
|
578
617
|
|
|
579
618
|
serialize(_options?: SerializeOptions): string {
|
|
@@ -592,12 +631,18 @@ export class AlterTableDropColumn extends AlterTableChange {
|
|
|
592
631
|
export class AlterTableAlterColumnType extends AlterTableChange {
|
|
593
632
|
public readonly table: Table;
|
|
594
633
|
public readonly column: ColumnProps;
|
|
634
|
+
public readonly previousColumn?: ColumnProps;
|
|
595
635
|
public readonly scope = "object" as const;
|
|
596
636
|
|
|
597
|
-
constructor(props: {
|
|
637
|
+
constructor(props: {
|
|
638
|
+
table: Table;
|
|
639
|
+
column: ColumnProps;
|
|
640
|
+
previousColumn?: ColumnProps;
|
|
641
|
+
}) {
|
|
598
642
|
super();
|
|
599
643
|
this.table = props.table;
|
|
600
644
|
this.column = props.column;
|
|
645
|
+
this.previousColumn = props.previousColumn;
|
|
601
646
|
}
|
|
602
647
|
|
|
603
648
|
get requires() {
|
|
@@ -607,6 +652,14 @@ export class AlterTableAlterColumnType extends AlterTableChange {
|
|
|
607
652
|
}
|
|
608
653
|
|
|
609
654
|
serialize(_options?: SerializeOptions): string {
|
|
655
|
+
// previousColumn is optional so direct serializer tests/fixtures can keep
|
|
656
|
+
// emitting canonical ALTER TYPE SQL without forcing a USING expression.
|
|
657
|
+
// When provided, we can detect true type changes and add USING for casts
|
|
658
|
+
// PostgreSQL cannot perform automatically.
|
|
659
|
+
const hasTypeChangedWithPreviousDefinition =
|
|
660
|
+
this.previousColumn?.data_type_str !== undefined &&
|
|
661
|
+
this.previousColumn.data_type_str !== this.column.data_type_str;
|
|
662
|
+
|
|
610
663
|
const parts: string[] = [
|
|
611
664
|
"ALTER TABLE",
|
|
612
665
|
`${this.table.schema}.${this.table.name}`,
|
|
@@ -618,6 +671,9 @@ export class AlterTableAlterColumnType extends AlterTableChange {
|
|
|
618
671
|
if (this.column.collation) {
|
|
619
672
|
parts.push("COLLATE", this.column.collation);
|
|
620
673
|
}
|
|
674
|
+
if (hasTypeChangedWithPreviousDefinition) {
|
|
675
|
+
parts.push("USING", `${this.column.name}::${this.column.data_type_str}`);
|
|
676
|
+
}
|
|
621
677
|
return parts.join(" ");
|
|
622
678
|
}
|
|
623
679
|
}
|
|
@@ -767,6 +767,9 @@ describe.concurrent("table.diff", () => {
|
|
|
767
767
|
expect(
|
|
768
768
|
typeChanges.some((c) => c instanceof AlterTableAlterColumnType),
|
|
769
769
|
).toBe(true);
|
|
770
|
+
expect(typeChanges.map((c) => c.serialize())).toContain(
|
|
771
|
+
"ALTER TABLE public.t2 ALTER COLUMN a TYPE text USING a::text",
|
|
772
|
+
);
|
|
770
773
|
|
|
771
774
|
const defaultAdded = new Table({
|
|
772
775
|
...base,
|
|
@@ -817,6 +820,46 @@ describe.concurrent("table.diff", () => {
|
|
|
817
820
|
expect(
|
|
818
821
|
notNullDropped.some((c) => c instanceof AlterTableAlterColumnDropNotNull),
|
|
819
822
|
).toBe(true);
|
|
823
|
+
|
|
824
|
+
const withDefault = new Table({
|
|
825
|
+
...base,
|
|
826
|
+
name: "t2",
|
|
827
|
+
columns: [
|
|
828
|
+
{
|
|
829
|
+
...withCol.columns[0],
|
|
830
|
+
data_type: "text",
|
|
831
|
+
data_type_str: "text",
|
|
832
|
+
default: "'active'",
|
|
833
|
+
},
|
|
834
|
+
],
|
|
835
|
+
});
|
|
836
|
+
const typeChangedWithDefault = new Table({
|
|
837
|
+
...base,
|
|
838
|
+
name: "t2",
|
|
839
|
+
columns: [
|
|
840
|
+
{
|
|
841
|
+
...withDefault.columns[0],
|
|
842
|
+
data_type: "USER-DEFINED",
|
|
843
|
+
data_type_str: "test_schema.status",
|
|
844
|
+
is_custom_type: true,
|
|
845
|
+
custom_type_type: "e",
|
|
846
|
+
custom_type_category: "E",
|
|
847
|
+
custom_type_schema: "test_schema",
|
|
848
|
+
custom_type_name: "status",
|
|
849
|
+
default: "'active'::test_schema.status",
|
|
850
|
+
},
|
|
851
|
+
],
|
|
852
|
+
});
|
|
853
|
+
const typeChangesWithDefault = diffTables(
|
|
854
|
+
testContext,
|
|
855
|
+
{ [withDefault.stableId]: withDefault },
|
|
856
|
+
{ [typeChangedWithDefault.stableId]: typeChangedWithDefault },
|
|
857
|
+
);
|
|
858
|
+
expect(typeChangesWithDefault.map((c) => c.serialize())).toEqual([
|
|
859
|
+
"ALTER TABLE public.t2 ALTER COLUMN a DROP DEFAULT",
|
|
860
|
+
"ALTER TABLE public.t2 ALTER COLUMN a TYPE test_schema.status USING a::test_schema.status",
|
|
861
|
+
"ALTER TABLE public.t2 ALTER COLUMN a SET DEFAULT 'active'::test_schema.status",
|
|
862
|
+
]);
|
|
820
863
|
});
|
|
821
864
|
|
|
822
865
|
test("identity transitions emit drop/add/set-generated changes", () => {
|
|
@@ -245,15 +245,13 @@ export function diffTables(
|
|
|
245
245
|
|
|
246
246
|
// REPLICA IDENTITY: If non-default, emit ALTER TABLE ... REPLICA IDENTITY
|
|
247
247
|
if (branchTable.replica_identity !== "d") {
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
);
|
|
256
|
-
}
|
|
248
|
+
changes.push(
|
|
249
|
+
new AlterTableSetReplicaIdentity({
|
|
250
|
+
table: branchTable,
|
|
251
|
+
mode: branchTable.replica_identity,
|
|
252
|
+
indexName: branchTable.replica_identity_index,
|
|
253
|
+
}),
|
|
254
|
+
);
|
|
257
255
|
}
|
|
258
256
|
|
|
259
257
|
changes.push(
|
|
@@ -404,16 +402,23 @@ export function diffTables(
|
|
|
404
402
|
}
|
|
405
403
|
|
|
406
404
|
// REPLICA IDENTITY
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
405
|
+
// Re-emit when the mode changes, or when staying in 'i' mode but pointing
|
|
406
|
+
// at a different index. The index named on the branch must already exist
|
|
407
|
+
// before this ALTER runs; AlterTableSetReplicaIdentity declares that
|
|
408
|
+
// dependency in its `requires`.
|
|
409
|
+
const replicaIdentityChanged =
|
|
410
|
+
mainTable.replica_identity !== branchTable.replica_identity ||
|
|
411
|
+
(branchTable.replica_identity === "i" &&
|
|
412
|
+
mainTable.replica_identity_index !==
|
|
413
|
+
branchTable.replica_identity_index);
|
|
414
|
+
if (replicaIdentityChanged) {
|
|
415
|
+
changes.push(
|
|
416
|
+
new AlterTableSetReplicaIdentity({
|
|
417
|
+
table: mainTable,
|
|
418
|
+
mode: branchTable.replica_identity,
|
|
419
|
+
indexName: branchTable.replica_identity_index,
|
|
420
|
+
}),
|
|
421
|
+
);
|
|
417
422
|
}
|
|
418
423
|
|
|
419
424
|
// OWNER
|
|
@@ -701,19 +706,39 @@ export function diffTables(
|
|
|
701
706
|
const branchCol = branchCols.get(name);
|
|
702
707
|
if (!branchCol) continue;
|
|
703
708
|
|
|
709
|
+
const columnTypeChanged =
|
|
710
|
+
mainCol.data_type_str !== branchCol.data_type_str;
|
|
711
|
+
const columnCollationChanged = mainCol.collation !== branchCol.collation;
|
|
712
|
+
const needsDefaultSafeFlow =
|
|
713
|
+
columnTypeChanged && mainCol.default !== null;
|
|
714
|
+
|
|
704
715
|
// TYPE or COLLATION change
|
|
705
|
-
if (
|
|
706
|
-
mainCol.data_type_str !== branchCol.data_type_str ||
|
|
707
|
-
mainCol.collation !== branchCol.collation
|
|
708
|
-
) {
|
|
716
|
+
if (columnTypeChanged || columnCollationChanged) {
|
|
709
717
|
// Skip if parent has the same type/collation change
|
|
710
718
|
if (!parentHasSameColumnPropertyChange(name, "type")) {
|
|
719
|
+
if (needsDefaultSafeFlow) {
|
|
720
|
+
changes.push(
|
|
721
|
+
new AlterTableAlterColumnDropDefault({
|
|
722
|
+
table: branchTable,
|
|
723
|
+
column: branchCol,
|
|
724
|
+
}),
|
|
725
|
+
);
|
|
726
|
+
}
|
|
711
727
|
changes.push(
|
|
712
728
|
new AlterTableAlterColumnType({
|
|
713
729
|
table: branchTable,
|
|
714
730
|
column: branchCol,
|
|
731
|
+
previousColumn: mainCol,
|
|
715
732
|
}),
|
|
716
733
|
);
|
|
734
|
+
if (needsDefaultSafeFlow && branchCol.default !== null) {
|
|
735
|
+
changes.push(
|
|
736
|
+
new AlterTableAlterColumnSetDefault({
|
|
737
|
+
table: branchTable,
|
|
738
|
+
column: branchCol,
|
|
739
|
+
}),
|
|
740
|
+
);
|
|
741
|
+
}
|
|
717
742
|
}
|
|
718
743
|
}
|
|
719
744
|
|
|
@@ -734,6 +759,10 @@ export function diffTables(
|
|
|
734
759
|
if (mainCol.default !== branchCol.default) {
|
|
735
760
|
// Skip if parent has the same default change
|
|
736
761
|
if (!parentHasSameColumnPropertyChange(name, "default")) {
|
|
762
|
+
if (needsDefaultSafeFlow) {
|
|
763
|
+
// Defaults were already dropped/re-set in the type-change flow above.
|
|
764
|
+
continue;
|
|
765
|
+
}
|
|
737
766
|
if (branchCol.default === null) {
|
|
738
767
|
// Drop default value
|
|
739
768
|
changes.push(
|