@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.
- package/dist/cli/bin/cli.js +8 -1
- package/dist/cli/commands/plan.js +33 -1
- package/dist/cli/utils.d.ts +3 -0
- package/dist/cli/utils.js +6 -3
- package/dist/core/objects/base.change.d.ts +39 -2
- package/dist/core/objects/base.change.js +32 -3
- package/dist/core/objects/subscription/changes/subscription.alter.d.ts +1 -0
- package/dist/core/objects/subscription/changes/subscription.alter.js +5 -0
- package/dist/core/objects/subscription/changes/subscription.create.js +10 -1
- package/dist/core/objects/subscription/changes/subscription.drop.d.ts +1 -0
- package/dist/core/objects/subscription/changes/subscription.drop.js +5 -0
- package/dist/core/objects/type/enum/changes/enum.alter.d.ts +1 -0
- package/dist/core/objects/type/enum/changes/enum.alter.js +4 -0
- package/dist/core/plan/apply.d.ts +10 -1
- package/dist/core/plan/apply.js +64 -29
- package/dist/core/plan/create.js +5 -31
- package/dist/core/plan/execution.d.ts +21 -0
- package/dist/core/plan/execution.js +76 -0
- package/dist/core/plan/index.d.ts +1 -1
- package/dist/core/plan/index.js +1 -1
- package/dist/core/plan/io.d.ts +2 -1
- package/dist/core/plan/io.js +4 -2
- package/dist/core/plan/normalize.d.ts +11 -0
- package/dist/core/plan/normalize.js +33 -0
- package/dist/core/plan/render.d.ts +32 -0
- package/dist/core/plan/render.js +104 -0
- package/dist/core/plan/types.d.ts +47 -3
- package/dist/core/plan/types.js +18 -1
- package/dist/core/sort/sort-changes.js +5 -4
- package/dist/core/sort/unorderable-cycle-error.d.ts +18 -0
- package/dist/core/sort/unorderable-cycle-error.js +20 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.js +2 -1
- package/package.json +1 -1
- package/src/cli/bin/cli.ts +9 -1
- package/src/cli/commands/plan.ts +47 -1
- package/src/cli/utils.ts +21 -5
- package/src/core/catalog.snapshot.test.ts +2 -1
- package/src/core/objects/base.change.ts +44 -3
- package/src/core/objects/subscription/changes/subscription.alter.ts +6 -0
- package/src/core/objects/subscription/changes/subscription.create.test.ts +4 -1
- package/src/core/objects/subscription/changes/subscription.create.ts +10 -1
- package/src/core/objects/subscription/changes/subscription.drop.ts +6 -0
- package/src/core/objects/subscription/changes/subscription.traits.test.ts +83 -0
- package/src/core/objects/type/enum/changes/enum.alter.ts +5 -0
- package/src/core/plan/apply.ts +84 -39
- package/src/core/plan/create.ts +5 -46
- package/src/core/plan/execution.test.ts +231 -0
- package/src/core/plan/execution.ts +115 -0
- package/src/core/plan/index.ts +1 -1
- package/src/core/plan/io.ts +4 -2
- package/src/core/plan/normalize.test.ts +69 -0
- package/src/core/plan/normalize.ts +40 -0
- package/src/core/plan/render.test.ts +134 -0
- package/src/core/plan/render.ts +153 -0
- package/src/core/plan/sql-format/format-off.test.ts +1 -1
- package/src/core/plan/sql-format/format-pretty-lower-leading.test.ts +1 -0
- package/src/core/plan/sql-format/format-pretty-narrow.test.ts +2 -1
- package/src/core/plan/sql-format/format-pretty-preserve.test.ts +2 -1
- package/src/core/plan/sql-format/format-pretty-upper.test.ts +2 -1
- package/src/core/plan/types.ts +63 -3
- package/src/core/sort/sort-changes.ts +9 -4
- package/src/core/sort/unorderable-cycle-error.test.ts +60 -0
- package/src/core/sort/unorderable-cycle-error.ts +23 -0
- package/src/index.ts +18 -1
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plan rendering - turn migration units into executable SQL scripts.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { normalizePlan } from "./normalize.ts";
|
|
6
|
+
import type { SqlFormatOptions } from "./sql-format.ts";
|
|
7
|
+
import { formatSqlStatements } from "./sql-format.ts";
|
|
8
|
+
import type {
|
|
9
|
+
ExecutionBoundaryReason,
|
|
10
|
+
MigrationUnit,
|
|
11
|
+
Plan,
|
|
12
|
+
SerializedPlan,
|
|
13
|
+
} from "./types.ts";
|
|
14
|
+
|
|
15
|
+
const STATEMENT_DELIMITER = ";\n\n";
|
|
16
|
+
|
|
17
|
+
export interface RenderPlanSqlOptions {
|
|
18
|
+
sqlFormatOptions?: SqlFormatOptions;
|
|
19
|
+
includeTransactions?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface RenderedPlanFile {
|
|
23
|
+
path: string;
|
|
24
|
+
sql: string;
|
|
25
|
+
unit: MigrationUnit;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Render the whole plan as a single SQL script. Each migration unit is
|
|
30
|
+
* delimited with header comments and, for transactional units, wrapped in
|
|
31
|
+
* explicit BEGIN/COMMIT.
|
|
32
|
+
*/
|
|
33
|
+
export function renderPlanSql(
|
|
34
|
+
plan: SerializedPlan,
|
|
35
|
+
options: RenderPlanSqlOptions = {},
|
|
36
|
+
): string {
|
|
37
|
+
const normalized = normalizePlan(plan);
|
|
38
|
+
return normalized.units
|
|
39
|
+
.map((unit, index) => renderUnitSql(normalized, unit, index, options))
|
|
40
|
+
.join("\n\n");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Render the plan as one numbered SQL file per migration unit. Session
|
|
45
|
+
* statements are repeated in every file because each file may be executed
|
|
46
|
+
* in its own session.
|
|
47
|
+
*/
|
|
48
|
+
export function renderPlanFiles(
|
|
49
|
+
plan: SerializedPlan,
|
|
50
|
+
options: RenderPlanSqlOptions = {},
|
|
51
|
+
): RenderedPlanFile[] {
|
|
52
|
+
const normalized = normalizePlan(plan);
|
|
53
|
+
return normalized.units.map((unit, index) => ({
|
|
54
|
+
path: `${String(index + 1).padStart(3, "0")}_${unitName(unit.reason)}.sql`,
|
|
55
|
+
sql: renderUnitSql(normalized, unit, index, options),
|
|
56
|
+
unit,
|
|
57
|
+
}));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Flatten a plan back into the ordered statement list, session statements
|
|
62
|
+
* included. Execution context (transaction boundaries) is lost — use
|
|
63
|
+
* `renderPlanSql`/`renderPlanFiles` or `plan.units` when it matters.
|
|
64
|
+
*/
|
|
65
|
+
export function flattenPlanStatements(plan: SerializedPlan): string[] {
|
|
66
|
+
const normalized = normalizePlan(plan);
|
|
67
|
+
return [
|
|
68
|
+
...normalized.sessionStatements,
|
|
69
|
+
...normalized.units.flatMap((unit) => unit.statements),
|
|
70
|
+
];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Display name for a migration unit, derived from its boundary reason. */
|
|
74
|
+
function unitName(reason: ExecutionBoundaryReason): string {
|
|
75
|
+
switch (reason) {
|
|
76
|
+
case "default":
|
|
77
|
+
return "schema_changes";
|
|
78
|
+
case "enum_value_visibility":
|
|
79
|
+
return "after_enum_values";
|
|
80
|
+
case "non_transactional":
|
|
81
|
+
return "non_transactional";
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function renderUnitSql(
|
|
86
|
+
plan: Plan,
|
|
87
|
+
unit: MigrationUnit,
|
|
88
|
+
index: number,
|
|
89
|
+
options: RenderPlanSqlOptions,
|
|
90
|
+
): string {
|
|
91
|
+
const includeTransactions = options.includeTransactions !== false;
|
|
92
|
+
const body =
|
|
93
|
+
options.sqlFormatOptions != null
|
|
94
|
+
? formatSqlStatements(unit.statements, options.sqlFormatOptions)
|
|
95
|
+
: unit.statements;
|
|
96
|
+
|
|
97
|
+
const lines: string[] = [
|
|
98
|
+
`-- Migration unit ${index + 1}: ${unitName(unit.reason)}`,
|
|
99
|
+
`-- Transaction mode: ${unit.transactionMode}`,
|
|
100
|
+
`-- Boundary reason: ${unit.reason}`,
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
if (unit.transactionMode === "none") {
|
|
104
|
+
// PostgreSQL runs every statement of a multi-command simple-query string
|
|
105
|
+
// in an implicit transaction block, so this unit can never execute as
|
|
106
|
+
// part of a single query string — no SET/COMMIT shuffling changes that.
|
|
107
|
+
lines.push(
|
|
108
|
+
"-- Run statement-by-statement (psql does this; do not use psql -1 or",
|
|
109
|
+
"-- send this script as a single multi-statement query string).",
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
lines.push("");
|
|
114
|
+
|
|
115
|
+
if (plan.sessionStatements.length > 0) {
|
|
116
|
+
lines.push(renderStatements(plan.sessionStatements));
|
|
117
|
+
lines.push("");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (includeTransactions && unit.transactionMode === "transactional") {
|
|
121
|
+
lines.push("BEGIN;");
|
|
122
|
+
lines.push("");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
lines.push(renderStatements(body));
|
|
126
|
+
|
|
127
|
+
if (includeTransactions && unit.transactionMode === "transactional") {
|
|
128
|
+
lines.push("");
|
|
129
|
+
lines.push("COMMIT;");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return lines.join("\n").trimEnd();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function renderStatements(statements: string[]): string {
|
|
136
|
+
if (statements.length === 0) return "";
|
|
137
|
+
return `${statements.map(trimTerminator).join(STATEMENT_DELIMITER)};`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Strip trailing semicolons so every rendered statement ends with exactly
|
|
142
|
+
* one. pg-delta's own serializers emit no terminator, but plan JSON is a
|
|
143
|
+
* persisted artifact — legacy v1 files, hand-built units, or user-edited
|
|
144
|
+
* plans may already carry one, and joining those blindly would render ";;".
|
|
145
|
+
*/
|
|
146
|
+
function trimTerminator(statement: string): string {
|
|
147
|
+
const trimmed = statement.trim();
|
|
148
|
+
let end = trimmed.length;
|
|
149
|
+
while (end > 0 && trimmed.charCodeAt(end - 1) === 59) {
|
|
150
|
+
end--;
|
|
151
|
+
}
|
|
152
|
+
return trimmed.slice(0, end);
|
|
153
|
+
}
|
|
@@ -659,7 +659,7 @@ describe("sql formatting snapshots", () => {
|
|
|
659
659
|
ALTER DEFAULT PRIVILEGES FOR ROLE app_user IN SCHEMA public REVOKE SELECT ON TABLES FROM app_reader;
|
|
660
660
|
|
|
661
661
|
-- subscription.create
|
|
662
|
-
CREATE SUBSCRIPTION sub_replica CONNECTION 'host=primary.db port=5432 dbname=mydb' PUBLICATION pub_custom WITH (slot_name = 'sub_replica_slot', binary = true, streaming = 'parallel', synchronous_commit = 'remote_apply', disable_on_error = true, failover = true);
|
|
662
|
+
CREATE SUBSCRIPTION sub_replica CONNECTION 'host=primary.db port=5432 dbname=mydb' PUBLICATION pub_custom WITH (slot_name = 'sub_replica_slot', binary = true, streaming = 'parallel', synchronous_commit = 'remote_apply', disable_on_error = true, failover = true, create_slot = false);
|
|
663
663
|
|
|
664
664
|
-- subscription.drop
|
|
665
665
|
DROP SUBSCRIPTION sub_replica;
|
|
@@ -1047,7 +1047,8 @@ describe("sql formatting snapshots", () => {
|
|
|
1047
1047
|
streaming = 'parallel',
|
|
1048
1048
|
synchronous_commit = 'remote_apply',
|
|
1049
1049
|
disable_on_error = true,
|
|
1050
|
-
failover = true
|
|
1050
|
+
failover = true,
|
|
1051
|
+
create_slot = false
|
|
1051
1052
|
);
|
|
1052
1053
|
|
|
1053
1054
|
-- subscription.drop
|
package/src/core/plan/types.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import z from "zod";
|
|
6
6
|
import type { Change } from "../change.types.ts";
|
|
7
7
|
import type { Integration } from "../integrations/integration.types.ts";
|
|
8
|
+
import type { CommitBoundaryReason } from "../objects/base.change.ts";
|
|
8
9
|
|
|
9
10
|
// ============================================================================
|
|
10
11
|
// Core Types
|
|
@@ -14,6 +15,34 @@ export type PlanRisk =
|
|
|
14
15
|
| { level: "safe" }
|
|
15
16
|
| { level: "data_loss"; statements: string[] };
|
|
16
17
|
|
|
18
|
+
export type TransactionMode = "transactional" | "none";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Why a migration unit starts a new execution boundary.
|
|
22
|
+
*
|
|
23
|
+
* - `"default"` — the first (or only) unit of the plan.
|
|
24
|
+
* - `"non_transactional"` — the unit's statement cannot run inside a
|
|
25
|
+
* transaction block (see `BaseChange.nonTransactional`).
|
|
26
|
+
* - commit-visibility kinds (see `BaseChange.commitBoundary`) — the previous
|
|
27
|
+
* unit produced effects that are only usable after COMMIT.
|
|
28
|
+
*/
|
|
29
|
+
export type ExecutionBoundaryReason =
|
|
30
|
+
| "default"
|
|
31
|
+
| "non_transactional"
|
|
32
|
+
| CommitBoundaryReason;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* An ordered group of SQL statements that share one execution context.
|
|
36
|
+
*
|
|
37
|
+
* Transactional units are applied inside an explicit BEGIN/COMMIT;
|
|
38
|
+
* non-transactional units run their single statement without a wrapper.
|
|
39
|
+
*/
|
|
40
|
+
export interface MigrationUnit {
|
|
41
|
+
transactionMode: TransactionMode;
|
|
42
|
+
reason: ExecutionBoundaryReason;
|
|
43
|
+
statements: string[];
|
|
44
|
+
}
|
|
45
|
+
|
|
17
46
|
/**
|
|
18
47
|
* All supported object types in the system.
|
|
19
48
|
* Derived from the Change union type's objectType discriminant.
|
|
@@ -127,7 +156,26 @@ export const PlanSchema = z.object({
|
|
|
127
156
|
target: z.object({
|
|
128
157
|
fingerprint: z.string(),
|
|
129
158
|
}),
|
|
130
|
-
|
|
159
|
+
units: z
|
|
160
|
+
.array(
|
|
161
|
+
z.object({
|
|
162
|
+
transactionMode: z.enum(["transactional", "none"]),
|
|
163
|
+
reason: z.enum([
|
|
164
|
+
"default",
|
|
165
|
+
"non_transactional",
|
|
166
|
+
"enum_value_visibility",
|
|
167
|
+
]),
|
|
168
|
+
statements: z.array(z.string()),
|
|
169
|
+
}),
|
|
170
|
+
)
|
|
171
|
+
.optional(),
|
|
172
|
+
/** Session-level statements (SET ROLE, ...) applied once before the units. */
|
|
173
|
+
sessionStatements: z.array(z.string()).optional(),
|
|
174
|
+
/**
|
|
175
|
+
* Legacy v1 plans only: the flat statement list. Converted to units by
|
|
176
|
+
* `normalizePlan`; never emitted by `createPlan`/`serializePlan`.
|
|
177
|
+
*/
|
|
178
|
+
statements: z.array(z.string()).optional(),
|
|
131
179
|
role: z.string().optional(),
|
|
132
180
|
filter: z.any().optional(), // FilterDSL - complex recursive type, validated at compile time
|
|
133
181
|
serialize: z.any().optional(), // SerializeDSL - complex recursive type, validated at compile time
|
|
@@ -144,10 +192,22 @@ export const PlanSchema = z.object({
|
|
|
144
192
|
.optional(),
|
|
145
193
|
});
|
|
146
194
|
|
|
195
|
+
export type SerializedPlan = z.infer<typeof PlanSchema>;
|
|
196
|
+
|
|
147
197
|
/**
|
|
148
|
-
* A migration plan containing all changes to transform one database schema
|
|
198
|
+
* A migration plan containing all changes to transform one database schema
|
|
199
|
+
* into another, as an ordered list of execution-aware migration units.
|
|
200
|
+
*
|
|
201
|
+
* `units` and `sessionStatements` are the single source of truth: render via
|
|
202
|
+
* `renderPlanSql`/`renderPlanFiles`, or flatten via `flattenPlanStatements`.
|
|
149
203
|
*/
|
|
150
|
-
export type Plan =
|
|
204
|
+
export type Plan = Omit<
|
|
205
|
+
SerializedPlan,
|
|
206
|
+
"units" | "statements" | "sessionStatements"
|
|
207
|
+
> & {
|
|
208
|
+
units: MigrationUnit[];
|
|
209
|
+
sessionStatements: string[];
|
|
210
|
+
};
|
|
151
211
|
|
|
152
212
|
/**
|
|
153
213
|
* Options for creating a plan.
|
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
performStableTopologicalSort,
|
|
40
40
|
} from "./topological-sort.ts";
|
|
41
41
|
import type { PgDependRow, PhaseSortOptions } from "./types.ts";
|
|
42
|
+
import { UnorderableCycleError } from "./unorderable-cycle-error.ts";
|
|
42
43
|
import { getExecutionPhase, type Phase } from "./utils.ts";
|
|
43
44
|
|
|
44
45
|
// `sortPhaseChanges` caps the change-injection breaker at one round per
|
|
@@ -238,7 +239,9 @@ function attemptSortRound(
|
|
|
238
239
|
|
|
239
240
|
if (!topologicalOrder || topologicalOrder.length !== phaseChanges.length) {
|
|
240
241
|
// This should never happen if findCycle returned null, but guard anyway
|
|
241
|
-
throw new
|
|
242
|
+
throw new UnorderableCycleError(
|
|
243
|
+
"CycleError: dependency graph contains a cycle",
|
|
244
|
+
);
|
|
242
245
|
}
|
|
243
246
|
|
|
244
247
|
return {
|
|
@@ -287,12 +290,13 @@ function sortPhaseChanges(
|
|
|
287
290
|
phaseChanges,
|
|
288
291
|
);
|
|
289
292
|
if (broken === null) {
|
|
290
|
-
throw new
|
|
293
|
+
throw new UnorderableCycleError(
|
|
291
294
|
formatCycleError(
|
|
292
295
|
result.cycleNodeIndexes,
|
|
293
296
|
phaseChanges,
|
|
294
297
|
result.cycleEdges,
|
|
295
298
|
),
|
|
299
|
+
result.cycleNodeIndexes.map((index) => phaseChanges[index]),
|
|
296
300
|
);
|
|
297
301
|
}
|
|
298
302
|
|
|
@@ -300,12 +304,13 @@ function sortPhaseChanges(
|
|
|
300
304
|
// the breaker isn't making progress. Throw with full context.
|
|
301
305
|
const signature = normalizeCycle(result.cycleNodeIndexes);
|
|
302
306
|
if (breakerRoundSignatures.has(signature)) {
|
|
303
|
-
throw new
|
|
307
|
+
throw new UnorderableCycleError(
|
|
304
308
|
formatCycleError(
|
|
305
309
|
result.cycleNodeIndexes,
|
|
306
310
|
phaseChanges,
|
|
307
311
|
result.cycleEdges,
|
|
308
312
|
),
|
|
313
|
+
result.cycleNodeIndexes.map((index) => phaseChanges[index]),
|
|
309
314
|
);
|
|
310
315
|
}
|
|
311
316
|
breakerRoundSignatures.add(signature);
|
|
@@ -313,7 +318,7 @@ function sortPhaseChanges(
|
|
|
313
318
|
phaseChanges = broken;
|
|
314
319
|
}
|
|
315
320
|
|
|
316
|
-
throw new
|
|
321
|
+
throw new UnorderableCycleError(
|
|
317
322
|
`CycleError: change-injection breaker exceeded ${maxRounds} rounds (one per node in the phase) — likely a buggy breaker rule`,
|
|
318
323
|
);
|
|
319
324
|
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { createEmptyCatalog } from "../catalog.model.ts";
|
|
3
|
+
import type { Change } from "../change.types.ts";
|
|
4
|
+
import { BaseChange } from "../objects/base.change.ts";
|
|
5
|
+
import { sortChanges } from "./sort-changes.ts";
|
|
6
|
+
import { UnorderableCycleError } from "./unorderable-cycle-error.ts";
|
|
7
|
+
|
|
8
|
+
class MutualCreateChange extends BaseChange {
|
|
9
|
+
readonly operation = "create";
|
|
10
|
+
readonly objectType = "table";
|
|
11
|
+
readonly scope = "object";
|
|
12
|
+
readonly table: { schema: string; name: string };
|
|
13
|
+
private readonly dependsOn: string;
|
|
14
|
+
|
|
15
|
+
constructor(name: string, dependsOn: string) {
|
|
16
|
+
super();
|
|
17
|
+
this.table = { schema: "public", name };
|
|
18
|
+
this.dependsOn = dependsOn;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
override get creates() {
|
|
22
|
+
return [`table:public.${this.table.name}`];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
override get requires() {
|
|
26
|
+
return [`table:public.${this.dependsOn}`];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
serialize(): string {
|
|
30
|
+
return `CREATE TABLE public.${this.table.name} ()`;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
describe("UnorderableCycleError", () => {
|
|
35
|
+
test("sortChanges throws a typed error carrying the offending cycle", async () => {
|
|
36
|
+
const a = new MutualCreateChange("a", "b");
|
|
37
|
+
const b = new MutualCreateChange("b", "a");
|
|
38
|
+
const catalog = await createEmptyCatalog(170000, "postgres");
|
|
39
|
+
|
|
40
|
+
let thrown: unknown;
|
|
41
|
+
try {
|
|
42
|
+
sortChanges({ mainCatalog: catalog, branchCatalog: catalog }, [
|
|
43
|
+
a,
|
|
44
|
+
b,
|
|
45
|
+
] as unknown as Change[]);
|
|
46
|
+
} catch (error) {
|
|
47
|
+
thrown = error;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
expect(thrown).toBeInstanceOf(UnorderableCycleError);
|
|
51
|
+
if (!(thrown instanceof UnorderableCycleError)) {
|
|
52
|
+
throw new Error("expected UnorderableCycleError");
|
|
53
|
+
}
|
|
54
|
+
expect(thrown.name).toBe("UnorderableCycleError");
|
|
55
|
+
expect(thrown.message).toContain("CycleError");
|
|
56
|
+
expect(new Set(thrown.cycle)).toEqual(
|
|
57
|
+
new Set<Change>([a, b] as unknown as Change[]),
|
|
58
|
+
);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Change } from "../change.types.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Thrown by `sortChanges` when the dependency graph contains a cycle that
|
|
5
|
+
* neither weak-edge filtering nor the change-injection cycle breakers could
|
|
6
|
+
* resolve.
|
|
7
|
+
*
|
|
8
|
+
* `message` is the human-readable `formatCycleError` output (it starts with
|
|
9
|
+
* "CycleError:" for backward compatibility with log greps).
|
|
10
|
+
*/
|
|
11
|
+
export class UnorderableCycleError extends Error {
|
|
12
|
+
override readonly name = "UnorderableCycleError";
|
|
13
|
+
/**
|
|
14
|
+
* Changes participating in the cycle, in cycle order. Empty when the
|
|
15
|
+
* failure came from an internal guard rather than a concrete cycle.
|
|
16
|
+
*/
|
|
17
|
+
readonly cycle: readonly Change[];
|
|
18
|
+
|
|
19
|
+
constructor(message: string, cycle: readonly Change[] = []) {
|
|
20
|
+
super(message);
|
|
21
|
+
this.cycle = cycle;
|
|
22
|
+
}
|
|
23
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -30,12 +30,29 @@ export type {
|
|
|
30
30
|
export type { IntegrationDSL } from "./core/integrations/integration-dsl.ts";
|
|
31
31
|
|
|
32
32
|
// Plan operations
|
|
33
|
+
export type { Change } from "./core/change.types.ts";
|
|
33
34
|
export { applyPlan } from "./core/plan/apply.ts";
|
|
34
35
|
export type { CatalogInput } from "./core/plan/create.ts";
|
|
35
36
|
export { createPlan } from "./core/plan/create.ts";
|
|
37
|
+
export type {
|
|
38
|
+
RenderedPlanFile,
|
|
39
|
+
RenderPlanSqlOptions,
|
|
40
|
+
} from "./core/plan/render.ts";
|
|
41
|
+
export {
|
|
42
|
+
flattenPlanStatements,
|
|
43
|
+
renderPlanFiles,
|
|
44
|
+
renderPlanSql,
|
|
45
|
+
} from "./core/plan/render.ts";
|
|
36
46
|
export type { SqlFormatOptions } from "./core/plan/sql-format.ts";
|
|
37
47
|
export { formatSqlStatements } from "./core/plan/sql-format.ts";
|
|
38
|
-
export type {
|
|
48
|
+
export type {
|
|
49
|
+
CreatePlanOptions,
|
|
50
|
+
ExecutionBoundaryReason,
|
|
51
|
+
MigrationUnit,
|
|
52
|
+
Plan,
|
|
53
|
+
TransactionMode,
|
|
54
|
+
} from "./core/plan/types.ts";
|
|
55
|
+
export { UnorderableCycleError } from "./core/sort/unorderable-cycle-error.ts";
|
|
39
56
|
|
|
40
57
|
// Postgres config
|
|
41
58
|
export { createManagedPool } from "./core/postgres-config.ts";
|