qubu 0.0.0 → 0.3.3
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/canonical-BbnqavJm.mjs +972 -0
- package/dist/codegen.d.mts +107 -0
- package/dist/codegen.mjs +1199 -0
- package/dist/column-CXMxx8Hq.mjs +118 -0
- package/dist/complete-D5Djh-zo.mjs +1788 -0
- package/dist/complete-types-BdFqUfbb.d.mts +371 -0
- package/dist/core.d.mts +33 -0
- package/dist/core.mjs +36 -0
- package/dist/ddl.d.mts +118 -0
- package/dist/ddl.mjs +1120 -0
- package/dist/dialect-b2-Z6uBF.mjs +13 -0
- package/dist/diff.d.mts +2 -0
- package/dist/diff.mjs +883 -0
- package/dist/drizzle-mysql.d.mts +24 -0
- package/dist/drizzle-mysql.mjs +72 -0
- package/dist/drizzle-postgres.d.mts +24 -0
- package/dist/drizzle-postgres.mjs +73 -0
- package/dist/drizzle-sqlite.d.mts +24 -0
- package/dist/drizzle-sqlite.mjs +82 -0
- package/dist/drizzle.d.mts +13 -0
- package/dist/drizzle.mjs +2 -0
- package/dist/errors-BGCoLe_r.mjs +14 -0
- package/dist/errors-Dxv73YJu.mjs +26 -0
- package/dist/explain-CkIK13L_.mjs +98 -0
- package/dist/index-Ds7-mhJi.d.mts +146 -0
- package/dist/index-Dug5HnLB.d.mts +204 -0
- package/dist/index.d.mts +2 -0
- package/dist/index.mjs +1276 -0
- package/dist/introspection.d.mts +117 -0
- package/dist/introspection.mjs +4341 -0
- package/dist/json-CUZlv4HT.mjs +169 -0
- package/dist/migration.d.mts +215 -0
- package/dist/migration.mjs +1161 -0
- package/dist/mysql-DqkqXB6A.mjs +355 -0
- package/dist/mysql.d.mts +5 -0
- package/dist/mysql.mjs +50 -0
- package/dist/naming-QVCOnSj2.mjs +20 -0
- package/dist/omit-OxV58AwX.mjs +5 -0
- package/dist/on-conflict-BxnxubMb.mjs +143 -0
- package/dist/postgres-DEBBeh52.mjs +235 -0
- package/dist/postgres.d.mts +123 -0
- package/dist/postgres.mjs +52 -0
- package/dist/registry-BufIskVN.mjs +569 -0
- package/dist/relational-DCZrrNia.mjs +77 -0
- package/dist/runtime-BTr-MTlo.mjs +193 -0
- package/dist/schema.d.mts +2 -0
- package/dist/schema.mjs +5 -0
- package/dist/serialize-PF1cfH2P.mjs +556 -0
- package/dist/snapshot-CWPgzxNx.mjs +392 -0
- package/dist/snapshot.d.mts +4 -0
- package/dist/snapshot.mjs +8 -0
- package/dist/source-DUoJVXmL.mjs +68 -0
- package/dist/sqlite-BU6DBxef.mjs +320 -0
- package/dist/sqlite.d.mts +5 -0
- package/dist/sqlite.mjs +48 -0
- package/dist/standard-BTVYKh_F.mjs +12 -0
- package/dist/table-llv9tsZ8.mjs +384 -0
- package/dist/types-4Q076HKo.d.mts +3636 -0
- package/dist/types-BX0mckiU.d.mts +644 -0
- package/dist/types-CO1KaRNc.d.mts +46 -0
- package/dist/types-Cec0xzo4.mjs +116 -0
- package/dist/value-BvilP0oz.mjs +29 -0
- package/dist/vite/ambient.d.ts +449 -0
- package/dist/vite.d.mts +35 -0
- package/dist/vite.mjs +498 -0
- package/docs/config.json +21 -0
- package/docs/dialects-and-execution.md +427 -0
- package/docs/getting-started.md +96 -0
- package/docs/guides/compose-queries.md +225 -0
- package/docs/guides/drizzle.md +129 -0
- package/docs/guides/extensions/dialects.md +40 -0
- package/docs/guides/extensions/overview.md +36 -0
- package/docs/guides/extensions/sources-and-clauses.md +82 -0
- package/docs/guides/extensions/typed-expressions.md +94 -0
- package/docs/guides/extensions/unsafe-syntax.md +27 -0
- package/docs/guides/json.md +65 -0
- package/docs/guides/mutations.md +138 -0
- package/docs/guides/select/conditions.md +110 -0
- package/docs/guides/select/grouping-and-windows.md +82 -0
- package/docs/guides/select/ordering-and-pagination.md +78 -0
- package/docs/guides/select/overview.md +139 -0
- package/docs/guides/sql-templates.md +157 -0
- package/docs/guides/vite-plugin.md +92 -0
- package/docs/index.md +111 -0
- package/docs/query-model/fragments.md +109 -0
- package/docs/query-model/result-shapes.md +125 -0
- package/docs/query-model/source-scope.md +182 -0
- package/docs/reference/introspection-support.md +196 -0
- package/docs/reference/mysql-snapshot.md +55 -0
- package/docs/reference/postgres-snapshot.md +61 -0
- package/docs/reference/sqlite-snapshot.md +53 -0
- package/docs/reference/supported-surface.md +104 -0
- package/docs/schema/catalog-model.md +58 -0
- package/docs/schema/code-generation.md +139 -0
- package/docs/schema/columns-and-writes.md +130 -0
- package/docs/schema/constraints-and-indexes.md +180 -0
- package/docs/schema/ddl-emission.md +75 -0
- package/docs/schema/diff.md +75 -0
- package/docs/schema/introspection.md +214 -0
- package/docs/schema/migration-plans.md +76 -0
- package/docs/schema/snapshots.md +93 -0
- package/docs/schema/storage-and-schema-sql.md +104 -0
- package/docs/schema/tables-and-names.md +106 -0
- package/docs/sql-semantic-types.md +141 -0
- package/docs/troubleshooting.md +151 -0
- package/package.json +132 -10
- package/skills/qubu/SKILL.md +42 -0
- package/skills/qubu/agents/openai.yaml +6 -0
- package/readme.md +0 -1
|
@@ -0,0 +1,1161 @@
|
|
|
1
|
+
import { a as toSnapshotJsonValue, n as canonicalJson } from "./canonical-BbnqavJm.mjs";
|
|
2
|
+
//#region src/migration/types.ts
|
|
3
|
+
/** The versioned envelope tag for dialect-neutral migration plans. */
|
|
4
|
+
const migrationPlanFormat = "qubu-migration-plan";
|
|
5
|
+
/** The first strict migration-plan format version. */
|
|
6
|
+
const migrationPlanVersion = 1;
|
|
7
|
+
/** A validation error raised by throwing plan APIs. */
|
|
8
|
+
var MigrationPlanValidationError = class extends TypeError {
|
|
9
|
+
name = "MigrationPlanValidationError";
|
|
10
|
+
diagnostics;
|
|
11
|
+
issues;
|
|
12
|
+
constructor(diagnostics) {
|
|
13
|
+
super(diagnostics.map((diagnostic) => diagnostic.message).join("\n"));
|
|
14
|
+
this.diagnostics = Object.freeze([...diagnostics]);
|
|
15
|
+
this.issues = this.diagnostics;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
//#endregion
|
|
19
|
+
//#region src/migration/plan.ts
|
|
20
|
+
const objectKinds = /* @__PURE__ */ new Set([
|
|
21
|
+
"namespace",
|
|
22
|
+
"table",
|
|
23
|
+
"column",
|
|
24
|
+
"constraint",
|
|
25
|
+
"index",
|
|
26
|
+
"view",
|
|
27
|
+
"materialized-view",
|
|
28
|
+
"sequence",
|
|
29
|
+
"enum",
|
|
30
|
+
"domain",
|
|
31
|
+
"collation",
|
|
32
|
+
"trigger",
|
|
33
|
+
"routine",
|
|
34
|
+
"partition",
|
|
35
|
+
"policy",
|
|
36
|
+
"extension",
|
|
37
|
+
"comment",
|
|
38
|
+
"ownership",
|
|
39
|
+
"deferred-object",
|
|
40
|
+
"opaque-object"
|
|
41
|
+
]);
|
|
42
|
+
const safetyValues = /* @__PURE__ */ new Set([
|
|
43
|
+
"safe",
|
|
44
|
+
"review-required",
|
|
45
|
+
"destructive",
|
|
46
|
+
"unsupported",
|
|
47
|
+
"unknown"
|
|
48
|
+
]);
|
|
49
|
+
const operationValues = /* @__PURE__ */ new Set([
|
|
50
|
+
"add",
|
|
51
|
+
"remove",
|
|
52
|
+
"property-change",
|
|
53
|
+
"physical-rename",
|
|
54
|
+
"custom-sql"
|
|
55
|
+
]);
|
|
56
|
+
const lockValues = /* @__PURE__ */ new Set([
|
|
57
|
+
"none",
|
|
58
|
+
"shared",
|
|
59
|
+
"exclusive",
|
|
60
|
+
"unknown"
|
|
61
|
+
]);
|
|
62
|
+
const transactionValues = /* @__PURE__ */ new Set([
|
|
63
|
+
"required",
|
|
64
|
+
"optional",
|
|
65
|
+
"forbidden",
|
|
66
|
+
"unknown"
|
|
67
|
+
]);
|
|
68
|
+
const reversibilityValues = /* @__PURE__ */ new Set(["reversible", "irreversible"]);
|
|
69
|
+
const dependencyReasons = /* @__PURE__ */ new Set([
|
|
70
|
+
"parent-before-child",
|
|
71
|
+
"child-before-parent",
|
|
72
|
+
"reference-before-dependent",
|
|
73
|
+
"explicit-custom-sql"
|
|
74
|
+
]);
|
|
75
|
+
const safetyRank = {
|
|
76
|
+
safe: 0,
|
|
77
|
+
"review-required": 1,
|
|
78
|
+
destructive: 2,
|
|
79
|
+
unsupported: 3,
|
|
80
|
+
unknown: 4
|
|
81
|
+
};
|
|
82
|
+
/**
|
|
83
|
+
* Convert a resolved SnapshotDiff into an immutable migration plan.
|
|
84
|
+
*
|
|
85
|
+
* This function only describes operations. It does not render SQL, open a
|
|
86
|
+
* connection, execute a transaction, or infer SQL from opaque snapshot data.
|
|
87
|
+
* A plan that still needs a safety decision is returned with `ok: false` so it
|
|
88
|
+
* can be reviewed without being mistaken for an executable plan.
|
|
89
|
+
*/
|
|
90
|
+
function createMigrationPlan(diff, options = {}) {
|
|
91
|
+
const diagnostics = [];
|
|
92
|
+
const dialect = diff.afterDialect ?? diff.beforeDialect;
|
|
93
|
+
if (dialect === void 0) diagnostics.push(planDiagnostic("invalid-plan", "A migration plan needs a source or target snapshot dialect", []));
|
|
94
|
+
if (diff.beforeDialect !== void 0 && diff.afterDialect !== void 0 && diff.beforeDialect.name !== diff.afterDialect.name) diagnostics.push(planDiagnostic("dialect-mismatch", `A migration plan cannot span dialects "${diff.beforeDialect.name}" and "${diff.afterDialect.name}"`, [], { dialect: diff.afterDialect }));
|
|
95
|
+
const sourceDiagnostics = diff.diagnostics.map(mapDiffDiagnostic);
|
|
96
|
+
diagnostics.push(...sourceDiagnostics);
|
|
97
|
+
const decisions = normalizeDecisions(options.decisions, diagnostics);
|
|
98
|
+
const contexts = [];
|
|
99
|
+
const operationIds = /* @__PURE__ */ new Set();
|
|
100
|
+
const operationTargets = /* @__PURE__ */ new Map();
|
|
101
|
+
for (const source of diff.operations) {
|
|
102
|
+
const id = operationId(source);
|
|
103
|
+
if (operationIds.has(id)) {
|
|
104
|
+
diagnostics.push(planDiagnostic("invalid-plan", `Diff operations produced duplicate migration ID "${id}"`, sourcePath(source), sourceContext(source)));
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
operationIds.add(id);
|
|
108
|
+
const context = {
|
|
109
|
+
source,
|
|
110
|
+
operation: makeOperation(id, source, classifySafety(source, diff.diagnostics), dialect ?? neutralDialect, 0)
|
|
111
|
+
};
|
|
112
|
+
contexts.push(context);
|
|
113
|
+
operationTargets.set(targetKeyFromSource(source), context);
|
|
114
|
+
}
|
|
115
|
+
const customContexts = addCustomSql(contexts, operationIds, operationTargets, options.customSql, dialect ?? neutralDialect, diagnostics);
|
|
116
|
+
contexts.push(...customContexts);
|
|
117
|
+
const approvedDecisions = [...decisions];
|
|
118
|
+
for (const context of contexts) {
|
|
119
|
+
const safety = context.operation.safety;
|
|
120
|
+
const explicit = findDecision(context, decisions, diff.diagnostics, options);
|
|
121
|
+
const optionAllowed = optionAllows(safety, options);
|
|
122
|
+
let status = "approved";
|
|
123
|
+
let decision = explicit;
|
|
124
|
+
if (explicit?.action === "skip") status = "skipped";
|
|
125
|
+
else if (explicit?.action === "allow" || optionAllowed) {
|
|
126
|
+
status = "approved";
|
|
127
|
+
if (explicit === void 0 && optionAllowed) {
|
|
128
|
+
decision = {
|
|
129
|
+
action: "allow",
|
|
130
|
+
reason: `Allowed by migration plan option for ${safety} facts`,
|
|
131
|
+
operationId: context.operation.id
|
|
132
|
+
};
|
|
133
|
+
approvedDecisions.push(decision);
|
|
134
|
+
}
|
|
135
|
+
} else if (safety !== "safe") {
|
|
136
|
+
status = "decision-required";
|
|
137
|
+
diagnostics.push(planDiagnostic("decision-required", `Operation "${context.operation.id}" has ${safety} safety and needs an explicit decision`, context.operation.path, {
|
|
138
|
+
operationId: context.operation.id,
|
|
139
|
+
kind: context.operation.kind,
|
|
140
|
+
namespace: context.operation.namespace,
|
|
141
|
+
logicalId: context.operation.logicalId,
|
|
142
|
+
physicalName: context.operation.physicalName,
|
|
143
|
+
dialect: context.operation.dialect,
|
|
144
|
+
evidence: context.operation.evidence
|
|
145
|
+
}));
|
|
146
|
+
}
|
|
147
|
+
context.operation = updateOperation(context.operation, {
|
|
148
|
+
status,
|
|
149
|
+
decision
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
requireDecisionsForUnmatchedDiagnostics(diff.diagnostics, contexts, decisions, options, diagnostics);
|
|
153
|
+
const dependencyResult = buildDependencies(contexts, diagnostics);
|
|
154
|
+
const operations = topologicalOrder(contexts, dependencyResult, diagnostics).map((context, position) => updateOperation(context.operation, {
|
|
155
|
+
position,
|
|
156
|
+
dependsOn: dependencyResult.byOperation.get(context.operation.id) ?? []
|
|
157
|
+
}));
|
|
158
|
+
const dependencies = dependencyResult.edges;
|
|
159
|
+
const planDiagnostics = sortDiagnostics(diagnostics);
|
|
160
|
+
const safety = highestSafety(operations);
|
|
161
|
+
const ready = planDiagnostics.every((item) => item.severity !== "error") && operations.every((operation) => operation.status !== "decision-required");
|
|
162
|
+
const plan = freezePlan({
|
|
163
|
+
format: migrationPlanFormat,
|
|
164
|
+
version: 1,
|
|
165
|
+
dialect: dialect ?? neutralDialect,
|
|
166
|
+
...diff.beforeDigest === void 0 ? {} : { beforeDigest: diff.beforeDigest },
|
|
167
|
+
...diff.afterDigest === void 0 ? {} : { afterDigest: diff.afterDigest },
|
|
168
|
+
safety,
|
|
169
|
+
ready,
|
|
170
|
+
operations,
|
|
171
|
+
dependencies,
|
|
172
|
+
decisions: sortDecisions(approvedDecisions),
|
|
173
|
+
diagnostics: planDiagnostics
|
|
174
|
+
});
|
|
175
|
+
return ready ? {
|
|
176
|
+
ok: true,
|
|
177
|
+
plan,
|
|
178
|
+
diagnostics: planDiagnostics
|
|
179
|
+
} : {
|
|
180
|
+
ok: false,
|
|
181
|
+
plan,
|
|
182
|
+
diagnostics: planDiagnostics
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
/** Alias for callers that treat plan creation as the main migration action. */
|
|
186
|
+
const planMigration = createMigrationPlan;
|
|
187
|
+
const createPlan = createMigrationPlan;
|
|
188
|
+
/** Build a plan or throw one structured validation error when it is blocked. */
|
|
189
|
+
function assertMigrationPlanFromDiff(diff, options = {}) {
|
|
190
|
+
const result = createMigrationPlan(diff, options);
|
|
191
|
+
if (!result.ok) throw new MigrationPlanValidationError(result.diagnostics);
|
|
192
|
+
return result.plan;
|
|
193
|
+
}
|
|
194
|
+
/** Return a fixed-order immutable plan after strict validation. */
|
|
195
|
+
function canonicalizeMigrationPlan(input) {
|
|
196
|
+
return assertMigrationPlan(input);
|
|
197
|
+
}
|
|
198
|
+
/** Encode a validated plan as deterministic JSON. */
|
|
199
|
+
function encodeMigrationPlan(input) {
|
|
200
|
+
return canonicalJson(toSnapshotJsonValue(assertMigrationPlan(input)));
|
|
201
|
+
}
|
|
202
|
+
/** Decode and strictly validate a migration-plan JSON value. */
|
|
203
|
+
function decodeMigrationPlan(input) {
|
|
204
|
+
let value = input;
|
|
205
|
+
if (typeof input === "string") try {
|
|
206
|
+
value = JSON.parse(input);
|
|
207
|
+
} catch (error) {
|
|
208
|
+
return {
|
|
209
|
+
ok: false,
|
|
210
|
+
diagnostics: freeze([planDiagnostic("invalid-plan", `Migration plan JSON could not be parsed: ${error instanceof Error ? error.message : String(error)}`, [])])
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
const validation = validatePlanValue(value);
|
|
214
|
+
if (!validation.ok) return validation;
|
|
215
|
+
return {
|
|
216
|
+
ok: true,
|
|
217
|
+
value: validation.value
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
/** Validate a plan without throwing. */
|
|
221
|
+
function validateMigrationPlan(input) {
|
|
222
|
+
return decodeMigrationPlan(input);
|
|
223
|
+
}
|
|
224
|
+
/** Validate a plan and throw one structured error on failure. */
|
|
225
|
+
function assertMigrationPlan(input) {
|
|
226
|
+
const result = validatePlanValue(input);
|
|
227
|
+
if (!result.ok) throw new MigrationPlanValidationError(result.diagnostics);
|
|
228
|
+
return result.value;
|
|
229
|
+
}
|
|
230
|
+
/** Compute a deterministic digest for cache keys and fixture assertions. */
|
|
231
|
+
function migrationPlanDigest(input) {
|
|
232
|
+
const plan = typeof input === "string" ? decodeMigrationPlan(input) : {
|
|
233
|
+
ok: true,
|
|
234
|
+
value: assertMigrationPlan(input)
|
|
235
|
+
};
|
|
236
|
+
if (!plan.ok) throw new MigrationPlanValidationError(plan.diagnostics);
|
|
237
|
+
return digestText(encodeMigrationPlan(plan.value));
|
|
238
|
+
}
|
|
239
|
+
const encodePlan = encodeMigrationPlan;
|
|
240
|
+
const decodePlan = decodeMigrationPlan;
|
|
241
|
+
const assertPlan = assertMigrationPlan;
|
|
242
|
+
const digestMigrationPlan = migrationPlanDigest;
|
|
243
|
+
const neutralDialect = Object.freeze({
|
|
244
|
+
name: "neutral",
|
|
245
|
+
version: 1
|
|
246
|
+
});
|
|
247
|
+
function makeOperation(id, source, safety, dialect, position) {
|
|
248
|
+
const origin = {
|
|
249
|
+
type: source.type,
|
|
250
|
+
kind: source.objectKind,
|
|
251
|
+
...source.namespace === void 0 ? {} : { namespace: source.namespace },
|
|
252
|
+
path: source.path,
|
|
253
|
+
...source.logicalId === void 0 ? {} : { logicalId: source.logicalId },
|
|
254
|
+
...source.physicalName === void 0 ? {} : { physicalName: source.physicalName },
|
|
255
|
+
...source.physicalReference === void 0 ? {} : { physicalReference: source.physicalReference },
|
|
256
|
+
...source.provenance === void 0 ? {} : { provenance: source.provenance },
|
|
257
|
+
...source.before === void 0 ? {} : { before: source.before },
|
|
258
|
+
...source.after === void 0 ? {} : { after: source.after },
|
|
259
|
+
evidence: source.evidence
|
|
260
|
+
};
|
|
261
|
+
const reversible = source.type !== "remove";
|
|
262
|
+
const preconditions = preconditionsFor(source);
|
|
263
|
+
return {
|
|
264
|
+
id,
|
|
265
|
+
type: source.type,
|
|
266
|
+
kind: source.objectKind,
|
|
267
|
+
objectKind: source.objectKind,
|
|
268
|
+
...source.namespace === void 0 ? {} : { namespace: source.namespace },
|
|
269
|
+
path: source.path,
|
|
270
|
+
...source.logicalId === void 0 ? {} : { logicalId: source.logicalId },
|
|
271
|
+
...source.physicalName === void 0 ? {} : { physicalName: source.physicalName },
|
|
272
|
+
...source.physicalReference === void 0 ? {} : { physicalReference: source.physicalReference },
|
|
273
|
+
...source.provenance === void 0 ? {} : { provenance: source.provenance },
|
|
274
|
+
dialect,
|
|
275
|
+
safety,
|
|
276
|
+
lock: lockFor(source),
|
|
277
|
+
transaction: transactionFor(source),
|
|
278
|
+
reversible,
|
|
279
|
+
reversibility: reversible ? "reversible" : "irreversible",
|
|
280
|
+
...reversible ? {} : { irreversibleReason: "Removing an existing object cannot be reversed without retained data" },
|
|
281
|
+
preconditions,
|
|
282
|
+
dependsOn: [],
|
|
283
|
+
evidence: source.evidence,
|
|
284
|
+
origin,
|
|
285
|
+
status: "approved",
|
|
286
|
+
position
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
function preconditionsFor(source) {
|
|
290
|
+
const object = source.before ?? source.after;
|
|
291
|
+
const base = {
|
|
292
|
+
path: source.path,
|
|
293
|
+
kind: source.objectKind,
|
|
294
|
+
...source.namespace === void 0 ? {} : { namespace: source.namespace },
|
|
295
|
+
...source.logicalId === void 0 ? {} : { logicalId: source.logicalId },
|
|
296
|
+
...source.physicalName === void 0 ? {} : { physicalName: source.physicalName }
|
|
297
|
+
};
|
|
298
|
+
if (source.type === "add") return [{
|
|
299
|
+
...base,
|
|
300
|
+
type: "object-absent"
|
|
301
|
+
}];
|
|
302
|
+
if (source.type === "remove") return [{
|
|
303
|
+
...base,
|
|
304
|
+
type: "object-present",
|
|
305
|
+
...object === void 0 ? {} : { digest: digestJson(object.value) }
|
|
306
|
+
}];
|
|
307
|
+
const result = [{
|
|
308
|
+
...base,
|
|
309
|
+
type: "object-present",
|
|
310
|
+
...source.before === void 0 ? {} : { digest: digestJson(source.before.value) }
|
|
311
|
+
}];
|
|
312
|
+
if (source.type === "property-change") {
|
|
313
|
+
for (const change of source.changedProperties ?? []) if (change.before !== void 0) result.push({
|
|
314
|
+
...base,
|
|
315
|
+
type: "property-equals",
|
|
316
|
+
property: change.path,
|
|
317
|
+
value: change.before
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
return result;
|
|
321
|
+
}
|
|
322
|
+
function lockFor(source) {
|
|
323
|
+
if (source.kind === "comment" || source.kind === "ownership") return "shared";
|
|
324
|
+
if (source.type === "add") return "exclusive";
|
|
325
|
+
if (source.type === "remove") return "exclusive";
|
|
326
|
+
if (source.type === "physical-rename") return "exclusive";
|
|
327
|
+
return source.destructive ? "exclusive" : "shared";
|
|
328
|
+
}
|
|
329
|
+
function transactionFor(source) {
|
|
330
|
+
if (source.kind === "opaque-object" || source.kind === "deferred-object") return "unknown";
|
|
331
|
+
if (source.type === "remove" || source.destructive) return "required";
|
|
332
|
+
if (source.type === "physical-rename") return "optional";
|
|
333
|
+
return "optional";
|
|
334
|
+
}
|
|
335
|
+
function classifySafety(source, diagnostics) {
|
|
336
|
+
const relevant = diagnostics.filter((diagnostic) => pathRelated(diagnostic.path, source.path));
|
|
337
|
+
if (relevant.some((diagnostic) => diagnostic.code === "unknown" || diagnostic.code === "lossy") || source.kind === "opaque-object" || source.kind === "deferred-object") return "unknown";
|
|
338
|
+
if (relevant.some((diagnostic) => diagnostic.code === "unsupported")) return "unsupported";
|
|
339
|
+
if (source.destructive || relevant.some((diagnostic) => diagnostic.code === "destructive")) return "destructive";
|
|
340
|
+
if (source.type === "physical-rename" || source.type === "property-change" || relevant.some((diagnostic) => diagnostic.code === "ambiguous")) return "review-required";
|
|
341
|
+
return "safe";
|
|
342
|
+
}
|
|
343
|
+
function addCustomSql(contexts, operationIds, targets, inputs, dialect, diagnostics) {
|
|
344
|
+
if (inputs === void 0) return [];
|
|
345
|
+
const result = [];
|
|
346
|
+
for (const [index, input] of inputs.entries()) {
|
|
347
|
+
if (!isRecord(input)) {
|
|
348
|
+
diagnostics.push(planDiagnostic("custom-sql", "Custom SQL input must be an object", ["customSql", index]));
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
const candidate = input;
|
|
352
|
+
if (typeof candidate.sql !== "string" || candidate.sql.length === 0) {
|
|
353
|
+
diagnostics.push(planDiagnostic("custom-sql", "Custom SQL must be a non-empty string", [
|
|
354
|
+
"customSql",
|
|
355
|
+
index,
|
|
356
|
+
"sql"
|
|
357
|
+
]));
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
if (!isDialect(candidate.dialect)) {
|
|
361
|
+
diagnostics.push(planDiagnostic("custom-sql", "Custom SQL needs a dialect descriptor", [
|
|
362
|
+
"customSql",
|
|
363
|
+
index,
|
|
364
|
+
"dialect"
|
|
365
|
+
]));
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
if (!isSafety(candidate.safety) || typeof candidate.reason !== "string" || candidate.reason.length === 0) {
|
|
369
|
+
diagnostics.push(planDiagnostic("custom-sql", "Custom SQL needs a safety classification and reason", ["customSql", index]));
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
if (candidate.position !== void 0 && !validPosition(candidate.position)) {
|
|
373
|
+
diagnostics.push(planDiagnostic("custom-sql", "Custom SQL position must be a non-negative integer", [
|
|
374
|
+
"customSql",
|
|
375
|
+
index,
|
|
376
|
+
"position"
|
|
377
|
+
]));
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
if (candidate.reversible !== void 0 && typeof candidate.reversible !== "boolean") {
|
|
381
|
+
diagnostics.push(planDiagnostic("custom-sql", "Custom SQL reversible must be boolean", [
|
|
382
|
+
"customSql",
|
|
383
|
+
index,
|
|
384
|
+
"reversible"
|
|
385
|
+
]));
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
if (candidate.dependsOn !== void 0 && (!Array.isArray(candidate.dependsOn) || !candidate.dependsOn.every((dependency) => typeof dependency === "string"))) {
|
|
389
|
+
diagnostics.push(planDiagnostic("custom-sql", "Custom SQL dependsOn must contain operation IDs", [
|
|
390
|
+
"customSql",
|
|
391
|
+
index,
|
|
392
|
+
"dependsOn"
|
|
393
|
+
]));
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
if (candidate.path !== void 0 && !isPath(candidate.path)) {
|
|
397
|
+
diagnostics.push(planDiagnostic("custom-sql", "Custom SQL path must be an array", [
|
|
398
|
+
"customSql",
|
|
399
|
+
index,
|
|
400
|
+
"path"
|
|
401
|
+
]));
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
if (candidate.kind !== void 0 && !isObjectKind(candidate.kind)) {
|
|
405
|
+
diagnostics.push(planDiagnostic("custom-sql", "Custom SQL kind is invalid", [
|
|
406
|
+
"customSql",
|
|
407
|
+
index,
|
|
408
|
+
"kind"
|
|
409
|
+
]));
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
if (candidate.namespace !== void 0 && typeof candidate.namespace !== "string") {
|
|
413
|
+
diagnostics.push(planDiagnostic("custom-sql", "Custom SQL namespace must be a string", [
|
|
414
|
+
"customSql",
|
|
415
|
+
index,
|
|
416
|
+
"namespace"
|
|
417
|
+
]));
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
if (candidate.operationId !== void 0 && typeof candidate.operationId !== "string") {
|
|
421
|
+
diagnostics.push(planDiagnostic("custom-sql", "Custom SQL operationId must be a string", [
|
|
422
|
+
"customSql",
|
|
423
|
+
index,
|
|
424
|
+
"operationId"
|
|
425
|
+
]));
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
const normalized = {
|
|
429
|
+
sql: candidate.sql,
|
|
430
|
+
dialect: candidate.dialect,
|
|
431
|
+
safety: candidate.safety,
|
|
432
|
+
reason: candidate.reason,
|
|
433
|
+
...candidate.reversible === void 0 ? {} : { reversible: candidate.reversible },
|
|
434
|
+
...candidate.operationId === void 0 ? {} : { operationId: candidate.operationId },
|
|
435
|
+
...candidate.kind === void 0 ? {} : { kind: candidate.kind },
|
|
436
|
+
...candidate.namespace === void 0 ? {} : { namespace: candidate.namespace },
|
|
437
|
+
...candidate.path === void 0 ? {} : { path: candidate.path },
|
|
438
|
+
...candidate.position === void 0 ? {} : { position: candidate.position },
|
|
439
|
+
...candidate.dependsOn === void 0 ? {} : { dependsOn: candidate.dependsOn }
|
|
440
|
+
};
|
|
441
|
+
if (normalized.dialect.name !== dialect.name) diagnostics.push(planDiagnostic("dialect-mismatch", `Custom SQL dialect "${normalized.dialect.name}" does not match plan dialect "${dialect.name}"`, [
|
|
442
|
+
"customSql",
|
|
443
|
+
index,
|
|
444
|
+
"dialect"
|
|
445
|
+
], { dialect: normalized.dialect }));
|
|
446
|
+
const target = resolveCustomTarget(normalized, targets);
|
|
447
|
+
if (normalized.operationId !== void 0 && target === void 0) diagnostics.push(planDiagnostic("custom-sql", `Custom SQL target operation "${normalized.operationId}" does not exist`, [
|
|
448
|
+
"customSql",
|
|
449
|
+
index,
|
|
450
|
+
"operationId"
|
|
451
|
+
]));
|
|
452
|
+
const id = customOperationId(normalized, index);
|
|
453
|
+
if (operationIds.has(id)) {
|
|
454
|
+
diagnostics.push(planDiagnostic("custom-sql", `Duplicate custom SQL operation ID "${id}"`, ["customSql", index]));
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
operationIds.add(id);
|
|
458
|
+
const targetObject = target?.operation.origin?.after ?? target?.operation.origin?.before;
|
|
459
|
+
const path = normalized.path ?? targetObject?.path ?? [];
|
|
460
|
+
const namespace = normalized.namespace ?? targetObject?.namespace;
|
|
461
|
+
const logicalId = targetObject?.id;
|
|
462
|
+
const customSql = {
|
|
463
|
+
sql: normalized.sql,
|
|
464
|
+
dialect: normalized.dialect,
|
|
465
|
+
safety: normalized.safety,
|
|
466
|
+
position: normalized.position ?? contexts.length + index,
|
|
467
|
+
reason: normalized.reason,
|
|
468
|
+
reversible: normalized.reversible ?? false
|
|
469
|
+
};
|
|
470
|
+
const context = { operation: {
|
|
471
|
+
id,
|
|
472
|
+
type: "custom-sql",
|
|
473
|
+
kind: "custom-sql",
|
|
474
|
+
objectKind: "custom-sql",
|
|
475
|
+
...namespace === void 0 ? {} : { namespace },
|
|
476
|
+
path,
|
|
477
|
+
...logicalId === void 0 ? {} : { logicalId },
|
|
478
|
+
dialect: normalized.dialect,
|
|
479
|
+
safety: normalized.safety,
|
|
480
|
+
lock: normalized.safety === "safe" ? "shared" : "exclusive",
|
|
481
|
+
transaction: "optional",
|
|
482
|
+
reversible: customSql.reversible,
|
|
483
|
+
reversibility: customSql.reversible ? "reversible" : "irreversible",
|
|
484
|
+
...customSql.reversible ? {} : { irreversibleReason: "Custom SQL has no declared reverse operation" },
|
|
485
|
+
preconditions: [],
|
|
486
|
+
dependsOn: [.../* @__PURE__ */ new Set([...normalized.dependsOn ?? [], ...target === void 0 ? [] : [target.operation.id]])].sort(),
|
|
487
|
+
...targetObject?.physicalReference === void 0 ? {} : { physicalReference: targetObject.physicalReference },
|
|
488
|
+
...targetObject?.provenance === void 0 ? {} : { provenance: targetObject.provenance },
|
|
489
|
+
evidence: target?.operation.evidence ?? [],
|
|
490
|
+
customSql,
|
|
491
|
+
status: "approved",
|
|
492
|
+
position: 0
|
|
493
|
+
} };
|
|
494
|
+
result.push(context);
|
|
495
|
+
if (target !== void 0 && targetObject !== void 0) targets.set(targetKeyFromObject(targetObject), target);
|
|
496
|
+
}
|
|
497
|
+
return result;
|
|
498
|
+
}
|
|
499
|
+
function resolveCustomTarget(input, targets) {
|
|
500
|
+
if (input.operationId !== void 0) {
|
|
501
|
+
for (const target of targets.values()) if (target.operation.id === input.operationId) return target;
|
|
502
|
+
}
|
|
503
|
+
if (input.kind !== void 0 && input.path !== void 0) {
|
|
504
|
+
for (const target of targets.values()) if (target.operation.kind === input.kind && target.operation.namespace === input.namespace && samePath(target.operation.path, input.path)) return target;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
function findDecision(context, decisions, diagnostics, options) {
|
|
508
|
+
const direct = decisions.find((decision) => {
|
|
509
|
+
if (decision.operationId === context.operation.id) return true;
|
|
510
|
+
if (decision.kind !== void 0 && decision.kind === context.operation.kind && (decision.namespace === void 0 || decision.namespace === context.operation.namespace) && (decision.path === void 0 || samePath(decision.path, context.operation.path))) return true;
|
|
511
|
+
return false;
|
|
512
|
+
});
|
|
513
|
+
if (direct !== void 0) return direct;
|
|
514
|
+
const relevantCode = diagnostics.find((diagnostic) => pathRelated(diagnostic.path, context.operation.path))?.code;
|
|
515
|
+
if (relevantCode !== void 0) return decisions.find((decision) => decision.code === mapDiffCode(relevantCode));
|
|
516
|
+
if (optionAllows(context.operation.safety, options)) return void 0;
|
|
517
|
+
}
|
|
518
|
+
function requireDecisionsForUnmatchedDiagnostics(diagnostics, contexts, decisions, options, planDiagnostics) {
|
|
519
|
+
for (const source of diagnostics) {
|
|
520
|
+
if (source.code !== "unknown" && source.code !== "lossy" && source.code !== "unsupported") continue;
|
|
521
|
+
const hasOperation = contexts.some((context) => pathRelated(source.path, context.operation.path));
|
|
522
|
+
const safety = source.code === "lossy" ? "unknown" : source.code;
|
|
523
|
+
if (hasOperation || optionAllows(safety, options)) continue;
|
|
524
|
+
if (decisions.find((item) => item.code === mapDiffCode(source.code) && (item.path === void 0 || samePath(item.path, source.path))) !== void 0) continue;
|
|
525
|
+
planDiagnostics.push(planDiagnostic(mapDiffCode(source.code), `Diff diagnostic "${source.code}" needs an explicit migration decision`, source.path, {
|
|
526
|
+
kind: source.objectKind ?? source.kind,
|
|
527
|
+
namespace: source.namespace,
|
|
528
|
+
logicalId: source.logicalId,
|
|
529
|
+
physicalName: source.physicalName,
|
|
530
|
+
dialect: source.dialect,
|
|
531
|
+
evidence: source.evidence,
|
|
532
|
+
source
|
|
533
|
+
}));
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
function buildDependencies(contexts, diagnostics) {
|
|
537
|
+
const edges = [];
|
|
538
|
+
const edgeKeys = /* @__PURE__ */ new Set();
|
|
539
|
+
const byObject = /* @__PURE__ */ new Map();
|
|
540
|
+
for (const context of contexts) {
|
|
541
|
+
const object = context.operation.origin?.after ?? context.operation.origin?.before;
|
|
542
|
+
if (object !== void 0) byObject.set(targetKeyFromObject(object), context);
|
|
543
|
+
}
|
|
544
|
+
const addEdge = (from, to, reason) => {
|
|
545
|
+
if (from.id === to.id) return;
|
|
546
|
+
const key = `${from.id}\u0000${to.id}\u0000${reason}`;
|
|
547
|
+
if (edgeKeys.has(key)) return;
|
|
548
|
+
edgeKeys.add(key);
|
|
549
|
+
edges.push({
|
|
550
|
+
from: from.id,
|
|
551
|
+
to: to.id,
|
|
552
|
+
reason
|
|
553
|
+
});
|
|
554
|
+
};
|
|
555
|
+
for (const context of contexts) {
|
|
556
|
+
const operation = context.operation;
|
|
557
|
+
const object = operation.origin?.after ?? operation.origin?.before;
|
|
558
|
+
const parent = object?.parent;
|
|
559
|
+
if (parent !== void 0 && object !== void 0) {
|
|
560
|
+
const parentContext = byObject.get(targetKeyFromReference(parent, object.namespace));
|
|
561
|
+
if (parentContext !== void 0) {
|
|
562
|
+
if (operation.type === "remove") addEdge(operation, parentContext.operation, "child-before-parent");
|
|
563
|
+
else addEdge(parentContext.operation, operation, "parent-before-child");
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
if (operation.type !== "remove" && object !== void 0) for (const reference of referencedObjects(object.value)) {
|
|
567
|
+
const referenceContext = byObject.get(targetKeyFromReference(reference, object.namespace));
|
|
568
|
+
if (referenceContext !== void 0) addEdge(referenceContext.operation, operation, "reference-before-dependent");
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
for (const context of contexts) for (const dependency of context.operation.dependsOn) {
|
|
572
|
+
const target = contexts.find((item) => item.operation.id === dependency);
|
|
573
|
+
if (target === void 0) {
|
|
574
|
+
diagnostics.push(planDiagnostic("custom-sql", `Operation "${context.operation.id}" depends on unknown operation "${dependency}"`, context.operation.path, { operationId: context.operation.id }));
|
|
575
|
+
continue;
|
|
576
|
+
}
|
|
577
|
+
addEdge(target.operation, context.operation, "explicit-custom-sql");
|
|
578
|
+
}
|
|
579
|
+
const byOperation = /* @__PURE__ */ new Map();
|
|
580
|
+
for (const context of contexts) byOperation.set(context.operation.id, freeze(edges.filter((edge) => edge.to === context.operation.id).map((edge) => edge.from).sort()));
|
|
581
|
+
return {
|
|
582
|
+
edges: freeze(edges.sort((left, right) => left.from.localeCompare(right.from) || left.to.localeCompare(right.to) || left.reason.localeCompare(right.reason))),
|
|
583
|
+
byOperation
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
function topologicalOrder(contexts, dependencyResult, diagnostics) {
|
|
587
|
+
const byId = new Map(contexts.map((context) => [context.operation.id, context]));
|
|
588
|
+
const pending = /* @__PURE__ */ new Map();
|
|
589
|
+
for (const context of contexts) pending.set(context.operation.id, new Set(dependencyResult.byOperation.get(context.operation.id) ?? []));
|
|
590
|
+
const ready = [...contexts].filter((context) => (pending.get(context.operation.id)?.size ?? 0) === 0).sort((left, right) => left.operation.id.localeCompare(right.operation.id));
|
|
591
|
+
const result = [];
|
|
592
|
+
while (ready.length > 0) {
|
|
593
|
+
const context = ready.shift();
|
|
594
|
+
result.push(context);
|
|
595
|
+
for (const edge of dependencyResult.edges) {
|
|
596
|
+
if (edge.from !== context.operation.id) continue;
|
|
597
|
+
const waiting = pending.get(edge.to);
|
|
598
|
+
if (waiting === void 0) continue;
|
|
599
|
+
waiting.delete(edge.from);
|
|
600
|
+
if (waiting.size === 0) {
|
|
601
|
+
const next = byId.get(edge.to);
|
|
602
|
+
if (next !== void 0) {
|
|
603
|
+
ready.push(next);
|
|
604
|
+
ready.sort((left, right) => left.operation.id.localeCompare(right.operation.id));
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
if (result.length !== contexts.length) {
|
|
610
|
+
diagnostics.push(planDiagnostic("dependency-cycle", "Migration operation dependencies contain a cycle", []));
|
|
611
|
+
return [...contexts].sort((left, right) => left.operation.id.localeCompare(right.operation.id));
|
|
612
|
+
}
|
|
613
|
+
return result;
|
|
614
|
+
}
|
|
615
|
+
function referencedObjects(value) {
|
|
616
|
+
const result = [];
|
|
617
|
+
const visit = (current, key) => {
|
|
618
|
+
if (Array.isArray(current)) {
|
|
619
|
+
for (const child of current) visit(child, key);
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
if (current === null || typeof current !== "object") return;
|
|
623
|
+
if (key !== void 0 && (key === "target" || key === "table" || key === "parent" || key === "object" || key === "backingConstraint" || key === "dependencies") && isJsonRecord(current) && isObjectKind(current.kind) && typeof current.id === "string") result.push({
|
|
624
|
+
kind: current.kind,
|
|
625
|
+
id: current.id
|
|
626
|
+
});
|
|
627
|
+
for (const [childKey, child] of Object.entries(current)) visit(child, childKey);
|
|
628
|
+
};
|
|
629
|
+
visit(value, void 0);
|
|
630
|
+
return result;
|
|
631
|
+
}
|
|
632
|
+
function sourcePath(source) {
|
|
633
|
+
return source.path;
|
|
634
|
+
}
|
|
635
|
+
function sourceContext(source) {
|
|
636
|
+
return {
|
|
637
|
+
kind: source.objectKind,
|
|
638
|
+
...source.namespace === void 0 ? {} : { namespace: source.namespace },
|
|
639
|
+
...source.logicalId === void 0 ? {} : { logicalId: source.logicalId },
|
|
640
|
+
...source.physicalName === void 0 ? {} : { physicalName: source.physicalName },
|
|
641
|
+
dialect: source.dialect,
|
|
642
|
+
evidence: source.evidence
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
function operationId(source) {
|
|
646
|
+
const identity = {
|
|
647
|
+
type: source.type,
|
|
648
|
+
kind: source.objectKind,
|
|
649
|
+
path: source.path
|
|
650
|
+
};
|
|
651
|
+
if (source.namespace !== void 0) identity.namespace = source.namespace;
|
|
652
|
+
if (source.logicalId !== void 0) identity.logicalId = source.logicalId;
|
|
653
|
+
if (source.physicalName !== void 0) identity.physicalName = source.physicalName;
|
|
654
|
+
return `op_${digestText(canonicalJson(toSnapshotJsonValue(identity))).slice(8)}`;
|
|
655
|
+
}
|
|
656
|
+
function customOperationId(input, index) {
|
|
657
|
+
const identity = {
|
|
658
|
+
index,
|
|
659
|
+
sql: input.sql,
|
|
660
|
+
dialect: input.dialect,
|
|
661
|
+
safety: input.safety,
|
|
662
|
+
reason: input.reason
|
|
663
|
+
};
|
|
664
|
+
if (input.reversible !== void 0) identity.reversible = input.reversible;
|
|
665
|
+
if (input.operationId !== void 0) identity.operationId = input.operationId;
|
|
666
|
+
if (input.kind !== void 0) identity.kind = input.kind;
|
|
667
|
+
if (input.namespace !== void 0) identity.namespace = input.namespace;
|
|
668
|
+
if (input.path !== void 0) identity.path = input.path;
|
|
669
|
+
if (input.position !== void 0) identity.position = input.position;
|
|
670
|
+
if (input.dependsOn !== void 0) identity.dependsOn = input.dependsOn;
|
|
671
|
+
return `custom_${digestText(canonicalJson(toSnapshotJsonValue(identity))).slice(8)}`;
|
|
672
|
+
}
|
|
673
|
+
function targetKeyFromSource(source) {
|
|
674
|
+
const object = source.after ?? source.before;
|
|
675
|
+
return object === void 0 ? `${source.objectKind}\u0000${source.namespace ?? ""}\u0000${source.logicalId ?? ""}` : targetKeyFromObject(object);
|
|
676
|
+
}
|
|
677
|
+
function targetKeyFromObject(object) {
|
|
678
|
+
return targetKeyFromReference({
|
|
679
|
+
kind: object.kind,
|
|
680
|
+
id: object.id
|
|
681
|
+
}, object.namespace);
|
|
682
|
+
}
|
|
683
|
+
function targetKeyFromReference(reference, namespace) {
|
|
684
|
+
return `${reference.kind}\u0000${namespace ?? ""}\u0000${reference.id}`;
|
|
685
|
+
}
|
|
686
|
+
function updateOperation(operation, updates) {
|
|
687
|
+
return {
|
|
688
|
+
...operation,
|
|
689
|
+
...updates
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
function highestSafety(operations) {
|
|
693
|
+
let result = "safe";
|
|
694
|
+
for (const operation of operations) if (safetyRank[operation.safety] > safetyRank[result]) result = operation.safety;
|
|
695
|
+
return result;
|
|
696
|
+
}
|
|
697
|
+
function optionAllows(safety, options) {
|
|
698
|
+
if (safety === "unknown") return options.allowUnknown === true || options.allowLossy === true;
|
|
699
|
+
if (safety === "unsupported") return options.allowUnsupported === true;
|
|
700
|
+
if (safety === "destructive") return options.allowDestructive === true;
|
|
701
|
+
if (safety === "review-required") return options.allowReviewRequired === true;
|
|
702
|
+
return true;
|
|
703
|
+
}
|
|
704
|
+
function normalizeDecisions(values, diagnostics) {
|
|
705
|
+
if (values === void 0) return [];
|
|
706
|
+
const result = [];
|
|
707
|
+
for (const [index, value] of values.entries()) {
|
|
708
|
+
if (!isRecord(value) || value.action !== "allow" && value.action !== "skip" || typeof value.reason !== "string" || value.reason.length === 0) {
|
|
709
|
+
diagnostics.push(planDiagnostic("invalid-plan", "Migration decisions need action and a non-empty reason", ["decisions", index]));
|
|
710
|
+
continue;
|
|
711
|
+
}
|
|
712
|
+
result.push({
|
|
713
|
+
action: value.action,
|
|
714
|
+
reason: value.reason,
|
|
715
|
+
...typeof value.operationId === "string" ? { operationId: value.operationId } : {},
|
|
716
|
+
...isObjectKind(value.kind) ? { kind: value.kind } : {},
|
|
717
|
+
...typeof value.namespace === "string" ? { namespace: value.namespace } : {},
|
|
718
|
+
...isPath(value.path) ? { path: freeze([...value.path]) } : {},
|
|
719
|
+
...isDiagnosticCode(value.code) ? { code: value.code } : {}
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
return sortDecisions(result);
|
|
723
|
+
}
|
|
724
|
+
function sortDecisions(values) {
|
|
725
|
+
return freeze([...values].sort((left, right) => (left.operationId ?? "").localeCompare(right.operationId ?? "") || (left.kind ?? "").localeCompare(right.kind ?? "") || JSON.stringify(left.path ?? []).localeCompare(JSON.stringify(right.path ?? [])) || left.action.localeCompare(right.action) || left.reason.localeCompare(right.reason)));
|
|
726
|
+
}
|
|
727
|
+
function mapDiffDiagnostic(source) {
|
|
728
|
+
return planDiagnostic(mapDiffCode(source.code), source.message, source.path, {
|
|
729
|
+
kind: source.objectKind ?? source.kind,
|
|
730
|
+
namespace: source.namespace,
|
|
731
|
+
logicalId: source.logicalId,
|
|
732
|
+
physicalName: source.physicalName,
|
|
733
|
+
dialect: source.dialect,
|
|
734
|
+
evidence: source.evidence,
|
|
735
|
+
source,
|
|
736
|
+
severity: source.severity
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
function mapDiffCode(code) {
|
|
740
|
+
if (code === "unknown") return "unknown";
|
|
741
|
+
if (code === "lossy") return "lossy";
|
|
742
|
+
if (code === "unsupported") return "unsupported";
|
|
743
|
+
if (code === "destructive") return "destructive";
|
|
744
|
+
if (code === "ambiguous") return "ambiguous";
|
|
745
|
+
if (code === "dialect-mismatch") return "dialect-mismatch";
|
|
746
|
+
return "invalid-plan";
|
|
747
|
+
}
|
|
748
|
+
function pathRelated(left, right) {
|
|
749
|
+
return pathStarts(left, right) || pathStarts(right, left);
|
|
750
|
+
}
|
|
751
|
+
function pathStarts(path, prefix) {
|
|
752
|
+
return prefix.length <= path.length && prefix.every((item, index) => item === path[index]);
|
|
753
|
+
}
|
|
754
|
+
function planDiagnostic(code, message, path, options = {}) {
|
|
755
|
+
return {
|
|
756
|
+
code,
|
|
757
|
+
severity: options.severity ?? (code === "decision-required" || code === "dependency-cycle" || code === "invalid-plan" || code === "custom-sql" || code === "dialect-mismatch" || code === "non-canonical" ? "error" : "warning"),
|
|
758
|
+
message,
|
|
759
|
+
path: freeze([...path]),
|
|
760
|
+
...options.operationId === void 0 ? {} : { operationId: options.operationId },
|
|
761
|
+
...options.kind === void 0 ? {} : { kind: options.kind },
|
|
762
|
+
...options.namespace === void 0 ? {} : { namespace: options.namespace },
|
|
763
|
+
...options.logicalId === void 0 ? {} : { logicalId: options.logicalId },
|
|
764
|
+
...options.physicalName === void 0 ? {} : { physicalName: options.physicalName },
|
|
765
|
+
...options.dialect === void 0 ? {} : { dialect: options.dialect },
|
|
766
|
+
...options.evidence === void 0 ? {} : { evidence: options.evidence },
|
|
767
|
+
...options.source === void 0 ? {} : { source: options.source }
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
function sortDiagnostics(values) {
|
|
771
|
+
return freeze([...values].sort((left, right) => left.code.localeCompare(right.code) || JSON.stringify(left.path).localeCompare(JSON.stringify(right.path)) || left.message.localeCompare(right.message)));
|
|
772
|
+
}
|
|
773
|
+
function validatePlanValue(input) {
|
|
774
|
+
let value = input;
|
|
775
|
+
if (!isRecord(value)) return {
|
|
776
|
+
ok: false,
|
|
777
|
+
diagnostics: freeze([planDiagnostic("invalid-plan", "Migration plan must be an object", [])])
|
|
778
|
+
};
|
|
779
|
+
try {
|
|
780
|
+
value = toSnapshotJsonValue(value);
|
|
781
|
+
} catch (error) {
|
|
782
|
+
return {
|
|
783
|
+
ok: false,
|
|
784
|
+
diagnostics: freeze([planDiagnostic("invalid-plan", error instanceof Error ? error.message : String(error), [])])
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
const diagnostics = [];
|
|
788
|
+
const plan = value;
|
|
789
|
+
requireKeys(plan, [
|
|
790
|
+
"format",
|
|
791
|
+
"version",
|
|
792
|
+
"dialect",
|
|
793
|
+
"safety",
|
|
794
|
+
"ready",
|
|
795
|
+
"operations",
|
|
796
|
+
"dependencies",
|
|
797
|
+
"decisions",
|
|
798
|
+
"diagnostics"
|
|
799
|
+
], [], diagnostics, ["beforeDigest", "afterDigest"]);
|
|
800
|
+
if (plan.format !== "qubu-migration-plan") diagnostics.push(planDiagnostic("invalid-plan", `Migration plan format must be "${migrationPlanFormat}"`, ["format"]));
|
|
801
|
+
if (plan.version !== 1) diagnostics.push(planDiagnostic("invalid-plan", `Unsupported migration plan version: ${String(plan.version)}`, ["version"]));
|
|
802
|
+
validateDialect(plan.dialect, ["dialect"], diagnostics);
|
|
803
|
+
if (!isSafety(plan.safety)) diagnostics.push(planDiagnostic("invalid-plan", "Plan safety is invalid", ["safety"]));
|
|
804
|
+
if (typeof plan.ready !== "boolean") diagnostics.push(planDiagnostic("invalid-plan", "Plan ready must be boolean", ["ready"]));
|
|
805
|
+
if (!Array.isArray(plan.operations)) diagnostics.push(planDiagnostic("invalid-plan", "Plan operations must be an array", ["operations"]));
|
|
806
|
+
if (!Array.isArray(plan.dependencies)) diagnostics.push(planDiagnostic("invalid-plan", "Plan dependencies must be an array", ["dependencies"]));
|
|
807
|
+
if (!Array.isArray(plan.decisions)) diagnostics.push(planDiagnostic("invalid-plan", "Plan decisions must be an array", ["decisions"]));
|
|
808
|
+
if (!Array.isArray(plan.diagnostics)) diagnostics.push(planDiagnostic("invalid-plan", "Plan diagnostics must be an array", ["diagnostics"]));
|
|
809
|
+
if (typeof plan.beforeDigest === "string" || plan.beforeDigest === void 0) {} else diagnostics.push(planDiagnostic("invalid-plan", "beforeDigest must be a string", ["beforeDigest"]));
|
|
810
|
+
if (typeof plan.afterDigest === "string" || plan.afterDigest === void 0) {} else diagnostics.push(planDiagnostic("invalid-plan", "afterDigest must be a string", ["afterDigest"]));
|
|
811
|
+
if (Array.isArray(plan.operations)) validateOperations(plan.operations, diagnostics);
|
|
812
|
+
if (Array.isArray(plan.dependencies)) validateDependencies(plan, diagnostics);
|
|
813
|
+
if (Array.isArray(plan.decisions)) validateDecisions(plan.decisions, diagnostics);
|
|
814
|
+
if (Array.isArray(plan.diagnostics)) validateDiagnostics(plan.diagnostics, diagnostics);
|
|
815
|
+
if (Array.isArray(plan.operations) && Array.isArray(plan.diagnostics) && plan.operations.every((operation) => isRecord(operation))) {
|
|
816
|
+
const expectedReady = plan.diagnostics.every((diagnostic) => diagnostic.severity !== "error") && plan.operations.every((operation) => operation.status !== "decision-required");
|
|
817
|
+
if (typeof plan.ready === "boolean" && plan.ready !== expectedReady) diagnostics.push(planDiagnostic("non-canonical", "Plan ready must match its diagnostics and operation statuses", ["ready"]));
|
|
818
|
+
if (isSafety(plan.safety)) {
|
|
819
|
+
const expectedSafety = highestSafety(plan.operations);
|
|
820
|
+
if (plan.safety !== expectedSafety) diagnostics.push(planDiagnostic("non-canonical", "Plan safety must match its highest operation safety", ["safety"]));
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
if (diagnostics.length > 0) return {
|
|
824
|
+
ok: false,
|
|
825
|
+
diagnostics: freeze(sortDiagnostics(diagnostics))
|
|
826
|
+
};
|
|
827
|
+
return {
|
|
828
|
+
ok: true,
|
|
829
|
+
value: freezePlan(plan)
|
|
830
|
+
};
|
|
831
|
+
}
|
|
832
|
+
function validateOperations(operations, diagnostics) {
|
|
833
|
+
const ids = /* @__PURE__ */ new Set();
|
|
834
|
+
for (const [index, operation] of operations.entries()) {
|
|
835
|
+
const path = ["operations", index];
|
|
836
|
+
if (!isRecord(operation)) {
|
|
837
|
+
diagnostics.push(planDiagnostic("invalid-plan", "Operation must be an object", path));
|
|
838
|
+
continue;
|
|
839
|
+
}
|
|
840
|
+
const candidate = operation;
|
|
841
|
+
requireKeys(operation, [
|
|
842
|
+
"id",
|
|
843
|
+
"type",
|
|
844
|
+
"kind",
|
|
845
|
+
"objectKind",
|
|
846
|
+
"path",
|
|
847
|
+
"dialect",
|
|
848
|
+
"safety",
|
|
849
|
+
"lock",
|
|
850
|
+
"transaction",
|
|
851
|
+
"reversible",
|
|
852
|
+
"reversibility",
|
|
853
|
+
"preconditions",
|
|
854
|
+
"dependsOn",
|
|
855
|
+
"evidence",
|
|
856
|
+
"status",
|
|
857
|
+
"position"
|
|
858
|
+
], path, diagnostics, [
|
|
859
|
+
"namespace",
|
|
860
|
+
"logicalId",
|
|
861
|
+
"physicalName",
|
|
862
|
+
"physicalReference",
|
|
863
|
+
"provenance",
|
|
864
|
+
"irreversibleReason",
|
|
865
|
+
"origin",
|
|
866
|
+
"customSql",
|
|
867
|
+
"decision"
|
|
868
|
+
]);
|
|
869
|
+
if (typeof candidate.id !== "string" || candidate.id.length === 0) diagnostics.push(planDiagnostic("invalid-plan", "Operation ID must be non-empty", [...path, "id"]));
|
|
870
|
+
else if (ids.has(candidate.id)) diagnostics.push(planDiagnostic("invalid-plan", `Duplicate operation ID "${candidate.id}"`, [...path, "id"]));
|
|
871
|
+
else ids.add(candidate.id);
|
|
872
|
+
if (!operationValues.has(candidate.type)) diagnostics.push(planDiagnostic("invalid-plan", "Operation type is invalid", [...path, "type"]));
|
|
873
|
+
if (!isObjectKind(candidate.kind) && candidate.kind !== "custom-sql") diagnostics.push(planDiagnostic("invalid-plan", "Operation kind is invalid", [...path, "kind"]));
|
|
874
|
+
if (candidate.objectKind !== candidate.kind) diagnostics.push(planDiagnostic("invalid-plan", "Operation objectKind must match kind", [...path, "objectKind"]));
|
|
875
|
+
if (candidate.type === "custom-sql" && candidate.customSql === void 0) diagnostics.push(planDiagnostic("custom-sql", "Custom SQL operation is missing its explicit SQL record", [...path, "customSql"]));
|
|
876
|
+
if (candidate.type === "custom-sql" && candidate.customSql !== void 0) validateCustomSql(candidate.customSql, [...path, "customSql"], diagnostics);
|
|
877
|
+
if (candidate.type !== "custom-sql" && candidate.customSql !== void 0) diagnostics.push(planDiagnostic("invalid-plan", "Only custom-sql operations may contain customSql", [...path, "customSql"]));
|
|
878
|
+
if (!isSafety(candidate.safety)) diagnostics.push(planDiagnostic("invalid-plan", "Operation safety is invalid", [...path, "safety"]));
|
|
879
|
+
if (!lockValues.has(candidate.lock)) diagnostics.push(planDiagnostic("invalid-plan", "Operation lock requirement is invalid", [...path, "lock"]));
|
|
880
|
+
if (!transactionValues.has(candidate.transaction)) diagnostics.push(planDiagnostic("invalid-plan", "Operation transaction requirement is invalid", [...path, "transaction"]));
|
|
881
|
+
if (!isPath(candidate.path)) diagnostics.push(planDiagnostic("invalid-plan", "Operation path must be an array", [...path, "path"]));
|
|
882
|
+
if (!isDialect(candidate.dialect)) diagnostics.push(planDiagnostic("invalid-plan", "Operation dialect is invalid", [...path, "dialect"]));
|
|
883
|
+
if (typeof candidate.reversible !== "boolean") diagnostics.push(planDiagnostic("invalid-plan", "Operation reversible must be boolean", [...path, "reversible"]));
|
|
884
|
+
if (typeof candidate.reversible === "boolean" && candidate.reversible !== (candidate.reversibility === "reversible")) diagnostics.push(planDiagnostic("invalid-plan", "Operation reversibility does not match reversible", [...path, "reversibility"]));
|
|
885
|
+
if (!reversibilityValues.has(candidate.reversibility)) diagnostics.push(planDiagnostic("invalid-plan", "Operation reversibility is invalid", [...path, "reversibility"]));
|
|
886
|
+
if (!Array.isArray(candidate.preconditions)) diagnostics.push(planDiagnostic("invalid-plan", "Operation preconditions must be an array", [...path, "preconditions"]));
|
|
887
|
+
else validatePreconditions(candidate.preconditions, [...path, "preconditions"], diagnostics);
|
|
888
|
+
if (!Array.isArray(candidate.dependsOn)) diagnostics.push(planDiagnostic("invalid-plan", "Operation dependsOn must be an array", [...path, "dependsOn"]));
|
|
889
|
+
else {
|
|
890
|
+
if (!candidate.dependsOn.every((dependency) => typeof dependency === "string")) diagnostics.push(planDiagnostic("invalid-plan", "Operation dependsOn must contain operation IDs", [...path, "dependsOn"]));
|
|
891
|
+
if (!isSorted(candidate.dependsOn)) diagnostics.push(planDiagnostic("non-canonical", "Operation dependsOn must be sorted", [...path, "dependsOn"]));
|
|
892
|
+
}
|
|
893
|
+
if (!Array.isArray(candidate.evidence)) diagnostics.push(planDiagnostic("invalid-plan", "Operation evidence must be an array", [...path, "evidence"]));
|
|
894
|
+
if (candidate.status !== "approved" && candidate.status !== "decision-required" && candidate.status !== "skipped") diagnostics.push(planDiagnostic("invalid-plan", "Operation status is invalid", [...path, "status"]));
|
|
895
|
+
if (!Number.isSafeInteger(candidate.position) || candidate.position < 0) diagnostics.push(planDiagnostic("invalid-plan", "Operation position must be a non-negative integer", [...path, "position"]));
|
|
896
|
+
if (candidate.position !== index) diagnostics.push(planDiagnostic("non-canonical", "Operations must use contiguous topological positions", [...path, "position"]));
|
|
897
|
+
if (candidate.origin !== void 0) validateOrigin(candidate.origin, [...path, "origin"], diagnostics);
|
|
898
|
+
if (candidate.decision !== void 0) validateDecision(candidate.decision, [...path, "decision"], diagnostics);
|
|
899
|
+
if (candidate.reversible === false && typeof candidate.irreversibleReason !== "string") diagnostics.push(planDiagnostic("invalid-plan", "Irreversible operations need a reason", [...path, "irreversibleReason"]));
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
function validateDependencies(plan, diagnostics) {
|
|
903
|
+
const validOperations = plan.operations.filter(isRecord);
|
|
904
|
+
const ids = new Set(validOperations.map((operation) => operation.id));
|
|
905
|
+
const positions = new Map(validOperations.map((operation, index) => [operation.id, index]));
|
|
906
|
+
const seen = /* @__PURE__ */ new Set();
|
|
907
|
+
for (const [index, dependency] of plan.dependencies.entries()) {
|
|
908
|
+
const path = ["dependencies", index];
|
|
909
|
+
if (!isRecord(dependency)) {
|
|
910
|
+
diagnostics.push(planDiagnostic("invalid-plan", "Dependency must be an object", path));
|
|
911
|
+
continue;
|
|
912
|
+
}
|
|
913
|
+
const candidate = dependency;
|
|
914
|
+
requireKeys(dependency, [
|
|
915
|
+
"from",
|
|
916
|
+
"to",
|
|
917
|
+
"reason"
|
|
918
|
+
], path, diagnostics);
|
|
919
|
+
if (typeof candidate.from !== "string" || typeof candidate.to !== "string") diagnostics.push(planDiagnostic("invalid-plan", "Dependency endpoints must be operation IDs", path));
|
|
920
|
+
if (!dependencyReasons.has(candidate.reason)) diagnostics.push(planDiagnostic("invalid-plan", "Dependency reason is invalid", [...path, "reason"]));
|
|
921
|
+
if (!ids.has(candidate.from) || !ids.has(candidate.to)) diagnostics.push(planDiagnostic("invalid-plan", "Dependency references an unknown operation", path));
|
|
922
|
+
const fromPosition = positions.get(candidate.from);
|
|
923
|
+
const toPosition = positions.get(candidate.to);
|
|
924
|
+
if (fromPosition !== void 0 && toPosition !== void 0 && fromPosition >= toPosition) diagnostics.push(planDiagnostic("dependency-cycle", "Dependency source must appear before dependent operation", path));
|
|
925
|
+
const key = `${candidate.from}\u0000${candidate.to}\u0000${candidate.reason}`;
|
|
926
|
+
if (seen.has(key)) diagnostics.push(planDiagnostic("invalid-plan", "Duplicate dependency edge", path));
|
|
927
|
+
seen.add(key);
|
|
928
|
+
if (index > 0) {
|
|
929
|
+
const previous = plan.dependencies[index - 1];
|
|
930
|
+
if (isRecord(previous) && compareDependency(previous, candidate) > 0) diagnostics.push(planDiagnostic("non-canonical", "Dependencies must be sorted", path));
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
for (const [index, operation] of plan.operations.entries()) {
|
|
934
|
+
if (!isRecord(operation) || !Array.isArray(operation.dependsOn)) continue;
|
|
935
|
+
for (const dependency of operation.dependsOn) {
|
|
936
|
+
if (!plan.dependencies.some((edge) => edge.from === dependency && edge.to === operation.id)) diagnostics.push(planDiagnostic("invalid-plan", "Operation dependsOn is missing its dependency edge", [
|
|
937
|
+
"operations",
|
|
938
|
+
index,
|
|
939
|
+
"dependsOn"
|
|
940
|
+
]));
|
|
941
|
+
if (!ids.has(dependency)) diagnostics.push(planDiagnostic("invalid-plan", "Operation dependsOn references an unknown operation", [
|
|
942
|
+
"operations",
|
|
943
|
+
index,
|
|
944
|
+
"dependsOn"
|
|
945
|
+
]));
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
function validatePreconditions(preconditions, path, diagnostics) {
|
|
950
|
+
for (const [index, precondition] of preconditions.entries()) {
|
|
951
|
+
const itemPath = [...path, index];
|
|
952
|
+
if (!isRecord(precondition)) {
|
|
953
|
+
diagnostics.push(planDiagnostic("invalid-plan", "Precondition must be an object", itemPath));
|
|
954
|
+
continue;
|
|
955
|
+
}
|
|
956
|
+
requireKeys(precondition, [
|
|
957
|
+
"type",
|
|
958
|
+
"path",
|
|
959
|
+
"kind"
|
|
960
|
+
], itemPath, diagnostics, [
|
|
961
|
+
"namespace",
|
|
962
|
+
"logicalId",
|
|
963
|
+
"physicalName",
|
|
964
|
+
"digest",
|
|
965
|
+
"property",
|
|
966
|
+
"value"
|
|
967
|
+
]);
|
|
968
|
+
if (precondition.type !== "snapshot-digest" && precondition.type !== "object-present" && precondition.type !== "object-absent" && precondition.type !== "property-equals") diagnostics.push(planDiagnostic("invalid-plan", "Precondition type is invalid", [...itemPath, "type"]));
|
|
969
|
+
if (!isPath(precondition.path)) diagnostics.push(planDiagnostic("invalid-plan", "Precondition path must be an array", [...itemPath, "path"]));
|
|
970
|
+
if (!isObjectKind(precondition.kind) && precondition.kind !== "custom-sql") diagnostics.push(planDiagnostic("invalid-plan", "Precondition kind is invalid", [...itemPath, "kind"]));
|
|
971
|
+
if (precondition.property !== void 0 && !isPath(precondition.property)) diagnostics.push(planDiagnostic("invalid-plan", "Precondition property must be an array", [...itemPath, "property"]));
|
|
972
|
+
if (precondition.digest !== void 0 && typeof precondition.digest !== "string") diagnostics.push(planDiagnostic("invalid-plan", "Precondition digest must be a string", [...itemPath, "digest"]));
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
function validateCustomSql(value, path, diagnostics) {
|
|
976
|
+
if (!isRecord(value)) {
|
|
977
|
+
diagnostics.push(planDiagnostic("custom-sql", "Custom SQL record must be an object", path));
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
980
|
+
requireKeys(value, [
|
|
981
|
+
"sql",
|
|
982
|
+
"dialect",
|
|
983
|
+
"safety",
|
|
984
|
+
"position",
|
|
985
|
+
"reason",
|
|
986
|
+
"reversible"
|
|
987
|
+
], path, diagnostics);
|
|
988
|
+
if (typeof value.sql !== "string" || value.sql.length === 0) diagnostics.push(planDiagnostic("custom-sql", "Custom SQL must be a non-empty string", [...path, "sql"]));
|
|
989
|
+
if (!isDialect(value.dialect)) diagnostics.push(planDiagnostic("custom-sql", "Custom SQL dialect is invalid", [...path, "dialect"]));
|
|
990
|
+
if (!isSafety(value.safety)) diagnostics.push(planDiagnostic("custom-sql", "Custom SQL safety is invalid", [...path, "safety"]));
|
|
991
|
+
if (!validPosition(value.position)) diagnostics.push(planDiagnostic("custom-sql", "Custom SQL position must be a non-negative integer", [...path, "position"]));
|
|
992
|
+
if (typeof value.reason !== "string" || value.reason.length === 0) diagnostics.push(planDiagnostic("custom-sql", "Custom SQL reason must be non-empty", [...path, "reason"]));
|
|
993
|
+
if (typeof value.reversible !== "boolean") diagnostics.push(planDiagnostic("custom-sql", "Custom SQL reversible must be boolean", [...path, "reversible"]));
|
|
994
|
+
}
|
|
995
|
+
function validateOrigin(value, path, diagnostics) {
|
|
996
|
+
if (!isRecord(value)) {
|
|
997
|
+
diagnostics.push(planDiagnostic("invalid-plan", "Operation origin must be an object", path));
|
|
998
|
+
return;
|
|
999
|
+
}
|
|
1000
|
+
requireKeys(value, [
|
|
1001
|
+
"type",
|
|
1002
|
+
"kind",
|
|
1003
|
+
"path",
|
|
1004
|
+
"evidence"
|
|
1005
|
+
], path, diagnostics, [
|
|
1006
|
+
"namespace",
|
|
1007
|
+
"logicalId",
|
|
1008
|
+
"physicalName",
|
|
1009
|
+
"physicalReference",
|
|
1010
|
+
"provenance",
|
|
1011
|
+
"before",
|
|
1012
|
+
"after"
|
|
1013
|
+
]);
|
|
1014
|
+
if (!operationValues.has(value.type) || value.type === "custom-sql") diagnostics.push(planDiagnostic("invalid-plan", "Operation origin type is invalid", [...path, "type"]));
|
|
1015
|
+
if (!isObjectKind(value.kind)) diagnostics.push(planDiagnostic("invalid-plan", "Operation origin kind is invalid", [...path, "kind"]));
|
|
1016
|
+
if (!isPath(value.path)) diagnostics.push(planDiagnostic("invalid-plan", "Operation origin path must be an array", [...path, "path"]));
|
|
1017
|
+
if (!Array.isArray(value.evidence)) diagnostics.push(planDiagnostic("invalid-plan", "Operation origin evidence must be an array", [...path, "evidence"]));
|
|
1018
|
+
}
|
|
1019
|
+
function validateDecision(value, path, diagnostics) {
|
|
1020
|
+
if (!isRecord(value)) {
|
|
1021
|
+
diagnostics.push(planDiagnostic("invalid-plan", "Decision must be an object", path));
|
|
1022
|
+
return;
|
|
1023
|
+
}
|
|
1024
|
+
requireKeys(value, ["action", "reason"], path, diagnostics, [
|
|
1025
|
+
"operationId",
|
|
1026
|
+
"kind",
|
|
1027
|
+
"namespace",
|
|
1028
|
+
"path",
|
|
1029
|
+
"code"
|
|
1030
|
+
]);
|
|
1031
|
+
if (value.action !== "allow" && value.action !== "skip") diagnostics.push(planDiagnostic("invalid-plan", "Decision action is invalid", [...path, "action"]));
|
|
1032
|
+
if (typeof value.reason !== "string" || value.reason.length === 0) diagnostics.push(planDiagnostic("invalid-plan", "Decision reason must be non-empty", [...path, "reason"]));
|
|
1033
|
+
if (value.kind !== void 0 && !isObjectKind(value.kind)) diagnostics.push(planDiagnostic("invalid-plan", "Decision kind is invalid", [...path, "kind"]));
|
|
1034
|
+
if (value.path !== void 0 && !isPath(value.path)) diagnostics.push(planDiagnostic("invalid-plan", "Decision path must be an array", [...path, "path"]));
|
|
1035
|
+
if (value.code !== void 0 && !isDiagnosticCode(value.code)) diagnostics.push(planDiagnostic("invalid-plan", "Decision code is invalid", [...path, "code"]));
|
|
1036
|
+
}
|
|
1037
|
+
function compareDependency(left, right) {
|
|
1038
|
+
return left.from.localeCompare(right.from) || left.to.localeCompare(right.to) || left.reason.localeCompare(right.reason);
|
|
1039
|
+
}
|
|
1040
|
+
function isSorted(values) {
|
|
1041
|
+
for (let index = 1; index < values.length; index += 1) if (values[index - 1].localeCompare(values[index]) > 0) return false;
|
|
1042
|
+
return true;
|
|
1043
|
+
}
|
|
1044
|
+
function validateDecisions(decisions, diagnostics) {
|
|
1045
|
+
for (const [index, decision] of decisions.entries()) {
|
|
1046
|
+
const path = ["decisions", index];
|
|
1047
|
+
if (!isRecord(decision)) {
|
|
1048
|
+
diagnostics.push(planDiagnostic("invalid-plan", "Decision must be an object", path));
|
|
1049
|
+
continue;
|
|
1050
|
+
}
|
|
1051
|
+
validateDecision(decision, path, diagnostics);
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
function validateDiagnostics(diagnostics, output) {
|
|
1055
|
+
for (const [index, diagnostic] of diagnostics.entries()) {
|
|
1056
|
+
const path = ["diagnostics", index];
|
|
1057
|
+
if (!isRecord(diagnostic)) {
|
|
1058
|
+
output.push(planDiagnostic("invalid-plan", "Diagnostic must be an object", path));
|
|
1059
|
+
continue;
|
|
1060
|
+
}
|
|
1061
|
+
requireKeys(diagnostic, [
|
|
1062
|
+
"code",
|
|
1063
|
+
"severity",
|
|
1064
|
+
"message",
|
|
1065
|
+
"path"
|
|
1066
|
+
], path, output, [
|
|
1067
|
+
"operationId",
|
|
1068
|
+
"kind",
|
|
1069
|
+
"namespace",
|
|
1070
|
+
"logicalId",
|
|
1071
|
+
"physicalName",
|
|
1072
|
+
"dialect",
|
|
1073
|
+
"evidence",
|
|
1074
|
+
"source"
|
|
1075
|
+
]);
|
|
1076
|
+
if (!isDiagnosticCode(diagnostic.code)) output.push(planDiagnostic("invalid-plan", "Diagnostic code is invalid", [...path, "code"]));
|
|
1077
|
+
if (diagnostic.severity !== "error" && diagnostic.severity !== "warning") output.push(planDiagnostic("invalid-plan", "Diagnostic severity is invalid", [...path, "severity"]));
|
|
1078
|
+
if (typeof diagnostic.message !== "string") output.push(planDiagnostic("invalid-plan", "Diagnostic message must be a string", [...path, "message"]));
|
|
1079
|
+
if (!isPath(diagnostic.path)) output.push(planDiagnostic("invalid-plan", "Diagnostic path must be an array", [...path, "path"]));
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
function validateDialect(value, path, diagnostics) {
|
|
1083
|
+
if (!isDialect(value)) diagnostics.push(planDiagnostic("invalid-plan", "Dialect must contain a name and positive version", path));
|
|
1084
|
+
}
|
|
1085
|
+
function requireKeys(value, required, path, diagnostics, optional = []) {
|
|
1086
|
+
const allowed = /* @__PURE__ */ new Set([...required, ...optional]);
|
|
1087
|
+
for (const key of required) if (!(key in value)) diagnostics.push(planDiagnostic("invalid-plan", `Missing required field "${key}"`, [...path, key]));
|
|
1088
|
+
for (const key of Object.keys(value)) if (!allowed.has(key)) diagnostics.push(planDiagnostic("invalid-plan", `Unknown plan field "${key}"`, [...path, key]));
|
|
1089
|
+
}
|
|
1090
|
+
function isDialect(value) {
|
|
1091
|
+
return isRecord(value) && typeof value.name === "string" && value.name.length > 0 && typeof value.version === "number" && Number.isSafeInteger(value.version) && value.version > 0;
|
|
1092
|
+
}
|
|
1093
|
+
function isSafety(value) {
|
|
1094
|
+
return typeof value === "string" && safetyValues.has(value);
|
|
1095
|
+
}
|
|
1096
|
+
function isDiagnosticCode(value) {
|
|
1097
|
+
return typeof value === "string" && (/* @__PURE__ */ new Set([
|
|
1098
|
+
"decision-required",
|
|
1099
|
+
"dependency-cycle",
|
|
1100
|
+
"invalid-plan",
|
|
1101
|
+
"unknown",
|
|
1102
|
+
"lossy",
|
|
1103
|
+
"unsupported",
|
|
1104
|
+
"destructive",
|
|
1105
|
+
"ambiguous",
|
|
1106
|
+
"custom-sql",
|
|
1107
|
+
"dialect-mismatch",
|
|
1108
|
+
"non-canonical"
|
|
1109
|
+
])).has(value);
|
|
1110
|
+
}
|
|
1111
|
+
function isObjectKind(value) {
|
|
1112
|
+
return typeof value === "string" && objectKinds.has(value);
|
|
1113
|
+
}
|
|
1114
|
+
function isPath(value) {
|
|
1115
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string" || Number.isSafeInteger(item));
|
|
1116
|
+
}
|
|
1117
|
+
function isRecord(value) {
|
|
1118
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1119
|
+
}
|
|
1120
|
+
function isJsonRecord(value) {
|
|
1121
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1122
|
+
}
|
|
1123
|
+
function validPosition(value) {
|
|
1124
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
1125
|
+
}
|
|
1126
|
+
function samePath(left, right) {
|
|
1127
|
+
return left.length === right.length && left.every((item, index) => item === right[index]);
|
|
1128
|
+
}
|
|
1129
|
+
function digestJson(value) {
|
|
1130
|
+
return digestText(canonicalJson(value));
|
|
1131
|
+
}
|
|
1132
|
+
function digestText(source) {
|
|
1133
|
+
let hash = 14695981039346656037n;
|
|
1134
|
+
const prime = 1099511628211n;
|
|
1135
|
+
const mask = 18446744073709551615n;
|
|
1136
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
1137
|
+
const codePoint = source.codePointAt(index) ?? 0;
|
|
1138
|
+
if (codePoint > 65535) index += 1;
|
|
1139
|
+
for (const byte of new TextEncoder().encode(String.fromCodePoint(codePoint))) {
|
|
1140
|
+
hash ^= BigInt(byte);
|
|
1141
|
+
hash = hash * prime & mask;
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
return `fnv1a64:${hash.toString(16).padStart(16, "0")}`;
|
|
1145
|
+
}
|
|
1146
|
+
function freezePlan(plan) {
|
|
1147
|
+
return deepFreeze(plan);
|
|
1148
|
+
}
|
|
1149
|
+
function freeze(value) {
|
|
1150
|
+
return deepFreeze(value);
|
|
1151
|
+
}
|
|
1152
|
+
function deepFreeze(value, seen = /* @__PURE__ */ new WeakSet()) {
|
|
1153
|
+
if (value === null || typeof value !== "object") return value;
|
|
1154
|
+
if (seen.has(value)) return value;
|
|
1155
|
+
seen.add(value);
|
|
1156
|
+
if (Array.isArray(value)) for (const item of value) deepFreeze(item, seen);
|
|
1157
|
+
else for (const child of Object.values(value)) deepFreeze(child, seen);
|
|
1158
|
+
return Object.freeze(value);
|
|
1159
|
+
}
|
|
1160
|
+
//#endregion
|
|
1161
|
+
export { MigrationPlanValidationError, assertMigrationPlan, assertMigrationPlanFromDiff, assertPlan, canonicalizeMigrationPlan, createMigrationPlan, createPlan, decodeMigrationPlan, decodePlan, digestMigrationPlan, encodeMigrationPlan, encodePlan, migrationPlanDigest, migrationPlanFormat, migrationPlanVersion, planMigration, validateMigrationPlan };
|