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

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 (65) hide show
  1. package/dist/cli/bin/cli.js +8 -1
  2. package/dist/cli/commands/plan.js +33 -1
  3. package/dist/cli/utils.d.ts +3 -0
  4. package/dist/cli/utils.js +6 -3
  5. package/dist/core/objects/base.change.d.ts +39 -2
  6. package/dist/core/objects/base.change.js +32 -3
  7. package/dist/core/objects/subscription/changes/subscription.alter.d.ts +1 -0
  8. package/dist/core/objects/subscription/changes/subscription.alter.js +5 -0
  9. package/dist/core/objects/subscription/changes/subscription.create.js +10 -1
  10. package/dist/core/objects/subscription/changes/subscription.drop.d.ts +1 -0
  11. package/dist/core/objects/subscription/changes/subscription.drop.js +5 -0
  12. package/dist/core/objects/type/enum/changes/enum.alter.d.ts +1 -0
  13. package/dist/core/objects/type/enum/changes/enum.alter.js +4 -0
  14. package/dist/core/plan/apply.d.ts +10 -1
  15. package/dist/core/plan/apply.js +64 -29
  16. package/dist/core/plan/create.js +5 -31
  17. package/dist/core/plan/execution.d.ts +21 -0
  18. package/dist/core/plan/execution.js +76 -0
  19. package/dist/core/plan/index.d.ts +1 -1
  20. package/dist/core/plan/index.js +1 -1
  21. package/dist/core/plan/io.d.ts +2 -1
  22. package/dist/core/plan/io.js +4 -2
  23. package/dist/core/plan/normalize.d.ts +11 -0
  24. package/dist/core/plan/normalize.js +33 -0
  25. package/dist/core/plan/render.d.ts +32 -0
  26. package/dist/core/plan/render.js +104 -0
  27. package/dist/core/plan/types.d.ts +47 -3
  28. package/dist/core/plan/types.js +18 -1
  29. package/dist/core/sort/sort-changes.js +5 -4
  30. package/dist/core/sort/unorderable-cycle-error.d.ts +18 -0
  31. package/dist/core/sort/unorderable-cycle-error.js +20 -0
  32. package/dist/index.d.ts +5 -1
  33. package/dist/index.js +2 -1
  34. package/package.json +1 -1
  35. package/src/cli/bin/cli.ts +9 -1
  36. package/src/cli/commands/plan.ts +47 -1
  37. package/src/cli/utils.ts +21 -5
  38. package/src/core/catalog.snapshot.test.ts +2 -1
  39. package/src/core/objects/base.change.ts +44 -3
  40. package/src/core/objects/subscription/changes/subscription.alter.ts +6 -0
  41. package/src/core/objects/subscription/changes/subscription.create.test.ts +4 -1
  42. package/src/core/objects/subscription/changes/subscription.create.ts +10 -1
  43. package/src/core/objects/subscription/changes/subscription.drop.ts +6 -0
  44. package/src/core/objects/subscription/changes/subscription.traits.test.ts +83 -0
  45. package/src/core/objects/type/enum/changes/enum.alter.ts +5 -0
  46. package/src/core/plan/apply.ts +84 -39
  47. package/src/core/plan/create.ts +5 -46
  48. package/src/core/plan/execution.test.ts +231 -0
  49. package/src/core/plan/execution.ts +115 -0
  50. package/src/core/plan/index.ts +1 -1
  51. package/src/core/plan/io.ts +4 -2
  52. package/src/core/plan/normalize.test.ts +69 -0
  53. package/src/core/plan/normalize.ts +40 -0
  54. package/src/core/plan/render.test.ts +134 -0
  55. package/src/core/plan/render.ts +153 -0
  56. package/src/core/plan/sql-format/format-off.test.ts +1 -1
  57. package/src/core/plan/sql-format/format-pretty-lower-leading.test.ts +1 -0
  58. package/src/core/plan/sql-format/format-pretty-narrow.test.ts +2 -1
  59. package/src/core/plan/sql-format/format-pretty-preserve.test.ts +2 -1
  60. package/src/core/plan/sql-format/format-pretty-upper.test.ts +2 -1
  61. package/src/core/plan/types.ts +63 -3
  62. package/src/core/sort/sort-changes.ts +9 -4
  63. package/src/core/sort/unorderable-cycle-error.test.ts +60 -0
  64. package/src/core/sort/unorderable-cycle-error.ts +23 -0
  65. package/src/index.ts +18 -1
@@ -0,0 +1,231 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import type { Change } from "../change.types.ts";
3
+ import { BaseChange } from "../objects/base.change.ts";
4
+ import type { ColumnProps } from "../objects/base.model.ts";
5
+ import { AlterTableAlterColumnSetDefault } from "../objects/table/changes/table.alter.ts";
6
+ import type { Table } from "../objects/table/table.model.ts";
7
+ import { AlterEnumAddValue } from "../objects/type/enum/changes/enum.alter.ts";
8
+ import { Enum } from "../objects/type/enum/enum.model.ts";
9
+ import { buildExecutionPlan } from "./execution.ts";
10
+
11
+ describe("buildExecutionPlan", () => {
12
+ test("splits after enum values before subsequent statements", () => {
13
+ const userRole = createEnum(["admin", "user", "store"]);
14
+ const column = createEnumColumn("'store'::public.user_role");
15
+
16
+ const execution = buildExecutionPlan([
17
+ new AlterEnumAddValue({
18
+ enum: userRole,
19
+ newValue: "store",
20
+ position: { after: "user" },
21
+ }),
22
+ new AlterTableAlterColumnSetDefault({
23
+ table: createTable(column),
24
+ column,
25
+ }),
26
+ ]);
27
+
28
+ expect(execution.units).toHaveLength(2);
29
+ expect(execution.units[0].statements).toHaveLength(1);
30
+ expect(execution.units[1].reason).toBe("enum_value_visibility");
31
+ expect(execution.units[1].transactionMode).toBe("transactional");
32
+ });
33
+
34
+ test("keeps newly added enum values in one unit when nothing uses them", () => {
35
+ const userRole = createEnum(["admin", "user", "store"]);
36
+
37
+ const execution = buildExecutionPlan([
38
+ new AlterEnumAddValue({
39
+ enum: userRole,
40
+ newValue: "store",
41
+ position: { after: "user" },
42
+ }),
43
+ ]);
44
+
45
+ expect(execution.units).toHaveLength(1);
46
+ expect(execution.units[0].reason).toBe("default");
47
+ });
48
+
49
+ test("groups multiple enum additions before a dependent consumer", () => {
50
+ const userRole = createEnum(["admin", "user", "store", "auditor"]);
51
+ const column = createEnumColumn("'auditor'::public.user_role");
52
+
53
+ const execution = buildExecutionPlan([
54
+ new AlterEnumAddValue({
55
+ enum: userRole,
56
+ newValue: "store",
57
+ position: { after: "user" },
58
+ }),
59
+ new AlterEnumAddValue({
60
+ enum: userRole,
61
+ newValue: "auditor",
62
+ position: { after: "store" },
63
+ }),
64
+ new AlterTableAlterColumnSetDefault({
65
+ table: createTable(column),
66
+ column,
67
+ }),
68
+ ]);
69
+
70
+ expect(execution.units).toHaveLength(2);
71
+ expect(execution.units[0].statements).toMatchInlineSnapshot(`
72
+ [
73
+ "ALTER TYPE public.user_role ADD VALUE 'store' AFTER 'user'",
74
+ "ALTER TYPE public.user_role ADD VALUE 'auditor' AFTER 'store'",
75
+ ]
76
+ `);
77
+ });
78
+
79
+ test("splits after enum values before opaque later statements", () => {
80
+ const userRole = createEnum(["admin", "user", "store"]);
81
+
82
+ const execution = buildExecutionPlan([
83
+ new AlterEnumAddValue({
84
+ enum: userRole,
85
+ newValue: "store",
86
+ position: { after: "user" },
87
+ }),
88
+ new OpaqueEnumConsumerChange() as unknown as Change,
89
+ ]);
90
+
91
+ expect(execution.units).toHaveLength(2);
92
+ expect(execution.units[1].reason).toBe("enum_value_visibility");
93
+ expect(execution.units[1].statements[0]).toBe(
94
+ "CREATE VIEW public.store_profiles AS SELECT 'store'::public.user_role AS role",
95
+ );
96
+ });
97
+
98
+ test("puts non-transactional statements in their own unit", () => {
99
+ const userRole = createEnum(["admin", "user", "store"]);
100
+
101
+ const execution = buildExecutionPlan([
102
+ new AlterEnumAddValue({
103
+ enum: userRole,
104
+ newValue: "store",
105
+ position: { after: "user" },
106
+ }),
107
+ new NonTransactionalChange() as unknown as Change,
108
+ new OpaqueEnumConsumerChange() as unknown as Change,
109
+ ]);
110
+
111
+ expect(execution.units).toMatchInlineSnapshot(`
112
+ [
113
+ {
114
+ "reason": "default",
115
+ "statements": [
116
+ "ALTER TYPE public.user_role ADD VALUE 'store' AFTER 'user'",
117
+ ],
118
+ "transactionMode": "transactional",
119
+ },
120
+ {
121
+ "reason": "non_transactional",
122
+ "statements": [
123
+ "CREATE INDEX CONCURRENTLY users_email_idx ON public.users (email)",
124
+ ],
125
+ "transactionMode": "none",
126
+ },
127
+ {
128
+ "reason": "default",
129
+ "statements": [
130
+ "CREATE VIEW public.store_profiles AS SELECT 'store'::public.user_role AS role",
131
+ ],
132
+ "transactionMode": "transactional",
133
+ },
134
+ ]
135
+ `);
136
+ });
137
+
138
+ test("routes SET ROLE and check_function_bodies into session statements, not units", () => {
139
+ const execution = buildExecutionPlan(
140
+ [new ProcedureChange() as unknown as Change],
141
+ { role: "app_owner" },
142
+ );
143
+
144
+ expect(execution.sessionStatements).toEqual([
145
+ 'SET ROLE "app_owner"',
146
+ "SET check_function_bodies = false",
147
+ ]);
148
+ expect(execution.units).toHaveLength(1);
149
+ expect(execution.units[0].statements).toEqual([
150
+ "CREATE PROCEDURE public.noop() LANGUAGE sql AS $$ SELECT 1 $$",
151
+ ]);
152
+ });
153
+ });
154
+
155
+ class NonTransactionalChange extends BaseChange {
156
+ readonly operation = "create";
157
+ readonly objectType = "index";
158
+ readonly scope = "object";
159
+
160
+ override get nonTransactional() {
161
+ return true;
162
+ }
163
+
164
+ serialize(): string {
165
+ return "CREATE INDEX CONCURRENTLY users_email_idx ON public.users (email)";
166
+ }
167
+ }
168
+
169
+ class OpaqueEnumConsumerChange extends BaseChange {
170
+ readonly operation = "create";
171
+ readonly objectType = "view";
172
+ readonly scope = "object";
173
+
174
+ serialize(): string {
175
+ return "CREATE VIEW public.store_profiles AS SELECT 'store'::public.user_role AS role";
176
+ }
177
+ }
178
+
179
+ class ProcedureChange extends BaseChange {
180
+ readonly operation = "create";
181
+ readonly objectType = "procedure";
182
+ readonly scope = "object";
183
+
184
+ serialize(): string {
185
+ return "CREATE PROCEDURE public.noop() LANGUAGE sql AS $$ SELECT 1 $$";
186
+ }
187
+ }
188
+
189
+ function createEnum(labels: string[]): Enum {
190
+ return new Enum({
191
+ schema: "public",
192
+ name: "user_role",
193
+ owner: "postgres",
194
+ labels: labels.map((label, index) => ({
195
+ label,
196
+ sort_order: index + 1,
197
+ })),
198
+ comment: null,
199
+ privileges: [],
200
+ });
201
+ }
202
+
203
+ function createEnumColumn(defaultValue: string): ColumnProps {
204
+ return {
205
+ name: "role",
206
+ position: 1,
207
+ data_type: "USER-DEFINED",
208
+ data_type_str: "public.user_role",
209
+ is_custom_type: true,
210
+ custom_type_type: "e",
211
+ custom_type_category: "E",
212
+ custom_type_schema: "public",
213
+ custom_type_name: "user_role",
214
+ not_null: false,
215
+ is_identity: false,
216
+ is_identity_always: false,
217
+ is_generated: false,
218
+ collation: null,
219
+ default: defaultValue,
220
+ comment: null,
221
+ };
222
+ }
223
+
224
+ function createTable(column: ColumnProps): Table {
225
+ return {
226
+ schema: "public",
227
+ name: "profiles",
228
+ stableId: "table:public.profiles",
229
+ columns: [column],
230
+ } as unknown as Table;
231
+ }
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Execution planning - group sorted changes into transaction-aware
3
+ * migration units.
4
+ *
5
+ * Execution semantics come from the `nonTransactional` and `commitBoundary`
6
+ * traits declared on the change classes (see `base.change.ts`), never from
7
+ * inspecting rendered SQL.
8
+ */
9
+
10
+ import { escapeIdentifier } from "pg";
11
+ import type { Change } from "../change.types.ts";
12
+ import type { ResolvedIntegration } from "../integrations/integration.types.ts";
13
+ import type { CommitBoundaryReason } from "../objects/base.change.ts";
14
+ import type { ExecutionBoundaryReason, MigrationUnit } from "./types.ts";
15
+
16
+ interface BuildExecutionPlanOptions {
17
+ integration?: ResolvedIntegration;
18
+ role?: string;
19
+ }
20
+
21
+ interface ExecutionPlan {
22
+ units: MigrationUnit[];
23
+ sessionStatements: string[];
24
+ }
25
+
26
+ export function buildExecutionPlan(
27
+ changes: Change[],
28
+ options: BuildExecutionPlanOptions = {},
29
+ ): ExecutionPlan {
30
+ return {
31
+ units: buildMigrationUnits(changes, options.integration),
32
+ sessionStatements: buildSessionStatements(changes, options),
33
+ };
34
+ }
35
+
36
+ function buildSessionStatements(
37
+ changes: Change[],
38
+ options: BuildExecutionPlanOptions,
39
+ ): string[] {
40
+ const statements: string[] = [];
41
+
42
+ if (options.role) {
43
+ statements.push(`SET ROLE ${escapeIdentifier(options.role)}`);
44
+ }
45
+
46
+ if (hasRoutineChanges(changes)) {
47
+ statements.push("SET check_function_bodies = false");
48
+ }
49
+
50
+ return statements;
51
+ }
52
+
53
+ /**
54
+ * Check if any changes involve routines (procedures or aggregates).
55
+ * Used to determine if we need to disable function body checking.
56
+ */
57
+ function hasRoutineChanges(changes: Change[]): boolean {
58
+ return changes.some(
59
+ (change) =>
60
+ change.objectType === "procedure" || change.objectType === "aggregate",
61
+ );
62
+ }
63
+
64
+ function buildMigrationUnits(
65
+ changes: Change[],
66
+ integration?: ResolvedIntegration,
67
+ ): MigrationUnit[] {
68
+ const units: MigrationUnit[] = [];
69
+ let current: string[] = [];
70
+ let reason: ExecutionBoundaryReason = "default";
71
+ let pendingBoundary: CommitBoundaryReason | null = null;
72
+
73
+ function flush(): void {
74
+ if (current.length === 0) return;
75
+ units.push({
76
+ transactionMode: "transactional",
77
+ reason,
78
+ statements: current,
79
+ });
80
+ current = [];
81
+ }
82
+
83
+ for (const change of changes) {
84
+ const sql = integration?.serialize?.(change) ?? change.serialize();
85
+ const boundary = change.commitBoundary;
86
+
87
+ if (change.nonTransactional) {
88
+ flush();
89
+ pendingBoundary = null;
90
+ reason = "default";
91
+ units.push({
92
+ transactionMode: "none",
93
+ reason: "non_transactional",
94
+ statements: [sql],
95
+ });
96
+ continue;
97
+ }
98
+
99
+ // Only producers of the same boundary kind share a unit; anything else
100
+ // (a different kind or a non-producer) runs after the producers' COMMIT.
101
+ if (pendingBoundary !== null && boundary !== pendingBoundary) {
102
+ flush();
103
+ reason = pendingBoundary;
104
+ pendingBoundary = null;
105
+ }
106
+
107
+ current.push(sql);
108
+ if (boundary !== null) {
109
+ pendingBoundary = boundary;
110
+ }
111
+ }
112
+
113
+ flush();
114
+ return units;
115
+ }
@@ -9,7 +9,7 @@
9
9
  * if (planResult) {
10
10
  * const { plan, sortedChanges, ctx } = planResult;
11
11
  * const hierarchy = groupChangesHierarchically(ctx, sortedChanges);
12
- * console.log(plan.statements);
12
+ * console.log(renderPlanSql(plan));
13
13
  * }
14
14
  * ```
15
15
  */
@@ -2,6 +2,7 @@
2
2
  * Plan I/O utilities for serializing and deserializing plans to/from JSON.
3
3
  */
4
4
 
5
+ import { normalizePlan } from "./normalize.ts";
5
6
  import { type Plan, PlanSchema } from "./types.ts";
6
7
 
7
8
  /**
@@ -12,9 +13,10 @@ export function serializePlan(plan: Plan): string {
12
13
  }
13
14
 
14
15
  /**
15
- * Deserialize a plan from JSON string.
16
+ * Deserialize a plan from JSON string. Legacy v1 plans (flat `statements`)
17
+ * are normalized into migration units.
16
18
  */
17
19
  export function deserializePlan(json: string): Plan {
18
20
  const parsed = JSON.parse(json);
19
- return PlanSchema.parse(parsed);
21
+ return normalizePlan(PlanSchema.parse(parsed));
20
22
  }
@@ -0,0 +1,69 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { normalizePlan } from "./normalize.ts";
3
+ import type { SerializedPlan } from "./types.ts";
4
+
5
+ describe("normalizePlan", () => {
6
+ test("hydrates legacy v1 plans into a single transactional unit", () => {
7
+ const legacy: SerializedPlan = {
8
+ version: 1,
9
+ source: { fingerprint: "source" },
10
+ target: { fingerprint: "target" },
11
+ role: "app_owner",
12
+ statements: [
13
+ 'SET ROLE "app_owner"',
14
+ "CREATE TABLE public.users (id integer)",
15
+ "CREATE INDEX users_id_idx ON public.users (id)",
16
+ ],
17
+ };
18
+
19
+ const plan = normalizePlan(legacy);
20
+
21
+ expect(plan.sessionStatements).toEqual(['SET ROLE "app_owner"']);
22
+ expect(plan.units).toMatchInlineSnapshot(`
23
+ [
24
+ {
25
+ "reason": "default",
26
+ "statements": [
27
+ "CREATE TABLE public.users (id integer)",
28
+ "CREATE INDEX users_id_idx ON public.users (id)",
29
+ ],
30
+ "transactionMode": "transactional",
31
+ },
32
+ ]
33
+ `);
34
+ expect("statements" in plan).toBe(false);
35
+ });
36
+
37
+ test("hydrates legacy v1 plans with only SET statements into zero units", () => {
38
+ const legacy: SerializedPlan = {
39
+ version: 1,
40
+ source: { fingerprint: "source" },
41
+ target: { fingerprint: "target" },
42
+ statements: ['SET ROLE "app_owner"'],
43
+ };
44
+
45
+ const plan = normalizePlan(legacy);
46
+ expect(plan.units).toEqual([]);
47
+ expect(plan.sessionStatements).toEqual(['SET ROLE "app_owner"']);
48
+ });
49
+
50
+ test("passes v2 plans through and defaults sessionStatements", () => {
51
+ const units = [
52
+ {
53
+ transactionMode: "transactional" as const,
54
+ reason: "default" as const,
55
+ statements: ["CREATE TABLE public.users (id integer)"],
56
+ },
57
+ ];
58
+ const v2: SerializedPlan = {
59
+ version: 2,
60
+ source: { fingerprint: "source" },
61
+ target: { fingerprint: "target" },
62
+ units,
63
+ };
64
+
65
+ const plan = normalizePlan(v2);
66
+ expect(plan.units).toEqual(units);
67
+ expect(plan.sessionStatements).toEqual([]);
68
+ });
69
+ });
@@ -0,0 +1,40 @@
1
+ import type { MigrationUnit, Plan, SerializedPlan } from "./types.ts";
2
+
3
+ /**
4
+ * Normalize a plan into the v2 shape: `units` + `sessionStatements`.
5
+ *
6
+ * Legacy v1 plans carry a flat `statements` array instead of units. Their
7
+ * leading SET statements become session statements, and the remaining
8
+ * statements become a single transactional unit — faithful to how the v1
9
+ * applier executed them (one multi-statement query, i.e. one implicit
10
+ * transaction).
11
+ */
12
+ export function normalizePlan(plan: SerializedPlan): Plan {
13
+ const { statements, ...rest } = plan;
14
+ return {
15
+ ...rest,
16
+ units: plan.units ?? legacyUnits(statements ?? []),
17
+ sessionStatements:
18
+ plan.sessionStatements ??
19
+ (statements ?? []).filter((statement) => isSessionStatement(statement)),
20
+ };
21
+ }
22
+
23
+ function isSessionStatement(statement: string): boolean {
24
+ return /^SET\s+/i.test(statement.trim());
25
+ }
26
+
27
+ function legacyUnits(statements: string[]): MigrationUnit[] {
28
+ const schemaStatements = statements.filter(
29
+ (statement) => !isSessionStatement(statement),
30
+ );
31
+ if (schemaStatements.length === 0) return [];
32
+
33
+ return [
34
+ {
35
+ transactionMode: "transactional",
36
+ reason: "default",
37
+ statements: schemaStatements,
38
+ },
39
+ ];
40
+ }
@@ -0,0 +1,134 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import {
3
+ flattenPlanStatements,
4
+ renderPlanFiles,
5
+ renderPlanSql,
6
+ } from "./render.ts";
7
+ import type { Plan } from "./types.ts";
8
+
9
+ describe("plan rendering", () => {
10
+ test("renders single SQL scripts with unit boundary comments", () => {
11
+ expect(renderPlanSql(createPlan())).toMatchInlineSnapshot(`
12
+ "-- Migration unit 1: schema_changes
13
+ -- Transaction mode: transactional
14
+ -- Boundary reason: default
15
+
16
+ SET ROLE app_owner;
17
+
18
+ BEGIN;
19
+
20
+ ALTER TYPE public.user_role ADD VALUE 'store';
21
+
22
+ COMMIT;
23
+
24
+ -- Migration unit 2: after_enum_values
25
+ -- Transaction mode: transactional
26
+ -- Boundary reason: enum_value_visibility
27
+
28
+ SET ROLE app_owner;
29
+
30
+ BEGIN;
31
+
32
+ ALTER TABLE public.profiles ALTER COLUMN role SET DEFAULT 'store'::public.user_role;
33
+
34
+ COMMIT;"
35
+ `);
36
+ });
37
+
38
+ test("renders numbered migration files from units", () => {
39
+ const files = renderPlanFiles(createPlan());
40
+
41
+ expect(files.map((file) => file.path)).toMatchInlineSnapshot(`
42
+ [
43
+ "001_schema_changes.sql",
44
+ "002_after_enum_values.sql",
45
+ ]
46
+ `);
47
+
48
+ expect(files[0].sql).toMatchInlineSnapshot(`
49
+ "-- Migration unit 1: schema_changes
50
+ -- Transaction mode: transactional
51
+ -- Boundary reason: default
52
+
53
+ SET ROLE app_owner;
54
+
55
+ BEGIN;
56
+
57
+ ALTER TYPE public.user_role ADD VALUE 'store';
58
+
59
+ COMMIT;"
60
+ `);
61
+
62
+ expect(files[1].sql).toMatchInlineSnapshot(`
63
+ "-- Migration unit 2: after_enum_values
64
+ -- Transaction mode: transactional
65
+ -- Boundary reason: enum_value_visibility
66
+
67
+ SET ROLE app_owner;
68
+
69
+ BEGIN;
70
+
71
+ ALTER TABLE public.profiles ALTER COLUMN role SET DEFAULT 'store'::public.user_role;
72
+
73
+ COMMIT;"
74
+ `);
75
+ });
76
+
77
+ test("renders non-transactional units without transaction wrappers", () => {
78
+ const plan = createPlan();
79
+ plan.units = [
80
+ {
81
+ transactionMode: "none",
82
+ reason: "non_transactional",
83
+ statements: [
84
+ "CREATE INDEX CONCURRENTLY users_email_idx ON public.users (email)",
85
+ ],
86
+ },
87
+ ];
88
+
89
+ expect(renderPlanSql(plan)).toMatchInlineSnapshot(`
90
+ "-- Migration unit 1: non_transactional
91
+ -- Transaction mode: none
92
+ -- Boundary reason: non_transactional
93
+ -- Run statement-by-statement (psql does this; do not use psql -1 or
94
+ -- send this script as a single multi-statement query string).
95
+
96
+ SET ROLE app_owner;
97
+
98
+ CREATE INDEX CONCURRENTLY users_email_idx ON public.users (email);"
99
+ `);
100
+ });
101
+
102
+ test("flattenPlanStatements includes session statements", () => {
103
+ expect(flattenPlanStatements(createPlan())).toEqual([
104
+ "SET ROLE app_owner",
105
+ "ALTER TYPE public.user_role ADD VALUE 'store'",
106
+ "ALTER TABLE public.profiles ALTER COLUMN role SET DEFAULT 'store'::public.user_role",
107
+ ]);
108
+ });
109
+ });
110
+
111
+ function createPlan(): Plan {
112
+ return {
113
+ version: 2,
114
+ source: { fingerprint: "source" },
115
+ target: { fingerprint: "target" },
116
+ role: "app_owner",
117
+ sessionStatements: ["SET ROLE app_owner"],
118
+ units: [
119
+ {
120
+ transactionMode: "transactional",
121
+ reason: "default",
122
+ statements: ["ALTER TYPE public.user_role ADD VALUE 'store'"],
123
+ },
124
+ {
125
+ transactionMode: "transactional",
126
+ reason: "enum_value_visibility",
127
+ statements: [
128
+ "ALTER TABLE public.profiles ALTER COLUMN role SET DEFAULT 'store'::public.user_role",
129
+ ],
130
+ },
131
+ ],
132
+ risk: { level: "safe" },
133
+ };
134
+ }