@dbsp/cli 3.0.0 → 4.0.0
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/README.md +1 -0
- package/dist/{chunk-G6NS4BAV.js → chunk-MFQPE7CY.js} +7 -8
- package/dist/chunk-MFQPE7CY.js.map +1 -0
- package/dist/generators/schema-codegen.js +1 -1
- package/dist/index.js +1318 -1805
- package/dist/index.js.map +1 -1
- package/package.json +8 -8
- package/dist/chunk-G6NS4BAV.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
createDbConnection,
|
|
4
4
|
generateSchemaFileWithDiagnostics,
|
|
5
5
|
redactDbUrl
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-MFQPE7CY.js";
|
|
7
7
|
import {
|
|
8
8
|
loadSchema,
|
|
9
9
|
loadSchemaFromCwd
|
|
@@ -15,37 +15,40 @@ import {
|
|
|
15
15
|
|
|
16
16
|
// src/index.ts
|
|
17
17
|
import { createRequire } from "module";
|
|
18
|
-
import { Command as
|
|
18
|
+
import { Command as Command13, CommanderError } from "commander";
|
|
19
19
|
|
|
20
20
|
// src/commands/apply.ts
|
|
21
21
|
import { AsyncLocalStorage } from "async_hooks";
|
|
22
|
-
import { createHash
|
|
22
|
+
import { createHash } from "crypto";
|
|
23
23
|
import { readFile } from "fs/promises";
|
|
24
24
|
import { createInterface } from "readline/promises";
|
|
25
25
|
import {
|
|
26
26
|
appendIntentJournal,
|
|
27
|
-
appendTransitionAuthorization,
|
|
28
27
|
createPgTransitionLessor as createPgTransitionLessor3,
|
|
29
|
-
createPgTransitionPack as createPgTransitionPack2,
|
|
30
28
|
escapeDiagnosticText as escapeDiagnosticText3,
|
|
31
|
-
|
|
29
|
+
executionIdsForRun,
|
|
30
|
+
readPgLedgerAddressChain,
|
|
31
|
+
readPgLedgerReservationsForExecution,
|
|
32
32
|
readTransitionJournal,
|
|
33
33
|
TransitionRunIdentityMismatchError,
|
|
34
|
-
validatePgManagedLedgerCurrency,
|
|
35
34
|
withPgTransitionRunLock
|
|
36
35
|
} from "@dbsp/adapter-pgsql";
|
|
37
|
-
import {
|
|
36
|
+
import {
|
|
37
|
+
applyPgTransitionRun,
|
|
38
|
+
comparePgsqlDeclaredAdoptionSchema as comparePgsqlDeclaredAdoptionSchema2,
|
|
39
|
+
executeGeneratorPlan,
|
|
40
|
+
lockPgJournalRun,
|
|
41
|
+
modelForDeclaredAdoption as modelForDeclaredAdoption2
|
|
42
|
+
} from "@dbsp/adapter-pgsql/internal";
|
|
38
43
|
import {
|
|
39
44
|
acquireExclusiveTransitionLease,
|
|
40
45
|
acquireTransitionLease as acquireTransitionLease3,
|
|
41
|
-
createApplier,
|
|
42
|
-
createPackRegistry as createPackRegistry2,
|
|
43
46
|
selectorMatchesResource,
|
|
44
47
|
transitionPlanDigest as transitionPlanDigest2,
|
|
45
|
-
validateNormalizedManagedStepManifest as
|
|
48
|
+
validateNormalizedManagedStepManifest as validateNormalizedManagedStepManifest2
|
|
46
49
|
} from "@dbsp/core";
|
|
47
50
|
import { mintDurablyLoadedRun } from "@dbsp/core/internal";
|
|
48
|
-
import { REFUSAL_VOCABULARY as REFUSAL_VOCABULARY2 } from "@dbsp/types";
|
|
51
|
+
import { ledgerAddressKey, REFUSAL_VOCABULARY as REFUSAL_VOCABULARY2 } from "@dbsp/types";
|
|
49
52
|
import { Command as Command2 } from "commander";
|
|
50
53
|
|
|
51
54
|
// src/utils/output.ts
|
|
@@ -59,932 +62,47 @@ function printCliJson(value) {
|
|
|
59
62
|
console.log(serializeCliJson(value));
|
|
60
63
|
}
|
|
61
64
|
|
|
62
|
-
// src/commands/generator-execution.ts
|
|
63
|
-
import { createHash, randomUUID } from "crypto";
|
|
64
|
-
import { isDeepStrictEqual } from "util";
|
|
65
|
-
import {
|
|
66
|
-
compareSchemata,
|
|
67
|
-
createPgsqlAdapter,
|
|
68
|
-
dbTypesEqual,
|
|
69
|
-
executePgAdmittedOperation,
|
|
70
|
-
executePgDeclaredAdoption,
|
|
71
|
-
executePgPersistedTableReaddress,
|
|
72
|
-
preflightPgDeclaredAdoption,
|
|
73
|
-
readPgCatalogueIdentity,
|
|
74
|
-
readPgLedgerAddressChain,
|
|
75
|
-
readPgLedgerScopeCurrency,
|
|
76
|
-
readPgRemovalEffectsClosure
|
|
77
|
-
} from "@dbsp/adapter-pgsql";
|
|
78
|
-
import {
|
|
79
|
-
outcomeClaimEventId,
|
|
80
|
-
outcomeClaimId,
|
|
81
|
-
projectLedgerChain,
|
|
82
|
-
validateNormalizedManagedStepManifest
|
|
83
|
-
} from "@dbsp/core";
|
|
84
|
-
import { decideDestructiveDecision } from "@dbsp/core/internal";
|
|
85
|
-
import { ledgerAddressKey } from "@dbsp/types";
|
|
86
|
-
function managedSteps(manifest) {
|
|
87
|
-
return manifest.steps;
|
|
88
|
-
}
|
|
89
|
-
function matchesReviewedReplacementSelector(reviewed, provided) {
|
|
90
|
-
return provided === reviewed || reviewed.startsWith("table:") && provided === reviewed.slice("table:".length);
|
|
91
|
-
}
|
|
92
|
-
function modelForAdoption(table) {
|
|
93
|
-
const tables = /* @__PURE__ */ new Map([[table.name, table]]);
|
|
94
|
-
const relations = /* @__PURE__ */ new Map();
|
|
95
|
-
return {
|
|
96
|
-
tables,
|
|
97
|
-
relations,
|
|
98
|
-
getTable: (name) => tables.get(name),
|
|
99
|
-
getRelation: (name) => relations.get(name),
|
|
100
|
-
getRelationsFrom: () => [],
|
|
101
|
-
getRelationsTo: () => [],
|
|
102
|
-
isAmbiguous: () => ({ ambiguous: false, options: [] })
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
async function adoptionShapeMatches(pool, schema, shape) {
|
|
106
|
-
const live = await createPgsqlAdapter(pool).introspect({ schema });
|
|
107
|
-
const diff = compareSchemata(modelForAdoption(shape), live);
|
|
108
|
-
return !diff.changes.some((change) => change.table === shape.name);
|
|
109
|
-
}
|
|
110
|
-
var SYSTEM_LEDGER_SCHEMAS = /* @__PURE__ */ new Set([
|
|
111
|
-
"pg_toast",
|
|
112
|
-
"pg_catalog",
|
|
113
|
-
"information_schema"
|
|
114
|
-
]);
|
|
115
|
-
var SystemSchemaLedgerHomeError = class extends Error {
|
|
116
|
-
address;
|
|
117
|
-
constructor(address) {
|
|
118
|
-
super(
|
|
119
|
-
`SystemSchemaLedgerHomeError: ledger home is unavailable for system schema ${address.schema}: ${ledgerAddressKey(address)}`
|
|
120
|
-
);
|
|
121
|
-
this.name = "SystemSchemaLedgerHomeError";
|
|
122
|
-
this.address = address;
|
|
123
|
-
}
|
|
124
|
-
};
|
|
125
|
-
function home(address) {
|
|
126
|
-
if (address.scope !== "database" && address.schema && SYSTEM_LEDGER_SCHEMAS.has(address.schema))
|
|
127
|
-
throw new SystemSchemaLedgerHomeError(address);
|
|
128
|
-
return address.scope === "database" ? { scope: "database" } : address.schema ? { scope: "schema", schema: address.schema } : (() => {
|
|
129
|
-
throw new Error(
|
|
130
|
-
`schema-scoped generated address ${address.name} has no schema ledger`
|
|
131
|
-
);
|
|
132
|
-
})();
|
|
133
|
-
}
|
|
134
|
-
function acceptance(planDigest, approval) {
|
|
135
|
-
return approval.approvals.some(
|
|
136
|
-
(grant) => grant.class === `destructive-plan-accepted:${planDigest}`
|
|
137
|
-
) === true ? "destructive-plan-accepted" : "absent";
|
|
138
|
-
}
|
|
139
|
-
function withheldDestructiveAuthority(evidence) {
|
|
140
|
-
if (evidence.declaration !== "requires-removal" && evidence.declaration !== "requires-lossy-change" && evidence.declaration !== "replacement-requested-by-plan")
|
|
141
|
-
return "destructive declaration authority";
|
|
142
|
-
if (evidence.ownership !== "managed-by-me")
|
|
143
|
-
return "destructive ownership authority";
|
|
144
|
-
if (evidence.catalogueIdentity !== "matches-recorded")
|
|
145
|
-
return "destructive catalogue identity authority";
|
|
146
|
-
if (evidence.operatorAcceptance !== "destructive-plan-accepted")
|
|
147
|
-
return "destructive operator acceptance authority";
|
|
148
|
-
if (evidence.containment !== void 0 && evidence.containment !== "all-contained-or-managed")
|
|
149
|
-
return "destructive containment authority";
|
|
150
|
-
if (evidence.ledgerLineage !== "matches-database")
|
|
151
|
-
return "destructive ledger lineage authority";
|
|
152
|
-
return void 0;
|
|
153
|
-
}
|
|
154
|
-
function withheldDestructiveAuthorityFromReason(reason) {
|
|
155
|
-
if (reason.includes("operator acceptance"))
|
|
156
|
-
return "destructive operator acceptance authority";
|
|
157
|
-
if (reason.includes("ledger lineage"))
|
|
158
|
-
return "destructive ledger lineage authority";
|
|
159
|
-
return void 0;
|
|
160
|
-
}
|
|
161
|
-
async function databaseId(pool) {
|
|
162
|
-
const result = await pool.query("SELECT current_database() AS database_id");
|
|
163
|
-
const database = result.rows[0]?.database_id;
|
|
164
|
-
if (typeof database !== "string" || database.length === 0)
|
|
165
|
-
throw new Error(
|
|
166
|
-
"schema-differ generator could not read current database identity"
|
|
167
|
-
);
|
|
168
|
-
return database;
|
|
169
|
-
}
|
|
170
|
-
async function managed(executor, address) {
|
|
171
|
-
const chain = await readPgLedgerAddressChain(
|
|
172
|
-
executor,
|
|
173
|
-
home(address),
|
|
174
|
-
address
|
|
175
|
-
);
|
|
176
|
-
const projection = projectLedgerChain(chain);
|
|
177
|
-
return projection.kind === "projected-ledger-chain" && projection.stableState === "managed";
|
|
178
|
-
}
|
|
179
|
-
async function alreadyAppliedCreation(executor, address) {
|
|
180
|
-
const live = await readPgCatalogueIdentity(executor, address);
|
|
181
|
-
if (!live?.catalogueIdentity) return false;
|
|
182
|
-
const chain = await readPgLedgerAddressChain(
|
|
183
|
-
executor,
|
|
184
|
-
home(address),
|
|
185
|
-
address
|
|
186
|
-
);
|
|
187
|
-
const projection = projectLedgerChain(chain);
|
|
188
|
-
const recorded = chain.terminalMember?.catalogueIdentity;
|
|
189
|
-
return projection.kind === "projected-ledger-chain" && projection.stableState === "managed" && recorded !== void 0 && isDeepStrictEqual(recorded, live.catalogueIdentity);
|
|
190
|
-
}
|
|
191
|
-
function observed(address) {
|
|
192
|
-
return {
|
|
193
|
-
value: { kind: address.kind, name: address.name },
|
|
194
|
-
digest: `generator:${address.kind}:${address.name}`
|
|
195
|
-
};
|
|
196
|
-
}
|
|
197
|
-
function normalizedDefinition(value) {
|
|
198
|
-
return value.replaceAll('"', "").replace(/\s+/gu, " ").replace(/;$/u, "").trim().toLowerCase();
|
|
199
|
-
}
|
|
200
|
-
function generatedPayload(value) {
|
|
201
|
-
return {
|
|
202
|
-
value,
|
|
203
|
-
digest: createHash("sha256").update(JSON.stringify(value)).digest("hex")
|
|
204
|
-
};
|
|
205
|
-
}
|
|
206
|
-
function textList(value) {
|
|
207
|
-
return Array.isArray(value) ? value.map((item) => String(item)) : [];
|
|
208
|
-
}
|
|
209
|
-
function foreignKeyAction(code) {
|
|
210
|
-
switch (code) {
|
|
211
|
-
case "a":
|
|
212
|
-
return "NO ACTION";
|
|
213
|
-
case "r":
|
|
214
|
-
return "RESTRICT";
|
|
215
|
-
case "c":
|
|
216
|
-
return "CASCADE";
|
|
217
|
-
case "n":
|
|
218
|
-
return "SET NULL";
|
|
219
|
-
case "d":
|
|
220
|
-
return "SET DEFAULT";
|
|
221
|
-
default:
|
|
222
|
-
return void 0;
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
function generatedPostcondition(step, address) {
|
|
226
|
-
const value = step.expectedDeclaration?.value;
|
|
227
|
-
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
228
|
-
throw new Error(
|
|
229
|
-
`generated ${address.kind} step ${step.stepKey} has no structural postcondition`
|
|
230
|
-
);
|
|
231
|
-
const postcondition = value;
|
|
232
|
-
return postcondition;
|
|
233
|
-
}
|
|
234
|
-
async function readGeneratedPostcondition(executor, step, address) {
|
|
235
|
-
const parent = address.parent?.name;
|
|
236
|
-
if (address.kind === "column" && parent && address.schema) {
|
|
237
|
-
const postcondition = generatedPostcondition(step, address);
|
|
238
|
-
if (postcondition.kind !== "column")
|
|
239
|
-
throw new Error(
|
|
240
|
-
`generated column ${address.name} has a non-column structural postcondition`
|
|
241
|
-
);
|
|
242
|
-
const expected = postcondition.column;
|
|
243
|
-
if (expected.name !== address.name)
|
|
244
|
-
throw new Error(
|
|
245
|
-
`generated column ${address.name} structural postcondition names another column`
|
|
246
|
-
);
|
|
247
|
-
const row = (await executor.query(
|
|
248
|
-
`SELECT pg_catalog.format_type(attribute.atttypid, attribute.atttypmod) AS column_type, attribute.attnotnull AS is_not_null, pg_catalog.pg_get_expr(default_value.adbin, default_value.adrelid) AS column_default, collation.collname AS collation_name, attribute.attidentity AS identity_kind FROM pg_catalog.pg_attribute attribute JOIN pg_catalog.pg_class relation ON relation.oid = attribute.attrelid JOIN pg_catalog.pg_namespace namespace ON namespace.oid = relation.relnamespace LEFT JOIN pg_catalog.pg_attrdef default_value ON default_value.adrelid = attribute.attrelid AND default_value.adnum = attribute.attnum LEFT JOIN pg_catalog.pg_collation collation ON collation.oid = attribute.attcollation WHERE namespace.nspname = $1 AND relation.relname = $2 AND attribute.attname = $3 AND attribute.attnum > 0 AND NOT attribute.attisdropped`,
|
|
249
|
-
[address.schema, parent, address.name]
|
|
250
|
-
)).rows[0];
|
|
251
|
-
if (!row) throw new Error(`generated column ${address.name} is absent`);
|
|
252
|
-
const type = String(row.column_type ?? "");
|
|
253
|
-
const nullable = row.is_not_null === true ? false : row.is_not_null === false ? true : void 0;
|
|
254
|
-
const actualDefault = row.column_default == null ? void 0 : String(row.column_default);
|
|
255
|
-
const actualCollation = row.collation_name == null ? null : String(row.collation_name);
|
|
256
|
-
const actualIdentity = row.identity_kind === "a" ? "always" : row.identity_kind === "d" ? "byDefault" : null;
|
|
257
|
-
if (expected.type !== void 0 && !dbTypesEqual(type, expected.type))
|
|
258
|
-
throw new Error(
|
|
259
|
-
`generated column ${address.name} type postcondition differs`
|
|
260
|
-
);
|
|
261
|
-
if (expected.nullable !== void 0 && nullable !== expected.nullable)
|
|
262
|
-
throw new Error(
|
|
263
|
-
`generated column ${address.name} nullability postcondition differs`
|
|
264
|
-
);
|
|
265
|
-
if (expected.hasDefault === true && (actualDefault === void 0 || expected.default === void 0 || actualDefault !== expected.default))
|
|
266
|
-
throw new Error(
|
|
267
|
-
`generated column ${address.name} default postcondition differs`
|
|
268
|
-
);
|
|
269
|
-
if (expected.hasDefault === false && actualDefault !== void 0)
|
|
270
|
-
throw new Error(
|
|
271
|
-
`generated column ${address.name} default postcondition differs`
|
|
272
|
-
);
|
|
273
|
-
if (expected.collation !== void 0 && actualCollation !== expected.collation)
|
|
274
|
-
throw new Error(
|
|
275
|
-
`generated column ${address.name} collation postcondition differs`
|
|
276
|
-
);
|
|
277
|
-
if (expected.identity !== void 0 && actualIdentity !== expected.identity)
|
|
278
|
-
throw new Error(
|
|
279
|
-
`generated column ${address.name} identity postcondition differs`
|
|
280
|
-
);
|
|
281
|
-
return generatedPayload({
|
|
282
|
-
kind: "column",
|
|
283
|
-
type,
|
|
284
|
-
nullable,
|
|
285
|
-
default: actualDefault
|
|
286
|
-
});
|
|
287
|
-
}
|
|
288
|
-
if (address.kind === "constraint" && parent && address.schema) {
|
|
289
|
-
const postcondition = generatedPostcondition(step, address);
|
|
290
|
-
if (postcondition.kind !== "constraint")
|
|
291
|
-
throw new Error(
|
|
292
|
-
`generated constraint ${address.name} has a non-constraint structural postcondition`
|
|
293
|
-
);
|
|
294
|
-
const expected = postcondition.constraint;
|
|
295
|
-
const row = (await executor.query(
|
|
296
|
-
`SELECT constraint_item.contype AS constraint_type, pg_catalog.pg_get_constraintdef(constraint_item.oid, true) AS constraint_definition, ARRAY(SELECT attribute.attname::text FROM unnest(constraint_item.conkey) WITH ORDINALITY AS key_column(attnum, position) JOIN pg_catalog.pg_attribute attribute ON attribute.attrelid = constraint_item.conrelid AND attribute.attnum = key_column.attnum ORDER BY key_column.position) AS key_columns, referenced_namespace.nspname AS referenced_schema, referenced_relation.relname AS referenced_table, ARRAY(SELECT attribute.attname::text FROM unnest(constraint_item.confkey) WITH ORDINALITY AS key_column(attnum, position) JOIN pg_catalog.pg_attribute attribute ON attribute.attrelid = constraint_item.confrelid AND attribute.attnum = key_column.attnum ORDER BY key_column.position) AS referenced_columns, constraint_item.confdeltype AS on_delete, constraint_item.confupdtype AS on_update, constraint_item.condeferrable AS is_deferrable, constraint_item.condeferred AS is_deferred, constraint_item.convalidated AS is_validated FROM pg_catalog.pg_constraint constraint_item JOIN pg_catalog.pg_class relation ON relation.oid = constraint_item.conrelid JOIN pg_catalog.pg_namespace namespace ON namespace.oid = relation.relnamespace LEFT JOIN pg_catalog.pg_class referenced_relation ON referenced_relation.oid = constraint_item.confrelid LEFT JOIN pg_catalog.pg_namespace referenced_namespace ON referenced_namespace.oid = referenced_relation.relnamespace WHERE namespace.nspname = $1 AND relation.relname = $2 AND constraint_item.conname = $3`,
|
|
297
|
-
[address.schema, parent, address.name]
|
|
298
|
-
)).rows[0];
|
|
299
|
-
if (!row) throw new Error(`generated constraint ${address.name} is absent`);
|
|
300
|
-
const definition = String(row.constraint_definition ?? "");
|
|
301
|
-
if (String(row.constraint_type) !== expected.type)
|
|
302
|
-
throw new Error(
|
|
303
|
-
`generated constraint ${address.name} postcondition differs`
|
|
304
|
-
);
|
|
305
|
-
if ((expected.type === "p" || expected.type === "u") && JSON.stringify(textList(row.key_columns)) !== JSON.stringify(expected.columns))
|
|
306
|
-
throw new Error(
|
|
307
|
-
`generated constraint ${address.name} postcondition differs`
|
|
308
|
-
);
|
|
309
|
-
if (expected.type === "c") {
|
|
310
|
-
if (normalizedDefinition(definition) !== normalizedDefinition(expected.definition) || row.is_validated === true === expected.notValid)
|
|
311
|
-
throw new Error(
|
|
312
|
-
`generated constraint ${address.name} postcondition differs`
|
|
313
|
-
);
|
|
314
|
-
}
|
|
315
|
-
if (expected.type === "f") {
|
|
316
|
-
if (JSON.stringify(textList(row.key_columns)) !== JSON.stringify(expected.columns) || String(row.referenced_table ?? "") !== expected.references.table || expected.references.schema !== void 0 && String(row.referenced_schema ?? "") !== expected.references.schema || JSON.stringify(textList(row.referenced_columns)) !== JSON.stringify(expected.references.columns) || foreignKeyAction(row.on_delete) !== expected.onDelete || foreignKeyAction(row.on_update) !== expected.onUpdate || (row.is_deferrable === true && row.is_deferred === true) !== expected.deferred || row.is_validated === true === expected.notValid)
|
|
317
|
-
throw new Error(
|
|
318
|
-
`generated constraint ${address.name} postcondition differs`
|
|
319
|
-
);
|
|
320
|
-
}
|
|
321
|
-
return generatedPayload({
|
|
322
|
-
kind: "constraint",
|
|
323
|
-
type: String(row.constraint_type ?? ""),
|
|
324
|
-
definition
|
|
325
|
-
});
|
|
326
|
-
}
|
|
327
|
-
if (address.kind === "index" && parent && address.schema) {
|
|
328
|
-
const postcondition = generatedPostcondition(step, address);
|
|
329
|
-
if (postcondition.kind !== "index")
|
|
330
|
-
throw new Error(
|
|
331
|
-
`generated index ${address.name} has a non-index structural postcondition`
|
|
332
|
-
);
|
|
333
|
-
const row = (await executor.query(
|
|
334
|
-
`SELECT index_relation.relname AS index_name, index_meta.indisunique AS is_unique, pg_catalog.pg_get_indexdef(index_meta.indexrelid, 0, true) AS index_definition FROM pg_catalog.pg_index index_meta JOIN pg_catalog.pg_class relation ON relation.oid = index_meta.indrelid JOIN pg_catalog.pg_namespace namespace ON namespace.oid = relation.relnamespace JOIN pg_catalog.pg_class index_relation ON index_relation.oid = index_meta.indexrelid WHERE namespace.nspname = $1 AND relation.relname = $2 AND index_relation.relname = $3`,
|
|
335
|
-
[address.schema, parent, address.name]
|
|
336
|
-
)).rows[0];
|
|
337
|
-
if (!row) throw new Error(`generated index ${address.name} is absent`);
|
|
338
|
-
const definition = String(row.index_definition ?? "");
|
|
339
|
-
if (normalizedDefinition(definition) !== normalizedDefinition(postcondition.definition))
|
|
340
|
-
throw new Error(`generated index ${address.name} postcondition differs`);
|
|
341
|
-
return generatedPayload({
|
|
342
|
-
kind: "index",
|
|
343
|
-
unique: row.is_unique === true,
|
|
344
|
-
definition
|
|
345
|
-
});
|
|
346
|
-
}
|
|
347
|
-
if (address.kind === "table" && address.schema) {
|
|
348
|
-
const postcondition = generatedPostcondition(step, address);
|
|
349
|
-
if (postcondition.kind !== "table")
|
|
350
|
-
throw new Error(
|
|
351
|
-
`generated table ${address.name} has a non-table structural postcondition`
|
|
352
|
-
);
|
|
353
|
-
const rows = (await executor.query(
|
|
354
|
-
`SELECT attribute.attname AS column_name, pg_catalog.format_type(attribute.atttypid, attribute.atttypmod) AS column_type, attribute.attnotnull AS is_not_null, pg_catalog.pg_get_expr(default_value.adbin, default_value.adrelid) AS column_default FROM pg_catalog.pg_attribute attribute JOIN pg_catalog.pg_class relation ON relation.oid = attribute.attrelid JOIN pg_catalog.pg_namespace namespace ON namespace.oid = relation.relnamespace LEFT JOIN pg_catalog.pg_attrdef default_value ON default_value.adrelid = attribute.attrelid AND default_value.adnum = attribute.attnum WHERE namespace.nspname = $1 AND relation.relname = $2 AND attribute.attnum > 0 AND NOT attribute.attisdropped ORDER BY attribute.attnum`,
|
|
355
|
-
[address.schema, address.name]
|
|
356
|
-
)).rows;
|
|
357
|
-
const expected = postcondition.columns;
|
|
358
|
-
if (expected.length === 0 || rows.length !== expected.length)
|
|
359
|
-
throw new Error(
|
|
360
|
-
`generated table ${address.name} column postcondition differs: expected ${JSON.stringify(expected)}, live ${JSON.stringify(rows)}`
|
|
361
|
-
);
|
|
362
|
-
for (const specification of expected) {
|
|
363
|
-
const actual = rows.find((row) => row.column_name === specification.name);
|
|
364
|
-
if (!actual || specification.type !== void 0 && !dbTypesEqual(
|
|
365
|
-
String(actual.column_type ?? ""),
|
|
366
|
-
specification.type
|
|
367
|
-
) || specification.nullable !== void 0 && actual.is_not_null !== true !== specification.nullable || specification.hasDefault === true && (specification.default === void 0 || actual.column_default == null || String(actual.column_default) !== specification.default) || specification.hasDefault === false && actual.column_default != null)
|
|
368
|
-
throw new Error(
|
|
369
|
-
`generated table ${address.name} column postcondition differs: expected ${JSON.stringify(specification)}, live ${JSON.stringify(actual)}`
|
|
370
|
-
);
|
|
371
|
-
}
|
|
372
|
-
return generatedPayload({
|
|
373
|
-
kind: "table",
|
|
374
|
-
columns: rows.map((row) => ({
|
|
375
|
-
name: String(row.column_name),
|
|
376
|
-
type: String(row.column_type),
|
|
377
|
-
nullable: row.is_not_null !== true,
|
|
378
|
-
default: row.column_default == null ? void 0 : String(row.column_default)
|
|
379
|
-
}))
|
|
380
|
-
});
|
|
381
|
-
}
|
|
382
|
-
if (address.kind === "enum" && address.schema) {
|
|
383
|
-
const postcondition = generatedPostcondition(step, address);
|
|
384
|
-
if (postcondition.kind !== "enum")
|
|
385
|
-
throw new Error(
|
|
386
|
-
`generated enum ${address.name} has a non-enum structural postcondition`
|
|
387
|
-
);
|
|
388
|
-
const rows = (await executor.query(
|
|
389
|
-
`SELECT enum_label.enumlabel AS label FROM pg_catalog.pg_type type JOIN pg_catalog.pg_namespace namespace ON namespace.oid = type.typnamespace JOIN pg_catalog.pg_enum enum_label ON enum_label.enumtypid = type.oid WHERE namespace.nspname = $1 AND type.typname = $2 ORDER BY enum_label.enumsortorder`,
|
|
390
|
-
[address.schema, address.name]
|
|
391
|
-
)).rows;
|
|
392
|
-
const labels = rows.map((row) => String(row.label));
|
|
393
|
-
if (JSON.stringify(labels) !== JSON.stringify(postcondition.labels))
|
|
394
|
-
throw new Error(`generated enum ${address.name} postcondition differs`);
|
|
395
|
-
return generatedPayload({ kind: "enum", labels });
|
|
396
|
-
}
|
|
397
|
-
if (address.kind === "sequence" && address.schema) {
|
|
398
|
-
const postcondition = generatedPostcondition(step, address);
|
|
399
|
-
if (postcondition.kind !== "sequence")
|
|
400
|
-
throw new Error(
|
|
401
|
-
`generated sequence ${address.name} has a non-sequence structural postcondition`
|
|
402
|
-
);
|
|
403
|
-
const row = (await executor.query(
|
|
404
|
-
`SELECT sequence.start_value::text AS start_value, sequence.increment_by::text AS increment_by, sequence.min_value::text AS min_value, sequence.max_value::text AS max_value, sequence.cache_size::text AS cache_size, sequence.cycle AS cycle FROM pg_catalog.pg_sequences sequence WHERE sequence.schemaname = $1 AND sequence.sequencename = $2`,
|
|
405
|
-
[address.schema, address.name]
|
|
406
|
-
)).rows[0];
|
|
407
|
-
if (!row) throw new Error(`generated sequence ${address.name} is absent`);
|
|
408
|
-
const actual = {
|
|
409
|
-
startValue: String(row.start_value),
|
|
410
|
-
incrementBy: String(row.increment_by),
|
|
411
|
-
minValue: String(row.min_value),
|
|
412
|
-
maxValue: String(row.max_value),
|
|
413
|
-
cycle: row.cycle === true
|
|
414
|
-
};
|
|
415
|
-
for (const key of [
|
|
416
|
-
"startValue",
|
|
417
|
-
"incrementBy",
|
|
418
|
-
"minValue",
|
|
419
|
-
"maxValue",
|
|
420
|
-
"cycle"
|
|
421
|
-
])
|
|
422
|
-
if (postcondition[key] !== void 0 && postcondition[key] !== actual[key])
|
|
423
|
-
throw new Error(
|
|
424
|
-
`generated sequence ${address.name} postcondition differs`
|
|
425
|
-
);
|
|
426
|
-
return generatedPayload({ kind: "sequence", ...row });
|
|
427
|
-
}
|
|
428
|
-
if (address.kind === "extension") {
|
|
429
|
-
const postcondition = generatedPostcondition(step, address);
|
|
430
|
-
if (postcondition.kind !== "extension")
|
|
431
|
-
throw new Error(
|
|
432
|
-
`generated extension ${address.name} has a non-extension structural postcondition`
|
|
433
|
-
);
|
|
434
|
-
const row = (await executor.query(
|
|
435
|
-
`SELECT extension.extversion AS version FROM pg_catalog.pg_extension extension WHERE extension.extname = $1`,
|
|
436
|
-
[address.name]
|
|
437
|
-
)).rows[0];
|
|
438
|
-
if (!row)
|
|
439
|
-
throw new Error(
|
|
440
|
-
`generated extension ${address.name} version postcondition differs`
|
|
441
|
-
);
|
|
442
|
-
if (postcondition.version !== void 0 && String(row.version) !== postcondition.version)
|
|
443
|
-
throw new Error(
|
|
444
|
-
`generated extension ${address.name} version postcondition differs`
|
|
445
|
-
);
|
|
446
|
-
return generatedPayload({
|
|
447
|
-
kind: "extension",
|
|
448
|
-
version: String(row.version)
|
|
449
|
-
});
|
|
450
|
-
}
|
|
451
|
-
throw new Error(`generated ${address.kind} has no declarable read-back`);
|
|
452
|
-
}
|
|
453
|
-
function containedBy(root, candidate) {
|
|
454
|
-
for (let parent = candidate.parent; parent; parent = parent.parent) {
|
|
455
|
-
if (parent.engine === root.engine && parent.database === root.database && parent.schema === root.schema && parent.kind === root.kind && parent.name === root.name)
|
|
456
|
-
return true;
|
|
457
|
-
}
|
|
458
|
-
return false;
|
|
459
|
-
}
|
|
460
|
-
async function destructiveEvidence(input) {
|
|
461
|
-
const { executor, address } = input;
|
|
462
|
-
let ownership = "uncomputable";
|
|
463
|
-
let catalogueIdentity = "catalogue-unavailable";
|
|
464
|
-
let ledgerLineage = "unreadable";
|
|
465
|
-
try {
|
|
466
|
-
const chain = await readPgLedgerAddressChain(
|
|
467
|
-
executor,
|
|
468
|
-
home(address),
|
|
469
|
-
address
|
|
470
|
-
);
|
|
471
|
-
const projection = projectLedgerChain(chain);
|
|
472
|
-
ownership = projection.kind === "projected-ledger-chain" ? projection.stableState === "managed" ? "managed-by-me" : projection.stableState === "unknown" ? "unknown" : "blocked" : "uncomputable";
|
|
473
|
-
const live = await readPgCatalogueIdentity(executor, address);
|
|
474
|
-
const recorded = chain.terminalMember?.catalogueIdentity;
|
|
475
|
-
catalogueIdentity = !live ? "object-absent" : recorded === void 0 ? "differs" : isDeepStrictEqual(live.catalogueIdentity, recorded) ? "matches-recorded" : "differs";
|
|
476
|
-
const currency = await readPgLedgerScopeCurrency(executor, home(address));
|
|
477
|
-
ledgerLineage = currency.kind === "current" ? "matches-database" : "differs";
|
|
478
|
-
} catch {
|
|
479
|
-
}
|
|
480
|
-
let containment;
|
|
481
|
-
let containmentOutcome;
|
|
482
|
-
if (input.classification === "removal") {
|
|
483
|
-
containment = await readPgRemovalEffectsClosure({
|
|
484
|
-
executor,
|
|
485
|
-
root: address,
|
|
486
|
-
isManaged: (candidate) => managed(executor, candidate)
|
|
487
|
-
});
|
|
488
|
-
containmentOutcome = containment.kind;
|
|
489
|
-
}
|
|
490
|
-
return {
|
|
491
|
-
evidence: {
|
|
492
|
-
declaration: input.classification === "removal" ? input.selection?.kind === "replacement" ? "replacement-requested-by-plan" : "requires-removal" : "requires-lossy-change",
|
|
493
|
-
...input.selection?.kind === "replacement" ? { replacementAddress: address } : {},
|
|
494
|
-
ownership,
|
|
495
|
-
catalogueIdentity,
|
|
496
|
-
operatorAcceptance: acceptance(input.planDigest, input.approval),
|
|
497
|
-
...containmentOutcome === void 0 ? {} : { containment: containmentOutcome },
|
|
498
|
-
...containment?.kind === "reaches-unmanaged" ? { containmentUnmanaged: containment.unmanaged } : {},
|
|
499
|
-
...containment?.kind === "undecidable" ? { containmentReason: containment.reason } : {},
|
|
500
|
-
ledgerLineage
|
|
501
|
-
},
|
|
502
|
-
...containment === void 0 ? {} : { containment }
|
|
503
|
-
};
|
|
504
|
-
}
|
|
505
|
-
async function removalContainment(executor, address) {
|
|
506
|
-
return readPgRemovalEffectsClosure({
|
|
507
|
-
executor,
|
|
508
|
-
root: address,
|
|
509
|
-
isManaged: (candidate) => managed(executor, candidate)
|
|
510
|
-
});
|
|
511
|
-
}
|
|
512
|
-
async function executeGeneratorPlan(input) {
|
|
513
|
-
const validation = input.manifest ? { ok: true, manifest: input.manifest } : validateNormalizedManagedStepManifest(
|
|
514
|
-
input.plan?.steps ?? []
|
|
515
|
-
);
|
|
516
|
-
if (!validation.ok)
|
|
517
|
-
return { outcome: "execution-failed", detail: validation.detail };
|
|
518
|
-
const manifest = validation.manifest;
|
|
519
|
-
const approval = input.approval ?? {
|
|
520
|
-
approvals: (input.accepts ?? []).map((value) => ({ class: value }))
|
|
521
|
-
};
|
|
522
|
-
const completedStepKeys = [];
|
|
523
|
-
let destructiveRefusal;
|
|
524
|
-
let destructiveClosureReplanRequired = false;
|
|
525
|
-
const partial = (detail) => completedStepKeys.length === 0 ? { outcome: "execution-failed", detail } : {
|
|
526
|
-
outcome: "partially-applied",
|
|
527
|
-
detail,
|
|
528
|
-
completedStepKeys,
|
|
529
|
-
notStartedStepKeys: managedSteps(manifest).filter((step) => !completedStepKeys.includes(step.stepKey)).map((step) => step.stepKey)
|
|
530
|
-
};
|
|
531
|
-
try {
|
|
532
|
-
const database = await databaseId(input.pool);
|
|
533
|
-
const executionId = `dbsp.generator.execution.${randomUUID()}`;
|
|
534
|
-
await input.recordAttempt?.(executionId);
|
|
535
|
-
const steps = managedSteps(manifest);
|
|
536
|
-
for (const step of steps) {
|
|
537
|
-
if (step.lifecycle?.kind === "adoption-refused")
|
|
538
|
-
return {
|
|
539
|
-
outcome: "adoption-refused",
|
|
540
|
-
detail: `declared adoption for ${step.address?.name ?? step.stepKey} refuses live shape mismatch`
|
|
541
|
-
};
|
|
542
|
-
const lifecycle = step.lifecycle;
|
|
543
|
-
if (lifecycle?.kind !== "adoption") continue;
|
|
544
|
-
const address = step.address;
|
|
545
|
-
if (!address || !step.expectedDeclaration || !step.expectedCatalogueIdentity)
|
|
546
|
-
return {
|
|
547
|
-
outcome: "execution-failed",
|
|
548
|
-
detail: `adoption step ${step.stepKey} has incomplete normalized material`
|
|
549
|
-
};
|
|
550
|
-
const preflight = await preflightPgDeclaredAdoption({
|
|
551
|
-
executor: input.pool,
|
|
552
|
-
home: home(address),
|
|
553
|
-
address,
|
|
554
|
-
declaration: step.expectedDeclaration,
|
|
555
|
-
expectedCatalogueIdentity: step.expectedCatalogueIdentity,
|
|
556
|
-
shapeMatches: () => adoptionShapeMatches(input.pool, input.schema, lifecycle.shape)
|
|
557
|
-
});
|
|
558
|
-
if (preflight.outcome !== "ready" && preflight.outcome !== "no-op")
|
|
559
|
-
return preflight.outcome === "adoption-refused" ? { outcome: "adoption-refused", detail: preflight.detail } : { outcome: "execution-failed", detail: preflight.detail };
|
|
560
|
-
}
|
|
561
|
-
const replacementSelectors = steps.filter((step) => step.selection?.kind === "replacement").map((step) => step.selection?.selector).filter((selector) => selector !== void 0);
|
|
562
|
-
if (replacementSelectors.length > 0 && !input.replaces?.length)
|
|
563
|
-
return {
|
|
564
|
-
outcome: "destructive-authority-refused",
|
|
565
|
-
detail: "replacement requires a named --replace selector from the reviewed plan",
|
|
566
|
-
refusal: { withheldAuthority: "destructive declaration authority" }
|
|
567
|
-
};
|
|
568
|
-
for (const selector of input.replaces ?? [])
|
|
569
|
-
if (!replacementSelectors.some(
|
|
570
|
-
(reviewed) => matchesReviewedReplacementSelector(reviewed, selector)
|
|
571
|
-
))
|
|
572
|
-
return {
|
|
573
|
-
outcome: "destructive-authority-refused",
|
|
574
|
-
detail: `replacement ${selector} was not requested by the reviewed plan`,
|
|
575
|
-
refusal: { withheldAuthority: "destructive declaration authority" }
|
|
576
|
-
};
|
|
577
|
-
if (replacementSelectors.some(
|
|
578
|
-
(reviewed) => !input.replaces?.some(
|
|
579
|
-
(provided) => matchesReviewedReplacementSelector(reviewed, provided)
|
|
580
|
-
)
|
|
581
|
-
))
|
|
582
|
-
return {
|
|
583
|
-
outcome: "selection-incomplete",
|
|
584
|
-
detail: "replacement selection does not cover every reviewed replacement"
|
|
585
|
-
};
|
|
586
|
-
for (const step of steps) {
|
|
587
|
-
if (step.lifecycle?.kind === "adoption-refused") continue;
|
|
588
|
-
if (step.lifecycle?.kind === "adoption") {
|
|
589
|
-
const lifecycle = step.lifecycle;
|
|
590
|
-
const address2 = step.address;
|
|
591
|
-
if (!address2 || !step.expectedDeclaration || !step.expectedCatalogueIdentity)
|
|
592
|
-
return {
|
|
593
|
-
outcome: "execution-failed",
|
|
594
|
-
detail: `adoption step ${step.stepKey} has incomplete normalized material`
|
|
595
|
-
};
|
|
596
|
-
const adopted = await executePgDeclaredAdoption({
|
|
597
|
-
executor: input.pool,
|
|
598
|
-
run: input.run,
|
|
599
|
-
manifest,
|
|
600
|
-
recomputedPlanDigest: input.planDigest,
|
|
601
|
-
approval,
|
|
602
|
-
step,
|
|
603
|
-
home: home(address2),
|
|
604
|
-
address: address2,
|
|
605
|
-
declaration: step.expectedDeclaration,
|
|
606
|
-
expectedCatalogueIdentity: step.expectedCatalogueIdentity,
|
|
607
|
-
shapeMatches: () => adoptionShapeMatches(input.pool, input.schema, lifecycle.shape),
|
|
608
|
-
...input.observer === void 0 ? {} : { observer: input.observer }
|
|
609
|
-
});
|
|
610
|
-
if (adopted.outcome === "completed" || adopted.outcome === "no-op") {
|
|
611
|
-
completedStepKeys.push(step.stepKey);
|
|
612
|
-
continue;
|
|
613
|
-
}
|
|
614
|
-
return adopted.outcome === "adoption-refused" ? { outcome: "adoption-refused", detail: adopted.detail } : { outcome: "execution-failed", detail: adopted.detail };
|
|
615
|
-
}
|
|
616
|
-
if (step.lifecycle?.kind === "readdress") {
|
|
617
|
-
const result = await executePgPersistedTableReaddress({
|
|
618
|
-
executor: input.pool,
|
|
619
|
-
run: input.run,
|
|
620
|
-
manifest,
|
|
621
|
-
recomputedPlanDigest: input.planDigest,
|
|
622
|
-
approval,
|
|
623
|
-
step,
|
|
624
|
-
database,
|
|
625
|
-
targetSchema: input.schema,
|
|
626
|
-
...input.observer === void 0 ? {} : { observer: input.observer }
|
|
627
|
-
});
|
|
628
|
-
if (result.outcome === "completed" || result.outcome === "no-op") {
|
|
629
|
-
completedStepKeys.push(step.stepKey);
|
|
630
|
-
continue;
|
|
631
|
-
}
|
|
632
|
-
return result;
|
|
633
|
-
}
|
|
634
|
-
if (step.statementBundle.statements.length === 0) {
|
|
635
|
-
completedStepKeys.push(step.stepKey);
|
|
636
|
-
continue;
|
|
637
|
-
}
|
|
638
|
-
const address = step.address ?? step.closure?.root;
|
|
639
|
-
if (!address)
|
|
640
|
-
return {
|
|
641
|
-
outcome: "execution-failed",
|
|
642
|
-
detail: `managed step ${step.stepKey} has no address`
|
|
643
|
-
};
|
|
644
|
-
const plannedClaimKey = step.plannedClaimKeys[0];
|
|
645
|
-
if (!plannedClaimKey)
|
|
646
|
-
return {
|
|
647
|
-
outcome: "execution-failed",
|
|
648
|
-
detail: `managed step ${step.stepKey} has no planned claim key`
|
|
649
|
-
};
|
|
650
|
-
if (step.classification === "non-destructive" && step.requiresVacancy && await alreadyAppliedCreation(input.pool, address)) {
|
|
651
|
-
completedStepKeys.push(step.stepKey);
|
|
652
|
-
continue;
|
|
653
|
-
}
|
|
654
|
-
const claimKind = step.claimKind;
|
|
655
|
-
const rootClaimId = outcomeClaimId(executionId, plannedClaimKey, address);
|
|
656
|
-
const claim = {
|
|
657
|
-
claimId: rootClaimId,
|
|
658
|
-
claimSpecies: "sql-bearing",
|
|
659
|
-
executionId,
|
|
660
|
-
plannedClaimKey,
|
|
661
|
-
claimGroupId: rootClaimId,
|
|
662
|
-
rootClaimId,
|
|
663
|
-
address,
|
|
664
|
-
claimKind,
|
|
665
|
-
statementBundle: step.statementBundle,
|
|
666
|
-
requiresVacancy: step.requiresVacancy
|
|
667
|
-
};
|
|
668
|
-
const baseReservation = {
|
|
669
|
-
address,
|
|
670
|
-
claimKind,
|
|
671
|
-
executionId,
|
|
672
|
-
rootClaimId: claim.claimId,
|
|
673
|
-
homeLedger: home(address)
|
|
674
|
-
};
|
|
675
|
-
if (step.classification === "non-destructive") {
|
|
676
|
-
const result = await executePgAdmittedOperation(input.pool, {
|
|
677
|
-
run: input.run,
|
|
678
|
-
approval,
|
|
679
|
-
manifest,
|
|
680
|
-
recomputedPlanDigest: input.planDigest,
|
|
681
|
-
operation: {
|
|
682
|
-
kind: "single-outcome",
|
|
683
|
-
request: {
|
|
684
|
-
plan: claim,
|
|
685
|
-
reservations: [baseReservation],
|
|
686
|
-
resolution: {
|
|
687
|
-
eventId: outcomeClaimEventId(claim.claimId, "observed"),
|
|
688
|
-
eventKind: "observed"
|
|
689
|
-
},
|
|
690
|
-
// Transactional DDL is visible only on the admitted session until
|
|
691
|
-
// its terminal ledger fact commits with it.
|
|
692
|
-
readBack: async (session) => readGeneratedPostcondition(session, step, address),
|
|
693
|
-
recordCatalogueIdentity: true,
|
|
694
|
-
...input.observer === void 0 ? {} : { observer: input.observer },
|
|
695
|
-
vacancy: async (executor) => await readPgCatalogueIdentity(executor, address) ? {
|
|
696
|
-
kind: "occupied",
|
|
697
|
-
reason: `creation claim ${claim.claimId} refuses occupied live address ${address.name}`
|
|
698
|
-
} : { kind: "vacant" }
|
|
699
|
-
}
|
|
700
|
-
}
|
|
701
|
-
});
|
|
702
|
-
if (result.kind === "outcome-recovery-required")
|
|
703
|
-
return {
|
|
704
|
-
outcome: "recovery-required",
|
|
705
|
-
claimId: result.claimId,
|
|
706
|
-
detail: `claim ${result.claimId} remains open and requires recovery: ${result.reason}`
|
|
707
|
-
};
|
|
708
|
-
if ("reason" in result) return partial(result.reason);
|
|
709
|
-
completedStepKeys.push(step.stepKey);
|
|
710
|
-
continue;
|
|
711
|
-
}
|
|
712
|
-
const containment = step.classification === "removal" ? await removalContainment(input.pool, address) : void 0;
|
|
713
|
-
const closureMembers = /* @__PURE__ */ new Map();
|
|
714
|
-
for (const member of step.closure?.members ?? []) {
|
|
715
|
-
closureMembers.set(ledgerAddressKey(member.address), {
|
|
716
|
-
address: member.address,
|
|
717
|
-
plannedClaimKey: member.plannedClaimKey
|
|
718
|
-
});
|
|
719
|
-
}
|
|
720
|
-
if (containment?.kind === "all-contained-or-managed") {
|
|
721
|
-
for (const effect of containment.effects) {
|
|
722
|
-
if (effect.internalOwned) continue;
|
|
723
|
-
if (!containedBy(address, effect.address)) continue;
|
|
724
|
-
if (!await managed(input.pool, effect.address)) continue;
|
|
725
|
-
const key = ledgerAddressKey(effect.address);
|
|
726
|
-
if (closureMembers.has(key)) continue;
|
|
727
|
-
closureMembers.set(key, {
|
|
728
|
-
address: effect.address,
|
|
729
|
-
plannedClaimKey: `closure:${effect.address.kind}:${effect.address.name}`
|
|
730
|
-
});
|
|
731
|
-
}
|
|
732
|
-
}
|
|
733
|
-
const containedClaims = [];
|
|
734
|
-
for (const member of closureMembers.values()) {
|
|
735
|
-
const effect = member.address;
|
|
736
|
-
const childClaimId = outcomeClaimId(
|
|
737
|
-
executionId,
|
|
738
|
-
plannedClaimKey,
|
|
739
|
-
effect,
|
|
740
|
-
member.plannedClaimKey
|
|
741
|
-
);
|
|
742
|
-
const childReservation = {
|
|
743
|
-
...baseReservation,
|
|
744
|
-
address: effect,
|
|
745
|
-
rootClaimId: claim.claimId
|
|
746
|
-
};
|
|
747
|
-
containedClaims.push({
|
|
748
|
-
plan: {
|
|
749
|
-
...claim,
|
|
750
|
-
claimId: childClaimId,
|
|
751
|
-
claimSpecies: "cascade-covered",
|
|
752
|
-
address: effect,
|
|
753
|
-
plannedClaimKey: member.plannedClaimKey,
|
|
754
|
-
// Each member contributes only its terminal absence fact; SQL is
|
|
755
|
-
// exclusively carried by the destructive root manifest claim.
|
|
756
|
-
statementBundle: { statements: [] }
|
|
757
|
-
},
|
|
758
|
-
reservation: childReservation
|
|
759
|
-
});
|
|
760
|
-
}
|
|
761
|
-
const admitRequest = {
|
|
762
|
-
plan: claim,
|
|
763
|
-
reservations: [baseReservation],
|
|
764
|
-
members: containedClaims.map(({ plan, reservation }) => ({
|
|
765
|
-
plan,
|
|
766
|
-
reservations: [reservation]
|
|
767
|
-
})),
|
|
768
|
-
destructiveDecision: async (executor) => {
|
|
769
|
-
const lockedAuthority = await destructiveEvidence({
|
|
770
|
-
executor,
|
|
771
|
-
address,
|
|
772
|
-
classification: step.classification,
|
|
773
|
-
selection: step.selection,
|
|
774
|
-
planDigest: input.planDigest,
|
|
775
|
-
approval
|
|
776
|
-
});
|
|
777
|
-
if (step.classification === "removal" && (containment?.kind !== "all-contained-or-managed" || lockedAuthority.containment?.kind !== "all-contained-or-managed" || containment.closureDigest !== lockedAuthority.containment.closureDigest)) {
|
|
778
|
-
destructiveClosureReplanRequired = true;
|
|
779
|
-
const decision2 = decideDestructiveDecision(
|
|
780
|
-
{ kind: "removal", address },
|
|
781
|
-
{ ...lockedAuthority.evidence, containment: "undecidable" }
|
|
782
|
-
);
|
|
783
|
-
if (decision2.kind === "destructive-decision-refused")
|
|
784
|
-
destructiveRefusal = {
|
|
785
|
-
withheldAuthority: withheldDestructiveAuthority({
|
|
786
|
-
...lockedAuthority.evidence,
|
|
787
|
-
containment: "undecidable"
|
|
788
|
-
}) ?? "destructive containment authority"
|
|
789
|
-
};
|
|
790
|
-
return decision2;
|
|
791
|
-
}
|
|
792
|
-
const decision = decideDestructiveDecision(
|
|
793
|
-
{
|
|
794
|
-
kind: step.classification === "removal" ? "removal" : "data-destructive",
|
|
795
|
-
address,
|
|
796
|
-
...step.classification === "removal" && lockedAuthority.containment?.kind === "all-contained-or-managed" ? { closureDigest: lockedAuthority.containment.closureDigest } : {}
|
|
797
|
-
},
|
|
798
|
-
lockedAuthority.evidence
|
|
799
|
-
);
|
|
800
|
-
if (decision.kind === "destructive-decision-refused")
|
|
801
|
-
destructiveRefusal = {
|
|
802
|
-
withheldAuthority: withheldDestructiveAuthority(lockedAuthority.evidence) ?? "destructive authority"
|
|
803
|
-
};
|
|
804
|
-
return decision;
|
|
805
|
-
},
|
|
806
|
-
...input.observer === void 0 ? {} : { observer: input.observer }
|
|
807
|
-
};
|
|
808
|
-
const executed = await executePgAdmittedOperation(input.pool, {
|
|
809
|
-
run: input.run,
|
|
810
|
-
approval,
|
|
811
|
-
manifest,
|
|
812
|
-
recomputedPlanDigest: input.planDigest,
|
|
813
|
-
operation: {
|
|
814
|
-
kind: "destructive-outcome",
|
|
815
|
-
request: admitRequest,
|
|
816
|
-
readBackAndResolve: async (session) => {
|
|
817
|
-
const live = await readPgCatalogueIdentity(session, address);
|
|
818
|
-
const survivors = step.classification === "removal" && live ? [address] : [];
|
|
819
|
-
const childLives = await Promise.all(
|
|
820
|
-
containedClaims.map(async (child) => ({
|
|
821
|
-
child,
|
|
822
|
-
live: await readPgCatalogueIdentity(
|
|
823
|
-
session,
|
|
824
|
-
child.plan.address
|
|
825
|
-
)
|
|
826
|
-
}))
|
|
827
|
-
);
|
|
828
|
-
for (const { child, live: childLive } of childLives)
|
|
829
|
-
if (childLive) survivors.push(child.plan.address);
|
|
830
|
-
if (survivors.length > 0) {
|
|
831
|
-
const survivorNames = survivors.map(
|
|
832
|
-
(survivor) => `${survivor.kind} ${survivor.schema ?? "<database>"}.${survivor.name}`
|
|
833
|
-
);
|
|
834
|
-
const survivorObservation = {
|
|
835
|
-
value: { survivors: survivorNames },
|
|
836
|
-
digest: createHash("sha256").update(JSON.stringify(survivorNames)).digest("hex")
|
|
837
|
-
};
|
|
838
|
-
return {
|
|
839
|
-
rootClaimId: claim.claimId,
|
|
840
|
-
members: [
|
|
841
|
-
{
|
|
842
|
-
target: home(address),
|
|
843
|
-
member: {
|
|
844
|
-
eventId: outcomeClaimEventId(
|
|
845
|
-
claim.claimId,
|
|
846
|
-
"indeterminate"
|
|
847
|
-
),
|
|
848
|
-
executionId,
|
|
849
|
-
plannedClaimKey: claim.plannedClaimKey,
|
|
850
|
-
claimGroupId: claim.claimGroupId,
|
|
851
|
-
rootClaimId: claim.rootClaimId,
|
|
852
|
-
address,
|
|
853
|
-
eventKind: "indeterminate",
|
|
854
|
-
observed: survivorObservation
|
|
855
|
-
}
|
|
856
|
-
},
|
|
857
|
-
...containedClaims.map((child) => ({
|
|
858
|
-
target: home(child.plan.address),
|
|
859
|
-
member: {
|
|
860
|
-
eventId: outcomeClaimEventId(
|
|
861
|
-
child.plan.claimId,
|
|
862
|
-
"indeterminate"
|
|
863
|
-
),
|
|
864
|
-
executionId,
|
|
865
|
-
plannedClaimKey: child.plan.plannedClaimKey,
|
|
866
|
-
claimGroupId: claim.claimId,
|
|
867
|
-
rootClaimId: claim.claimId,
|
|
868
|
-
address: child.plan.address,
|
|
869
|
-
eventKind: "indeterminate",
|
|
870
|
-
observed: survivorObservation
|
|
871
|
-
}
|
|
872
|
-
}))
|
|
873
|
-
],
|
|
874
|
-
reservations: [
|
|
875
|
-
baseReservation,
|
|
876
|
-
...containedClaims.map(({ reservation }) => reservation)
|
|
877
|
-
]
|
|
878
|
-
};
|
|
879
|
-
}
|
|
880
|
-
const terminals = [
|
|
881
|
-
{
|
|
882
|
-
target: home(address),
|
|
883
|
-
member: {
|
|
884
|
-
eventId: outcomeClaimEventId(
|
|
885
|
-
claim.claimId,
|
|
886
|
-
step.classification === "removal" ? "absent" : "observed"
|
|
887
|
-
),
|
|
888
|
-
executionId,
|
|
889
|
-
plannedClaimKey: claim.plannedClaimKey,
|
|
890
|
-
claimGroupId: claim.claimGroupId,
|
|
891
|
-
rootClaimId: claim.rootClaimId,
|
|
892
|
-
address,
|
|
893
|
-
eventKind: step.classification === "removal" ? "absent" : "observed",
|
|
894
|
-
// The append protocol reads this address's terminal on its
|
|
895
|
-
// pinned transaction session after the post-DDL read-back.
|
|
896
|
-
...live?.catalogueIdentity ? { catalogueIdentity: live.catalogueIdentity } : {},
|
|
897
|
-
...step.classification === "removal" ? {} : { observed: observed(address) }
|
|
898
|
-
}
|
|
899
|
-
}
|
|
900
|
-
];
|
|
901
|
-
for (const child of containedClaims) {
|
|
902
|
-
terminals.push({
|
|
903
|
-
target: home(child.plan.address),
|
|
904
|
-
member: {
|
|
905
|
-
eventId: outcomeClaimEventId(child.plan.claimId, "absent"),
|
|
906
|
-
executionId,
|
|
907
|
-
plannedClaimKey: child.plan.plannedClaimKey,
|
|
908
|
-
claimGroupId: claim.claimId,
|
|
909
|
-
rootClaimId: claim.claimId,
|
|
910
|
-
address: child.plan.address,
|
|
911
|
-
eventKind: "absent",
|
|
912
|
-
predecessor: child.plan.claimId
|
|
913
|
-
}
|
|
914
|
-
});
|
|
915
|
-
}
|
|
916
|
-
return {
|
|
917
|
-
rootClaimId: claim.claimId,
|
|
918
|
-
members: terminals,
|
|
919
|
-
reservations: [
|
|
920
|
-
baseReservation,
|
|
921
|
-
...containedClaims.map(({ reservation }) => reservation)
|
|
922
|
-
]
|
|
923
|
-
};
|
|
924
|
-
}
|
|
925
|
-
}
|
|
926
|
-
});
|
|
927
|
-
if (executed.kind === "outcome-recovery-required")
|
|
928
|
-
return {
|
|
929
|
-
outcome: "recovery-required",
|
|
930
|
-
claimId: executed.claimId,
|
|
931
|
-
detail: `claim ${executed.claimId} remains open and requires recovery: ${executed.reason}`
|
|
932
|
-
};
|
|
933
|
-
if (executed.kind === "outcome-transport-ambiguous")
|
|
934
|
-
return {
|
|
935
|
-
outcome: "execution-failed",
|
|
936
|
-
detail: executed.reason
|
|
937
|
-
};
|
|
938
|
-
if (executed.kind === "outcome-protocol-pending")
|
|
939
|
-
return partial(
|
|
940
|
-
`destructive claim ${claim.claimId} remains pending after executing: ${executed.reason}`
|
|
941
|
-
);
|
|
942
|
-
if (executed.kind !== "executed-destructive-outcome")
|
|
943
|
-
return {
|
|
944
|
-
outcome: "destructive-authority-refused",
|
|
945
|
-
detail: destructiveClosureReplanRequired ? `${executed.reason}; destructive closure changed under lock; replan required` : executed.reason,
|
|
946
|
-
...destructiveRefusal === void 0 && withheldDestructiveAuthorityFromReason(executed.reason) === void 0 ? {} : {
|
|
947
|
-
refusal: destructiveRefusal ?? {
|
|
948
|
-
withheldAuthority: withheldDestructiveAuthorityFromReason(executed.reason) ?? "destructive authority"
|
|
949
|
-
}
|
|
950
|
-
}
|
|
951
|
-
};
|
|
952
|
-
completedStepKeys.push(step.stepKey);
|
|
953
|
-
}
|
|
954
|
-
return { outcome: "completed" };
|
|
955
|
-
} catch (error) {
|
|
956
|
-
return partial(error instanceof Error ? error.message : String(error));
|
|
957
|
-
}
|
|
958
|
-
}
|
|
959
|
-
|
|
960
65
|
// src/commands/generator-plan.ts
|
|
961
|
-
import {
|
|
66
|
+
import { randomUUID } from "crypto";
|
|
962
67
|
import {
|
|
963
68
|
assertDeclarableChangeKind,
|
|
964
69
|
classifyGeneratedMutation,
|
|
965
70
|
comparePgsqlDatabaseSchema,
|
|
966
|
-
|
|
71
|
+
createPgPhysicalModel,
|
|
72
|
+
createPgsqlAdapter,
|
|
967
73
|
createPgsqlGeneratedManagedStep,
|
|
968
74
|
createPgTransitionLessor,
|
|
969
75
|
createPgTransitionRunPersister,
|
|
970
76
|
generatedPostconditionForChange,
|
|
971
77
|
generateMigrationSQL,
|
|
972
|
-
|
|
78
|
+
ReferencedKeyRemovalError,
|
|
79
|
+
readPgCatalogueIdentity,
|
|
973
80
|
renderPgTableReaddressStatements
|
|
974
81
|
} from "@dbsp/adapter-pgsql";
|
|
82
|
+
import {
|
|
83
|
+
collectReferencedKeyRemovalConflicts,
|
|
84
|
+
comparePgsqlDeclaredAdoptionSchema,
|
|
85
|
+
createPgsqlDeclaredAdoptionStep,
|
|
86
|
+
generateMigrationSQL as generateMigrationSQLForPhysicalDiff,
|
|
87
|
+
modelForDeclaredAdoption,
|
|
88
|
+
pgsqlDeclaredAdoptionDeclaration
|
|
89
|
+
} from "@dbsp/adapter-pgsql/internal";
|
|
975
90
|
import {
|
|
976
91
|
acquireTransitionLease,
|
|
92
|
+
canonicalJsonDigest,
|
|
977
93
|
transitionPlanDigest,
|
|
978
|
-
validateNormalizedManagedStepManifest
|
|
94
|
+
validateNormalizedManagedStepManifest
|
|
979
95
|
} from "@dbsp/core";
|
|
980
|
-
function
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
96
|
+
function decodeGeneratorPlanMaterial(material) {
|
|
97
|
+
if (material === null || typeof material !== "object" || Array.isArray(material) || !("kind" in material) || material.kind !== "schema-differ-generator")
|
|
98
|
+
throw new Error("persisted generator material has an invalid kind");
|
|
99
|
+
const dbCasing = "dbCasing" in material ? material.dbCasing : void 0;
|
|
100
|
+
if (dbCasing !== void 0 && dbCasing !== "snake_case" && dbCasing !== "camelCase")
|
|
101
|
+
throw new Error("persisted generator material has an invalid dbCasing");
|
|
102
|
+
return {
|
|
103
|
+
...material,
|
|
104
|
+
dbCasing: dbCasing ?? "preserve"
|
|
105
|
+
};
|
|
988
106
|
}
|
|
989
107
|
function replacementStatements(table, schema) {
|
|
990
108
|
const quote = (value) => `"${value.replaceAll('"', '""')}"`;
|
|
@@ -1008,7 +126,7 @@ function replacementStatements(table, schema) {
|
|
|
1008
126
|
};
|
|
1009
127
|
return {
|
|
1010
128
|
retireStatements: [`DROP TABLE ${quote(schema)}.${quote(table.name)}`],
|
|
1011
|
-
createStatements:
|
|
129
|
+
createStatements: generateMigrationSQLForPhysicalDiff(createDiff, {
|
|
1012
130
|
includeDestructive: true,
|
|
1013
131
|
schemaName: schema
|
|
1014
132
|
})
|
|
@@ -1127,6 +245,65 @@ function changeTargetsDeclaredTable(change, table) {
|
|
|
1127
245
|
"alter_sequence"
|
|
1128
246
|
])).has(change.kind);
|
|
1129
247
|
}
|
|
248
|
+
function getGeneratorPhase(kind) {
|
|
249
|
+
switch (kind) {
|
|
250
|
+
case "drop_foreign_key":
|
|
251
|
+
case "drop_check_constraint":
|
|
252
|
+
return 0;
|
|
253
|
+
case "drop_index":
|
|
254
|
+
return 1;
|
|
255
|
+
case "drop_column":
|
|
256
|
+
return 2;
|
|
257
|
+
case "drop_primary_key":
|
|
258
|
+
return 3;
|
|
259
|
+
case "drop_table":
|
|
260
|
+
case "drop_enum":
|
|
261
|
+
case "drop_extension":
|
|
262
|
+
case "drop_sequence":
|
|
263
|
+
return 4;
|
|
264
|
+
case "create_enum":
|
|
265
|
+
case "create_extension":
|
|
266
|
+
case "create_sequence":
|
|
267
|
+
return 5;
|
|
268
|
+
case "create_table":
|
|
269
|
+
case "readdress_table":
|
|
270
|
+
return 6;
|
|
271
|
+
case "add_column":
|
|
272
|
+
return 7;
|
|
273
|
+
case "alter_sequence":
|
|
274
|
+
case "alter_column_type":
|
|
275
|
+
case "alter_column_nullable":
|
|
276
|
+
case "alter_column_default":
|
|
277
|
+
case "alter_column_collation":
|
|
278
|
+
case "alter_column_identity":
|
|
279
|
+
case "alter_column_auto_increment":
|
|
280
|
+
return 8;
|
|
281
|
+
case "alter_column_unique":
|
|
282
|
+
case "add_primary_key":
|
|
283
|
+
return 9;
|
|
284
|
+
case "create_index":
|
|
285
|
+
return 10;
|
|
286
|
+
case "add_foreign_key":
|
|
287
|
+
return 11;
|
|
288
|
+
case "alter_foreign_key":
|
|
289
|
+
return 12;
|
|
290
|
+
case "add_check_constraint":
|
|
291
|
+
return 13;
|
|
292
|
+
case "alter_enum_add_value":
|
|
293
|
+
return 14;
|
|
294
|
+
case "add_comment":
|
|
295
|
+
case "drop_comment":
|
|
296
|
+
return 15;
|
|
297
|
+
case "validate_constraint":
|
|
298
|
+
return 16;
|
|
299
|
+
case "enable_rls":
|
|
300
|
+
case "disable_rls":
|
|
301
|
+
return 17;
|
|
302
|
+
case "create_policy":
|
|
303
|
+
case "drop_policy":
|
|
304
|
+
return 18;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
1130
307
|
function requiredAdoptionIdentity(identities, table) {
|
|
1131
308
|
const identity = identities.get(table);
|
|
1132
309
|
if (!identity)
|
|
@@ -1134,7 +311,7 @@ function requiredAdoptionIdentity(identities, table) {
|
|
|
1134
311
|
return identity;
|
|
1135
312
|
}
|
|
1136
313
|
function asDurableGeneratorPlan(material, steps) {
|
|
1137
|
-
const validation =
|
|
314
|
+
const validation = validateNormalizedManagedStepManifest(steps);
|
|
1138
315
|
if (!validation.ok)
|
|
1139
316
|
throw new Error(
|
|
1140
317
|
`generator planning refuses invalid managed-step manifest: ${validation.detail}`
|
|
@@ -1169,7 +346,7 @@ function render(plan, planDigest) {
|
|
|
1169
346
|
]
|
|
1170
347
|
].join("\n");
|
|
1171
348
|
}
|
|
1172
|
-
async function
|
|
349
|
+
async function databaseId(pool) {
|
|
1173
350
|
const result = await pool.query("SELECT current_database() AS database_id");
|
|
1174
351
|
const value = result.rows[0]?.database_id;
|
|
1175
352
|
if (typeof value !== "string" || value.length === 0)
|
|
@@ -1180,19 +357,39 @@ async function databaseId2(pool) {
|
|
|
1180
357
|
}
|
|
1181
358
|
async function runGeneratorPlan(input) {
|
|
1182
359
|
const loaded = await loadSchema(input.schemaFile);
|
|
360
|
+
for (const sequence of loaded.model.sequences?.values() ?? []) {
|
|
361
|
+
if (sequence.adopt === true)
|
|
362
|
+
throw new Error(
|
|
363
|
+
`generator planning refuses sequence adoption for ${sequence.name}: sequence adoption is available through convergePg / dbsp migrate`
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
const schema = input.schema ?? "public";
|
|
367
|
+
const dbCasing = loaded.dbCasing ?? "preserve";
|
|
368
|
+
const physical = createPgPhysicalModel({
|
|
369
|
+
mode: "logical",
|
|
370
|
+
model: loaded.model,
|
|
371
|
+
schema,
|
|
372
|
+
...loaded.dbCasing === void 0 ? {} : { dbCasing: loaded.dbCasing }
|
|
373
|
+
});
|
|
374
|
+
const physicalTable = (name) => physical.inventory.get({ kind: "table", schema, name });
|
|
375
|
+
for (const table of loaded.model.tables.values()) {
|
|
376
|
+
if (table.replace !== true) continue;
|
|
377
|
+
const physicalName = physicalTable(table.name);
|
|
378
|
+
if (physicalName !== table.name)
|
|
379
|
+
throw new Error(
|
|
380
|
+
`generator planning refuses replacement ${table.name}: dbCasing addresses physical table ${physicalName}; replacement requires preserve casing`
|
|
381
|
+
);
|
|
382
|
+
}
|
|
1183
383
|
const { pool } = await createDbConnection(input.db);
|
|
1184
384
|
try {
|
|
1185
|
-
const schema = input.schema ?? "public";
|
|
1186
385
|
const diff = await comparePgsqlDatabaseSchema(
|
|
1187
|
-
|
|
1188
|
-
|
|
386
|
+
createPgsqlAdapter(pool),
|
|
387
|
+
physical,
|
|
1189
388
|
{
|
|
1190
|
-
schema,
|
|
1191
389
|
// `apply --schema` owns one schema. Extensions are database-scoped,
|
|
1192
390
|
// so an extension not declared by this schema must not become a
|
|
1193
391
|
// schema-plan removal merely because it is installed in the database.
|
|
1194
|
-
ignoreUnmanagedExtensions: true
|
|
1195
|
-
...loaded.dbCasing ? { dbCasing: loaded.dbCasing } : {}
|
|
392
|
+
ignoreUnmanagedExtensions: true
|
|
1196
393
|
}
|
|
1197
394
|
);
|
|
1198
395
|
const declaredLifecycleWork = [...loaded.model.tables.values()].some(
|
|
@@ -1221,39 +418,57 @@ async function runGeneratorPlan(input) {
|
|
|
1221
418
|
planDigest: null
|
|
1222
419
|
};
|
|
1223
420
|
}
|
|
1224
|
-
const
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
421
|
+
const adoptionPhysicalNames = /* @__PURE__ */ new Map();
|
|
422
|
+
const adoptionMismatches = /* @__PURE__ */ new Set();
|
|
423
|
+
for (const table of loaded.model.tables.values()) {
|
|
424
|
+
if (table.adopt !== true) continue;
|
|
425
|
+
const physicalName = physicalTable(table.name);
|
|
426
|
+
adoptionPhysicalNames.set(table.name, physicalName);
|
|
427
|
+
const compared = await comparePgsqlDeclaredAdoptionSchema({
|
|
428
|
+
executor: pool,
|
|
429
|
+
model: modelForDeclaredAdoption(table),
|
|
430
|
+
schema,
|
|
431
|
+
dbCasing
|
|
432
|
+
});
|
|
433
|
+
if (compared.changes.length > 0) adoptionMismatches.add(physicalName);
|
|
434
|
+
}
|
|
435
|
+
const database = await databaseId(pool);
|
|
1232
436
|
const adoptionIdentities = /* @__PURE__ */ new Map();
|
|
1233
437
|
for (const table of loaded.model.tables.values()) {
|
|
1234
|
-
|
|
1235
|
-
|
|
438
|
+
const physicalName = adoptionPhysicalNames.get(table.name);
|
|
439
|
+
if (table.adopt !== true || physicalName === void 0 || adoptionMismatches.has(physicalName))
|
|
440
|
+
continue;
|
|
441
|
+
const live = await readPgCatalogueIdentity(pool, {
|
|
1236
442
|
engine: "postgresql",
|
|
1237
443
|
database,
|
|
1238
444
|
schema: input.schema ?? "public",
|
|
1239
445
|
kind: "table",
|
|
1240
|
-
name:
|
|
446
|
+
name: physicalName
|
|
1241
447
|
});
|
|
1242
448
|
if (!live?.catalogueIdentity)
|
|
1243
449
|
throw new Error(
|
|
1244
|
-
`declared adoption for ${
|
|
450
|
+
`declared adoption for ${physicalName} refuses absent live identity`
|
|
1245
451
|
);
|
|
1246
|
-
adoptionIdentities.set(
|
|
452
|
+
adoptionIdentities.set(physicalName, live.catalogueIdentity);
|
|
1247
453
|
}
|
|
1248
454
|
const executableDiff = {
|
|
1249
455
|
...diff,
|
|
1250
456
|
changes: diff.changes.filter(
|
|
1251
457
|
(change) => ![...loaded.model.tables.values()].some(
|
|
1252
|
-
(table) => (table.adopt === true || table.replace === true) && changeTargetsDeclaredTable(
|
|
458
|
+
(table) => (table.adopt === true || table.replace === true) && changeTargetsDeclaredTable(
|
|
459
|
+
change,
|
|
460
|
+
table.adopt === true ? adoptionPhysicalNames.get(table.name) ?? table.name : table.name
|
|
461
|
+
)
|
|
1253
462
|
)
|
|
1254
463
|
)
|
|
1255
464
|
};
|
|
1256
|
-
const
|
|
465
|
+
const phaseOrderedChanges = [...executableDiff.changes].sort(
|
|
466
|
+
(left, right) => getGeneratorPhase(left.kind) - getGeneratorPhase(right.kind)
|
|
467
|
+
);
|
|
468
|
+
const referencedKeyConflicts = collectReferencedKeyRemovalConflicts(phaseOrderedChanges);
|
|
469
|
+
if (referencedKeyConflicts.length > 0)
|
|
470
|
+
throw new ReferencedKeyRemovalError(referencedKeyConflicts);
|
|
471
|
+
const ordinaryChanges = phaseOrderedChanges.map((change) => ({
|
|
1257
472
|
kind: change.kind,
|
|
1258
473
|
table: change.table,
|
|
1259
474
|
...change.column ? { column: change.column } : {},
|
|
@@ -1264,45 +479,54 @@ async function runGeneratorPlan(input) {
|
|
|
1264
479
|
schema,
|
|
1265
480
|
change.meta.readdress
|
|
1266
481
|
) : generateMigrationSQL(
|
|
1267
|
-
{ ...executableDiff, changes: [change] },
|
|
1268
|
-
{ includeDestructive: true
|
|
482
|
+
{ ...executableDiff, physical: diff.physical, changes: [change] },
|
|
483
|
+
{ includeDestructive: true }
|
|
1269
484
|
),
|
|
1270
485
|
...change.kind === "readdress_table" && change.meta?.readdress ? { readdress: change.meta.readdress } : {}
|
|
1271
486
|
}));
|
|
1272
487
|
const material = {
|
|
1273
488
|
kind: "schema-differ-generator",
|
|
1274
489
|
planningSchema: schema,
|
|
490
|
+
...dbCasing === "preserve" ? {} : { dbCasing },
|
|
1275
491
|
changes: [
|
|
1276
492
|
...ordinaryChanges,
|
|
1277
|
-
...[...loaded.model.tables.values()].
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
493
|
+
...[...loaded.model.tables.values()].flatMap((table) => {
|
|
494
|
+
const physicalName = adoptionPhysicalNames.get(table.name);
|
|
495
|
+
if (physicalName === void 0 || !adoptionMismatches.has(physicalName))
|
|
496
|
+
return [];
|
|
497
|
+
return [
|
|
498
|
+
{
|
|
499
|
+
kind: "adoption_refused",
|
|
500
|
+
table: physicalName,
|
|
501
|
+
classification: "non-destructive",
|
|
502
|
+
details: `Refuse adoption of table "${physicalName}": live shape does not match declaration`,
|
|
503
|
+
statements: []
|
|
504
|
+
}
|
|
505
|
+
];
|
|
506
|
+
}),
|
|
507
|
+
// Adoption is admitted only by its declaration-scoped comparison.
|
|
508
|
+
...[...loaded.model.tables.values()].flatMap((table) => {
|
|
509
|
+
const physicalName = adoptionPhysicalNames.get(table.name);
|
|
510
|
+
if (table.adopt !== true || physicalName === void 0 || adoptionMismatches.has(physicalName))
|
|
511
|
+
return [];
|
|
512
|
+
return [
|
|
513
|
+
{
|
|
514
|
+
kind: "adopt_table",
|
|
515
|
+
table: physicalName,
|
|
516
|
+
classification: "non-destructive",
|
|
517
|
+
details: `Adopt existing table "${physicalName}" after live shape match`,
|
|
518
|
+
statements: [],
|
|
519
|
+
adoption: {
|
|
520
|
+
declaration: pgsqlDeclaredAdoptionDeclaration(table),
|
|
521
|
+
shape: table,
|
|
522
|
+
catalogueIdentity: requiredAdoptionIdentity(
|
|
523
|
+
adoptionIdentities,
|
|
524
|
+
physicalName
|
|
525
|
+
)
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
];
|
|
529
|
+
}),
|
|
1306
530
|
...[...loaded.model.tables.values()].filter((table) => table.replace === true).map((table) => ({
|
|
1307
531
|
kind: "replace_table",
|
|
1308
532
|
table: table.name,
|
|
@@ -1314,7 +538,7 @@ async function runGeneratorPlan(input) {
|
|
|
1314
538
|
]
|
|
1315
539
|
};
|
|
1316
540
|
const ordinaryStepSources = [];
|
|
1317
|
-
for (const [changeIndex, change] of
|
|
541
|
+
for (const [changeIndex, change] of phaseOrderedChanges.entries()) {
|
|
1318
542
|
const generated = ordinaryChanges[changeIndex];
|
|
1319
543
|
if (!generated)
|
|
1320
544
|
throw new Error(
|
|
@@ -1431,18 +655,27 @@ async function runGeneratorPlan(input) {
|
|
|
1431
655
|
continue;
|
|
1432
656
|
}
|
|
1433
657
|
if (change.kind === "adopt_table" && change.adoption) {
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
}
|
|
1445
|
-
|
|
658
|
+
const adoptionStep = createPgsqlDeclaredAdoptionStep({
|
|
659
|
+
address: {
|
|
660
|
+
scope: "schema",
|
|
661
|
+
engine: "postgresql",
|
|
662
|
+
database,
|
|
663
|
+
schema,
|
|
664
|
+
kind: "table",
|
|
665
|
+
name: change.table
|
|
666
|
+
},
|
|
667
|
+
table: change.adoption.shape,
|
|
668
|
+
stepKey: `generator:${base.order}:adoption`,
|
|
669
|
+
order: base.order,
|
|
670
|
+
catalogueIdentity: change.adoption.catalogueIdentity
|
|
671
|
+
});
|
|
672
|
+
lifecycleSteps.push({
|
|
673
|
+
...adoptionStep,
|
|
674
|
+
selection: {
|
|
675
|
+
kind: "adoption",
|
|
676
|
+
selector: `table:${change.table}`
|
|
677
|
+
}
|
|
678
|
+
});
|
|
1446
679
|
continue;
|
|
1447
680
|
}
|
|
1448
681
|
lifecycleSteps.push(
|
|
@@ -1479,10 +712,13 @@ async function runGeneratorPlan(input) {
|
|
|
1479
712
|
plan
|
|
1480
713
|
);
|
|
1481
714
|
const run = {
|
|
1482
|
-
runId: `dbsp-generator-${
|
|
715
|
+
runId: `dbsp-generator-${randomUUID()}`,
|
|
1483
716
|
planDigest,
|
|
1484
|
-
targetContextDigest:
|
|
1485
|
-
|
|
717
|
+
targetContextDigest: canonicalJsonDigest({
|
|
718
|
+
database: await databaseId(pool),
|
|
719
|
+
schema
|
|
720
|
+
}),
|
|
721
|
+
databaseId: await databaseId(pool),
|
|
1486
722
|
coreVersion: "schema-differ-generator-v1",
|
|
1487
723
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1488
724
|
replayability: material.changes.some(
|
|
@@ -1521,20 +757,25 @@ async function runGeneratorPlan(input) {
|
|
|
1521
757
|
import {
|
|
1522
758
|
acquirePgTransitionClient,
|
|
1523
759
|
createPgExecutionContract,
|
|
1524
|
-
|
|
760
|
+
createPgPhysicalModel as createPgPhysicalModel2,
|
|
761
|
+
createPgsqlAdapter as createPgsqlAdapter2,
|
|
1525
762
|
createPgTransitionLessor as createPgTransitionLessor2,
|
|
1526
763
|
createPgTransitionPack,
|
|
1527
764
|
createPgTransitionRunPersister as createPgTransitionRunPersister2,
|
|
1528
765
|
ensureTransitionJournal,
|
|
1529
766
|
escapeDiagnosticText,
|
|
1530
|
-
getNamingPluginForDbCasing,
|
|
1531
767
|
PgExecutionContractDerivationError,
|
|
1532
768
|
pgTargetIdentityMismatch,
|
|
1533
769
|
readPgExecutionTargetFromClient,
|
|
1534
770
|
readPgObservationContextFromLessor
|
|
1535
771
|
} from "@dbsp/adapter-pgsql";
|
|
772
|
+
import {
|
|
773
|
+
PgTransitionRunPersistenceIndeterminateError,
|
|
774
|
+
planPgTransitionRun
|
|
775
|
+
} from "@dbsp/adapter-pgsql/internal";
|
|
1536
776
|
import {
|
|
1537
777
|
acquireTransitionLease as acquireTransitionLease2,
|
|
778
|
+
assertCanonicalizableJson,
|
|
1538
779
|
bindDeclarationSet,
|
|
1539
780
|
bindExecutionContract,
|
|
1540
781
|
createComparator,
|
|
@@ -1545,6 +786,7 @@ import {
|
|
|
1545
786
|
validateDeclarationModel
|
|
1546
787
|
} from "@dbsp/core";
|
|
1547
788
|
import { Command } from "commander";
|
|
789
|
+
var planPhysicalPgTransitionRun = planPgTransitionRun;
|
|
1548
790
|
var PlanPersistenceIndeterminateError = class extends Error {
|
|
1549
791
|
runId;
|
|
1550
792
|
result;
|
|
@@ -1594,6 +836,15 @@ var PlanOperationAndCleanupError = class extends AggregateError {
|
|
|
1594
836
|
cleanupError;
|
|
1595
837
|
};
|
|
1596
838
|
var noCleanupFailure = { kind: "not-failed" };
|
|
839
|
+
function validateSourceDeclarationValues(model) {
|
|
840
|
+
for (const [tableKey, table] of model.tables)
|
|
841
|
+
for (const [index, column] of table.columns.entries())
|
|
842
|
+
if (column.default !== void 0)
|
|
843
|
+
assertCanonicalizableJson(
|
|
844
|
+
column.default,
|
|
845
|
+
`schema.tables[${JSON.stringify(tableKey)}].columns[${index}].default`
|
|
846
|
+
);
|
|
847
|
+
}
|
|
1597
848
|
function equivalenceContext(context) {
|
|
1598
849
|
return {
|
|
1599
850
|
engine: context.engine,
|
|
@@ -1674,20 +925,20 @@ var defaultPlanDeps = {
|
|
|
1674
925
|
async loadCurrent(pool, schema, expectedTargetIdentity) {
|
|
1675
926
|
const lease = await acquirePgTransitionClient(pool);
|
|
1676
927
|
try {
|
|
1677
|
-
const
|
|
928
|
+
const observed = await readPgExecutionTargetFromClient(
|
|
1678
929
|
lease.client,
|
|
1679
930
|
[schema ?? "public"]
|
|
1680
931
|
);
|
|
1681
932
|
const mismatch = pgTargetIdentityMismatch(
|
|
1682
933
|
expectedTargetIdentity,
|
|
1683
|
-
|
|
934
|
+
observed.identity
|
|
1684
935
|
);
|
|
1685
936
|
if (mismatch) {
|
|
1686
937
|
throw new Error(
|
|
1687
938
|
`PostgreSQL introspection target identity does not match the captured target: ${mismatch}`
|
|
1688
939
|
);
|
|
1689
940
|
}
|
|
1690
|
-
return await
|
|
941
|
+
return await createPgsqlAdapter2(lease.client, {
|
|
1691
942
|
borrowedClient: true
|
|
1692
943
|
}).introspect(schema === void 0 ? {} : { schema });
|
|
1693
944
|
} finally {
|
|
@@ -1702,18 +953,14 @@ var defaultPlanDeps = {
|
|
|
1702
953
|
expectedTargetIdentity
|
|
1703
954
|
);
|
|
1704
955
|
},
|
|
1705
|
-
createPlanner(
|
|
1706
|
-
const
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
})
|
|
1710
|
-
]);
|
|
1711
|
-
const comparator = createComparator(registry2);
|
|
1712
|
-
const prover = createProver(registry2);
|
|
956
|
+
createPlanner() {
|
|
957
|
+
const registry = createPackRegistry([createPgTransitionPack()]);
|
|
958
|
+
const comparator = createComparator(registry);
|
|
959
|
+
const prover = createProver(registry);
|
|
1713
960
|
return {
|
|
1714
961
|
compare: (desired, current, context) => comparator.compare(desired, current, equivalenceContext(context)),
|
|
1715
962
|
prove: (compare, pool, context) => prover.prove(compare, createPgTransitionLessor2(pool), context),
|
|
1716
|
-
render: (plan, context) => renderProvenPlanSql(
|
|
963
|
+
render: (plan, context) => renderProvenPlanSql(registry, plan, context)
|
|
1717
964
|
};
|
|
1718
965
|
},
|
|
1719
966
|
async persist(pool, run, plan) {
|
|
@@ -1748,9 +995,9 @@ function executionContractBlockedAssessment(error) {
|
|
|
1748
995
|
]
|
|
1749
996
|
};
|
|
1750
997
|
}
|
|
1751
|
-
function renderProvenPlanSql(
|
|
998
|
+
function renderProvenPlanSql(registry, plan, context) {
|
|
1752
999
|
const statements = plan.steps.map((step) => {
|
|
1753
|
-
const resolution =
|
|
1000
|
+
const resolution = registry.resolveOperation(step.operation);
|
|
1754
1001
|
if (!resolution.ok || !resolution.semantics.renderPlanSql) {
|
|
1755
1002
|
throw new Error(
|
|
1756
1003
|
`no SQL renderer is registered for proven operation ${step.operation.operationKind.name}`
|
|
@@ -1770,126 +1017,170 @@ async function runPlan(options, overrides) {
|
|
|
1770
1017
|
throw new Error(`unsupported plan format ${format}; expected sql or json`);
|
|
1771
1018
|
}
|
|
1772
1019
|
const loaded = await deps.loadSchema(options.schemaFile);
|
|
1773
|
-
|
|
1020
|
+
validateSourceDeclarationValues(loaded.model);
|
|
1021
|
+
const physical = createPgPhysicalModel2({
|
|
1022
|
+
mode: "logical",
|
|
1023
|
+
model: loaded.model,
|
|
1024
|
+
schema: options.schema ?? "public",
|
|
1025
|
+
...loaded.dbCasing === void 0 ? {} : { dbCasing: loaded.dbCasing }
|
|
1026
|
+
});
|
|
1027
|
+
validateDeclarationModel(physical.model);
|
|
1774
1028
|
const connection = await deps.createDbConnection(options.db);
|
|
1775
1029
|
const { pool } = connection;
|
|
1776
1030
|
let result;
|
|
1031
|
+
let renderedSql;
|
|
1777
1032
|
let operationError;
|
|
1778
1033
|
let failed = false;
|
|
1779
1034
|
try {
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
const prove = await planner.prove(compare, pool, context);
|
|
1798
|
-
if (prove.kind === "proven") {
|
|
1799
|
-
let executionContract;
|
|
1800
|
-
try {
|
|
1801
|
-
executionContract = await deps.buildExecutionContract(
|
|
1802
|
-
pool,
|
|
1803
|
-
options.schema,
|
|
1804
|
-
prove.plan,
|
|
1805
|
-
targetIdentity
|
|
1806
|
-
);
|
|
1807
|
-
} catch (error) {
|
|
1808
|
-
if (!(error instanceof PgExecutionContractDerivationError)) throw error;
|
|
1035
|
+
if (overrides === void 0) {
|
|
1036
|
+
const lifecycle = await planPhysicalPgTransitionRun(
|
|
1037
|
+
physical,
|
|
1038
|
+
async (client, schema) => createPgsqlAdapter2(client, {
|
|
1039
|
+
borrowedClient: true
|
|
1040
|
+
}).introspect(schema === void 0 ? {} : { schema }),
|
|
1041
|
+
pool,
|
|
1042
|
+
{
|
|
1043
|
+
persist: options.dryRun !== true,
|
|
1044
|
+
beforePersist: async (plan, proofContext) => {
|
|
1045
|
+
renderedSql = deps.createPlanner().render(plan, proofContext);
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
);
|
|
1049
|
+
if (lifecycle.kind === "proven") {
|
|
1050
|
+
if (renderedSql === void 0)
|
|
1051
|
+
throw new Error("internal error: durable plan SQL was not rendered");
|
|
1809
1052
|
result = {
|
|
1810
|
-
compareKind: compare.kind,
|
|
1811
|
-
proveKind: "
|
|
1812
|
-
assessment:
|
|
1053
|
+
compareKind: lifecycle.compare.kind,
|
|
1054
|
+
proveKind: "proven",
|
|
1055
|
+
assessment: lifecycle.assessment,
|
|
1056
|
+
persisted: options.dryRun !== true,
|
|
1057
|
+
runId: options.dryRun === true ? null : lifecycle.run.runId,
|
|
1058
|
+
planDigest: lifecycle.run.planDigest,
|
|
1059
|
+
plan: lifecycle.plan,
|
|
1060
|
+
sql: renderedSql
|
|
1061
|
+
};
|
|
1062
|
+
} else {
|
|
1063
|
+
result = {
|
|
1064
|
+
compareKind: lifecycle.compare.kind,
|
|
1065
|
+
proveKind: lifecycle.kind,
|
|
1066
|
+
assessment: lifecycle.assessment,
|
|
1813
1067
|
persisted: false,
|
|
1814
1068
|
runId: null,
|
|
1815
1069
|
planDigest: null
|
|
1816
1070
|
};
|
|
1817
1071
|
}
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1072
|
+
} else {
|
|
1073
|
+
const targetIdentity = await deps.captureTargetIdentity(
|
|
1074
|
+
pool,
|
|
1075
|
+
physical.schema
|
|
1076
|
+
);
|
|
1077
|
+
await deps.ensureTransitionJournal(pool);
|
|
1078
|
+
const current = await deps.loadCurrent(
|
|
1079
|
+
pool,
|
|
1080
|
+
physical.schema,
|
|
1081
|
+
targetIdentity
|
|
1082
|
+
);
|
|
1083
|
+
const context = await deps.readContext(
|
|
1084
|
+
pool,
|
|
1085
|
+
physical.schema,
|
|
1086
|
+
targetIdentity
|
|
1087
|
+
);
|
|
1088
|
+
const planner = deps.createPlanner();
|
|
1089
|
+
const compare = planner.compare(physical.model, current, context);
|
|
1090
|
+
const prove = await planner.prove(compare, pool, context);
|
|
1091
|
+
if (prove.kind === "proven") {
|
|
1092
|
+
let executionContract;
|
|
1093
|
+
try {
|
|
1094
|
+
executionContract = await deps.buildExecutionContract(
|
|
1095
|
+
pool,
|
|
1096
|
+
physical.schema,
|
|
1097
|
+
prove.plan,
|
|
1098
|
+
targetIdentity
|
|
1099
|
+
);
|
|
1100
|
+
} catch (error) {
|
|
1101
|
+
if (!(error instanceof PgExecutionContractDerivationError))
|
|
1102
|
+
throw error;
|
|
1103
|
+
result = {
|
|
1104
|
+
compareKind: compare.kind,
|
|
1105
|
+
proveKind: "blocked",
|
|
1106
|
+
assessment: executionContractBlockedAssessment(error),
|
|
1107
|
+
persisted: false,
|
|
1108
|
+
runId: null,
|
|
1109
|
+
planDigest: null
|
|
1110
|
+
};
|
|
1111
|
+
}
|
|
1112
|
+
if (executionContract !== void 0) {
|
|
1113
|
+
const contractedPlan = bindExecutionContract(
|
|
1114
|
+
prove.plan,
|
|
1115
|
+
executionContract
|
|
1116
|
+
);
|
|
1117
|
+
const durablePlan = bindDeclarationSet(
|
|
1118
|
+
contractedPlan,
|
|
1119
|
+
declarationSetFromModel(physical.model, {
|
|
1828
1120
|
engine: context.engine,
|
|
1829
1121
|
database: context.databaseId,
|
|
1830
|
-
schema:
|
|
1831
|
-
}
|
|
1832
|
-
getNamingPluginForDbCasing(loaded.dbCasing ?? "preserve")
|
|
1833
|
-
)
|
|
1834
|
-
);
|
|
1835
|
-
const run = createTransitionRunMetadata(durablePlan);
|
|
1836
|
-
const proofContext = prove.plan.observations.find(
|
|
1837
|
-
(observation) => observation.role === "evidence"
|
|
1838
|
-
)?.context;
|
|
1839
|
-
if (!proofContext) {
|
|
1840
|
-
throw new Error(
|
|
1841
|
-
"internal error: minted proven plan has no evidence observation context"
|
|
1122
|
+
schema: physical.schema
|
|
1123
|
+
})
|
|
1842
1124
|
);
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
{
|
|
1851
|
-
compareKind: compare.kind,
|
|
1852
|
-
proveKind: prove.kind,
|
|
1853
|
-
assessment: prove.assessment,
|
|
1854
|
-
persisted: "indeterminate",
|
|
1855
|
-
runId: run.runId,
|
|
1856
|
-
planDigest: run.planDigest
|
|
1857
|
-
},
|
|
1858
|
-
error
|
|
1125
|
+
const run = createTransitionRunMetadata(durablePlan);
|
|
1126
|
+
const proofContext = prove.plan.observations.find(
|
|
1127
|
+
(observation) => observation.role === "evidence"
|
|
1128
|
+
)?.context;
|
|
1129
|
+
if (!proofContext) {
|
|
1130
|
+
throw new Error(
|
|
1131
|
+
"internal error: minted proven plan has no evidence observation context"
|
|
1859
1132
|
);
|
|
1860
1133
|
}
|
|
1134
|
+
const sql = planner.render(durablePlan, proofContext);
|
|
1135
|
+
if (!options.dryRun) {
|
|
1136
|
+
try {
|
|
1137
|
+
await deps.persist(pool, run, durablePlan);
|
|
1138
|
+
} catch (error) {
|
|
1139
|
+
throw new PlanPersistenceIndeterminateError(
|
|
1140
|
+
{
|
|
1141
|
+
compareKind: compare.kind,
|
|
1142
|
+
proveKind: prove.kind,
|
|
1143
|
+
assessment: prove.assessment,
|
|
1144
|
+
persisted: "indeterminate",
|
|
1145
|
+
runId: run.runId,
|
|
1146
|
+
planDigest: run.planDigest
|
|
1147
|
+
},
|
|
1148
|
+
error
|
|
1149
|
+
);
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
result = {
|
|
1153
|
+
compareKind: compare.kind,
|
|
1154
|
+
proveKind: prove.kind,
|
|
1155
|
+
assessment: prove.assessment,
|
|
1156
|
+
persisted: !options.dryRun,
|
|
1157
|
+
runId: options.dryRun ? null : run.runId,
|
|
1158
|
+
planDigest: run.planDigest,
|
|
1159
|
+
plan: durablePlan,
|
|
1160
|
+
sql
|
|
1161
|
+
};
|
|
1861
1162
|
}
|
|
1163
|
+
}
|
|
1164
|
+
if (prove.kind === "no-drift") {
|
|
1862
1165
|
result = {
|
|
1863
1166
|
compareKind: compare.kind,
|
|
1864
1167
|
proveKind: prove.kind,
|
|
1865
1168
|
assessment: prove.assessment,
|
|
1866
|
-
persisted:
|
|
1867
|
-
runId:
|
|
1868
|
-
planDigest:
|
|
1869
|
-
|
|
1870
|
-
|
|
1169
|
+
persisted: false,
|
|
1170
|
+
runId: null,
|
|
1171
|
+
planDigest: null
|
|
1172
|
+
};
|
|
1173
|
+
} else if (prove.kind !== "proven") {
|
|
1174
|
+
result = {
|
|
1175
|
+
compareKind: compare.kind,
|
|
1176
|
+
proveKind: prove.kind,
|
|
1177
|
+
assessment: prove.assessment,
|
|
1178
|
+
persisted: false,
|
|
1179
|
+
runId: null,
|
|
1180
|
+
planDigest: null
|
|
1871
1181
|
};
|
|
1872
1182
|
}
|
|
1873
1183
|
}
|
|
1874
|
-
if (prove.kind === "no-drift") {
|
|
1875
|
-
result = {
|
|
1876
|
-
compareKind: compare.kind,
|
|
1877
|
-
proveKind: prove.kind,
|
|
1878
|
-
assessment: prove.assessment,
|
|
1879
|
-
persisted: false,
|
|
1880
|
-
runId: null,
|
|
1881
|
-
planDigest: null
|
|
1882
|
-
};
|
|
1883
|
-
} else if (prove.kind !== "proven") {
|
|
1884
|
-
result = {
|
|
1885
|
-
compareKind: compare.kind,
|
|
1886
|
-
proveKind: prove.kind,
|
|
1887
|
-
assessment: prove.assessment,
|
|
1888
|
-
persisted: false,
|
|
1889
|
-
runId: null,
|
|
1890
|
-
planDigest: null
|
|
1891
|
-
};
|
|
1892
|
-
}
|
|
1893
1184
|
} catch (error) {
|
|
1894
1185
|
failed = true;
|
|
1895
1186
|
operationError = error;
|
|
@@ -1901,6 +1192,20 @@ async function runPlan(options, overrides) {
|
|
|
1901
1192
|
cleanup = { kind: "failed", error };
|
|
1902
1193
|
}
|
|
1903
1194
|
if (failed) {
|
|
1195
|
+
if (operationError instanceof PgTransitionRunPersistenceIndeterminateError) {
|
|
1196
|
+
throw new PlanPersistenceIndeterminateError(
|
|
1197
|
+
{
|
|
1198
|
+
compareKind: operationError.compare.kind,
|
|
1199
|
+
proveKind: "proven",
|
|
1200
|
+
assessment: operationError.assessment,
|
|
1201
|
+
persisted: "indeterminate",
|
|
1202
|
+
runId: operationError.run.runId,
|
|
1203
|
+
planDigest: operationError.run.planDigest
|
|
1204
|
+
},
|
|
1205
|
+
operationError.persistenceError,
|
|
1206
|
+
cleanup
|
|
1207
|
+
);
|
|
1208
|
+
}
|
|
1904
1209
|
if (operationError instanceof PlanPersistenceIndeterminateError) {
|
|
1905
1210
|
throw new PlanPersistenceIndeterminateError(
|
|
1906
1211
|
operationError.result,
|
|
@@ -2059,13 +1364,79 @@ function formatPreAppendRefusalHuman(line, refusal) {
|
|
|
2059
1364
|
`resolving command: ${escapeDiagnosticText2(refusal.refusal.resolvingCommand)}`
|
|
2060
1365
|
].join("\n");
|
|
2061
1366
|
}
|
|
2062
|
-
|
|
2063
|
-
// src/commands/apply.ts
|
|
2064
|
-
function generatorRunHasPriorStepEvents(loaded) {
|
|
2065
|
-
return loaded.events.some((event) => {
|
|
2066
|
-
const executionId = event.event === "intent" && "executionId" in event.record ? event.record.executionId : void 0;
|
|
2067
|
-
return !(typeof executionId === "string" && event.stepId === `dbsp.generator.attempt:${executionId}` && event.operationRef === "dbsp.generator.attempt");
|
|
2068
|
-
});
|
|
1367
|
+
|
|
1368
|
+
// src/commands/apply.ts
|
|
1369
|
+
function generatorRunHasPriorStepEvents(loaded) {
|
|
1370
|
+
return loaded.events.some((event) => {
|
|
1371
|
+
const executionId = event.event === "intent" && "executionId" in event.record ? event.record.executionId : void 0;
|
|
1372
|
+
return !(typeof executionId === "string" && event.stepId === `dbsp.generator.attempt:${executionId}` && event.operationRef === "dbsp.generator.attempt");
|
|
1373
|
+
});
|
|
1374
|
+
}
|
|
1375
|
+
function ledgerHomeForGeneratorAddress(address) {
|
|
1376
|
+
if (address.scope === "database") return { scope: "database" };
|
|
1377
|
+
if (!address.schema)
|
|
1378
|
+
throw new Error(
|
|
1379
|
+
`schema-scoped generated address ${address.name} has no schema ledger`
|
|
1380
|
+
);
|
|
1381
|
+
return { scope: "schema", schema: address.schema };
|
|
1382
|
+
}
|
|
1383
|
+
function generatorPlanLedgerAddresses(steps) {
|
|
1384
|
+
const addresses = /* @__PURE__ */ new Map();
|
|
1385
|
+
for (const step of steps) {
|
|
1386
|
+
if (step.address !== void 0) {
|
|
1387
|
+
addresses.set(ledgerAddressKey(step.address), step.address);
|
|
1388
|
+
continue;
|
|
1389
|
+
}
|
|
1390
|
+
const closure = step.closure;
|
|
1391
|
+
if (!closure)
|
|
1392
|
+
throw new Error(`generated step ${step.stepKey} has no ledger address`);
|
|
1393
|
+
const candidates = [
|
|
1394
|
+
closure.root,
|
|
1395
|
+
...closure.members.map((member) => member.address)
|
|
1396
|
+
];
|
|
1397
|
+
for (const address of candidates)
|
|
1398
|
+
addresses.set(ledgerAddressKey(address), address);
|
|
1399
|
+
}
|
|
1400
|
+
return [...addresses.values()];
|
|
1401
|
+
}
|
|
1402
|
+
async function generatorRunHasLedgerEvidence(session, journal, steps) {
|
|
1403
|
+
const executionIds = executionIdsForRun(journal);
|
|
1404
|
+
const executionIdSet = new Set(executionIds);
|
|
1405
|
+
const addresses = generatorPlanLedgerAddresses(steps);
|
|
1406
|
+
const homes = /* @__PURE__ */ new Map();
|
|
1407
|
+
for (const address of addresses) {
|
|
1408
|
+
const home = ledgerHomeForGeneratorAddress(address);
|
|
1409
|
+
homes.set(`${home.scope}:${home.schema ?? ""}`, home);
|
|
1410
|
+
}
|
|
1411
|
+
for (const home of homes.values()) {
|
|
1412
|
+
for (const executionId of executionIds) {
|
|
1413
|
+
const reservations = await readPgLedgerReservationsForExecution(
|
|
1414
|
+
session,
|
|
1415
|
+
home,
|
|
1416
|
+
executionId
|
|
1417
|
+
);
|
|
1418
|
+
if (reservations.length > 0) return true;
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
for (const address of addresses) {
|
|
1422
|
+
const chain = await readPgLedgerAddressChain(
|
|
1423
|
+
session,
|
|
1424
|
+
ledgerHomeForGeneratorAddress(address),
|
|
1425
|
+
address
|
|
1426
|
+
);
|
|
1427
|
+
if (chain.events.some((event) => executionIdSet.has(event.executionId ?? "")))
|
|
1428
|
+
return true;
|
|
1429
|
+
}
|
|
1430
|
+
return false;
|
|
1431
|
+
}
|
|
1432
|
+
async function generatorRunRequiresRecovery(target, journal, steps) {
|
|
1433
|
+
if (generatorRunHasPriorStepEvents(journal)) return true;
|
|
1434
|
+
const lease = await acquireExclusiveTransitionLease(target);
|
|
1435
|
+
try {
|
|
1436
|
+
return await generatorRunHasLedgerEvidence(lease.session, journal, steps);
|
|
1437
|
+
} finally {
|
|
1438
|
+
await lease.release();
|
|
1439
|
+
}
|
|
2069
1440
|
}
|
|
2070
1441
|
var GENERATOR_ATTEMPT_OPERATION = {
|
|
2071
1442
|
ref: "dbsp.generator.attempt",
|
|
@@ -2252,8 +1623,8 @@ function validateAssumptionAcceptance(value, path = "acceptance") {
|
|
|
2252
1623
|
}
|
|
2253
1624
|
return candidate;
|
|
2254
1625
|
}
|
|
2255
|
-
function normaliseAcceptance(
|
|
2256
|
-
const validated = validateAssumptionAcceptance(
|
|
1626
|
+
function normaliseAcceptance(acceptance) {
|
|
1627
|
+
const validated = validateAssumptionAcceptance(acceptance);
|
|
2257
1628
|
const withinScope = validated.withinScope ? [
|
|
2258
1629
|
...new Map(
|
|
2259
1630
|
validated.withinScope.map((item) => [canonicalJson(item), item])
|
|
@@ -2281,7 +1652,7 @@ function authorizationDigest(runId, planDigest, policy, grants, actor, authorize
|
|
|
2281
1652
|
const authorizedInstant = new Date(authorizedAt);
|
|
2282
1653
|
if (Number.isNaN(authorizedInstant.getTime()))
|
|
2283
1654
|
throw new Error("authorization timestamp must be a valid instant");
|
|
2284
|
-
return
|
|
1655
|
+
return createHash("sha256").update(
|
|
2285
1656
|
canonicalJson({
|
|
2286
1657
|
runId,
|
|
2287
1658
|
planDigest,
|
|
@@ -2305,6 +1676,13 @@ function hasReusableAuthorization(authorizations, runId, planDigest, policy, gra
|
|
|
2305
1676
|
)
|
|
2306
1677
|
) ?? false;
|
|
2307
1678
|
}
|
|
1679
|
+
function acceptanceMatches(assumption, acceptance) {
|
|
1680
|
+
return acceptance.class === assumption.class && (!acceptance.fromTrustRoot || canonicalJson(acceptance.fromTrustRoot) === canonicalJson(assumption.asserter)) && (assumption.scope.length === 0 ? !acceptance.withinScope || acceptance.withinScope.length === 0 : !acceptance.withinScope || acceptance.withinScope.length === 0 || assumption.scope.every(
|
|
1681
|
+
(resource) => acceptance.withinScope?.some(
|
|
1682
|
+
(selector) => selectorMatchesResource(selector, resource)
|
|
1683
|
+
)
|
|
1684
|
+
));
|
|
1685
|
+
}
|
|
2308
1686
|
async function effectiveApplyPolicy(options) {
|
|
2309
1687
|
let fromFile = [];
|
|
2310
1688
|
if (options.acceptPolicy) {
|
|
@@ -2329,9 +1707,6 @@ async function effectiveApplyPolicy(options) {
|
|
|
2329
1707
|
)
|
|
2330
1708
|
};
|
|
2331
1709
|
}
|
|
2332
|
-
function registry() {
|
|
2333
|
-
return createPackRegistry2([createPgTransitionPack2({})]);
|
|
2334
|
-
}
|
|
2335
1710
|
var APPLY_OUTCOME_CONTRACT = [
|
|
2336
1711
|
["completed", 0, "all planned steps completed"],
|
|
2337
1712
|
["operation-failed-not-applied", 10, "operation failed before application"],
|
|
@@ -2370,7 +1745,7 @@ var APPLY_OUTCOME_CONTRACT = [
|
|
|
2370
1745
|
[
|
|
2371
1746
|
"prior-step-events-refusal",
|
|
2372
1747
|
19,
|
|
2373
|
-
"attempted runs must be classified by
|
|
1748
|
+
"attempted runs with prior generator step-attempt events must be classified by dbsp reconcile --db <database> <run-id>"
|
|
2374
1749
|
],
|
|
2375
1750
|
[
|
|
2376
1751
|
"compatibility-refusal",
|
|
@@ -2402,7 +1777,12 @@ var APPLY_OUTCOME_CONTRACT = [
|
|
|
2402
1777
|
[
|
|
2403
1778
|
"recovery-required",
|
|
2404
1779
|
64,
|
|
2405
|
-
"an admitted
|
|
1780
|
+
"an admitted claim remains open; run dbsp reconcile --db <database> <run-id>"
|
|
1781
|
+
],
|
|
1782
|
+
[
|
|
1783
|
+
"transport-ambiguous",
|
|
1784
|
+
65,
|
|
1785
|
+
"an admitted operation has an ambiguous transport outcome; reconcile the run before retrying"
|
|
2406
1786
|
],
|
|
2407
1787
|
["database-read-only", 34, "target cannot accept managed writes"],
|
|
2408
1788
|
[
|
|
@@ -2455,6 +1835,7 @@ function exitCodeForApplyOutcome(outcome) {
|
|
|
2455
1835
|
return applyExitCodes.get(outcome);
|
|
2456
1836
|
}
|
|
2457
1837
|
function outcomeForApplyResult(result) {
|
|
1838
|
+
if (result.unresolvedOutcome) return result.unresolvedOutcome.kind;
|
|
2458
1839
|
if (result.durableOutcome && isApplyExecutionOutcome(result.durableOutcome))
|
|
2459
1840
|
return result.durableOutcome;
|
|
2460
1841
|
if (result.assessment.lifecycle === "partially-applied")
|
|
@@ -2475,8 +1856,90 @@ function outcomeForApplyResult(result) {
|
|
|
2475
1856
|
return "outcome-unknown";
|
|
2476
1857
|
return "context-mismatch";
|
|
2477
1858
|
}
|
|
1859
|
+
function generatorApplyCommandResult(execution, runId, refusal) {
|
|
1860
|
+
const fields = refusal === void 0 ? {} : { refusal };
|
|
1861
|
+
switch (execution.outcome) {
|
|
1862
|
+
case "completed":
|
|
1863
|
+
return {
|
|
1864
|
+
outcome: execution.outcome,
|
|
1865
|
+
runId,
|
|
1866
|
+
result: execution,
|
|
1867
|
+
...fields
|
|
1868
|
+
};
|
|
1869
|
+
case "partially-applied":
|
|
1870
|
+
return {
|
|
1871
|
+
outcome: execution.outcome,
|
|
1872
|
+
runId,
|
|
1873
|
+
result: execution,
|
|
1874
|
+
...fields
|
|
1875
|
+
};
|
|
1876
|
+
case "selection-incomplete":
|
|
1877
|
+
return {
|
|
1878
|
+
outcome: execution.outcome,
|
|
1879
|
+
runId,
|
|
1880
|
+
result: execution,
|
|
1881
|
+
...fields
|
|
1882
|
+
};
|
|
1883
|
+
case "adoption-refused":
|
|
1884
|
+
return {
|
|
1885
|
+
outcome: execution.outcome,
|
|
1886
|
+
runId,
|
|
1887
|
+
result: execution,
|
|
1888
|
+
...fields
|
|
1889
|
+
};
|
|
1890
|
+
case "readdress-unsupported":
|
|
1891
|
+
return {
|
|
1892
|
+
outcome: execution.outcome,
|
|
1893
|
+
runId,
|
|
1894
|
+
result: execution,
|
|
1895
|
+
...fields
|
|
1896
|
+
};
|
|
1897
|
+
case "readdress-refused":
|
|
1898
|
+
return {
|
|
1899
|
+
outcome: execution.outcome,
|
|
1900
|
+
runId,
|
|
1901
|
+
result: execution,
|
|
1902
|
+
...fields
|
|
1903
|
+
};
|
|
1904
|
+
case "destructive-authority-refused":
|
|
1905
|
+
return {
|
|
1906
|
+
outcome: execution.outcome,
|
|
1907
|
+
runId,
|
|
1908
|
+
result: execution,
|
|
1909
|
+
...fields
|
|
1910
|
+
};
|
|
1911
|
+
case "prior-step-events-refusal":
|
|
1912
|
+
return {
|
|
1913
|
+
outcome: execution.outcome,
|
|
1914
|
+
runId,
|
|
1915
|
+
result: execution,
|
|
1916
|
+
...fields
|
|
1917
|
+
};
|
|
1918
|
+
case "recovery-required":
|
|
1919
|
+
return {
|
|
1920
|
+
outcome: execution.outcome,
|
|
1921
|
+
runId,
|
|
1922
|
+
result: execution,
|
|
1923
|
+
...fields
|
|
1924
|
+
};
|
|
1925
|
+
case "transport-ambiguous":
|
|
1926
|
+
return {
|
|
1927
|
+
outcome: execution.outcome,
|
|
1928
|
+
runId,
|
|
1929
|
+
result: execution,
|
|
1930
|
+
...fields
|
|
1931
|
+
};
|
|
1932
|
+
case "execution-failed":
|
|
1933
|
+
return {
|
|
1934
|
+
outcome: execution.outcome,
|
|
1935
|
+
runId,
|
|
1936
|
+
result: execution,
|
|
1937
|
+
...fields
|
|
1938
|
+
};
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
2478
1941
|
function formatApplyHuman(result) {
|
|
2479
|
-
const line = `${result.outcome}: ${result.runId}`;
|
|
1942
|
+
const line = `${escapeDiagnosticText3(result.outcome)}: ${escapeDiagnosticText3(result.runId)}`;
|
|
2480
1943
|
if (result.refusal) {
|
|
2481
1944
|
if ("address" in result.refusal)
|
|
2482
1945
|
return formatPreAppendRefusalHuman(line, result.refusal);
|
|
@@ -2488,17 +1951,59 @@ function formatApplyHuman(result) {
|
|
|
2488
1951
|
`resolving command: ${escapeDiagnosticText3(result.refusal.resolvingCommand)}`
|
|
2489
1952
|
].join("\n");
|
|
2490
1953
|
}
|
|
2491
|
-
if (result
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
1954
|
+
if (!("result" in result)) return line;
|
|
1955
|
+
if ("assessment" in result.result) {
|
|
1956
|
+
if (result.result.unresolvedOutcome) {
|
|
1957
|
+
const unresolved = result.result.unresolvedOutcome;
|
|
1958
|
+
return unresolved.kind === "recovery-required" ? [
|
|
1959
|
+
line,
|
|
1960
|
+
`claim: ${escapeDiagnosticText3(unresolved.claimId)}`,
|
|
1961
|
+
`detail: ${escapeDiagnosticText3(unresolved.detail)}`,
|
|
1962
|
+
`resolving command: dbsp reconcile --db <database> ${escapeDiagnosticText3(result.runId)}`
|
|
1963
|
+
].join("\n") : [
|
|
1964
|
+
line,
|
|
1965
|
+
`detail: ${escapeDiagnosticText3(unresolved.detail)}`,
|
|
1966
|
+
`resolving command: dbsp reconcile --db <database> ${escapeDiagnosticText3(result.runId)}`
|
|
1967
|
+
].join("\n");
|
|
1968
|
+
}
|
|
1969
|
+
if (result.outcome !== "plan-digest-mismatch") return line;
|
|
1970
|
+
const detail = result.result.assessment.reasons[0]?.detail;
|
|
1971
|
+
return detail ? `${line}
|
|
1972
|
+
${escapeDiagnosticText3(detail)}` : line;
|
|
1973
|
+
}
|
|
1974
|
+
const execution = result.result;
|
|
1975
|
+
switch (execution.outcome) {
|
|
1976
|
+
case "completed":
|
|
1977
|
+
return line;
|
|
1978
|
+
case "recovery-required":
|
|
1979
|
+
return [
|
|
1980
|
+
line,
|
|
1981
|
+
`claim: ${escapeDiagnosticText3(execution.claimId)}`,
|
|
1982
|
+
`detail: ${escapeDiagnosticText3(execution.detail)}`,
|
|
1983
|
+
`resolving command: dbsp reconcile --db <database> ${escapeDiagnosticText3(result.runId)}`
|
|
1984
|
+
].join("\n");
|
|
1985
|
+
case "transport-ambiguous":
|
|
1986
|
+
return [
|
|
1987
|
+
line,
|
|
1988
|
+
`detail: ${escapeDiagnosticText3(execution.detail)}`,
|
|
1989
|
+
`resolving command: dbsp reconcile --db <database> ${escapeDiagnosticText3(result.runId)}`
|
|
1990
|
+
].join("\n");
|
|
1991
|
+
case "partially-applied":
|
|
1992
|
+
case "selection-incomplete":
|
|
1993
|
+
case "adoption-refused":
|
|
1994
|
+
case "readdress-unsupported":
|
|
1995
|
+
case "readdress-refused":
|
|
1996
|
+
case "destructive-authority-refused":
|
|
1997
|
+
case "execution-failed":
|
|
1998
|
+
return `${line}
|
|
1999
|
+
detail: ${escapeDiagnosticText3(execution.detail)}`;
|
|
2000
|
+
case "prior-step-events-refusal":
|
|
2001
|
+
return [
|
|
2002
|
+
line,
|
|
2003
|
+
`detail: ${escapeDiagnosticText3(execution.detail)}`,
|
|
2004
|
+
`resolving command: dbsp reconcile --db <database> ${escapeDiagnosticText3(result.runId)}`
|
|
2005
|
+
].join("\n");
|
|
2006
|
+
}
|
|
2502
2007
|
}
|
|
2503
2008
|
function recordedPlanRefusal() {
|
|
2504
2009
|
return { ...REFUSAL_VOCABULARY2["ERR-10"], state: "recorded-plan" };
|
|
@@ -2701,7 +2206,15 @@ async function runApplyInternal(runId, options, pool) {
|
|
|
2701
2206
|
throw new Error(
|
|
2702
2207
|
"persisted run no longer contains a generator plan"
|
|
2703
2208
|
);
|
|
2704
|
-
|
|
2209
|
+
let material;
|
|
2210
|
+
try {
|
|
2211
|
+
material = decodeGeneratorPlanMaterial(current.plan.generator);
|
|
2212
|
+
} catch (error) {
|
|
2213
|
+
throw new RecordedPlanDigestMismatchError(
|
|
2214
|
+
`persisted generator material is invalid: ${errorDetail(error)}`
|
|
2215
|
+
);
|
|
2216
|
+
}
|
|
2217
|
+
const recordedSchema = material.planningSchema;
|
|
2705
2218
|
if (!recordedSchema)
|
|
2706
2219
|
throw new Error(
|
|
2707
2220
|
"persisted generator run has no recorded planning schema and is non-resumable"
|
|
@@ -2716,7 +2229,7 @@ async function runApplyInternal(runId, options, pool) {
|
|
|
2716
2229
|
}
|
|
2717
2230
|
if (actualDigest !== current.run.planDigest || actualDigest !== expectedPlanDigest)
|
|
2718
2231
|
throw new RecordedPlanDigestMismatchError();
|
|
2719
|
-
const manifest =
|
|
2232
|
+
const manifest = validateNormalizedManagedStepManifest2(
|
|
2720
2233
|
current.plan.steps
|
|
2721
2234
|
);
|
|
2722
2235
|
if (!manifest.ok)
|
|
@@ -2730,10 +2243,14 @@ async function runApplyInternal(runId, options, pool) {
|
|
|
2730
2243
|
throw new RecordedPlanDigestMismatchError(
|
|
2731
2244
|
`persisted generator manifest is invalid: ${lifecycleError}`
|
|
2732
2245
|
);
|
|
2733
|
-
if (
|
|
2246
|
+
if (await generatorRunRequiresRecovery(
|
|
2247
|
+
target,
|
|
2248
|
+
current,
|
|
2249
|
+
manifest.manifest.steps
|
|
2250
|
+
))
|
|
2734
2251
|
return {
|
|
2735
2252
|
outcome: "prior-step-events-refusal",
|
|
2736
|
-
detail: "run has prior generator step-attempt events; run dbsp
|
|
2253
|
+
detail: "run has prior generator step-attempt events; run dbsp reconcile --db <database> <run-id>"
|
|
2737
2254
|
};
|
|
2738
2255
|
return executeGeneratorPlan({
|
|
2739
2256
|
pool: owned,
|
|
@@ -2760,6 +2277,18 @@ async function runApplyInternal(runId, options, pool) {
|
|
|
2760
2277
|
await journalLease.release();
|
|
2761
2278
|
}
|
|
2762
2279
|
},
|
|
2280
|
+
verifyDeclaredAdoptionShape: async (executor, step) => {
|
|
2281
|
+
if (step.lifecycle?.kind !== "adoption") return false;
|
|
2282
|
+
const compared = await comparePgsqlDeclaredAdoptionSchema2({
|
|
2283
|
+
// Outcome protocol provides its claimed pg session here. It is
|
|
2284
|
+
// intentionally not the outer pool held by runApply.
|
|
2285
|
+
executor,
|
|
2286
|
+
model: modelForDeclaredAdoption2(step.lifecycle.shape),
|
|
2287
|
+
schema: recordedSchema,
|
|
2288
|
+
dbCasing: material.dbCasing
|
|
2289
|
+
});
|
|
2290
|
+
return compared.changes.length === 0;
|
|
2291
|
+
},
|
|
2763
2292
|
...options.replace === void 0 ? {} : { replaces: options.replace },
|
|
2764
2293
|
runId
|
|
2765
2294
|
});
|
|
@@ -2780,96 +2309,68 @@ async function runApplyInternal(runId, options, pool) {
|
|
|
2780
2309
|
withheldAuthority
|
|
2781
2310
|
}
|
|
2782
2311
|
};
|
|
2783
|
-
result =
|
|
2784
|
-
|
|
2312
|
+
result = generatorApplyCommandResult(
|
|
2313
|
+
execution,
|
|
2785
2314
|
runId,
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
};
|
|
2315
|
+
preAppendRefusal
|
|
2316
|
+
);
|
|
2789
2317
|
}
|
|
2790
2318
|
} else {
|
|
2791
|
-
const
|
|
2319
|
+
const applied = await applyPgTransitionRun(
|
|
2792
2320
|
owned,
|
|
2793
2321
|
runId,
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
}
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
prepareExecutionSession: async (session, contract, plan) => {
|
|
2808
|
-
const currency = await validatePgManagedLedgerCurrency(
|
|
2809
|
-
session,
|
|
2810
|
-
plan
|
|
2811
|
-
);
|
|
2812
|
-
if (currency)
|
|
2813
|
-
return {
|
|
2814
|
-
ok: false,
|
|
2815
|
-
kind: "refused",
|
|
2816
|
-
detail: currency
|
|
2817
|
-
};
|
|
2818
|
-
return preparePgExecutionSession(session, contract, plan);
|
|
2819
|
-
},
|
|
2820
|
-
policy,
|
|
2821
|
-
target,
|
|
2822
|
-
authorize: async (run, plan, session) => {
|
|
2823
|
-
const current = await loadOnTarget(target, run.runId);
|
|
2824
|
-
const grants = plan.assumptions.map((assumption) => ({
|
|
2825
|
-
assumptionId: assumption.id,
|
|
2826
|
-
grant: policy.accepts.findIndex(
|
|
2827
|
-
(grant) => acceptanceMatches(assumption, grant)
|
|
2828
|
-
)
|
|
2829
|
-
}));
|
|
2830
|
-
if (hasReusableAuthorization(
|
|
2831
|
-
current.authorizations,
|
|
2322
|
+
policy,
|
|
2323
|
+
expectedPlanDigest,
|
|
2324
|
+
{
|
|
2325
|
+
authorize: async ({ current, run, plan }) => {
|
|
2326
|
+
const grants = plan.assumptions.map((assumption) => ({
|
|
2327
|
+
assumptionId: assumption.id,
|
|
2328
|
+
grant: policy.accepts.findIndex(
|
|
2329
|
+
(grant) => acceptanceMatches(assumption, grant)
|
|
2330
|
+
)
|
|
2331
|
+
}));
|
|
2332
|
+
const reusable = current.authorizations?.find(
|
|
2333
|
+
(authorization) => hasReusableAuthorization(
|
|
2334
|
+
[authorization],
|
|
2832
2335
|
run.runId,
|
|
2833
2336
|
transitionPlanDigest2(plan),
|
|
2834
2337
|
policy.accepts,
|
|
2835
2338
|
grants
|
|
2836
|
-
)
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2339
|
+
)
|
|
2340
|
+
);
|
|
2341
|
+
if (reusable) return reusable;
|
|
2342
|
+
const actor = process.env.USER ?? process.env.LOGNAME ?? "unknown-local-actor";
|
|
2343
|
+
const authorizedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2344
|
+
return {
|
|
2345
|
+
runId: run.runId,
|
|
2346
|
+
policy: policy.accepts,
|
|
2347
|
+
grants,
|
|
2348
|
+
digest: authorizationDigest(
|
|
2841
2349
|
run.runId,
|
|
2842
2350
|
transitionPlanDigest2(plan),
|
|
2843
2351
|
policy.accepts,
|
|
2844
2352
|
grants,
|
|
2845
2353
|
actor,
|
|
2846
2354
|
authorizedAt
|
|
2847
|
-
)
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
digest: digest2,
|
|
2853
|
-
actor,
|
|
2854
|
-
authorizedAt
|
|
2855
|
-
};
|
|
2856
|
-
await appendTransitionAuthorization(session, record2);
|
|
2857
|
-
}
|
|
2858
|
-
});
|
|
2355
|
+
),
|
|
2356
|
+
actor,
|
|
2357
|
+
authorizedAt
|
|
2358
|
+
};
|
|
2359
|
+
}
|
|
2859
2360
|
}
|
|
2860
2361
|
);
|
|
2861
|
-
if (
|
|
2362
|
+
if (applied.kind === "busy") result = { outcome: "run-busy", runId };
|
|
2862
2363
|
else {
|
|
2863
|
-
const outcome = outcomeForApplyResult(
|
|
2364
|
+
const outcome = outcomeForApplyResult(applied.result);
|
|
2864
2365
|
const preAppendRefusal = applyPreAppendRefusal(
|
|
2865
2366
|
outcome,
|
|
2866
2367
|
persisted.plan,
|
|
2867
|
-
|
|
2368
|
+
applied.result
|
|
2868
2369
|
);
|
|
2869
2370
|
result = {
|
|
2870
2371
|
outcome,
|
|
2871
2372
|
runId,
|
|
2872
|
-
result:
|
|
2373
|
+
result: applied.result,
|
|
2873
2374
|
...preAppendRefusal === void 0 ? {} : { refusal: preAppendRefusal }
|
|
2874
2375
|
};
|
|
2875
2376
|
}
|
|
@@ -2965,9 +2466,7 @@ var applyCommand = new Command2("apply").description(
|
|
|
2965
2466
|
console.log("confirmation-declined: persisted run was not executed");
|
|
2966
2467
|
else if (result2.outcome === "not-executable") {
|
|
2967
2468
|
} else if (result2.outcome !== "dry-run" && "result" in result2)
|
|
2968
|
-
console.log(
|
|
2969
|
-
"runId" in result2.result ? formatApplyHuman(result2.result) : `${result2.result.outcome}${"detail" in result2.result ? `: ${result2.result.detail}` : ""}`
|
|
2970
|
-
);
|
|
2469
|
+
console.log(formatApplyHuman(result2.result));
|
|
2971
2470
|
}
|
|
2972
2471
|
process.exitCode = exitCode2;
|
|
2973
2472
|
return;
|
|
@@ -3066,13 +2565,19 @@ var generateCommand = new Command3("generate").description("Generate code from s
|
|
|
3066
2565
|
case "ddl": {
|
|
3067
2566
|
const dbCasing = mapCasingToDbCasing(options.casing) ?? schema.dbCasing ?? "snake_case";
|
|
3068
2567
|
const casingLabel = formatGenerateCasingLabel(dbCasing);
|
|
3069
|
-
const { createPgsqlCompileOnlyAdapter } = await import("@dbsp/adapter-pgsql");
|
|
2568
|
+
const { createPgPhysicalModel: createPgPhysicalModel6, createPgsqlCompileOnlyAdapter } = await import("@dbsp/adapter-pgsql");
|
|
3070
2569
|
const adapter = createPgsqlCompileOnlyAdapter({
|
|
3071
2570
|
dbCasing,
|
|
3072
2571
|
...options.schemaName ? { schemaName: options.schemaName } : {}
|
|
3073
2572
|
});
|
|
3074
2573
|
{
|
|
3075
|
-
const
|
|
2574
|
+
const physical = createPgPhysicalModel6({
|
|
2575
|
+
mode: "logical",
|
|
2576
|
+
model: schema.model,
|
|
2577
|
+
schema: options.schemaName ?? "public",
|
|
2578
|
+
dbCasing
|
|
2579
|
+
});
|
|
2580
|
+
const ddlStatements = adapter.generateDDL(physical, {
|
|
3076
2581
|
...options.drop !== void 0 && {
|
|
3077
2582
|
includeDropStatements: options.drop
|
|
3078
2583
|
},
|
|
@@ -3137,16 +2642,16 @@ function formatGenerateCasingLabel(dbCasing) {
|
|
|
3137
2642
|
}
|
|
3138
2643
|
|
|
3139
2644
|
// src/commands/inspect.ts
|
|
3140
|
-
import { isDeepStrictEqual
|
|
2645
|
+
import { isDeepStrictEqual } from "util";
|
|
3141
2646
|
import {
|
|
3142
2647
|
createPgTransitionLessor as createPgTransitionLessor4,
|
|
3143
2648
|
DBSP_LEDGER_EVENT_TABLE,
|
|
3144
2649
|
escapeDiagnosticText as escapeDiagnosticText4,
|
|
3145
|
-
readPgCatalogueIdentity as
|
|
2650
|
+
readPgCatalogueIdentity as readPgCatalogueIdentity2,
|
|
3146
2651
|
readPgLedgerAddressChain as readPgLedgerAddressChain2,
|
|
3147
2652
|
readPgLedgerMarker
|
|
3148
2653
|
} from "@dbsp/adapter-pgsql";
|
|
3149
|
-
import { acquireTransitionLease as acquireTransitionLease4, projectLedgerChain
|
|
2654
|
+
import { acquireTransitionLease as acquireTransitionLease4, projectLedgerChain } from "@dbsp/core";
|
|
3150
2655
|
import {
|
|
3151
2656
|
canonicalResourceParent
|
|
3152
2657
|
} from "@dbsp/types";
|
|
@@ -3192,7 +2697,7 @@ function inspectLiveDrift(projection, live) {
|
|
|
3192
2697
|
kind: "catalogue-drift",
|
|
3193
2698
|
detail: "ledger records a present catalogue identity but the object is absent"
|
|
3194
2699
|
};
|
|
3195
|
-
return
|
|
2700
|
+
return isDeepStrictEqual(terminal.catalogueIdentity, live.catalogueIdentity) ? { kind: "matches-ledger" } : {
|
|
3196
2701
|
kind: "catalogue-drift",
|
|
3197
2702
|
detail: "live catalogue identity differs from the ledger terminal"
|
|
3198
2703
|
};
|
|
@@ -3315,7 +2820,7 @@ async function runInspect(selector, options) {
|
|
|
3315
2820
|
);
|
|
3316
2821
|
let projection;
|
|
3317
2822
|
try {
|
|
3318
|
-
projection =
|
|
2823
|
+
projection = projectLedgerChain(
|
|
3319
2824
|
await readPgLedgerAddressChain2(lease.session, ledger, address)
|
|
3320
2825
|
);
|
|
3321
2826
|
} catch (error) {
|
|
@@ -3335,7 +2840,7 @@ async function runInspect(selector, options) {
|
|
|
3335
2840
|
};
|
|
3336
2841
|
}
|
|
3337
2842
|
try {
|
|
3338
|
-
const live = await
|
|
2843
|
+
const live = await readPgCatalogueIdentity2(lease.session, address);
|
|
3339
2844
|
const refusal = inspectRefusal(projection);
|
|
3340
2845
|
const openReaddress = projection?.kind === "projected-ledger-chain" && projection.openClaim?.kind === "readdress-intent" && projection.openClaim.event.pairId ? {
|
|
3341
2846
|
pairId: projection.openClaim.event.pairId,
|
|
@@ -3416,83 +2921,423 @@ function reportWarnings(warnings) {
|
|
|
3416
2921
|
console.log(` \u26A0\uFE0F ${warning}`);
|
|
3417
2922
|
}
|
|
3418
2923
|
}
|
|
3419
|
-
var introspectCommand = new Command5("introspect").description("Generate schema.ts from database introspection").requiredOption("-d, --db <url>", "Database connection URL (required)").option("-o, --out <file>", "Output schema file", "./dbsp.schema.ts").option("--schema-name <name>", "Database schema name", "public").option(
|
|
3420
|
-
"--exclude <patterns>",
|
|
3421
|
-
"Tables to exclude (comma-separated glob patterns)",
|
|
3422
|
-
"_migrations,_prisma*,pg_*"
|
|
3423
|
-
).option("--include <patterns>", "Tables to include (comma-separated)").option("--no-db-type-comments", "Omit original DB type comments").option(
|
|
3424
|
-
"--db-casing <casing>",
|
|
3425
|
-
"Database column casing (snake_case \u2192 camelCase in generated code)",
|
|
3426
|
-
"snake_case"
|
|
3427
|
-
).action(
|
|
3428
|
-
async (options) => {
|
|
3429
|
-
const redactedUrl = redactDbUrl(options.db);
|
|
3430
|
-
console.log(`\u{1F50D} Introspecting database: ${redactedUrl}`);
|
|
3431
|
-
console.log(` Schema: ${options.schemaName}`);
|
|
3432
|
-
if (options.exclude) {
|
|
3433
|
-
console.log(` Excluding: ${options.exclude}`);
|
|
3434
|
-
}
|
|
3435
|
-
console.log("");
|
|
3436
|
-
try {
|
|
3437
|
-
const { pool } = await createDbConnection(options.db);
|
|
2924
|
+
var introspectCommand = new Command5("introspect").description("Generate schema.ts from database introspection").requiredOption("-d, --db <url>", "Database connection URL (required)").option("-o, --out <file>", "Output schema file", "./dbsp.schema.ts").option("--schema-name <name>", "Database schema name", "public").option(
|
|
2925
|
+
"--exclude <patterns>",
|
|
2926
|
+
"Tables to exclude (comma-separated glob patterns)",
|
|
2927
|
+
"_migrations,_prisma*,pg_*"
|
|
2928
|
+
).option("--include <patterns>", "Tables to include (comma-separated)").option("--no-db-type-comments", "Omit original DB type comments").option(
|
|
2929
|
+
"--db-casing <casing>",
|
|
2930
|
+
"Database column casing (snake_case \u2192 camelCase in generated code)",
|
|
2931
|
+
"snake_case"
|
|
2932
|
+
).action(
|
|
2933
|
+
async (options) => {
|
|
2934
|
+
const redactedUrl = redactDbUrl(options.db);
|
|
2935
|
+
console.log(`\u{1F50D} Introspecting database: ${redactedUrl}`);
|
|
2936
|
+
console.log(` Schema: ${options.schemaName}`);
|
|
2937
|
+
if (options.exclude) {
|
|
2938
|
+
console.log(` Excluding: ${options.exclude}`);
|
|
2939
|
+
}
|
|
2940
|
+
console.log("");
|
|
2941
|
+
try {
|
|
2942
|
+
const { pool } = await createDbConnection(options.db);
|
|
2943
|
+
try {
|
|
2944
|
+
const { introspect } = await import("@dbsp/adapter-pgsql");
|
|
2945
|
+
const excludePatterns = options.exclude ? options.exclude.split(",").map((s) => s.trim()) : void 0;
|
|
2946
|
+
const includePatterns = options.include ? options.include.split(",").map((s) => s.trim()) : void 0;
|
|
2947
|
+
const model = await introspect(pool, {
|
|
2948
|
+
schema: options.schemaName,
|
|
2949
|
+
...excludePatterns ? { exclude: excludePatterns } : {},
|
|
2950
|
+
...includePatterns ? { include: includePatterns } : {}
|
|
2951
|
+
});
|
|
2952
|
+
const tableCount = model.tables.size;
|
|
2953
|
+
const relationCount = model.relations.size;
|
|
2954
|
+
const hierarchyCount = model.hierarchies?.length ?? 0;
|
|
2955
|
+
console.log(
|
|
2956
|
+
`\u{1F4CA} Found ${tableCount} tables, ${relationCount} relations, ${hierarchyCount} hierarchies`
|
|
2957
|
+
);
|
|
2958
|
+
const codegenOptions = {
|
|
2959
|
+
sourceUrl: options.db,
|
|
2960
|
+
includeDbTypeComments: options.dbTypeComments,
|
|
2961
|
+
introspectedAt: model.introspectedAt,
|
|
2962
|
+
dbCasing: options.dbCasing
|
|
2963
|
+
};
|
|
2964
|
+
const generated = generateSchemaFileWithDiagnostics(
|
|
2965
|
+
model,
|
|
2966
|
+
codegenOptions
|
|
2967
|
+
);
|
|
2968
|
+
const warnings = uniqueWarnings(generated.warnings);
|
|
2969
|
+
if (warnings.length > 0) {
|
|
2970
|
+
reportWarnings(warnings);
|
|
2971
|
+
}
|
|
2972
|
+
console.log("");
|
|
2973
|
+
const outPath = resolve2(process.cwd(), options.out);
|
|
2974
|
+
mkdirSync2(dirname2(outPath), { recursive: true });
|
|
2975
|
+
writeFileSync2(outPath, generated.code, "utf-8");
|
|
2976
|
+
console.log(`\u2705 Generated schema: ${outPath}`);
|
|
2977
|
+
console.log(` Tables: ${tableCount}`);
|
|
2978
|
+
console.log(` Relations: ${relationCount}`);
|
|
2979
|
+
} finally {
|
|
2980
|
+
await pool.end();
|
|
2981
|
+
}
|
|
2982
|
+
} catch (error) {
|
|
2983
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2984
|
+
console.error(`\u274C ${message}`);
|
|
2985
|
+
process.exit(1);
|
|
2986
|
+
}
|
|
2987
|
+
}
|
|
2988
|
+
);
|
|
2989
|
+
|
|
2990
|
+
// src/commands/migrate.ts
|
|
2991
|
+
import {
|
|
2992
|
+
convergePg,
|
|
2993
|
+
createPgPhysicalModel as createPgPhysicalModel3,
|
|
2994
|
+
escapeDiagnosticText as escapeDiagnosticText5,
|
|
2995
|
+
PgConvergeRefusalError
|
|
2996
|
+
} from "@dbsp/adapter-pgsql";
|
|
2997
|
+
import { Command as Command6 } from "commander";
|
|
2998
|
+
var MIGRATE_OUTCOME_CONTRACT = [
|
|
2999
|
+
["no-drift", 0, "database matches declaration"],
|
|
3000
|
+
["applied", 0, "admitted additions or adoptions applied"],
|
|
3001
|
+
["partially-applied", 11, "some target effects may be durable"],
|
|
3002
|
+
["busy", 21, "ledger lock or claims are busy; retry later"],
|
|
3003
|
+
["database-read-only", 34, "target cannot accept managed writes"],
|
|
3004
|
+
["load-failed", 27, "schema file could not be loaded"],
|
|
3005
|
+
["migrate-failed", 29, "migration, connection, or planning failure"],
|
|
3006
|
+
["adoption-refused", 62, "declared adoption no longer matches live object"],
|
|
3007
|
+
[
|
|
3008
|
+
"recovery-required",
|
|
3009
|
+
64,
|
|
3010
|
+
"admitted claim remains open; reconcile before retrying"
|
|
3011
|
+
],
|
|
3012
|
+
[
|
|
3013
|
+
"transport-ambiguous",
|
|
3014
|
+
65,
|
|
3015
|
+
"admitted operation has ambiguous transport outcome"
|
|
3016
|
+
],
|
|
3017
|
+
[
|
|
3018
|
+
"invalid-options",
|
|
3019
|
+
70,
|
|
3020
|
+
"an --external-index or --format value is malformed, or convergePg refused its options"
|
|
3021
|
+
],
|
|
3022
|
+
["ledger-absent", 71, "convergence ledger is not initialized"],
|
|
3023
|
+
["incompatible-ledger", 72, "ledger version is unsupported"],
|
|
3024
|
+
["unsupported-server", 73, "PostgreSQL server is unsupported"],
|
|
3025
|
+
["unsupported-change", 74, "requested schema change is not admitted"],
|
|
3026
|
+
["unmanaged-object", 75, "live object is not ledger-managed"],
|
|
3027
|
+
["unmanaged-parent", 76, "parent live object is not ledger-managed"],
|
|
3028
|
+
["concurrent-drift", 77, "live schema changed during admission"],
|
|
3029
|
+
["execution-refused", 78, "admitted execution step was refused"],
|
|
3030
|
+
["cleanup-failed", 79, "pool failed to close after convergence"]
|
|
3031
|
+
];
|
|
3032
|
+
var migrateExitCodes = new Map(
|
|
3033
|
+
MIGRATE_OUTCOME_CONTRACT.map(([outcome, exitCode]) => [outcome, exitCode])
|
|
3034
|
+
);
|
|
3035
|
+
function exitCodeForMigrateOutcome(outcome) {
|
|
3036
|
+
return migrateExitCodes.get(outcome);
|
|
3037
|
+
}
|
|
3038
|
+
var refusalOutcomes = {
|
|
3039
|
+
"invalid-options": "invalid-options",
|
|
3040
|
+
"unsupported-change": "unsupported-change",
|
|
3041
|
+
"unmanaged-object": "unmanaged-object",
|
|
3042
|
+
"unmanaged-parent": "unmanaged-parent",
|
|
3043
|
+
"concurrent-drift": "concurrent-drift",
|
|
3044
|
+
"ledger-absent": "ledger-absent",
|
|
3045
|
+
"incompatible-ledger": "incompatible-ledger",
|
|
3046
|
+
"unsupported-server": "unsupported-server",
|
|
3047
|
+
busy: "busy",
|
|
3048
|
+
"recovery-required": "recovery-required",
|
|
3049
|
+
"database-read-only": "database-read-only",
|
|
3050
|
+
"execution-refused": "execution-refused",
|
|
3051
|
+
"adoption-refused": "adoption-refused",
|
|
3052
|
+
// dbsp migrate never supplies application steps, but preserve the closed converge map.
|
|
3053
|
+
"application-step-changed": "migrate-failed",
|
|
3054
|
+
"application-step-failed": "migrate-failed",
|
|
3055
|
+
// dbsp migrate never supplies initialize, but preserve the closed converge map.
|
|
3056
|
+
"initialization-refused": "migrate-failed"
|
|
3057
|
+
};
|
|
3058
|
+
var defaultMigrateDeps = {
|
|
3059
|
+
loadSchema,
|
|
3060
|
+
createDbConnection,
|
|
3061
|
+
converge: convergePg
|
|
3062
|
+
};
|
|
3063
|
+
function resolvedDeps2(overrides) {
|
|
3064
|
+
return { ...defaultMigrateDeps, ...overrides };
|
|
3065
|
+
}
|
|
3066
|
+
function describeThrown2(error) {
|
|
3067
|
+
return error instanceof Error ? error.message : String(error);
|
|
3068
|
+
}
|
|
3069
|
+
function parseExternalIndexes(entries) {
|
|
3070
|
+
if (entries === void 0 || entries.length === 0) return void 0;
|
|
3071
|
+
return entries.map((entry) => {
|
|
3072
|
+
const separator = entry.indexOf(":");
|
|
3073
|
+
if (separator <= 0 || separator === entry.length - 1)
|
|
3074
|
+
throw new Error(
|
|
3075
|
+
`invalid --external-index ${entry}; expected <table>:<index>`
|
|
3076
|
+
);
|
|
3077
|
+
return {
|
|
3078
|
+
table: entry.slice(0, separator),
|
|
3079
|
+
name: entry.slice(separator + 1)
|
|
3080
|
+
};
|
|
3081
|
+
});
|
|
3082
|
+
}
|
|
3083
|
+
function resultForConverge(result, schema, schemaFile) {
|
|
3084
|
+
switch (result.kind) {
|
|
3085
|
+
case "no-drift":
|
|
3086
|
+
case "applied":
|
|
3087
|
+
return {
|
|
3088
|
+
outcome: result.kind,
|
|
3089
|
+
exitCode: exitCodeForMigrateOutcome(result.kind),
|
|
3090
|
+
schema,
|
|
3091
|
+
schemaFile,
|
|
3092
|
+
applied: result.applied
|
|
3093
|
+
};
|
|
3094
|
+
case "partially-applied":
|
|
3095
|
+
return {
|
|
3096
|
+
outcome: result.kind,
|
|
3097
|
+
exitCode: exitCodeForMigrateOutcome(result.kind),
|
|
3098
|
+
schema,
|
|
3099
|
+
schemaFile,
|
|
3100
|
+
completedStepKeys: result.completedStepKeys,
|
|
3101
|
+
notStartedStepKeys: result.notStartedStepKeys,
|
|
3102
|
+
detail: result.detail
|
|
3103
|
+
};
|
|
3104
|
+
case "transport-ambiguous":
|
|
3105
|
+
return {
|
|
3106
|
+
outcome: result.kind,
|
|
3107
|
+
exitCode: exitCodeForMigrateOutcome(result.kind),
|
|
3108
|
+
schema,
|
|
3109
|
+
schemaFile,
|
|
3110
|
+
detail: result.detail
|
|
3111
|
+
};
|
|
3112
|
+
}
|
|
3113
|
+
const unhandled = result;
|
|
3114
|
+
return unhandled;
|
|
3115
|
+
}
|
|
3116
|
+
function refusalResult(error, schema, schemaFile) {
|
|
3117
|
+
const outcome = refusalOutcomes[error.refusal];
|
|
3118
|
+
return {
|
|
3119
|
+
outcome,
|
|
3120
|
+
exitCode: exitCodeForMigrateOutcome(outcome),
|
|
3121
|
+
schema,
|
|
3122
|
+
schemaFile,
|
|
3123
|
+
changes: error.changes,
|
|
3124
|
+
...error.detail === void 0 ? {} : { detail: error.detail },
|
|
3125
|
+
...error.runIds === void 0 ? {} : { runIds: error.runIds },
|
|
3126
|
+
...error.busyRunIds === void 0 ? {} : { busyRunIds: error.busyRunIds },
|
|
3127
|
+
...error.executionIds === void 0 ? {} : { executionIds: error.executionIds }
|
|
3128
|
+
};
|
|
3129
|
+
}
|
|
3130
|
+
function failureResult(outcome, error, schema, schemaFile) {
|
|
3131
|
+
return {
|
|
3132
|
+
outcome,
|
|
3133
|
+
exitCode: exitCodeForMigrateOutcome(outcome),
|
|
3134
|
+
schema,
|
|
3135
|
+
schemaFile,
|
|
3136
|
+
error: describeThrown2(error)
|
|
3137
|
+
};
|
|
3138
|
+
}
|
|
3139
|
+
async function runMigrate(schemaPath, options, overrides) {
|
|
3140
|
+
const schema = options.schema ?? "public";
|
|
3141
|
+
const deps = resolvedDeps2(overrides);
|
|
3142
|
+
if (options.format !== void 0 && options.format !== "text" && options.format !== "json")
|
|
3143
|
+
return failureResult(
|
|
3144
|
+
"invalid-options",
|
|
3145
|
+
new Error(
|
|
3146
|
+
`unsupported migrate format ${options.format}; expected text or json`
|
|
3147
|
+
),
|
|
3148
|
+
schema,
|
|
3149
|
+
schemaPath
|
|
3150
|
+
);
|
|
3151
|
+
let externalIndexes;
|
|
3152
|
+
try {
|
|
3153
|
+
externalIndexes = parseExternalIndexes(options.externalIndex);
|
|
3154
|
+
} catch (error) {
|
|
3155
|
+
return failureResult("invalid-options", error, schema, schemaPath);
|
|
3156
|
+
}
|
|
3157
|
+
let loaded;
|
|
3158
|
+
try {
|
|
3159
|
+
loaded = await deps.loadSchema(schemaPath);
|
|
3160
|
+
} catch (error) {
|
|
3161
|
+
return failureResult("load-failed", error, schema, schemaPath);
|
|
3162
|
+
}
|
|
3163
|
+
let physical;
|
|
3164
|
+
try {
|
|
3165
|
+
physical = createPgPhysicalModel3({
|
|
3166
|
+
mode: "logical",
|
|
3167
|
+
model: loaded.model,
|
|
3168
|
+
schema,
|
|
3169
|
+
...loaded.dbCasing === void 0 ? {} : { dbCasing: loaded.dbCasing }
|
|
3170
|
+
});
|
|
3171
|
+
} catch (error) {
|
|
3172
|
+
return failureResult("migrate-failed", error, schema, schemaPath);
|
|
3173
|
+
}
|
|
3174
|
+
let pool;
|
|
3175
|
+
try {
|
|
3176
|
+
({ pool } = await deps.createDbConnection(options.db));
|
|
3177
|
+
} catch (error) {
|
|
3178
|
+
return failureResult("migrate-failed", error, schema, schemaPath);
|
|
3179
|
+
}
|
|
3180
|
+
let result;
|
|
3181
|
+
let convergeResult;
|
|
3182
|
+
try {
|
|
3183
|
+
convergeResult = await deps.converge(pool, physical, {
|
|
3184
|
+
...externalIndexes === void 0 ? {} : { externalIndexes }
|
|
3185
|
+
});
|
|
3186
|
+
result = resultForConverge(convergeResult, schema, schemaPath);
|
|
3187
|
+
} catch (error) {
|
|
3188
|
+
result = error instanceof PgConvergeRefusalError ? refusalResult(error, schema, schemaPath) : failureResult("migrate-failed", error, schema, schemaPath);
|
|
3189
|
+
}
|
|
3190
|
+
try {
|
|
3191
|
+
await pool.end();
|
|
3192
|
+
} catch (error) {
|
|
3193
|
+
const cleanupError = describeThrown2(error);
|
|
3194
|
+
if (result.exitCode === 0 && convergeResult !== void 0) {
|
|
3195
|
+
return {
|
|
3196
|
+
outcome: "cleanup-failed",
|
|
3197
|
+
exitCode: exitCodeForMigrateOutcome("cleanup-failed"),
|
|
3198
|
+
schema,
|
|
3199
|
+
schemaFile: schemaPath,
|
|
3200
|
+
cleanupError,
|
|
3201
|
+
result: convergeResult
|
|
3202
|
+
};
|
|
3203
|
+
}
|
|
3204
|
+
return { ...result, cleanupError };
|
|
3205
|
+
}
|
|
3206
|
+
return result;
|
|
3207
|
+
}
|
|
3208
|
+
function redactDiagnostic(value, db) {
|
|
3209
|
+
const sensitive = /* @__PURE__ */ new Set([db]);
|
|
3210
|
+
try {
|
|
3211
|
+
const url = new URL(db);
|
|
3212
|
+
const password = url.password;
|
|
3213
|
+
if (password) {
|
|
3214
|
+
sensitive.add(password);
|
|
3438
3215
|
try {
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
const includePatterns = options.include ? options.include.split(",").map((s) => s.trim()) : void 0;
|
|
3442
|
-
const model = await introspect(pool, {
|
|
3443
|
-
schema: options.schemaName,
|
|
3444
|
-
...excludePatterns ? { exclude: excludePatterns } : {},
|
|
3445
|
-
...includePatterns ? { include: includePatterns } : {}
|
|
3446
|
-
});
|
|
3447
|
-
const tableCount = model.tables.size;
|
|
3448
|
-
const relationCount = model.relations.size;
|
|
3449
|
-
const hierarchyCount = model.hierarchies?.length ?? 0;
|
|
3450
|
-
console.log(
|
|
3451
|
-
`\u{1F4CA} Found ${tableCount} tables, ${relationCount} relations, ${hierarchyCount} hierarchies`
|
|
3452
|
-
);
|
|
3453
|
-
const codegenOptions = {
|
|
3454
|
-
sourceUrl: options.db,
|
|
3455
|
-
includeDbTypeComments: options.dbTypeComments,
|
|
3456
|
-
introspectedAt: model.introspectedAt,
|
|
3457
|
-
dbCasing: options.dbCasing
|
|
3458
|
-
};
|
|
3459
|
-
const generated = generateSchemaFileWithDiagnostics(
|
|
3460
|
-
model,
|
|
3461
|
-
codegenOptions
|
|
3462
|
-
);
|
|
3463
|
-
const warnings = uniqueWarnings(generated.warnings);
|
|
3464
|
-
if (warnings.length > 0) {
|
|
3465
|
-
reportWarnings(warnings);
|
|
3466
|
-
}
|
|
3467
|
-
console.log("");
|
|
3468
|
-
const outPath = resolve2(process.cwd(), options.out);
|
|
3469
|
-
mkdirSync2(dirname2(outPath), { recursive: true });
|
|
3470
|
-
writeFileSync2(outPath, generated.code, "utf-8");
|
|
3471
|
-
console.log(`\u2705 Generated schema: ${outPath}`);
|
|
3472
|
-
console.log(` Tables: ${tableCount}`);
|
|
3473
|
-
console.log(` Relations: ${relationCount}`);
|
|
3474
|
-
} finally {
|
|
3475
|
-
await pool.end();
|
|
3216
|
+
sensitive.add(decodeURIComponent(password));
|
|
3217
|
+
} catch {
|
|
3476
3218
|
}
|
|
3477
|
-
} catch (error) {
|
|
3478
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
3479
|
-
console.error(`\u274C ${message}`);
|
|
3480
|
-
process.exit(1);
|
|
3481
3219
|
}
|
|
3220
|
+
for (const password2 of url.searchParams.getAll("password"))
|
|
3221
|
+
sensitive.add(password2);
|
|
3222
|
+
for (const pair of url.search.slice(1).split("&")) {
|
|
3223
|
+
const separator = pair.indexOf("=");
|
|
3224
|
+
const name = separator === -1 ? pair : pair.slice(0, separator);
|
|
3225
|
+
if (name !== "password") continue;
|
|
3226
|
+
if (separator !== -1) sensitive.add(pair.slice(separator + 1));
|
|
3227
|
+
}
|
|
3228
|
+
} catch {
|
|
3229
|
+
const match = /:\/\/[^/:]+:([^@]+)@/.exec(db);
|
|
3230
|
+
if (match?.[1]) sensitive.add(match[1]);
|
|
3231
|
+
}
|
|
3232
|
+
return [...sensitive].filter((entry) => entry.length > 0).sort((left, right) => right.length - left.length).reduce((text, entry) => text.replaceAll(entry, "<redacted>"), value);
|
|
3233
|
+
}
|
|
3234
|
+
function redactedConvergeResult(result, db) {
|
|
3235
|
+
return result.kind === "partially-applied" || result.kind === "transport-ambiguous" ? { ...result, detail: redactDiagnostic(result.detail, db) } : result;
|
|
3236
|
+
}
|
|
3237
|
+
function redactedResult(result, db) {
|
|
3238
|
+
return {
|
|
3239
|
+
...result,
|
|
3240
|
+
...result.detail === void 0 ? {} : { detail: redactDiagnostic(result.detail, db) },
|
|
3241
|
+
...result.error === void 0 ? {} : { error: redactDiagnostic(result.error, db) },
|
|
3242
|
+
...result.cleanupError === void 0 ? {} : { cleanupError: redactDiagnostic(result.cleanupError, db) },
|
|
3243
|
+
...result.result === void 0 ? {} : { result: redactedConvergeResult(result.result, db) }
|
|
3244
|
+
};
|
|
3245
|
+
}
|
|
3246
|
+
function formatMigrateJson(result, db) {
|
|
3247
|
+
const safe = redactedResult(result, db);
|
|
3248
|
+
const { schemaFile: _schemaFile, ...document } = safe;
|
|
3249
|
+
return document;
|
|
3250
|
+
}
|
|
3251
|
+
function formatMigrateHuman(result, db) {
|
|
3252
|
+
const safe = redactedResult(result, db);
|
|
3253
|
+
const diagnostic = (value) => escapeDiagnosticText5(value);
|
|
3254
|
+
const lines = [`${diagnostic(safe.outcome)}: ${diagnostic(safe.schema)}`];
|
|
3255
|
+
if (safe.applied?.length)
|
|
3256
|
+
for (const applied of safe.applied) lines.push(diagnostic(applied));
|
|
3257
|
+
if (safe.outcome === "partially-applied") {
|
|
3258
|
+
const stepKeys = (entries) => entries?.length ? entries.map(diagnostic).join(", ") : "none";
|
|
3259
|
+
lines.push(`completed: ${stepKeys(safe.completedStepKeys)}`);
|
|
3260
|
+
lines.push(`not started: ${stepKeys(safe.notStartedStepKeys)}`);
|
|
3261
|
+
}
|
|
3262
|
+
if (safe.detail !== void 0) lines.push(diagnostic(safe.detail));
|
|
3263
|
+
if (safe.changes?.length)
|
|
3264
|
+
for (const change of safe.changes)
|
|
3265
|
+
lines.push(diagnostic(JSON.stringify(change)));
|
|
3266
|
+
if (safe.outcome === "recovery-required") {
|
|
3267
|
+
if (safe.runIds?.length) {
|
|
3268
|
+
lines.push("dbsp reconcile --db <database> <run-id>");
|
|
3269
|
+
for (const runId of safe.runIds)
|
|
3270
|
+
lines.push(`run id: ${diagnostic(runId)}`);
|
|
3271
|
+
}
|
|
3272
|
+
if (safe.busyRunIds?.length) {
|
|
3273
|
+
for (const busyRunId of safe.busyRunIds)
|
|
3274
|
+
lines.push(`busy run id: ${diagnostic(busyRunId)}`);
|
|
3275
|
+
lines.push("these runs are still executing; retry later");
|
|
3276
|
+
}
|
|
3277
|
+
if (safe.executionIds?.length) {
|
|
3278
|
+
for (const executionId of safe.executionIds)
|
|
3279
|
+
lines.push(`execution id: ${diagnostic(executionId)}`);
|
|
3280
|
+
lines.push(
|
|
3281
|
+
"no dbsp command resolves a claim by execution id; the owner named in the detail above must resolve these claims"
|
|
3282
|
+
);
|
|
3283
|
+
}
|
|
3284
|
+
}
|
|
3285
|
+
if (safe.outcome === "ledger-absent")
|
|
3286
|
+
lines.push(
|
|
3287
|
+
"dbsp preflight --reinitialize --db <database> --schema-file <schema-file> --scope <schema> --out <adoption-file>",
|
|
3288
|
+
`schema file: ${diagnostic(safe.schemaFile)}`,
|
|
3289
|
+
`schema: ${diagnostic(safe.schema)}`
|
|
3290
|
+
);
|
|
3291
|
+
if (safe.outcome === "cleanup-failed" && safe.result !== void 0) {
|
|
3292
|
+
lines.push(`convergence: ${diagnostic(safe.result.kind)}`);
|
|
3293
|
+
if ("applied" in safe.result && safe.result.applied.length)
|
|
3294
|
+
for (const applied of safe.result.applied)
|
|
3295
|
+
lines.push(diagnostic(applied));
|
|
3296
|
+
}
|
|
3297
|
+
if (safe.error !== void 0) lines.push(diagnostic(safe.error));
|
|
3298
|
+
if (safe.cleanupError !== void 0)
|
|
3299
|
+
lines.push(`Connection cleanup failed: ${diagnostic(safe.cleanupError)}`);
|
|
3300
|
+
return lines.join("\n");
|
|
3301
|
+
}
|
|
3302
|
+
var migrateCommand = new Command6("migrate").description(
|
|
3303
|
+
"Converge without confirmation: applies only additions and declared adoptions admitted by convergePg; refused changes send no DDL, but execution can still be refused or partially applied. Use dbsp plan plus dbsp apply for reviewed changes."
|
|
3304
|
+
).argument("<schema-file>", "Schema DSL file").requiredOption("-d, --db <url>", "Database connection URL (required)").option("--schema <name>", "Database schema name", "public").option(
|
|
3305
|
+
"--external-index <model-table:index>",
|
|
3306
|
+
"Physical index to leave alone; the table is the model name (before dbCasing maps it), then the index by its exact PostgreSQL name; the table part cannot contain :; repeatable.",
|
|
3307
|
+
(value, previous = []) => {
|
|
3308
|
+
previous.push(value);
|
|
3309
|
+
return previous;
|
|
3310
|
+
}
|
|
3311
|
+
).option("--format <format>", "Output format: text or json", "text").addHelpText(
|
|
3312
|
+
"after",
|
|
3313
|
+
`
|
|
3314
|
+
Outcome contract:
|
|
3315
|
+
${MIGRATE_OUTCOME_CONTRACT.map(([outcome, exitCode, description]) => ` ${outcome} (${exitCode}): ${description}`).join("\n")}
|
|
3316
|
+
Command-line syntax errors (an unknown option, a missing --db or schema file) exit 1 with { status, error } under --format json, as for every dbsp command.`
|
|
3317
|
+
).exitOverride().configureOutput({ writeErr: () => {
|
|
3318
|
+
} }).action(
|
|
3319
|
+
async (schemaFile, options) => {
|
|
3320
|
+
const result = await runMigrate(schemaFile, options);
|
|
3321
|
+
if (options.format === "json")
|
|
3322
|
+
printCliJson(formatMigrateJson(result, options.db));
|
|
3323
|
+
else if (result.exitCode === 0)
|
|
3324
|
+
console.log(formatMigrateHuman(result, options.db));
|
|
3325
|
+
else console.error(formatMigrateHuman(result, options.db));
|
|
3326
|
+
process.exitCode = result.exitCode;
|
|
3482
3327
|
}
|
|
3483
3328
|
);
|
|
3484
3329
|
|
|
3485
3330
|
// src/commands/preflight.ts
|
|
3486
|
-
import { createHash as
|
|
3331
|
+
import { createHash as createHash2 } from "crypto";
|
|
3487
3332
|
import { mkdtemp, open, rename, rm } from "fs/promises";
|
|
3488
3333
|
import { dirname as dirname3, join } from "path";
|
|
3489
3334
|
import {
|
|
3490
|
-
|
|
3491
|
-
|
|
3335
|
+
createPgPhysicalModel as createPgPhysicalModel4,
|
|
3336
|
+
escapeDiagnosticText as escapeDiagnosticText6,
|
|
3492
3337
|
runPgReinitializePreflight
|
|
3493
3338
|
} from "@dbsp/adapter-pgsql";
|
|
3494
3339
|
import { declarationSetFromModel as declarationSetFromModel2 } from "@dbsp/core";
|
|
3495
|
-
import { Command as
|
|
3340
|
+
import { Command as Command7 } from "commander";
|
|
3496
3341
|
var defaultDeps = { createDbConnection, loadSchema };
|
|
3497
3342
|
var nodeAdoptionFileSystem = {
|
|
3498
3343
|
mkdtemp,
|
|
@@ -3542,13 +3387,9 @@ async function databaseName(pool) {
|
|
|
3542
3387
|
throw new Error("current database could not be read");
|
|
3543
3388
|
return name;
|
|
3544
3389
|
}
|
|
3545
|
-
function declarationsForScopes(model, database, scopes
|
|
3390
|
+
function declarationsForScopes(model, database, scopes) {
|
|
3546
3391
|
const declarations = scopes.flatMap(
|
|
3547
|
-
(schema) => declarationSetFromModel2(
|
|
3548
|
-
model,
|
|
3549
|
-
{ engine: "postgresql", database, schema },
|
|
3550
|
-
naming
|
|
3551
|
-
).declarations
|
|
3392
|
+
(schema) => declarationSetFromModel2(model, { engine: "postgresql", database, schema }).declarations
|
|
3552
3393
|
);
|
|
3553
3394
|
const seen = /* @__PURE__ */ new Set();
|
|
3554
3395
|
const uniqueDeclarations = declarations.filter((declaration) => {
|
|
@@ -3568,7 +3409,7 @@ function declarationsForScopes(model, database, scopes, naming) {
|
|
|
3568
3409
|
});
|
|
3569
3410
|
return {
|
|
3570
3411
|
version: 1,
|
|
3571
|
-
digest:
|
|
3412
|
+
digest: createHash2("sha256").update(canonical).digest("hex"),
|
|
3572
3413
|
declarations: uniqueDeclarations
|
|
3573
3414
|
};
|
|
3574
3415
|
}
|
|
@@ -3583,19 +3424,25 @@ async function runPreflight(options, deps = defaultDeps) {
|
|
|
3583
3424
|
throw new Error("dbsp preflight requires --reinitialize");
|
|
3584
3425
|
if (!options.out)
|
|
3585
3426
|
throw new Error("dbsp preflight --reinitialize requires --out <file>");
|
|
3586
|
-
|
|
3427
|
+
const physicalSchema = options.scopes[0];
|
|
3428
|
+
if (physicalSchema === void 0)
|
|
3587
3429
|
throw new Error(
|
|
3588
3430
|
"dbsp preflight requires at least one explicit --scope <schema>"
|
|
3589
3431
|
);
|
|
3590
3432
|
const loaded = await deps.loadSchema(options.schemaFile);
|
|
3433
|
+
const physical = createPgPhysicalModel4({
|
|
3434
|
+
mode: "logical",
|
|
3435
|
+
model: loaded.model,
|
|
3436
|
+
schema: physicalSchema,
|
|
3437
|
+
...loaded.dbCasing === void 0 ? {} : { dbCasing: loaded.dbCasing }
|
|
3438
|
+
});
|
|
3591
3439
|
const connection = await deps.createDbConnection(options.db);
|
|
3592
3440
|
try {
|
|
3593
3441
|
const database = await databaseName(connection.pool);
|
|
3594
3442
|
const declarations = declarationsForScopes(
|
|
3595
|
-
|
|
3443
|
+
physical.model,
|
|
3596
3444
|
database,
|
|
3597
|
-
options.scopes
|
|
3598
|
-
getNamingPluginForDbCasing2(loaded.dbCasing ?? "preserve")
|
|
3445
|
+
options.scopes
|
|
3599
3446
|
);
|
|
3600
3447
|
return await runPgReinitializePreflight({
|
|
3601
3448
|
pool: connection.pool,
|
|
@@ -3610,18 +3457,18 @@ async function runPreflight(options, deps = defaultDeps) {
|
|
|
3610
3457
|
function formatScope(scope) {
|
|
3611
3458
|
const name = scope.ledger.scope === "database" ? "dbsp_meta" : scope.ledger.schema ?? "unknown";
|
|
3612
3459
|
if (scope.outcome === "failed")
|
|
3613
|
-
return `${
|
|
3614
|
-
return scope.refusal ? `${
|
|
3460
|
+
return `${escapeDiagnosticText6(name)}: failed (${escapeDiagnosticText6(scope.reason.step)}: ${escapeDiagnosticText6(scope.reason.message)})`;
|
|
3461
|
+
return scope.refusal ? `${escapeDiagnosticText6(name)}: ${escapeDiagnosticText6(scope.outcome)} (${escapeDiagnosticText6(scope.refusal.code)}: ${escapeDiagnosticText6(scope.refusal.detail)})` : `${escapeDiagnosticText6(name)}: ${escapeDiagnosticText6(scope.outcome)}`;
|
|
3615
3462
|
}
|
|
3616
3463
|
function formatReinitializeSplit(scopes) {
|
|
3617
3464
|
const names = (predicate) => scopes.filter(predicate).map(
|
|
3618
|
-
(scope) =>
|
|
3465
|
+
(scope) => escapeDiagnosticText6(
|
|
3619
3466
|
scope.ledger.scope === "database" ? "dbsp_meta" : scope.ledger.schema ?? "unknown"
|
|
3620
3467
|
)
|
|
3621
3468
|
).join(",") || "none";
|
|
3622
3469
|
return `reinitialize-split: changed=${names((scope) => scope.outcome === "current")}; failed=${names((scope) => scope.outcome === "failed")}; not-attempted=${names((scope) => scope.outcome === "not-attempted")}`;
|
|
3623
3470
|
}
|
|
3624
|
-
var preflightCommand = new
|
|
3471
|
+
var preflightCommand = new Command7("preflight").description(
|
|
3625
3472
|
"Run the separately privileged managed-ledger reinitialize cutover"
|
|
3626
3473
|
).requiredOption("-d, --db <url>", "Database connection URL (required)").requiredOption(
|
|
3627
3474
|
"--reinitialize",
|
|
@@ -3646,7 +3493,7 @@ var preflightCommand = new Command6("preflight").description(
|
|
|
3646
3493
|
);
|
|
3647
3494
|
} catch (error) {
|
|
3648
3495
|
console.error(
|
|
3649
|
-
|
|
3496
|
+
escapeDiagnosticText6(
|
|
3650
3497
|
error instanceof Error ? error.message : String(error)
|
|
3651
3498
|
)
|
|
3652
3499
|
);
|
|
@@ -3655,31 +3502,12 @@ var preflightCommand = new Command6("preflight").description(
|
|
|
3655
3502
|
});
|
|
3656
3503
|
|
|
3657
3504
|
// src/commands/reconcile.ts
|
|
3658
|
-
import { createHash as createHash5 } from "crypto";
|
|
3659
3505
|
import {
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
isPgDatabaseReadOnlyError,
|
|
3664
|
-
readPgLedgerAddressChain as readPgLedgerAddressChain3,
|
|
3665
|
-
readPgLedgerReservationsForExecution,
|
|
3666
|
-
readPgLedgerScopeCurrency as readPgLedgerScopeCurrency2,
|
|
3667
|
-
readTransitionJournal as readTransitionJournal2,
|
|
3668
|
-
readVerifiedPgLedgerReservationsForPair,
|
|
3669
|
-
recoverPgReaddressPair,
|
|
3670
|
-
withPgTransitionRunLock as withPgTransitionRunLock2
|
|
3506
|
+
escapeDiagnosticText as escapeDiagnosticText7,
|
|
3507
|
+
PgReconcileTransitionRunError,
|
|
3508
|
+
reconcilePgTransitionRun
|
|
3671
3509
|
} from "@dbsp/adapter-pgsql";
|
|
3672
|
-
import {
|
|
3673
|
-
import {
|
|
3674
|
-
acquireExclusiveTransitionLease as acquireExclusiveTransitionLease2,
|
|
3675
|
-
assumptionAccepted,
|
|
3676
|
-
outcomeClaimId as outcomeClaimId2,
|
|
3677
|
-
projectLedgerChain as projectLedgerChain3,
|
|
3678
|
-
resourceScopeCovers,
|
|
3679
|
-
transitionPlanDigest as transitionPlanDigest3
|
|
3680
|
-
} from "@dbsp/core";
|
|
3681
|
-
import { sameLedgerAddress } from "@dbsp/types";
|
|
3682
|
-
import { Command as Command7 } from "commander";
|
|
3510
|
+
import { Command as Command8 } from "commander";
|
|
3683
3511
|
function pgSqlState(error) {
|
|
3684
3512
|
if (typeof error !== "object" || error === null || !("code" in error))
|
|
3685
3513
|
return void 0;
|
|
@@ -3705,172 +3533,121 @@ function classifyReconcileFailure(error, stage) {
|
|
|
3705
3533
|
if (stage === "journal") return "malformed-journal";
|
|
3706
3534
|
return "catalogue";
|
|
3707
3535
|
}
|
|
3708
|
-
function
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3536
|
+
function isUnresolvedRecoveryOutcome(report) {
|
|
3537
|
+
return [
|
|
3538
|
+
"pending",
|
|
3539
|
+
"blocked",
|
|
3540
|
+
"malformed-chain",
|
|
3541
|
+
"protocol-refused",
|
|
3542
|
+
"transport-ambiguous",
|
|
3543
|
+
"no-open-claim",
|
|
3544
|
+
"indeterminate-appended",
|
|
3545
|
+
"indeterminate-pair"
|
|
3546
|
+
].includes(report.outcome);
|
|
3547
|
+
}
|
|
3548
|
+
function ledgerCurrencyDetail(issue) {
|
|
3549
|
+
return issue.currency.kind === "not-current" && issue.currency.reason === "lineage" ? "ledger lineage mismatch; run dbsp preflight --reinitialize" : `ledger marker ${issue.currency.marker.kind}; run dbsp preflight --reinitialize`;
|
|
3550
|
+
}
|
|
3551
|
+
function refusalForSelectedIssue(issue) {
|
|
3552
|
+
if (issue === void 0) return void 0;
|
|
3553
|
+
if (issue.kind === "ledger-not-current") {
|
|
3554
|
+
const address = issue.affectedAddresses[0];
|
|
3555
|
+
return address === void 0 ? void 0 : preAppendRefusalFor("ERR-03", { address, state: "unknown" });
|
|
3556
|
+
}
|
|
3557
|
+
if (issue.kind === "database-read-only") {
|
|
3558
|
+
const address = issue.affectedAddresses[0];
|
|
3559
|
+
return address === void 0 ? void 0 : preAppendRefusalFor("ERR-07", { address, state: "unknown" });
|
|
3560
|
+
}
|
|
3561
|
+
return preAppendRefusalFor(
|
|
3562
|
+
issue.kind === "catalogue-unavailable" ? "ERR-09" : "ERR-08",
|
|
3563
|
+
{ address: issue.address, state: "unknown" }
|
|
3719
3564
|
);
|
|
3720
|
-
return {
|
|
3721
|
-
value,
|
|
3722
|
-
digest: createHash5("sha256").update(JSON.stringify(value)).digest("hex")
|
|
3723
|
-
};
|
|
3724
3565
|
}
|
|
3725
|
-
function
|
|
3726
|
-
if (
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
|
|
3735
|
-
|
|
3736
|
-
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
|
|
3746
|
-
|
|
3747
|
-
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3566
|
+
function mapRecovery(reports, issue) {
|
|
3567
|
+
if (reports === void 0) return void 0;
|
|
3568
|
+
const markerDetail = issue?.kind === "ledger-not-current" ? ledgerCurrencyDetail(issue) : void 0;
|
|
3569
|
+
return reports.map((report) => ({
|
|
3570
|
+
...report,
|
|
3571
|
+
...markerDetail === void 0 ? {} : { reason: markerDetail },
|
|
3572
|
+
...issue?.kind === "ledger-not-current" ? {
|
|
3573
|
+
refusal: preAppendRefusalFor("ERR-03", {
|
|
3574
|
+
address: report.address,
|
|
3575
|
+
state: "unknown"
|
|
3576
|
+
})
|
|
3577
|
+
} : report.failureCause === "catalogue" ? {
|
|
3578
|
+
refusal: preAppendRefusalFor("ERR-09", {
|
|
3579
|
+
address: report.address,
|
|
3580
|
+
state: "unknown"
|
|
3581
|
+
})
|
|
3582
|
+
} : report.failureCause === "malformed-journal" ? {
|
|
3583
|
+
refusal: preAppendRefusalFor("ERR-08", {
|
|
3584
|
+
address: report.address,
|
|
3585
|
+
state: "unknown"
|
|
3586
|
+
})
|
|
3587
|
+
} : {}
|
|
3588
|
+
}));
|
|
3589
|
+
}
|
|
3590
|
+
function unresolvedRecoveryDetail(reports) {
|
|
3591
|
+
const unresolved = reports.filter(isUnresolvedRecoveryOutcome);
|
|
3592
|
+
if (unresolved.length === 0) return void 0;
|
|
3593
|
+
return unresolved.map(
|
|
3594
|
+
(report) => `${escapeDiagnosticText7(report.address.name)}: ${escapeDiagnosticText7(report.reason ?? report.outcome)}`
|
|
3595
|
+
).join("; ");
|
|
3596
|
+
}
|
|
3597
|
+
function formatReconcileHuman(result) {
|
|
3598
|
+
const line = `${escapeDiagnosticText7(result.outcome)}: ${escapeDiagnosticText7(result.runId)}`;
|
|
3599
|
+
const base = result.refusal ? formatPreAppendRefusalHuman(line, result.refusal) : line;
|
|
3600
|
+
if (result.outcome === "reconcile-completed" || !result.recovery) return base;
|
|
3601
|
+
const unresolved = result.recovery.filter(isUnresolvedRecoveryOutcome);
|
|
3602
|
+
return unresolved.length === 0 ? base : [
|
|
3603
|
+
base,
|
|
3604
|
+
...unresolved.map(
|
|
3605
|
+
(report) => `${escapeDiagnosticText7(report.address.name)}: ${escapeDiagnosticText7(report.outcome)}${report.reason ? `: ${escapeDiagnosticText7(report.reason)}` : ""}`
|
|
3606
|
+
)
|
|
3607
|
+
].join("\n");
|
|
3608
|
+
}
|
|
3609
|
+
function mapAdapterResult(result) {
|
|
3610
|
+
if (result.kind === "busy")
|
|
3759
3611
|
return {
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
failureCause: "transport"
|
|
3612
|
+
outcome: "reconcile-run-unavailable",
|
|
3613
|
+
runId: result.runId,
|
|
3614
|
+
addresses: []
|
|
3764
3615
|
};
|
|
3765
|
-
|
|
3766
|
-
}
|
|
3767
|
-
function readdressRecoveryReport(reservations, result) {
|
|
3768
|
-
const first = reservations[0];
|
|
3769
|
-
if (!first) throw new Error("re-address recovery has no reservation");
|
|
3770
|
-
if (result.kind === "readdress-recovery-refused-pair")
|
|
3616
|
+
if (result.kind === "selection-unavailable")
|
|
3771
3617
|
return {
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
3618
|
+
outcome: "reconcile-claim-selection-unavailable",
|
|
3619
|
+
runId: result.runId,
|
|
3620
|
+
addresses: result.addresses,
|
|
3621
|
+
...result.detail === void 0 ? {} : { detail: result.detail }
|
|
3775
3622
|
};
|
|
3776
|
-
if (result.kind === "
|
|
3623
|
+
if (result.kind === "database-read-only") {
|
|
3624
|
+
const refusal = refusalForSelectedIssue(result.selectedIssue);
|
|
3777
3625
|
return {
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3626
|
+
outcome: "database-read-only",
|
|
3627
|
+
runId: result.runId,
|
|
3628
|
+
addresses: result.addresses,
|
|
3629
|
+
detail: result.selectedIssue.reason,
|
|
3630
|
+
...refusal === void 0 ? {} : { refusal }
|
|
3782
3631
|
};
|
|
3783
|
-
|
|
3632
|
+
}
|
|
3633
|
+
const recovery = mapRecovery(result.recovery, result.selectedIssue);
|
|
3634
|
+
const detail = result.selectedIssue?.kind === "ledger-not-current" ? ledgerCurrencyDetail(result.selectedIssue) : recovery === void 0 ? void 0 : unresolvedRecoveryDetail(recovery);
|
|
3635
|
+
if (result.kind === "unresolved") {
|
|
3636
|
+
const refusal = refusalForSelectedIssue(result.selectedIssue);
|
|
3784
3637
|
return {
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3638
|
+
outcome: "reconcile-unresolved",
|
|
3639
|
+
runId: result.runId,
|
|
3640
|
+
addresses: result.addresses,
|
|
3641
|
+
...detail === void 0 ? {} : { detail },
|
|
3642
|
+
...recovery === void 0 ? {} : { recovery },
|
|
3643
|
+
...refusal === void 0 ? {} : { refusal }
|
|
3790
3644
|
};
|
|
3791
|
-
return {
|
|
3792
|
-
address: first.address,
|
|
3793
|
-
outcome: "pending",
|
|
3794
|
-
reason: result.reason,
|
|
3795
|
-
pairId: result.pairId
|
|
3796
|
-
};
|
|
3797
|
-
}
|
|
3798
|
-
function unresolvedRecoveryDetail(reports) {
|
|
3799
|
-
const unresolved = reports.filter(
|
|
3800
|
-
(report) => report.outcome === "pending" || report.outcome === "blocked" || report.outcome === "malformed-chain" || report.outcome === "protocol-refused" || report.outcome === "transport-ambiguous" || report.outcome === "no-open-claim" || report.outcome === "indeterminate-pair"
|
|
3801
|
-
);
|
|
3802
|
-
if (unresolved.length === 0) return void 0;
|
|
3803
|
-
return unresolved.map(
|
|
3804
|
-
(report) => `${escapeDiagnosticText6(report.address.name)}: ${escapeDiagnosticText6(report.reason ?? report.outcome)}`
|
|
3805
|
-
).join("; ");
|
|
3806
|
-
}
|
|
3807
|
-
function formatReconcileHuman(result) {
|
|
3808
|
-
const line = `${escapeDiagnosticText6(result.outcome)}: ${escapeDiagnosticText6(result.runId)}`;
|
|
3809
|
-
return result.refusal ? formatPreAppendRefusalHuman(line, result.refusal) : line;
|
|
3810
|
-
}
|
|
3811
|
-
function executionIdsForRun(journal) {
|
|
3812
|
-
const executionIds = /* @__PURE__ */ new Set([journal.run.runId]);
|
|
3813
|
-
for (const event of journal.events) {
|
|
3814
|
-
const record2 = event.record;
|
|
3815
|
-
if (event.event === "intent" && "executionId" in record2 && typeof record2.executionId === "string")
|
|
3816
|
-
executionIds.add(record2.executionId);
|
|
3817
|
-
if (event.event === "observed" && "intent" in record2 && record2.intent && typeof record2.intent.executionId === "string")
|
|
3818
|
-
executionIds.add(record2.intent.executionId);
|
|
3819
3645
|
}
|
|
3820
|
-
if ("generator" in journal.plan)
|
|
3821
|
-
executionIds.add(`dbsp.generator.execution.${journal.run.runId}`);
|
|
3822
|
-
return [...executionIds];
|
|
3823
|
-
}
|
|
3824
|
-
function statementBundleDigest(statements) {
|
|
3825
|
-
return createHash5("sha256").update(
|
|
3826
|
-
JSON.stringify(statements.map(({ ordinal, sql }) => [ordinal, sql]))
|
|
3827
|
-
).digest("hex");
|
|
3828
|
-
}
|
|
3829
|
-
function recoveryEvidenceForClaim(input) {
|
|
3830
|
-
if (transitionPlanDigest3(input.journal.plan) !== input.journal.run.planDigest)
|
|
3831
|
-
return void 0;
|
|
3832
|
-
const step = input.journal.plan.steps.find(
|
|
3833
|
-
(candidate) => candidate.managedClaim?.plannedClaimKey === input.plannedClaimKey
|
|
3834
|
-
);
|
|
3835
|
-
const claim = step?.managedClaim;
|
|
3836
|
-
if (!step || !claim || !sameLedgerAddress(claim.address, input.row.address) || outcomeClaimId2(
|
|
3837
|
-
input.row.executionId,
|
|
3838
|
-
input.plannedClaimKey,
|
|
3839
|
-
input.row.address
|
|
3840
|
-
) !== input.row.rootClaimId)
|
|
3841
|
-
return void 0;
|
|
3842
|
-
const binding = step.guards.find(
|
|
3843
|
-
(guard) => guard.protocol.binding.kind === "external-ddl-exclusion" && resourceScopeCovers(guard.protocol.binding.scope, [input.row.address])
|
|
3844
|
-
)?.protocol.binding;
|
|
3845
|
-
if (binding?.kind !== "external-ddl-exclusion") return void 0;
|
|
3846
|
-
const assumption = input.journal.plan.assumptions.find(
|
|
3847
|
-
(candidate) => candidate.id === binding.assumption && candidate.class === "external-ddl-exclusion" && step.restsOnAssumptions.includes(candidate.id) && resourceScopeCovers(candidate.scope, [input.row.address])
|
|
3848
|
-
);
|
|
3849
|
-
if (!assumption) return void 0;
|
|
3850
|
-
const accepted = input.journal.authorizations?.some((authorization) => {
|
|
3851
|
-
const grant = authorization.grants.find(
|
|
3852
|
-
(candidate) => candidate.assumptionId === assumption.id
|
|
3853
|
-
);
|
|
3854
|
-
if (!grant) return false;
|
|
3855
|
-
const acceptance2 = authorization.policy[grant.grant];
|
|
3856
|
-
return acceptance2 !== void 0 && assumptionAccepted(assumption, { accepts: [acceptance2] });
|
|
3857
|
-
});
|
|
3858
|
-
if (!accepted) return void 0;
|
|
3859
|
-
const bundleDigest = statementBundleDigest(claim.statementBundle.statements);
|
|
3860
3646
|
return {
|
|
3861
|
-
|
|
3862
|
-
|
|
3863
|
-
|
|
3864
|
-
|
|
3865
|
-
plannedClaimKey: input.plannedClaimKey,
|
|
3866
|
-
admittedBundleDigest: bundleDigest,
|
|
3867
|
-
persistedBundleDigest: bundleDigest,
|
|
3868
|
-
recordedPreState: input.stableStateBeforeClaim,
|
|
3869
|
-
externalDdlExclusion: {
|
|
3870
|
-
planDigest: input.journal.run.planDigest,
|
|
3871
|
-
address: input.row.address,
|
|
3872
|
-
trustRoot: JSON.stringify(assumption.asserter)
|
|
3873
|
-
}
|
|
3647
|
+
outcome: "reconcile-completed",
|
|
3648
|
+
runId: result.runId,
|
|
3649
|
+
addresses: result.addresses,
|
|
3650
|
+
...recovery === void 0 ? {} : { recovery }
|
|
3874
3651
|
};
|
|
3875
3652
|
}
|
|
3876
3653
|
async function runReconcile(runId, options, pool) {
|
|
@@ -3885,303 +3662,28 @@ async function runReconcile(runId, options, pool) {
|
|
|
3885
3662
|
failureCause: classifyReconcileFailure(error, "reconcile")
|
|
3886
3663
|
};
|
|
3887
3664
|
}
|
|
3888
|
-
let stage = "reconcile";
|
|
3889
3665
|
try {
|
|
3890
|
-
|
|
3891
|
-
owned,
|
|
3892
|
-
runId,
|
|
3893
|
-
async (target) => {
|
|
3894
|
-
const lease = await acquireExclusiveTransitionLease2(target);
|
|
3895
|
-
try {
|
|
3896
|
-
stage = "journal";
|
|
3897
|
-
const journal = await readTransitionJournal2(lease.session, runId, {
|
|
3898
|
-
ensure: false
|
|
3899
|
-
});
|
|
3900
|
-
stage = "reconcile";
|
|
3901
|
-
const material = journal.plan.steps.map((step) => {
|
|
3902
|
-
const generatedStep = step;
|
|
3903
|
-
const generated = generatedStep.address;
|
|
3904
|
-
return step.managedClaim ?? (generated === void 0 ? void 0 : {
|
|
3905
|
-
address: generated,
|
|
3906
|
-
...generatedStep.plannedClaimKeys?.[0] === void 0 ? {} : {
|
|
3907
|
-
plannedClaimKey: generatedStep.plannedClaimKeys[0]
|
|
3908
|
-
}
|
|
3909
|
-
});
|
|
3910
|
-
}).filter(
|
|
3911
|
-
(claim) => claim !== void 0
|
|
3912
|
-
);
|
|
3913
|
-
try {
|
|
3914
|
-
await assertPgDatabaseWritable(lease.session);
|
|
3915
|
-
} catch (error) {
|
|
3916
|
-
if (isPgDatabaseReadOnlyError(error))
|
|
3917
|
-
return {
|
|
3918
|
-
kind: "database-read-only",
|
|
3919
|
-
detail: error.message,
|
|
3920
|
-
addresses: material.map((claim) => claim.address)
|
|
3921
|
-
};
|
|
3922
|
-
throw error;
|
|
3923
|
-
}
|
|
3924
|
-
if (material.length === 0)
|
|
3925
|
-
return {
|
|
3926
|
-
kind: "selection-unavailable",
|
|
3927
|
-
addresses: journal.plan.declarations?.declarations.map(
|
|
3928
|
-
(declaration) => declaration.address
|
|
3929
|
-
) ?? []
|
|
3930
|
-
};
|
|
3931
|
-
const homes = /* @__PURE__ */ new Map();
|
|
3932
|
-
for (const claim of material) {
|
|
3933
|
-
const home2 = ledgerHome(claim.address);
|
|
3934
|
-
homes.set(`${home2.scope}:${home2.schema ?? ""}`, home2);
|
|
3935
|
-
}
|
|
3936
|
-
for (const home2 of homes.values()) {
|
|
3937
|
-
const currency = await readPgLedgerScopeCurrency2(
|
|
3938
|
-
lease.session,
|
|
3939
|
-
home2
|
|
3940
|
-
);
|
|
3941
|
-
if (currency.kind === "current") continue;
|
|
3942
|
-
const detail = currency.kind === "not-current" && currency.reason === "lineage" ? "ledger lineage mismatch; run dbsp preflight --reinitialize" : `ledger marker ${currency.marker.kind}; run dbsp preflight --reinitialize`;
|
|
3943
|
-
const affected = material.filter((claim) => {
|
|
3944
|
-
const claimHome = ledgerHome(claim.address);
|
|
3945
|
-
return claimHome.scope === home2.scope && claimHome.schema === home2.schema;
|
|
3946
|
-
});
|
|
3947
|
-
return {
|
|
3948
|
-
kind: "unresolved",
|
|
3949
|
-
addresses: affected.map((claim) => claim.address),
|
|
3950
|
-
detail,
|
|
3951
|
-
recovery: affected.map((claim) => ({
|
|
3952
|
-
address: claim.address,
|
|
3953
|
-
outcome: "blocked",
|
|
3954
|
-
reason: detail,
|
|
3955
|
-
refusal: preAppendRefusalFor("ERR-03", {
|
|
3956
|
-
address: claim.address,
|
|
3957
|
-
state: "unknown"
|
|
3958
|
-
})
|
|
3959
|
-
}))
|
|
3960
|
-
};
|
|
3961
|
-
}
|
|
3962
|
-
const executionIds = executionIdsForRun(journal);
|
|
3963
|
-
const reservations = (await Promise.all(
|
|
3964
|
-
[...homes.values()].flatMap(
|
|
3965
|
-
(home2) => executionIds.map(
|
|
3966
|
-
(executionId) => readPgLedgerReservationsForExecution(
|
|
3967
|
-
lease.session,
|
|
3968
|
-
home2,
|
|
3969
|
-
executionId
|
|
3970
|
-
)
|
|
3971
|
-
)
|
|
3972
|
-
)
|
|
3973
|
-
)).flat();
|
|
3974
|
-
if (reservations.length === 0)
|
|
3975
|
-
return {
|
|
3976
|
-
kind: "selection-unavailable",
|
|
3977
|
-
addresses: material.map((x) => x.address)
|
|
3978
|
-
};
|
|
3979
|
-
const readdressPairs = /* @__PURE__ */ new Map();
|
|
3980
|
-
const byRoot = /* @__PURE__ */ new Map();
|
|
3981
|
-
for (const row of reservations) {
|
|
3982
|
-
if (row.claimKind === "readdress-intent" && row.pairId !== void 0) {
|
|
3983
|
-
const pairKey = `${row.executionId}:${row.pairId}`;
|
|
3984
|
-
readdressPairs.set(pairKey, [
|
|
3985
|
-
...readdressPairs.get(pairKey) ?? [],
|
|
3986
|
-
row
|
|
3987
|
-
]);
|
|
3988
|
-
continue;
|
|
3989
|
-
}
|
|
3990
|
-
byRoot.set(row.rootClaimId, [
|
|
3991
|
-
...byRoot.get(row.rootClaimId) ?? [],
|
|
3992
|
-
row
|
|
3993
|
-
]);
|
|
3994
|
-
}
|
|
3995
|
-
const recovery = [];
|
|
3996
|
-
for (const [_pairKey, pairRows] of readdressPairs) {
|
|
3997
|
-
const pairId = pairRows[0]?.pairId;
|
|
3998
|
-
const executionId = pairRows[0]?.executionId;
|
|
3999
|
-
if (!pairId || !executionId) continue;
|
|
4000
|
-
const readPairInSelectedHomes = readVerifiedPgLedgerReservationsForPair;
|
|
4001
|
-
const closure = await readPairInSelectedHomes(
|
|
4002
|
-
lease.session,
|
|
4003
|
-
pairId,
|
|
4004
|
-
[...homes.values()]
|
|
4005
|
-
);
|
|
4006
|
-
const recovered = await recoverPgReaddressPair(lease.session, {
|
|
4007
|
-
pairId,
|
|
4008
|
-
executionId,
|
|
4009
|
-
reservations: closure
|
|
4010
|
-
});
|
|
4011
|
-
recovery.push(readdressRecoveryReport(closure, recovered));
|
|
4012
|
-
}
|
|
4013
|
-
for (const rows of byRoot.values()) {
|
|
4014
|
-
const rootClaimId = rows[0]?.rootClaimId;
|
|
4015
|
-
if (!rootClaimId) continue;
|
|
4016
|
-
if (rows.some(
|
|
4017
|
-
(row) => !executionIds.includes(row.executionId) || row.rootClaimId !== rootClaimId
|
|
4018
|
-
))
|
|
4019
|
-
return {
|
|
4020
|
-
kind: "selection-unavailable",
|
|
4021
|
-
addresses: rows.map((row) => row.address),
|
|
4022
|
-
detail: `reservation disagreement for root claim ${rootClaimId}`
|
|
4023
|
-
};
|
|
4024
|
-
const rootCandidates = [];
|
|
4025
|
-
for (const row of rows) {
|
|
4026
|
-
stage = "catalogue";
|
|
4027
|
-
const chain = await readPgLedgerAddressChain3(
|
|
4028
|
-
lease.session,
|
|
4029
|
-
ledgerHome(row.address),
|
|
4030
|
-
row.address
|
|
4031
|
-
);
|
|
4032
|
-
stage = "reconcile";
|
|
4033
|
-
const projection = projectLedgerChain3(chain);
|
|
4034
|
-
if (projection.kind !== "projected-ledger-chain" || projection.openClaim === void 0)
|
|
4035
|
-
continue;
|
|
4036
|
-
const open2 = projection.openClaim.event;
|
|
4037
|
-
const openRoot = open2.rootClaimId ?? open2.eventId;
|
|
4038
|
-
if (open2.executionId !== row.executionId || openRoot !== rootClaimId)
|
|
4039
|
-
return {
|
|
4040
|
-
kind: "selection-unavailable",
|
|
4041
|
-
addresses: rows.map((item) => item.address),
|
|
4042
|
-
detail: `open chain member disagrees with reservation root ${rootClaimId}`
|
|
4043
|
-
};
|
|
4044
|
-
if (open2.eventId === rootClaimId)
|
|
4045
|
-
rootCandidates.push({
|
|
4046
|
-
row,
|
|
4047
|
-
stableStateBeforeClaim: projection.openClaim.stableStateBeforeClaim,
|
|
4048
|
-
...open2.plannedClaimKey === void 0 ? {} : { plannedClaimKey: open2.plannedClaimKey }
|
|
4049
|
-
});
|
|
4050
|
-
}
|
|
4051
|
-
if (rootCandidates.length !== 1)
|
|
4052
|
-
return {
|
|
4053
|
-
kind: "selection-unavailable",
|
|
4054
|
-
addresses: rows.map((row) => row.address),
|
|
4055
|
-
detail: `execution ${runId} has ${rootCandidates.length} open root members for ${rootClaimId}`
|
|
4056
|
-
};
|
|
4057
|
-
const selected = rootCandidates[0];
|
|
4058
|
-
if (!selected) continue;
|
|
4059
|
-
if (!selected.plannedClaimKey || !material.some(
|
|
4060
|
-
(claim) => claim.plannedClaimKey === selected.plannedClaimKey
|
|
4061
|
-
))
|
|
4062
|
-
return {
|
|
4063
|
-
kind: "selection-unavailable",
|
|
4064
|
-
addresses: rows.map((row) => row.address),
|
|
4065
|
-
detail: `open root ${rootClaimId} has no matching persisted managed step`
|
|
4066
|
-
};
|
|
4067
|
-
const step = journal.plan.steps.find(
|
|
4068
|
-
(candidate) => candidate.managedClaim?.plannedClaimKey === selected.plannedClaimKey || "plannedClaimKeys" in candidate && Array.isArray(candidate.plannedClaimKeys) && candidate.plannedClaimKeys.includes(
|
|
4069
|
-
selected.plannedClaimKey
|
|
4070
|
-
)
|
|
4071
|
-
);
|
|
4072
|
-
const operationReadBack = step?.operation?.operationKind.name === "CreateUniqueIndexConcurrently" ? async (executor, _address, identity) => {
|
|
4073
|
-
await assertCreateUniqueIndexConcurrentlyRecoveryNotInvalid(
|
|
4074
|
-
executor,
|
|
4075
|
-
step.operation
|
|
4076
|
-
);
|
|
4077
|
-
return {
|
|
4078
|
-
observed: recoveryPayload(identity),
|
|
4079
|
-
effect: "unverifiable"
|
|
4080
|
-
};
|
|
4081
|
-
} : void 0;
|
|
4082
|
-
const indeterminateEvidence = recoveryEvidenceForClaim({
|
|
4083
|
-
journal,
|
|
4084
|
-
row: selected.row,
|
|
4085
|
-
plannedClaimKey: selected.plannedClaimKey,
|
|
4086
|
-
stableStateBeforeClaim: selected.stableStateBeforeClaim
|
|
4087
|
-
});
|
|
4088
|
-
for (const row of rows) {
|
|
4089
|
-
const isRoot = sameLedgerAddress(
|
|
4090
|
-
row.address,
|
|
4091
|
-
selected.row.address
|
|
4092
|
-
);
|
|
4093
|
-
const recovered = await recoverPgOutcomeClaim(owned, {
|
|
4094
|
-
address: row.address,
|
|
4095
|
-
reservations: [row],
|
|
4096
|
-
resolutionEventId: isRoot ? `${rootClaimId}:reconcile:${runId}` : `${rootClaimId}:reconcile:${runId}:${row.address.kind}:${row.address.name}`,
|
|
4097
|
-
acceptedExternalDdlExclusion: isRoot && indeterminateEvidence !== void 0,
|
|
4098
|
-
resolveIndeterminate: true,
|
|
4099
|
-
readBack: async (_executor, _address, identity) => recoveryPayload(identity),
|
|
4100
|
-
...isRoot && indeterminateEvidence !== void 0 ? { indeterminateEvidence } : {},
|
|
4101
|
-
...isRoot && operationReadBack !== void 0 ? { operationReadBack } : {}
|
|
4102
|
-
});
|
|
4103
|
-
recovery.push(recoveryReport(row.address, recovered));
|
|
4104
|
-
}
|
|
4105
|
-
}
|
|
4106
|
-
const unresolved = unresolvedRecoveryDetail(recovery);
|
|
4107
|
-
if (unresolved)
|
|
4108
|
-
return {
|
|
4109
|
-
kind: "unresolved",
|
|
4110
|
-
addresses: reservations.map((item) => item.address),
|
|
4111
|
-
detail: unresolved,
|
|
4112
|
-
recovery
|
|
4113
|
-
};
|
|
4114
|
-
return {
|
|
4115
|
-
kind: "completed",
|
|
4116
|
-
addresses: reservations.map((item) => item.address),
|
|
4117
|
-
recovery
|
|
4118
|
-
};
|
|
4119
|
-
} finally {
|
|
4120
|
-
await lease.release();
|
|
4121
|
-
}
|
|
4122
|
-
}
|
|
4123
|
-
);
|
|
4124
|
-
if (locked.kind === "busy")
|
|
4125
|
-
return { outcome: "reconcile-run-unavailable", runId, addresses: [] };
|
|
4126
|
-
if (locked.value.kind === "selection-unavailable")
|
|
4127
|
-
return {
|
|
4128
|
-
outcome: "reconcile-claim-selection-unavailable",
|
|
4129
|
-
runId,
|
|
4130
|
-
addresses: locked.value.addresses,
|
|
4131
|
-
...locked.value.detail ? { detail: locked.value.detail } : {}
|
|
4132
|
-
};
|
|
4133
|
-
if (locked.value.kind === "database-read-only") {
|
|
4134
|
-
const address = locked.value.addresses[0];
|
|
4135
|
-
return {
|
|
4136
|
-
outcome: "database-read-only",
|
|
4137
|
-
runId,
|
|
4138
|
-
addresses: locked.value.addresses,
|
|
4139
|
-
detail: locked.value.detail,
|
|
4140
|
-
...address === void 0 ? {} : {
|
|
4141
|
-
refusal: preAppendRefusalFor("ERR-07", {
|
|
4142
|
-
address,
|
|
4143
|
-
state: "unknown"
|
|
4144
|
-
})
|
|
4145
|
-
}
|
|
4146
|
-
};
|
|
4147
|
-
}
|
|
4148
|
-
if (locked.value.kind === "unresolved") {
|
|
4149
|
-
const address = locked.value.addresses[0];
|
|
4150
|
-
const markerRefusal = address !== void 0 && locked.value.detail?.includes("run dbsp preflight --reinitialize") ? preAppendRefusalFor("ERR-03", { address, state: "unknown" }) : void 0;
|
|
4151
|
-
const recoveryRefusal = locked.value.recovery?.find(
|
|
4152
|
-
(report) => report.refusal !== void 0
|
|
4153
|
-
)?.refusal;
|
|
4154
|
-
const refusal = markerRefusal ?? recoveryRefusal;
|
|
4155
|
-
return {
|
|
4156
|
-
outcome: "reconcile-unresolved",
|
|
4157
|
-
runId,
|
|
4158
|
-
addresses: locked.value.addresses,
|
|
4159
|
-
...locked.value.detail ? { detail: locked.value.detail } : {},
|
|
4160
|
-
...locked.value.recovery ? { recovery: locked.value.recovery } : {},
|
|
4161
|
-
...refusal === void 0 ? {} : { refusal }
|
|
4162
|
-
};
|
|
4163
|
-
}
|
|
4164
|
-
return {
|
|
4165
|
-
outcome: "reconcile-completed",
|
|
4166
|
-
runId,
|
|
4167
|
-
addresses: locked.value.addresses,
|
|
4168
|
-
...locked.value.recovery ? { recovery: locked.value.recovery } : {}
|
|
4169
|
-
};
|
|
3666
|
+
return mapAdapterResult(await reconcilePgTransitionRun(owned, runId));
|
|
4170
3667
|
} catch (error) {
|
|
3668
|
+
const typed = error instanceof PgReconcileTransitionRunError ? error : void 0;
|
|
3669
|
+
const cause = typed?.originalCause ?? error;
|
|
4171
3670
|
return {
|
|
4172
3671
|
outcome: "reconcile-run-unavailable",
|
|
4173
3672
|
runId,
|
|
4174
3673
|
addresses: [],
|
|
4175
|
-
detail:
|
|
4176
|
-
|
|
3674
|
+
detail: escapeDiagnosticText7(
|
|
3675
|
+
cause instanceof Error ? cause.message : String(cause)
|
|
4177
3676
|
),
|
|
4178
|
-
failureCause: classifyReconcileFailure(
|
|
3677
|
+
failureCause: classifyReconcileFailure(
|
|
3678
|
+
cause,
|
|
3679
|
+
typed?.stage ?? "reconcile"
|
|
3680
|
+
)
|
|
4179
3681
|
};
|
|
4180
3682
|
} finally {
|
|
4181
3683
|
if (pool === void 0) await owned.end();
|
|
4182
3684
|
}
|
|
4183
3685
|
}
|
|
4184
|
-
var reconcileCommand = new
|
|
3686
|
+
var reconcileCommand = new Command8("reconcile").description("Resolve this run\u2019s open managed claims from live evidence only").argument("<run-id>", "Durable run identifier").requiredOption("-d, --db <url>", "Database connection URL (required)").option("--format <format>", "Output format: text or json", "text").action(async (runId, options) => {
|
|
4185
3687
|
const result = await runReconcile(runId, options);
|
|
4186
3688
|
if (options.format === "json") printCliJson(result);
|
|
4187
3689
|
else console.log(formatReconcileHuman(result));
|
|
@@ -4190,25 +3692,25 @@ var reconcileCommand = new Command7("reconcile").description("Resolve this run\u
|
|
|
4190
3692
|
|
|
4191
3693
|
// src/commands/recover.ts
|
|
4192
3694
|
import {
|
|
4193
|
-
assertPgDatabaseWritable
|
|
4194
|
-
createPgTransitionPack as
|
|
4195
|
-
isPgDatabaseReadOnlyError
|
|
3695
|
+
assertPgDatabaseWritable,
|
|
3696
|
+
createPgTransitionPack as createPgTransitionPack2,
|
|
3697
|
+
isPgDatabaseReadOnlyError,
|
|
4196
3698
|
preparePgRecoveryAdmission,
|
|
4197
|
-
readPgLedgerScopeCurrency
|
|
3699
|
+
readPgLedgerScopeCurrency,
|
|
4198
3700
|
readPgObservationContextFromLessor as readPgObservationContextFromLessor2,
|
|
4199
|
-
readTransitionJournal as
|
|
4200
|
-
withPgTransitionRunLock as
|
|
3701
|
+
readTransitionJournal as readTransitionJournal2,
|
|
3702
|
+
withPgTransitionRunLock as withPgTransitionRunLock2
|
|
4201
3703
|
} from "@dbsp/adapter-pgsql";
|
|
4202
3704
|
import {
|
|
4203
3705
|
acquireTransitionTargetLease,
|
|
4204
|
-
assumptionAccepted
|
|
4205
|
-
createApplier
|
|
4206
|
-
createPackRegistry as
|
|
3706
|
+
assumptionAccepted,
|
|
3707
|
+
createApplier,
|
|
3708
|
+
createPackRegistry as createPackRegistry2,
|
|
4207
3709
|
loadVerifiedRecoveryJournal,
|
|
4208
|
-
transitionPlanDigest as
|
|
3710
|
+
transitionPlanDigest as transitionPlanDigest3,
|
|
4209
3711
|
validateTransitionRelationalInvariants
|
|
4210
3712
|
} from "@dbsp/core";
|
|
4211
|
-
import { Command as
|
|
3713
|
+
import { Command as Command9 } from "commander";
|
|
4212
3714
|
var RECOVER_OUTCOME_CONTRACT = [
|
|
4213
3715
|
["completed", 0, "all attempted steps were durably reconciled"],
|
|
4214
3716
|
["recovery-resume-required", 40, "remaining work requires a new proof"],
|
|
@@ -4297,7 +3799,7 @@ function validateRecoveryAuthorization(journal) {
|
|
|
4297
3799
|
if (journal.plan.assumptions.every((assumption) => {
|
|
4298
3800
|
const grant = grants.get(assumption.id);
|
|
4299
3801
|
const acceptedGrant = grant === void 0 ? void 0 : accepts[grant];
|
|
4300
|
-
return grant !== void 0 && grant >= 0 && grant < accepts.length && acceptedGrant !== void 0 &&
|
|
3802
|
+
return grant !== void 0 && grant >= 0 && grant < accepts.length && acceptedGrant !== void 0 && assumptionAccepted(assumption, {
|
|
4301
3803
|
accepts: [acceptedGrant]
|
|
4302
3804
|
});
|
|
4303
3805
|
}))
|
|
@@ -4308,7 +3810,7 @@ function validateRecoveryAuthorization(journal) {
|
|
|
4308
3810
|
return { ok: false, outcome: "recovery-authorization-invalid" };
|
|
4309
3811
|
}
|
|
4310
3812
|
function recoveryPolicyForJournal(loaded, runId) {
|
|
4311
|
-
if (loaded.run.runId !== runId || loaded.run.planDigest !==
|
|
3813
|
+
if (loaded.run.runId !== runId || loaded.run.planDigest !== transitionPlanDigest3(loaded.plan) || !validateTransitionRelationalInvariants({ kind: "plan", plan: loaded.plan }).ok)
|
|
4312
3814
|
return { ok: false, outcome: "recovery-plan-invalid" };
|
|
4313
3815
|
return validateRecoveryAuthorization(loaded);
|
|
4314
3816
|
}
|
|
@@ -4336,12 +3838,12 @@ function outcomeForRecoveryResult(result) {
|
|
|
4336
3838
|
async function load(target, runId) {
|
|
4337
3839
|
const lease = await acquireTransitionTargetLease(target);
|
|
4338
3840
|
try {
|
|
4339
|
-
return await
|
|
3841
|
+
return await readTransitionJournal2(lease.session, runId, { ensure: false });
|
|
4340
3842
|
} finally {
|
|
4341
3843
|
await lease.release();
|
|
4342
3844
|
}
|
|
4343
3845
|
}
|
|
4344
|
-
function
|
|
3846
|
+
function ledgerHome(address) {
|
|
4345
3847
|
if (address.scope === "database") return { scope: "database" };
|
|
4346
3848
|
if (!address.schema)
|
|
4347
3849
|
throw new Error(
|
|
@@ -4359,24 +3861,24 @@ function recoveryAddresses(journal) {
|
|
|
4359
3861
|
return address === void 0 ? [] : [address];
|
|
4360
3862
|
});
|
|
4361
3863
|
}
|
|
4362
|
-
function formatLedgerHome(
|
|
4363
|
-
return
|
|
3864
|
+
function formatLedgerHome(home) {
|
|
3865
|
+
return home.scope === "database" ? "database" : `schema ${home.schema}`;
|
|
4364
3866
|
}
|
|
4365
3867
|
async function recoverMarkerRefusal(target, journal) {
|
|
4366
3868
|
const addresses = recoveryAddresses(journal);
|
|
4367
3869
|
const homes = [];
|
|
4368
3870
|
for (const address of addresses) {
|
|
4369
|
-
const
|
|
4370
|
-
if (homes.some((candidate) => sameLedgerHome(candidate.home,
|
|
3871
|
+
const home = ledgerHome(address);
|
|
3872
|
+
if (homes.some((candidate) => sameLedgerHome(candidate.home, home)))
|
|
4371
3873
|
continue;
|
|
4372
|
-
homes.push({ address, home
|
|
3874
|
+
homes.push({ address, home });
|
|
4373
3875
|
}
|
|
4374
3876
|
const lease = await acquireTransitionTargetLease(target);
|
|
4375
3877
|
try {
|
|
4376
|
-
for (const { address, home
|
|
4377
|
-
const currency = await
|
|
3878
|
+
for (const { address, home } of homes) {
|
|
3879
|
+
const currency = await readPgLedgerScopeCurrency(lease.session, home);
|
|
4378
3880
|
if (currency.kind === "current") continue;
|
|
4379
|
-
const detail = currency.kind === "not-current" && currency.reason === "lineage" ? `ledger lineage mismatch in ${formatLedgerHome(
|
|
3881
|
+
const detail = currency.kind === "not-current" && currency.reason === "lineage" ? `ledger lineage mismatch in ${formatLedgerHome(home)}; run dbsp preflight --reinitialize` : `ledger marker ${currency.marker.kind} in ${formatLedgerHome(home)}; run dbsp preflight --reinitialize`;
|
|
4380
3882
|
return {
|
|
4381
3883
|
detail,
|
|
4382
3884
|
refusal: preAppendRefusalFor("ERR-03", {
|
|
@@ -4396,7 +3898,7 @@ async function runRecover(runId, options, pool) {
|
|
|
4396
3898
|
const owned = pool === void 0 ? (await createDbConnection(options.db)).pool : pool;
|
|
4397
3899
|
let result;
|
|
4398
3900
|
try {
|
|
4399
|
-
const locked = await
|
|
3901
|
+
const locked = await withPgTransitionRunLock2(
|
|
4400
3902
|
owned,
|
|
4401
3903
|
runId,
|
|
4402
3904
|
async (target) => {
|
|
@@ -4419,9 +3921,9 @@ async function runRecover(runId, options, pool) {
|
|
|
4419
3921
|
}
|
|
4420
3922
|
const writabilityLease = await acquireTransitionTargetLease(target);
|
|
4421
3923
|
try {
|
|
4422
|
-
await
|
|
3924
|
+
await assertPgDatabaseWritable(writabilityLease.session);
|
|
4423
3925
|
} catch (error) {
|
|
4424
|
-
if (
|
|
3926
|
+
if (isPgDatabaseReadOnlyError(error))
|
|
4425
3927
|
return {
|
|
4426
3928
|
outcome: "database-read-only",
|
|
4427
3929
|
detail: error.message
|
|
@@ -4442,8 +3944,8 @@ async function runRecover(runId, options, pool) {
|
|
|
4442
3944
|
const authorization = loaded.journal.events.length === 0 ? void 0 : recoveryPolicyForJournal(loaded.journal, runId);
|
|
4443
3945
|
if (authorization && !authorization.ok)
|
|
4444
3946
|
return { outcome: authorization.outcome };
|
|
4445
|
-
const result2 = await
|
|
4446
|
-
|
|
3947
|
+
const result2 = await createApplier(
|
|
3948
|
+
createPackRegistry2([createPgTransitionPack2({})]),
|
|
4447
3949
|
{ persist: async () => void 0 }
|
|
4448
3950
|
).resume(
|
|
4449
3951
|
loaded.journal,
|
|
@@ -4477,7 +3979,7 @@ async function runRecover(runId, options, pool) {
|
|
|
4477
3979
|
}
|
|
4478
3980
|
return result;
|
|
4479
3981
|
}
|
|
4480
|
-
var recoverCommand = new
|
|
3982
|
+
var recoverCommand = new Command9("recover").description(
|
|
4481
3983
|
"Observe and classify an attempted durable run; performs no planned target DDL and does not re-authorize assumptions"
|
|
4482
3984
|
).argument("<run-id>", "Durable run identifier").requiredOption("-d, --db <url>", "Database connection URL (required)").requiredOption(
|
|
4483
3985
|
"--plan-digest <sha>",
|
|
@@ -4513,11 +4015,11 @@ ${RECOVER_OUTCOME_CONTRACT.map(([outcome, code, description]) => ` ${outcome} -
|
|
|
4513
4015
|
// src/commands/release.ts
|
|
4514
4016
|
import {
|
|
4515
4017
|
createPgTransitionLessor as createPgTransitionLessor5,
|
|
4516
|
-
escapeDiagnosticText as
|
|
4018
|
+
escapeDiagnosticText as escapeDiagnosticText8,
|
|
4517
4019
|
releasePgManagedAddress
|
|
4518
4020
|
} from "@dbsp/adapter-pgsql";
|
|
4519
4021
|
import { acquireTransitionLease as acquireTransitionLease5 } from "@dbsp/core";
|
|
4520
|
-
import { Command as
|
|
4022
|
+
import { Command as Command10 } from "commander";
|
|
4521
4023
|
async function runRelease(selector, options) {
|
|
4522
4024
|
const { pool } = await createDbConnection(options.db);
|
|
4523
4025
|
const schema = options.schema ?? "public";
|
|
@@ -4552,18 +4054,18 @@ async function runRelease(selector, options) {
|
|
|
4552
4054
|
function formatReleaseHuman(result) {
|
|
4553
4055
|
if (result.outcome === "released") return "released";
|
|
4554
4056
|
if (result.outcome === "database-read-only")
|
|
4555
|
-
return `database-read-only: ${
|
|
4057
|
+
return `database-read-only: ${escapeDiagnosticText8(result.detail)}`;
|
|
4556
4058
|
if (result.outcome === "release-unavailable")
|
|
4557
|
-
return `release-unavailable: ${
|
|
4059
|
+
return `release-unavailable: ${escapeDiagnosticText8(result.detail)}`;
|
|
4558
4060
|
return [
|
|
4559
|
-
`release-refused: ${
|
|
4560
|
-
`address: ${
|
|
4561
|
-
`state: ${
|
|
4562
|
-
`withheld authority: ${
|
|
4563
|
-
`resolving command: ${
|
|
4061
|
+
`release-refused: ${escapeDiagnosticText8(result.detail)}`,
|
|
4062
|
+
`address: ${escapeDiagnosticText8(JSON.stringify(result.address))}`,
|
|
4063
|
+
`state: ${escapeDiagnosticText8(result.refusal.state)}`,
|
|
4064
|
+
`withheld authority: ${escapeDiagnosticText8(result.refusal.withheldAuthority)}`,
|
|
4065
|
+
`resolving command: ${escapeDiagnosticText8(result.refusal.resolvingCommand)}`
|
|
4564
4066
|
].join("\n");
|
|
4565
4067
|
}
|
|
4566
|
-
var releaseCommand = new
|
|
4068
|
+
var releaseCommand = new Command10("release").description(
|
|
4567
4069
|
"End management for one address without touching the database object"
|
|
4568
4070
|
).argument("<address>", "Address as name or kind:name").requiredOption("-d, --db <url>", "Database connection URL (required)").option("--schema <name>", "Schema ledger containing the address", "public").option("--kind <kind>", "Kind for an unqualified address", "table").option(
|
|
4569
4071
|
"--parent <kind:name>",
|
|
@@ -4581,15 +4083,15 @@ var releaseCommand = new Command9("release").description(
|
|
|
4581
4083
|
const detail = error instanceof Error ? error.message : String(error);
|
|
4582
4084
|
if (options.format === "json")
|
|
4583
4085
|
printCliJson({ outcome: "release-refused", detail });
|
|
4584
|
-
else console.error(`release-refused: ${
|
|
4086
|
+
else console.error(`release-refused: ${escapeDiagnosticText8(detail)}`);
|
|
4585
4087
|
process.exitCode = 1;
|
|
4586
4088
|
}
|
|
4587
4089
|
});
|
|
4588
4090
|
|
|
4589
4091
|
// src/commands/repl.ts
|
|
4590
4092
|
import { readFileSync } from "fs";
|
|
4591
|
-
import { Command as
|
|
4592
|
-
var replCommand = new
|
|
4093
|
+
import { Command as Command11 } from "commander";
|
|
4094
|
+
var replCommand = new Command11("repl").description("Launch interactive REPL for exploring schema and queries").option("-s, --schema <path>", "Path to schema file (default: auto-detect)").option(
|
|
4593
4095
|
"-d, --db <url>",
|
|
4594
4096
|
"PostgreSQL connection URL for execution mode (e.g., postgres://localhost/mydb)"
|
|
4595
4097
|
).option("-e, --eval <query>", "Execute a single query and exit (batch mode)").option(
|
|
@@ -4711,10 +4213,11 @@ function mapReplCasingOption(casing) {
|
|
|
4711
4213
|
// src/commands/verify.ts
|
|
4712
4214
|
import {
|
|
4713
4215
|
comparePgsqlDatabaseSchema as comparePgsqlDatabaseSchema2,
|
|
4714
|
-
|
|
4715
|
-
|
|
4216
|
+
createPgPhysicalModel as createPgPhysicalModel5,
|
|
4217
|
+
createPgsqlAdapter as createPgsqlAdapter3,
|
|
4218
|
+
escapeDiagnosticText as escapeDiagnosticText9
|
|
4716
4219
|
} from "@dbsp/adapter-pgsql";
|
|
4717
|
-
import { Command as
|
|
4220
|
+
import { Command as Command12 } from "commander";
|
|
4718
4221
|
|
|
4719
4222
|
// src/verifier.ts
|
|
4720
4223
|
var CHANGE_TO_DRIFT = {
|
|
@@ -4744,6 +4247,10 @@ var CHANGE_TO_DRIFT = {
|
|
|
4744
4247
|
// Column enhancements
|
|
4745
4248
|
alter_column_collation: { type: "collation_mismatch", severity: "warning" },
|
|
4746
4249
|
alter_column_identity: { type: "identity_mismatch", severity: "warning" },
|
|
4250
|
+
alter_column_auto_increment: {
|
|
4251
|
+
type: "auto_increment_mismatch",
|
|
4252
|
+
severity: "error"
|
|
4253
|
+
},
|
|
4747
4254
|
// Comments
|
|
4748
4255
|
add_comment: { type: "comment_mismatch", severity: "info" },
|
|
4749
4256
|
drop_comment: { type: "comment_mismatch", severity: "info" },
|
|
@@ -4856,7 +4363,7 @@ function formatVerifyResult(result) {
|
|
|
4856
4363
|
}
|
|
4857
4364
|
|
|
4858
4365
|
// src/commands/verify.ts
|
|
4859
|
-
var verifyCommand = new
|
|
4366
|
+
var verifyCommand = new Command12("verify").description("Compare schema vs real database (drift detection)").option(
|
|
4860
4367
|
"-s, --schema <path>",
|
|
4861
4368
|
"Path to schema file (default: dbsp.schema.ts)"
|
|
4862
4369
|
).requiredOption("-d, --db <url>", "Database connection URL (required)").option("--schema-name <name>", "Database schema name (default: public)").option("--json", "Output as JSON").action(
|
|
@@ -4874,14 +4381,18 @@ var verifyCommand = new Command11("verify").description("Compare schema vs real
|
|
|
4874
4381
|
try {
|
|
4875
4382
|
const loaded = await loadSchema(schemaPath);
|
|
4876
4383
|
const schemaModel = loaded.model;
|
|
4384
|
+
const physical = createPgPhysicalModel5({
|
|
4385
|
+
mode: "logical",
|
|
4386
|
+
model: schemaModel,
|
|
4387
|
+
schema: options.schemaName ?? "public",
|
|
4388
|
+
...loaded.dbCasing === void 0 ? {} : { dbCasing: loaded.dbCasing }
|
|
4389
|
+
});
|
|
4877
4390
|
const { pool } = await createDbConnection(options.db);
|
|
4878
4391
|
try {
|
|
4879
|
-
const adapter =
|
|
4880
|
-
const diff = await comparePgsqlDatabaseSchema2(adapter,
|
|
4881
|
-
...options.schemaName ? { schema: options.schemaName } : {},
|
|
4882
|
-
...loaded.dbCasing !== void 0 ? { dbCasing: loaded.dbCasing } : {},
|
|
4392
|
+
const adapter = createPgsqlAdapter3(pool);
|
|
4393
|
+
const diff = await comparePgsqlDatabaseSchema2(adapter, physical, {
|
|
4883
4394
|
onExpressionCanonicalizationWarning: (warning) => console.warn(
|
|
4884
|
-
`\u26A0\uFE0F [${warning.kind} ${
|
|
4395
|
+
`\u26A0\uFE0F [${warning.kind} ${escapeDiagnosticText9(warning.table)}.${escapeDiagnosticText9(warning.name)}] ${warning.message}`
|
|
4885
4396
|
)
|
|
4886
4397
|
});
|
|
4887
4398
|
const dbModel = await adapter.introspect({
|
|
@@ -4918,12 +4429,13 @@ var verifyCommand = new Command11("verify").description("Compare schema vs real
|
|
|
4918
4429
|
);
|
|
4919
4430
|
|
|
4920
4431
|
// src/index.ts
|
|
4921
|
-
var program = new
|
|
4432
|
+
var program = new Command13();
|
|
4922
4433
|
var packageJson = createRequire(import.meta.url)("../package.json");
|
|
4923
4434
|
var jsonFormatCommands = /* @__PURE__ */ new Set([
|
|
4924
4435
|
"apply",
|
|
4925
4436
|
"inspect",
|
|
4926
4437
|
"plan",
|
|
4438
|
+
"migrate",
|
|
4927
4439
|
"recover",
|
|
4928
4440
|
"reconcile",
|
|
4929
4441
|
"release"
|
|
@@ -4951,6 +4463,7 @@ program.name("dbsp").description("Schema-first code generation for db-semantic-p
|
|
|
4951
4463
|
program.addCommand(generateCommand);
|
|
4952
4464
|
program.addCommand(introspectCommand);
|
|
4953
4465
|
program.addCommand(planCommand);
|
|
4466
|
+
program.addCommand(migrateCommand);
|
|
4954
4467
|
program.addCommand(preflightCommand);
|
|
4955
4468
|
program.addCommand(applyCommand);
|
|
4956
4469
|
program.addCommand(inspectCommand);
|