@supabase/pg-delta 1.0.0-alpha.27 → 1.0.0-alpha.29

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.
@@ -1,13 +1,18 @@
1
1
  import type { Catalog } from "./catalog.model.ts";
2
2
  import type { Change } from "./change.types.ts";
3
+ import type { ObjectDiffContext } from "./objects/diff-context.ts";
3
4
  import { CreateDomain } from "./objects/domain/changes/domain.create.ts";
4
5
  import { DropDomain } from "./objects/domain/changes/domain.drop.ts";
5
6
  import { CreateIndex } from "./objects/index/changes/index.create.ts";
6
7
  import { DropIndex } from "./objects/index/changes/index.drop.ts";
7
8
  import { CreateMaterializedView } from "./objects/materialized-view/changes/materialized-view.create.ts";
8
9
  import { DropMaterializedView } from "./objects/materialized-view/changes/materialized-view.drop.ts";
10
+ import { buildCreateMaterializedViewChanges } from "./objects/materialized-view/materialized-view.diff.ts";
9
11
  import { CreateProcedure } from "./objects/procedure/changes/procedure.create.ts";
10
12
  import { DropProcedure } from "./objects/procedure/changes/procedure.drop.ts";
13
+ import { CreateCommentOnRlsPolicy } from "./objects/rls-policy/changes/rls-policy.comment.ts";
14
+ import { CreateRlsPolicy } from "./objects/rls-policy/changes/rls-policy.create.ts";
15
+ import { DropRlsPolicy } from "./objects/rls-policy/changes/rls-policy.drop.ts";
11
16
  import { AlterTableAddConstraint } from "./objects/table/changes/table.alter.ts";
12
17
  import { CreateCommentOnConstraint } from "./objects/table/changes/table.comment.ts";
13
18
  import { CreateTable } from "./objects/table/changes/table.create.ts";
@@ -21,6 +26,7 @@ import { DropRange } from "./objects/type/range/changes/range.drop.ts";
21
26
  import { stableId } from "./objects/utils.ts";
22
27
  import { CreateView } from "./objects/view/changes/view.create.ts";
23
28
  import { DropView } from "./objects/view/changes/view.drop.ts";
29
+ import { buildCreateViewChanges } from "./objects/view/view.diff.ts";
24
30
 
25
31
  type ResolvedObject =
26
32
  | {
@@ -49,6 +55,11 @@ type ResolvedObject =
49
55
  main: Catalog["procedures"][string];
50
56
  branch: Catalog["procedures"][string];
51
57
  }
58
+ | {
59
+ kind: "rls_policy";
60
+ main: Catalog["rlsPolicies"][string];
61
+ branch: Catalog["rlsPolicies"][string];
62
+ }
52
63
  | {
53
64
  kind: "enum";
54
65
  main: Catalog["enums"][string];
@@ -87,10 +98,15 @@ export function expandReplaceDependencies({
87
98
  changes,
88
99
  mainCatalog,
89
100
  branchCatalog,
101
+ diffContext,
90
102
  }: {
91
103
  changes: Change[];
92
104
  mainCatalog: Catalog;
93
105
  branchCatalog: Catalog;
106
+ diffContext?: Pick<
107
+ ObjectDiffContext,
108
+ "version" | "currentUser" | "defaultPrivilegeState"
109
+ >;
94
110
  }): ExpandReplaceDependenciesResult {
95
111
  const createdIds = new Set<string>();
96
112
  const droppedIds = new Set<string>();
@@ -107,6 +123,16 @@ export function expandReplaceDependencies({
107
123
  }
108
124
  }
109
125
 
126
+ const promotedRlsPolicyIds = new Set<string>();
127
+ const additions: Change[] = collectInvalidatedRlsPolicyReplacements({
128
+ changes,
129
+ mainCatalog,
130
+ branchCatalog,
131
+ createdIds,
132
+ droppedIds,
133
+ promotedRlsPolicyIds,
134
+ });
135
+
110
136
  // Procedure stableIds are signature-qualified
111
137
  // (`procedure:schema.name(argtypes)`), so a function whose parameter types
112
138
  // change has different ids in `createdIds` and `droppedIds` and would not
@@ -150,7 +176,7 @@ export function expandReplaceDependencies({
150
176
  }
151
177
  }
152
178
 
153
- if (replaceRoots.size === 0) {
179
+ if (replaceRoots.size === 0 && additions.length === 0) {
154
180
  return {
155
181
  changes,
156
182
  replacedTableIds: new Set<string>(),
@@ -168,7 +194,6 @@ export function expandReplaceDependencies({
168
194
  list.add(dep.dependent_stable_id);
169
195
  }
170
196
 
171
- const additions: Change[] = [];
172
197
  const visitedTargets = new Set<string>();
173
198
  const visitedRefs = new Set<string>(replaceRoots);
174
199
  const queue: string[] = [...replaceRoots];
@@ -236,10 +261,14 @@ export function expandReplaceDependencies({
236
261
  const replacementChanges = buildReplaceChanges(resolved, {
237
262
  addDrop,
238
263
  addCreate,
264
+ diffContext,
239
265
  });
240
266
  if (!replacementChanges) continue;
241
267
 
242
268
  additions.push(...replacementChanges);
269
+ if (resolved.kind === "rls_policy") {
270
+ promotedRlsPolicyIds.add(targetId);
271
+ }
243
272
 
244
273
  // If we added a DropTable(T) for an existing table, mark T so any
245
274
  // pre-existing object-scope AlterTable*(T) changes get dropped below —
@@ -264,11 +293,90 @@ export function expandReplaceDependencies({
264
293
  }
265
294
 
266
295
  return {
267
- changes: [...changes, ...additions],
296
+ changes: [
297
+ ...removeSupersededRlsPolicyAlters(changes, promotedRlsPolicyIds),
298
+ ...additions,
299
+ ],
268
300
  replacedTableIds: tablesReplacedByExpansion,
269
301
  };
270
302
  }
271
303
 
304
+ function collectInvalidatedRlsPolicyReplacements({
305
+ changes,
306
+ mainCatalog,
307
+ branchCatalog,
308
+ createdIds,
309
+ droppedIds,
310
+ promotedRlsPolicyIds,
311
+ }: {
312
+ changes: Change[];
313
+ mainCatalog: Catalog;
314
+ branchCatalog: Catalog;
315
+ createdIds: Set<string>;
316
+ droppedIds: Set<string>;
317
+ promotedRlsPolicyIds: Set<string>;
318
+ }): Change[] {
319
+ // In-place rewrites report stable ids through `invalidates`: the referenced
320
+ // object keeps its identity, but dependents bound to the old definition must
321
+ // be torn down first. RLS policy expressions are tracked in pg_depend, so use
322
+ // those catalog edges to promote only policies that depend on an invalidated
323
+ // id, without coupling this expansion pass to a concrete table-change class.
324
+ const invalidatedIds = new Set<string>();
325
+ for (const change of changes) {
326
+ for (const invalidatedId of change.invalidates) {
327
+ invalidatedIds.add(invalidatedId);
328
+ }
329
+ }
330
+ if (invalidatedIds.size === 0) return [];
331
+
332
+ const replacements: Change[] = [];
333
+ for (const dep of mainCatalog.depends) {
334
+ if (!invalidatedIds.has(dep.referenced_stable_id)) continue;
335
+
336
+ const targetId = normalizeDependentId(dep.dependent_stable_id);
337
+ if (!targetId?.startsWith("rlsPolicy:")) continue;
338
+ if (promotedRlsPolicyIds.has(targetId)) continue;
339
+ if (createdIds.has(targetId) && droppedIds.has(targetId)) continue;
340
+
341
+ const resolved = resolveObjectForStableId(
342
+ targetId,
343
+ mainCatalog,
344
+ branchCatalog,
345
+ );
346
+ if (!resolved || resolved.kind !== "rls_policy") continue;
347
+
348
+ const addDrop = !droppedIds.has(targetId);
349
+ const addCreate = !createdIds.has(targetId);
350
+ const replacementChanges = buildReplaceChanges(resolved, {
351
+ addDrop,
352
+ addCreate,
353
+ });
354
+ if (!replacementChanges) continue;
355
+
356
+ replacements.push(...replacementChanges);
357
+ promotedRlsPolicyIds.add(targetId);
358
+ for (const change of replacementChanges) {
359
+ for (const id of change.creates ?? []) createdIds.add(id);
360
+ for (const id of change.drops ?? []) droppedIds.add(id);
361
+ }
362
+ }
363
+
364
+ return replacements;
365
+ }
366
+
367
+ function removeSupersededRlsPolicyAlters(
368
+ changes: Change[],
369
+ promotedRlsPolicyIds: ReadonlySet<string>,
370
+ ): Change[] {
371
+ if (promotedRlsPolicyIds.size === 0) return changes;
372
+ return changes.filter((change) => {
373
+ if (change.objectType !== "rls_policy" || change.operation !== "alter") {
374
+ return true;
375
+ }
376
+ return !promotedRlsPolicyIds.has(change.policy.stableId);
377
+ });
378
+ }
379
+
272
380
  function isOwnedSequenceColumnDependency(
273
381
  referencedId: string,
274
382
  dependentId: string,
@@ -395,6 +503,12 @@ function resolveObjectForStableId(
395
503
  return main && branch ? { kind: "procedure", main, branch } : null;
396
504
  }
397
505
 
506
+ if (stableId.startsWith("rlsPolicy:")) {
507
+ const main = mainCatalog.rlsPolicies[stableId];
508
+ const branch = branchCatalog.rlsPolicies[stableId];
509
+ return main && branch ? { kind: "rls_policy", main, branch } : null;
510
+ }
511
+
398
512
  if (stableId.startsWith("domain:")) {
399
513
  const main = mainCatalog.domains[stableId];
400
514
  const branch = branchCatalog.domains[stableId];
@@ -430,9 +544,16 @@ function resolveObjectForStableId(
430
544
 
431
545
  function buildReplaceChanges(
432
546
  resolved: ResolvedObject,
433
- options: { addDrop: boolean; addCreate: boolean },
547
+ options: {
548
+ addDrop: boolean;
549
+ addCreate: boolean;
550
+ diffContext?: Pick<
551
+ ObjectDiffContext,
552
+ "version" | "currentUser" | "defaultPrivilegeState"
553
+ >;
554
+ },
434
555
  ): Change[] | null {
435
- const { addDrop, addCreate } = options;
556
+ const { addDrop, addCreate, diffContext } = options;
436
557
 
437
558
  if (!addDrop && !addCreate) return null;
438
559
 
@@ -471,7 +592,9 @@ function buildReplaceChanges(
471
592
  case "view":
472
593
  return [
473
594
  ...(addDrop ? [new DropView({ view: resolved.main })] : []),
474
- ...(addCreate ? [new CreateView({ view: resolved.branch })] : []),
595
+ ...(addCreate
596
+ ? buildCreateViewReplacementChanges(resolved.branch, diffContext)
597
+ : []),
475
598
  ];
476
599
  case "materialized_view":
477
600
  return [
@@ -479,7 +602,13 @@ function buildReplaceChanges(
479
602
  ? [new DropMaterializedView({ materializedView: resolved.main })]
480
603
  : []),
481
604
  ...(addCreate
482
- ? [new CreateMaterializedView({ materializedView: resolved.branch })]
605
+ ? diffContext
606
+ ? buildCreateMaterializedViewChanges(diffContext, resolved.branch)
607
+ : [
608
+ new CreateMaterializedView({
609
+ materializedView: resolved.branch,
610
+ }),
611
+ ]
483
612
  : []),
484
613
  ];
485
614
  case "index":
@@ -519,6 +648,18 @@ function buildReplaceChanges(
519
648
  ? [new CreateProcedure({ procedure: resolved.branch })]
520
649
  : []),
521
650
  ];
651
+ case "rls_policy":
652
+ return [
653
+ ...(addDrop ? [new DropRlsPolicy({ policy: resolved.main })] : []),
654
+ ...(addCreate
655
+ ? [
656
+ new CreateRlsPolicy({ policy: resolved.branch }),
657
+ ...(resolved.branch.comment !== null
658
+ ? [new CreateCommentOnRlsPolicy({ policy: resolved.branch })]
659
+ : []),
660
+ ]
661
+ : []),
662
+ ];
522
663
  case "enum":
523
664
  return [
524
665
  ...(addDrop ? [new DropEnum({ enum: resolved.main })] : []),
@@ -547,3 +688,20 @@ function buildReplaceChanges(
547
688
  return null;
548
689
  }
549
690
  }
691
+
692
+ function buildCreateViewReplacementChanges(
693
+ view: Catalog["views"][string],
694
+ diffContext:
695
+ | Pick<
696
+ ObjectDiffContext,
697
+ "version" | "currentUser" | "defaultPrivilegeState"
698
+ >
699
+ | undefined,
700
+ ): Change[] {
701
+ // Dependency-closure replacements synthesize a create without going through
702
+ // `diffViews`, so replay the same owner/comment/security-label/ACL metadata
703
+ // that a normal non-alterable view replacement would emit.
704
+ return diffContext
705
+ ? buildCreateViewChanges(diffContext, view)
706
+ : [new CreateView({ view })];
707
+ }
@@ -51,6 +51,21 @@ export abstract class BaseChange {
51
51
  return [];
52
52
  }
53
53
 
54
+ /**
55
+ * Stable identifiers this change invalidates in place.
56
+ *
57
+ * Unlike `drops`, the object keeps its identity. This is an ordering-only
58
+ * signal for mutations that rewrite an existing object in a way that requires
59
+ * dependents bound to the old definition to be dropped before the mutation
60
+ * and rebuilt afterward.
61
+ *
62
+ * Defaults to an empty array. Override in subclasses that invalidate
63
+ * dependents without dropping the object.
64
+ */
65
+ get invalidates(): string[] {
66
+ return [];
67
+ }
68
+
54
69
  /**
55
70
  * Stable identifiers this change requires to exist beforehand.
56
71
  *
@@ -106,7 +106,7 @@ describe.concurrent("materialized-view.diff", () => {
106
106
  expect(changes[0]).toBeInstanceOf(AlterMaterializedViewChangeOwner);
107
107
  });
108
108
 
109
- test("drop + create on non-alterable change", () => {
109
+ test("drop + create with metadata on non-alterable change", () => {
110
110
  const main = new MaterializedView(base);
111
111
  const branch = new MaterializedView({ ...base, definition: "select 2" });
112
112
  const changes = diffMaterializedViews(
@@ -114,9 +114,10 @@ describe.concurrent("materialized-view.diff", () => {
114
114
  { [main.stableId]: main },
115
115
  { [branch.stableId]: branch },
116
116
  );
117
- expect(changes).toHaveLength(2);
117
+ expect(changes).toHaveLength(3);
118
118
  expect(changes[0]).toBeInstanceOf(DropMaterializedView);
119
119
  expect(changes[1]).toBeInstanceOf(CreateMaterializedView);
120
+ expect(changes[2]).toBeInstanceOf(AlterMaterializedViewChangeOwner);
120
121
  });
121
122
 
122
123
  test("alter storage parameters: set and reset", () => {
@@ -30,117 +30,126 @@ import {
30
30
  import type { MaterializedViewChange } from "./changes/materialized-view.types.ts";
31
31
  import type { MaterializedView } from "./materialized-view.model.ts";
32
32
 
33
- /**
34
- * Diff two sets of materialized views from main and branch catalogs.
35
- *
36
- * @param ctx - Context containing version, currentUser, and defaultPrivilegeState
37
- * @param main - The materialized views in the main catalog.
38
- * @param branch - The materialized views in the branch catalog.
39
- * @returns A list of changes to apply to main to make it match branch.
40
- */
41
- export function diffMaterializedViews(
33
+ export function buildCreateMaterializedViewChanges(
42
34
  ctx: Pick<
43
35
  ObjectDiffContext,
44
36
  "version" | "currentUser" | "defaultPrivilegeState"
45
37
  >,
46
- main: Record<string, MaterializedView>,
47
- branch: Record<string, MaterializedView>,
38
+ mv: MaterializedView,
48
39
  ): MaterializedViewChange[] {
49
- const { created, dropped, altered } = diffObjects(main, branch);
50
-
51
- const changes: MaterializedViewChange[] = [];
40
+ const changes: MaterializedViewChange[] = [
41
+ new CreateMaterializedView({
42
+ materializedView: mv,
43
+ }),
44
+ ];
52
45
 
53
- for (const materializedViewId of created) {
54
- const mv = branch[materializedViewId];
46
+ // OWNER: If the materialized view should be owned by someone other than the current user,
47
+ // emit ALTER MATERIALIZED VIEW ... OWNER TO after creation
48
+ if (mv.owner !== ctx.currentUser) {
55
49
  changes.push(
56
- new CreateMaterializedView({
50
+ new AlterMaterializedViewChangeOwner({
57
51
  materializedView: mv,
52
+ owner: mv.owner,
58
53
  }),
59
54
  );
55
+ }
60
56
 
61
- // OWNER: If the materialized view should be owned by someone other than the current user,
62
- // emit ALTER MATERIALIZED VIEW ... OWNER TO after creation
63
- if (mv.owner !== ctx.currentUser) {
57
+ // Materialized view comment on creation
58
+ if (mv.comment !== null) {
59
+ changes.push(
60
+ new CreateCommentOnMaterializedView({
61
+ materializedView: mv,
62
+ }),
63
+ );
64
+ }
65
+ // Column comments on creation
66
+ for (const col of mv.columns) {
67
+ if (col.comment !== null) {
64
68
  changes.push(
65
- new AlterMaterializedViewChangeOwner({
69
+ new CreateCommentOnMaterializedViewColumn({
66
70
  materializedView: mv,
67
- owner: mv.owner,
71
+ column: col,
68
72
  }),
69
73
  );
70
74
  }
75
+ }
71
76
 
72
- // Note: RLS (row_security, force_row_security) is a non-alterable property for materialized views.
73
- // If RLS needs to be enabled, the materialized view must be dropped and recreated, which is
74
- // handled in the "altered" section when non-alterable properties change.
77
+ // Security labels on the matview itself (columns of matviews are not
78
+ // supported targets of SECURITY LABEL, so we only label the relation).
79
+ for (const label of mv.security_labels) {
80
+ changes.push(
81
+ new CreateSecurityLabelOnMaterializedView({
82
+ materializedView: mv,
83
+ securityLabel: label,
84
+ }),
85
+ );
86
+ }
75
87
 
76
- // Materialized view comment on creation
77
- if (mv.comment !== null) {
78
- changes.push(
79
- new CreateCommentOnMaterializedView({
80
- materializedView: mv,
81
- }),
82
- );
83
- }
84
- // Column comments on creation
85
- for (const col of mv.columns) {
86
- if (col.comment !== null) {
87
- changes.push(
88
- new CreateCommentOnMaterializedViewColumn({
89
- materializedView: mv,
90
- column: col,
91
- }),
92
- );
93
- }
94
- }
88
+ // PRIVILEGES: For created objects, compare against default privileges state
89
+ // The migration script will run ALTER DEFAULT PRIVILEGES before CREATE (via constraint spec),
90
+ // so objects are created with the default privileges state in effect.
91
+ // We compare default privileges against desired privileges to generate REVOKE/GRANT statements
92
+ // needed to reach the final desired state.
93
+ const effectiveDefaults = ctx.defaultPrivilegeState.getEffectiveDefaults(
94
+ ctx.currentUser,
95
+ "materialized_view",
96
+ mv.schema ?? "",
97
+ );
98
+ const creatorFilteredDefaults =
99
+ mv.owner !== ctx.currentUser
100
+ ? effectiveDefaults.filter((p) => p.grantee !== ctx.currentUser)
101
+ : effectiveDefaults;
102
+ const desiredPrivileges = mv.privileges;
103
+ // Filter out owner privileges - owner always has ALL privileges implicitly
104
+ // and shouldn't be compared. Use the materialized view owner as the reference.
105
+ const privilegeResults = diffPrivileges(
106
+ creatorFilteredDefaults,
107
+ desiredPrivileges,
108
+ mv.owner,
109
+ );
95
110
 
96
- // Security labels on the matview itself (columns of matviews are not
97
- // supported targets of SECURITY LABEL, so we only label the relation).
98
- for (const label of mv.security_labels) {
99
- changes.push(
100
- new CreateSecurityLabelOnMaterializedView({
101
- materializedView: mv,
102
- securityLabel: label,
103
- }),
104
- );
105
- }
111
+ changes.push(
112
+ ...(emitColumnPrivilegeChanges(
113
+ privilegeResults,
114
+ mv,
115
+ mv,
116
+ "materializedView",
117
+ {
118
+ Grant: GrantMaterializedViewPrivileges,
119
+ Revoke: RevokeMaterializedViewPrivileges,
120
+ RevokeGrantOption: RevokeGrantOptionMaterializedViewPrivileges,
121
+ },
122
+ effectiveDefaults,
123
+ ctx.version,
124
+ ) as MaterializedViewChange[]),
125
+ );
106
126
 
107
- // PRIVILEGES: For created objects, compare against default privileges state
108
- // The migration script will run ALTER DEFAULT PRIVILEGES before CREATE (via constraint spec),
109
- // so objects are created with the default privileges state in effect.
110
- // We compare default privileges against desired privileges to generate REVOKE/GRANT statements
111
- // needed to reach the final desired state.
112
- const effectiveDefaults = ctx.defaultPrivilegeState.getEffectiveDefaults(
113
- ctx.currentUser,
114
- "materialized_view",
115
- mv.schema ?? "",
116
- );
117
- const creatorFilteredDefaults =
118
- mv.owner !== ctx.currentUser
119
- ? effectiveDefaults.filter((p) => p.grantee !== ctx.currentUser)
120
- : effectiveDefaults;
121
- const desiredPrivileges = mv.privileges;
122
- // Filter out owner privileges - owner always has ALL privileges implicitly
123
- // and shouldn't be compared. Use the materialized view owner as the reference.
124
- const privilegeResults = diffPrivileges(
125
- creatorFilteredDefaults,
126
- desiredPrivileges,
127
- mv.owner,
128
- );
127
+ return changes;
128
+ }
129
+
130
+ /**
131
+ * Diff two sets of materialized views from main and branch catalogs.
132
+ *
133
+ * @param ctx - Context containing version, currentUser, and defaultPrivilegeState
134
+ * @param main - The materialized views in the main catalog.
135
+ * @param branch - The materialized views in the branch catalog.
136
+ * @returns A list of changes to apply to main to make it match branch.
137
+ */
138
+ export function diffMaterializedViews(
139
+ ctx: Pick<
140
+ ObjectDiffContext,
141
+ "version" | "currentUser" | "defaultPrivilegeState"
142
+ >,
143
+ main: Record<string, MaterializedView>,
144
+ branch: Record<string, MaterializedView>,
145
+ ): MaterializedViewChange[] {
146
+ const { created, dropped, altered } = diffObjects(main, branch);
129
147
 
148
+ const changes: MaterializedViewChange[] = [];
149
+
150
+ for (const materializedViewId of created) {
130
151
  changes.push(
131
- ...(emitColumnPrivilegeChanges(
132
- privilegeResults,
133
- mv,
134
- mv,
135
- "materializedView",
136
- {
137
- Grant: GrantMaterializedViewPrivileges,
138
- Revoke: RevokeMaterializedViewPrivileges,
139
- RevokeGrantOption: RevokeGrantOptionMaterializedViewPrivileges,
140
- },
141
- effectiveDefaults,
142
- ctx.version,
143
- ) as MaterializedViewChange[]),
152
+ ...buildCreateMaterializedViewChanges(ctx, branch[materializedViewId]),
144
153
  );
145
154
  }
146
155
 
@@ -180,9 +189,7 @@ export function diffMaterializedViews(
180
189
  // Replace the entire materialized view (drop + create)
181
190
  changes.push(
182
191
  new DropMaterializedView({ materializedView: mainMaterializedView }),
183
- new CreateMaterializedView({
184
- materializedView: branchMaterializedView,
185
- }),
192
+ ...buildCreateMaterializedViewChanges(ctx, branchMaterializedView),
186
193
  );
187
194
  } else {
188
195
  // Only alterable properties changed - check each one
@@ -651,6 +651,15 @@ export class AlterTableAlterColumnType extends AlterTableChange {
651
651
  ];
652
652
  }
653
653
 
654
+ get invalidates() {
655
+ // ALTER COLUMN ... TYPE rewrites the column in place. The column keeps its
656
+ // identity, but anything bound to its old type (views, rules, etc.) must be
657
+ // dropped before the rewrite and rebuilt after, so report it as invalidated.
658
+ return [
659
+ stableId.column(this.table.schema, this.table.name, this.column.name),
660
+ ];
661
+ }
662
+
654
663
  serialize(_options?: SerializeOptions): string {
655
664
  // previousColumn is optional so direct serializer tests/fixtures can keep
656
665
  // emitting canonical ALTER TYPE SQL without forcing a USING expression.