@dbsp/cli 3.0.1 → 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 +1095 -1767
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
- 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,7 +15,7 @@ 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";
|
|
@@ -24,30 +24,31 @@ 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
|
-
|
|
32
|
-
readPgLedgerAddressChain
|
|
29
|
+
executionIdsForRun,
|
|
30
|
+
readPgLedgerAddressChain,
|
|
33
31
|
readPgLedgerReservationsForExecution,
|
|
34
32
|
readTransitionJournal,
|
|
35
33
|
TransitionRunIdentityMismatchError,
|
|
36
|
-
validatePgManagedLedgerCurrency,
|
|
37
34
|
withPgTransitionRunLock
|
|
38
35
|
} from "@dbsp/adapter-pgsql";
|
|
39
|
-
import {
|
|
36
|
+
import {
|
|
37
|
+
applyPgTransitionRun,
|
|
38
|
+
comparePgsqlDeclaredAdoptionSchema as comparePgsqlDeclaredAdoptionSchema2,
|
|
39
|
+
executeGeneratorPlan,
|
|
40
|
+
lockPgJournalRun,
|
|
41
|
+
modelForDeclaredAdoption as modelForDeclaredAdoption2
|
|
42
|
+
} from "@dbsp/adapter-pgsql/internal";
|
|
40
43
|
import {
|
|
41
44
|
acquireExclusiveTransitionLease,
|
|
42
45
|
acquireTransitionLease as acquireTransitionLease3,
|
|
43
|
-
createApplier,
|
|
44
|
-
createPackRegistry as createPackRegistry2,
|
|
45
46
|
selectorMatchesResource,
|
|
46
47
|
transitionPlanDigest as transitionPlanDigest2,
|
|
47
|
-
validateNormalizedManagedStepManifest as
|
|
48
|
+
validateNormalizedManagedStepManifest as validateNormalizedManagedStepManifest2
|
|
48
49
|
} from "@dbsp/core";
|
|
49
50
|
import { mintDurablyLoadedRun } from "@dbsp/core/internal";
|
|
50
|
-
import { ledgerAddressKey
|
|
51
|
+
import { ledgerAddressKey, REFUSAL_VOCABULARY as REFUSAL_VOCABULARY2 } from "@dbsp/types";
|
|
51
52
|
import { Command as Command2 } from "commander";
|
|
52
53
|
|
|
53
54
|
// src/utils/output.ts
|
|
@@ -61,907 +62,47 @@ function printCliJson(value) {
|
|
|
61
62
|
console.log(serializeCliJson(value));
|
|
62
63
|
}
|
|
63
64
|
|
|
64
|
-
// src/commands/execution-ids.ts
|
|
65
|
-
function executionIdsForRun(journal) {
|
|
66
|
-
const executionIds = /* @__PURE__ */ new Set([journal.run.runId]);
|
|
67
|
-
for (const event of journal.events) {
|
|
68
|
-
const record2 = event.record;
|
|
69
|
-
if (event.event === "intent" && "executionId" in record2 && typeof record2.executionId === "string")
|
|
70
|
-
executionIds.add(record2.executionId);
|
|
71
|
-
if (event.event === "observed" && "intent" in record2 && record2.intent && typeof record2.intent.executionId === "string")
|
|
72
|
-
executionIds.add(record2.intent.executionId);
|
|
73
|
-
}
|
|
74
|
-
if ("generator" in journal.plan)
|
|
75
|
-
executionIds.add(`dbsp.generator.execution.${journal.run.runId}`);
|
|
76
|
-
return [...executionIds];
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// src/commands/generator-execution.ts
|
|
80
|
-
import { randomUUID } from "crypto";
|
|
81
|
-
import { isDeepStrictEqual } from "util";
|
|
82
|
-
import {
|
|
83
|
-
assertGeneratedPostconditionSession,
|
|
84
|
-
compareSchemata,
|
|
85
|
-
createPgsqlAdapter,
|
|
86
|
-
decodeGeneratedPostconditionPayload,
|
|
87
|
-
executePgAdmittedOperation,
|
|
88
|
-
executePgDeclaredAdoption,
|
|
89
|
-
executePgPersistedTableReaddress,
|
|
90
|
-
preflightPgDeclaredAdoption,
|
|
91
|
-
readPgCatalogueIdentity,
|
|
92
|
-
readPgLedgerAddressChain,
|
|
93
|
-
readPgLedgerScopeCurrency,
|
|
94
|
-
readPgRemovalEffectsClosure,
|
|
95
|
-
toGeneratedPostconditionBindingAddress,
|
|
96
|
-
verifyGeneratedCheckPostcondition,
|
|
97
|
-
verifyGeneratedColumnPostcondition,
|
|
98
|
-
verifyGeneratedIdentityPostcondition,
|
|
99
|
-
verifyGeneratedIndexPostcondition,
|
|
100
|
-
verifyGeneratedTablePostcondition,
|
|
101
|
-
withGeneratedPostconditionSession
|
|
102
|
-
} from "@dbsp/adapter-pgsql";
|
|
103
|
-
import {
|
|
104
|
-
canonicalJson,
|
|
105
|
-
canonicalJsonDigest,
|
|
106
|
-
outcomeClaimEventId,
|
|
107
|
-
outcomeClaimId,
|
|
108
|
-
projectLedgerChain,
|
|
109
|
-
validateNormalizedManagedStepManifest
|
|
110
|
-
} from "@dbsp/core";
|
|
111
|
-
import { decideDestructiveDecision } from "@dbsp/core/internal";
|
|
112
|
-
import { ledgerAddressKey } from "@dbsp/types";
|
|
113
|
-
function managedSteps(manifest) {
|
|
114
|
-
return manifest.steps;
|
|
115
|
-
}
|
|
116
|
-
function matchesReviewedReplacementSelector(reviewed, provided) {
|
|
117
|
-
return provided === reviewed || reviewed.startsWith("table:") && provided === reviewed.slice("table:".length);
|
|
118
|
-
}
|
|
119
|
-
function modelForAdoption(table) {
|
|
120
|
-
const tables = /* @__PURE__ */ new Map([[table.name, table]]);
|
|
121
|
-
const relations = /* @__PURE__ */ new Map();
|
|
122
|
-
return {
|
|
123
|
-
tables,
|
|
124
|
-
relations,
|
|
125
|
-
getTable: (name) => tables.get(name),
|
|
126
|
-
getRelation: (name) => relations.get(name),
|
|
127
|
-
getRelationsFrom: () => [],
|
|
128
|
-
getRelationsTo: () => [],
|
|
129
|
-
isAmbiguous: () => ({ ambiguous: false, options: [] })
|
|
130
|
-
};
|
|
131
|
-
}
|
|
132
|
-
async function adoptionShapeMatches(pool, schema, shape) {
|
|
133
|
-
const live = await createPgsqlAdapter(pool).introspect({ schema });
|
|
134
|
-
const diff = compareSchemata(modelForAdoption(shape), live);
|
|
135
|
-
return !diff.changes.some((change) => change.table === shape.name);
|
|
136
|
-
}
|
|
137
|
-
var SYSTEM_LEDGER_SCHEMAS = /* @__PURE__ */ new Set([
|
|
138
|
-
"pg_toast",
|
|
139
|
-
"pg_catalog",
|
|
140
|
-
"information_schema"
|
|
141
|
-
]);
|
|
142
|
-
var SystemSchemaLedgerHomeError = class extends Error {
|
|
143
|
-
address;
|
|
144
|
-
constructor(address) {
|
|
145
|
-
super(
|
|
146
|
-
`SystemSchemaLedgerHomeError: ledger home is unavailable for system schema ${address.schema}: ${ledgerAddressKey(address)}`
|
|
147
|
-
);
|
|
148
|
-
this.name = "SystemSchemaLedgerHomeError";
|
|
149
|
-
this.address = address;
|
|
150
|
-
}
|
|
151
|
-
};
|
|
152
|
-
function home(address) {
|
|
153
|
-
if (address.scope !== "database" && address.schema && SYSTEM_LEDGER_SCHEMAS.has(address.schema))
|
|
154
|
-
throw new SystemSchemaLedgerHomeError(address);
|
|
155
|
-
return address.scope === "database" ? { scope: "database" } : address.schema ? { scope: "schema", schema: address.schema } : (() => {
|
|
156
|
-
throw new Error(
|
|
157
|
-
`schema-scoped generated address ${address.name} has no schema ledger`
|
|
158
|
-
);
|
|
159
|
-
})();
|
|
160
|
-
}
|
|
161
|
-
function acceptance(planDigest, approval) {
|
|
162
|
-
return approval.approvals.some(
|
|
163
|
-
(grant) => grant.class === `destructive-plan-accepted:${planDigest}`
|
|
164
|
-
) === true ? "destructive-plan-accepted" : "absent";
|
|
165
|
-
}
|
|
166
|
-
function withheldDestructiveAuthority(evidence) {
|
|
167
|
-
if (evidence.declaration !== "requires-removal" && evidence.declaration !== "requires-lossy-change" && evidence.declaration !== "replacement-requested-by-plan")
|
|
168
|
-
return "destructive declaration authority";
|
|
169
|
-
if (evidence.ownership !== "managed-by-me")
|
|
170
|
-
return "destructive ownership authority";
|
|
171
|
-
if (evidence.catalogueIdentity !== "matches-recorded")
|
|
172
|
-
return "destructive catalogue identity authority";
|
|
173
|
-
if (evidence.operatorAcceptance !== "destructive-plan-accepted")
|
|
174
|
-
return "destructive operator acceptance authority";
|
|
175
|
-
if (evidence.containment !== void 0 && evidence.containment !== "all-contained-or-managed")
|
|
176
|
-
return "destructive containment authority";
|
|
177
|
-
if (evidence.ledgerLineage !== "matches-database")
|
|
178
|
-
return "destructive ledger lineage authority";
|
|
179
|
-
return void 0;
|
|
180
|
-
}
|
|
181
|
-
function withheldDestructiveAuthorityFromReason(reason) {
|
|
182
|
-
if (reason.includes("operator acceptance"))
|
|
183
|
-
return "destructive operator acceptance authority";
|
|
184
|
-
if (reason.includes("ledger lineage"))
|
|
185
|
-
return "destructive ledger lineage authority";
|
|
186
|
-
return void 0;
|
|
187
|
-
}
|
|
188
|
-
async function databaseId(pool) {
|
|
189
|
-
const result = await pool.query("SELECT current_database() AS database_id");
|
|
190
|
-
const database = result.rows[0]?.database_id;
|
|
191
|
-
if (typeof database !== "string" || database.length === 0)
|
|
192
|
-
throw new Error(
|
|
193
|
-
"schema-differ generator could not read current database identity"
|
|
194
|
-
);
|
|
195
|
-
return database;
|
|
196
|
-
}
|
|
197
|
-
async function managed(executor, address) {
|
|
198
|
-
const chain = await readPgLedgerAddressChain(
|
|
199
|
-
executor,
|
|
200
|
-
home(address),
|
|
201
|
-
address
|
|
202
|
-
);
|
|
203
|
-
const projection = projectLedgerChain(chain);
|
|
204
|
-
return projection.kind === "projected-ledger-chain" && projection.stableState === "managed";
|
|
205
|
-
}
|
|
206
|
-
async function alreadyAppliedCreation(executor, address) {
|
|
207
|
-
const live = await readPgCatalogueIdentity(executor, address);
|
|
208
|
-
if (!live?.catalogueIdentity) return false;
|
|
209
|
-
const chain = await readPgLedgerAddressChain(
|
|
210
|
-
executor,
|
|
211
|
-
home(address),
|
|
212
|
-
address
|
|
213
|
-
);
|
|
214
|
-
const projection = projectLedgerChain(chain);
|
|
215
|
-
const recorded = chain.terminalMember?.catalogueIdentity;
|
|
216
|
-
return projection.kind === "projected-ledger-chain" && projection.stableState === "managed" && recorded !== void 0 && isDeepStrictEqual(recorded, live.catalogueIdentity);
|
|
217
|
-
}
|
|
218
|
-
function observed(address) {
|
|
219
|
-
return {
|
|
220
|
-
value: { kind: address.kind, name: address.name },
|
|
221
|
-
digest: `generator:${address.kind}:${address.name}`
|
|
222
|
-
};
|
|
223
|
-
}
|
|
224
|
-
function generatedPayload(value) {
|
|
225
|
-
const encoded = canonicalJson(value);
|
|
226
|
-
const normalized = JSON.parse(encoded);
|
|
227
|
-
return {
|
|
228
|
-
value: normalized,
|
|
229
|
-
digest: canonicalJsonDigest(normalized),
|
|
230
|
-
payloadKind: "generated-structural-observation"
|
|
231
|
-
};
|
|
232
|
-
}
|
|
233
|
-
async function identityObserved(executor, postcondition, address, kind) {
|
|
234
|
-
const verified = await verifyGeneratedIdentityPostcondition({
|
|
235
|
-
session: executor,
|
|
236
|
-
postcondition,
|
|
237
|
-
address,
|
|
238
|
-
kind
|
|
239
|
-
});
|
|
240
|
-
const value = JSON.parse(
|
|
241
|
-
canonicalJson({
|
|
242
|
-
kind: "identity-observed",
|
|
243
|
-
observedKind: verified.kind,
|
|
244
|
-
address: {
|
|
245
|
-
scope: address.scope,
|
|
246
|
-
...address.schema === void 0 ? {} : { schema: address.schema },
|
|
247
|
-
name: address.name
|
|
248
|
-
},
|
|
249
|
-
identity: verified.identity,
|
|
250
|
-
structuralSemantics: "unverified"
|
|
251
|
-
})
|
|
252
|
-
);
|
|
253
|
-
return {
|
|
254
|
-
catalogueIdentity: verified.catalogueIdentity,
|
|
255
|
-
observed: {
|
|
256
|
-
value,
|
|
257
|
-
digest: canonicalJsonDigest(value),
|
|
258
|
-
payloadKind: "generated-identity-observation"
|
|
259
|
-
}
|
|
260
|
-
};
|
|
261
|
-
}
|
|
262
|
-
function generatedPostcondition(step, address) {
|
|
263
|
-
const declaration = step.expectedDeclaration;
|
|
264
|
-
if (!declaration?.value || typeof declaration.value !== "object" || Array.isArray(declaration.value))
|
|
265
|
-
throw new Error(
|
|
266
|
-
`generated ${address.kind} step ${step.stepKey} has no decodable generated declaration`
|
|
267
|
-
);
|
|
268
|
-
return decodeGeneratedPostconditionPayload(declaration, step.stepKey);
|
|
269
|
-
}
|
|
270
|
-
async function readGeneratedV3Postcondition(executor, postcondition, address) {
|
|
271
|
-
const bindingAddress = toGeneratedPostconditionBindingAddress(address);
|
|
272
|
-
switch (postcondition.declaration.kind) {
|
|
273
|
-
case "column": {
|
|
274
|
-
const verified = await verifyGeneratedColumnPostcondition({
|
|
275
|
-
session: executor,
|
|
276
|
-
postcondition,
|
|
277
|
-
address: bindingAddress
|
|
278
|
-
});
|
|
279
|
-
return {
|
|
280
|
-
catalogueIdentity: verified.catalogueIdentity,
|
|
281
|
-
observed: generatedPayload({
|
|
282
|
-
kind: "column",
|
|
283
|
-
type: verified.projection.type,
|
|
284
|
-
nullable: verified.projection.nullable,
|
|
285
|
-
...verified.projection.default === void 0 ? {} : { default: verified.projection.default },
|
|
286
|
-
...verified.projection.collation === void 0 ? {} : { collation: verified.projection.collation },
|
|
287
|
-
...verified.projection.identity === void 0 ? {} : { identity: verified.projection.identity }
|
|
288
|
-
})
|
|
289
|
-
};
|
|
290
|
-
}
|
|
291
|
-
case "check": {
|
|
292
|
-
const verified = await verifyGeneratedCheckPostcondition({
|
|
293
|
-
session: executor,
|
|
294
|
-
postcondition,
|
|
295
|
-
address: bindingAddress
|
|
296
|
-
});
|
|
297
|
-
return {
|
|
298
|
-
catalogueIdentity: verified.catalogueIdentity,
|
|
299
|
-
observed: generatedPayload({
|
|
300
|
-
kind: verified.kind,
|
|
301
|
-
type: "c",
|
|
302
|
-
expression: verified.projection.expression,
|
|
303
|
-
validated: verified.projection.validated,
|
|
304
|
-
noInherit: verified.projection.noInherit,
|
|
305
|
-
enforced: verified.projection.enforced,
|
|
306
|
-
isLocal: verified.projection.isLocal,
|
|
307
|
-
inheritanceCount: verified.projection.inheritanceCount,
|
|
308
|
-
parentId: verified.projection.parentId
|
|
309
|
-
})
|
|
310
|
-
};
|
|
311
|
-
}
|
|
312
|
-
case "constraint": {
|
|
313
|
-
return identityObserved(
|
|
314
|
-
executor,
|
|
315
|
-
postcondition,
|
|
316
|
-
bindingAddress,
|
|
317
|
-
"constraint"
|
|
318
|
-
);
|
|
319
|
-
}
|
|
320
|
-
case "index": {
|
|
321
|
-
const verified = await verifyGeneratedIndexPostcondition({
|
|
322
|
-
session: executor,
|
|
323
|
-
postcondition,
|
|
324
|
-
address: bindingAddress
|
|
325
|
-
});
|
|
326
|
-
return {
|
|
327
|
-
catalogueIdentity: verified.catalogueIdentity,
|
|
328
|
-
observed: generatedPayload({
|
|
329
|
-
kind: verified.kind,
|
|
330
|
-
projection: verified.projection
|
|
331
|
-
})
|
|
332
|
-
};
|
|
333
|
-
}
|
|
334
|
-
case "table": {
|
|
335
|
-
const verified = await verifyGeneratedTablePostcondition({
|
|
336
|
-
session: executor,
|
|
337
|
-
postcondition,
|
|
338
|
-
address: bindingAddress
|
|
339
|
-
});
|
|
340
|
-
return {
|
|
341
|
-
catalogueIdentity: verified.catalogueIdentity,
|
|
342
|
-
observed: generatedPayload({
|
|
343
|
-
kind: verified.kind,
|
|
344
|
-
columns: verified.projection.columns.map((column) => ({
|
|
345
|
-
name: column.name,
|
|
346
|
-
type: column.type,
|
|
347
|
-
nullable: column.nullable,
|
|
348
|
-
...column.default === void 0 ? {} : { default: column.default },
|
|
349
|
-
...column.collation === void 0 ? {} : { collation: column.collation },
|
|
350
|
-
...column.identity === void 0 ? {} : { identity: column.identity }
|
|
351
|
-
}))
|
|
352
|
-
})
|
|
353
|
-
};
|
|
354
|
-
}
|
|
355
|
-
case "enum": {
|
|
356
|
-
return identityObserved(executor, postcondition, bindingAddress, "enum");
|
|
357
|
-
}
|
|
358
|
-
case "sequence": {
|
|
359
|
-
return identityObserved(
|
|
360
|
-
executor,
|
|
361
|
-
postcondition,
|
|
362
|
-
bindingAddress,
|
|
363
|
-
"sequence"
|
|
364
|
-
);
|
|
365
|
-
}
|
|
366
|
-
case "extension": {
|
|
367
|
-
return identityObserved(
|
|
368
|
-
executor,
|
|
369
|
-
postcondition,
|
|
370
|
-
bindingAddress,
|
|
371
|
-
"extension"
|
|
372
|
-
);
|
|
373
|
-
}
|
|
374
|
-
case "absent": {
|
|
375
|
-
const live = await readPgCatalogueIdentity(executor, address);
|
|
376
|
-
if (live)
|
|
377
|
-
throw new Error(
|
|
378
|
-
`generated ${address.kind} absence postcondition differs: ${address.name} is still present`
|
|
379
|
-
);
|
|
380
|
-
return { observed: generatedPayload({ kind: "absent" }) };
|
|
381
|
-
}
|
|
382
|
-
default:
|
|
383
|
-
throw new Error("generated v3 postcondition has no declarable read-back");
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
async function readGeneratedPostcondition(executor, step, address) {
|
|
387
|
-
executor = assertGeneratedPostconditionSession(executor);
|
|
388
|
-
const decodedPostcondition = generatedPostcondition(step, address);
|
|
389
|
-
if (decodedPostcondition.postconditionVersion === 3)
|
|
390
|
-
return (await readGeneratedV3Postcondition(
|
|
391
|
-
executor,
|
|
392
|
-
decodedPostcondition,
|
|
393
|
-
address
|
|
394
|
-
)).observed;
|
|
395
|
-
throw new Error(
|
|
396
|
-
"generated postcondition decoder returned no supported version"
|
|
397
|
-
);
|
|
398
|
-
}
|
|
399
|
-
function containedBy(root, candidate) {
|
|
400
|
-
for (let parent = candidate.parent; parent; parent = parent.parent) {
|
|
401
|
-
if (parent.engine === root.engine && parent.database === root.database && parent.schema === root.schema && parent.kind === root.kind && parent.name === root.name)
|
|
402
|
-
return true;
|
|
403
|
-
}
|
|
404
|
-
return false;
|
|
405
|
-
}
|
|
406
|
-
async function destructiveEvidence(input) {
|
|
407
|
-
const { executor, address } = input;
|
|
408
|
-
let ownership = "uncomputable";
|
|
409
|
-
let catalogueIdentity = "catalogue-unavailable";
|
|
410
|
-
let ledgerLineage = "unreadable";
|
|
411
|
-
try {
|
|
412
|
-
const chain = await readPgLedgerAddressChain(
|
|
413
|
-
executor,
|
|
414
|
-
home(address),
|
|
415
|
-
address
|
|
416
|
-
);
|
|
417
|
-
const projection = projectLedgerChain(chain);
|
|
418
|
-
ownership = projection.kind === "projected-ledger-chain" ? projection.stableState === "managed" ? "managed-by-me" : projection.stableState === "unknown" ? "unknown" : "blocked" : "uncomputable";
|
|
419
|
-
const live = await readPgCatalogueIdentity(executor, address);
|
|
420
|
-
const recorded = chain.terminalMember?.catalogueIdentity;
|
|
421
|
-
catalogueIdentity = !live ? "object-absent" : recorded === void 0 ? "differs" : isDeepStrictEqual(live.catalogueIdentity, recorded) ? "matches-recorded" : "differs";
|
|
422
|
-
const currency = await readPgLedgerScopeCurrency(executor, home(address));
|
|
423
|
-
ledgerLineage = currency.kind === "current" ? "matches-database" : "differs";
|
|
424
|
-
} catch {
|
|
425
|
-
}
|
|
426
|
-
let containment;
|
|
427
|
-
let containmentOutcome;
|
|
428
|
-
if (input.classification === "removal") {
|
|
429
|
-
containment = await readPgRemovalEffectsClosure({
|
|
430
|
-
executor,
|
|
431
|
-
root: address,
|
|
432
|
-
isManaged: (candidate) => managed(executor, candidate)
|
|
433
|
-
});
|
|
434
|
-
containmentOutcome = containment.kind;
|
|
435
|
-
}
|
|
436
|
-
return {
|
|
437
|
-
evidence: {
|
|
438
|
-
declaration: input.classification === "removal" ? input.selection?.kind === "replacement" ? "replacement-requested-by-plan" : "requires-removal" : "requires-lossy-change",
|
|
439
|
-
...input.selection?.kind === "replacement" ? { replacementAddress: address } : {},
|
|
440
|
-
ownership,
|
|
441
|
-
catalogueIdentity,
|
|
442
|
-
operatorAcceptance: acceptance(input.planDigest, input.approval),
|
|
443
|
-
...containmentOutcome === void 0 ? {} : { containment: containmentOutcome },
|
|
444
|
-
...containment?.kind === "reaches-unmanaged" ? { containmentUnmanaged: containment.unmanaged } : {},
|
|
445
|
-
...containment?.kind === "undecidable" ? { containmentReason: containment.reason } : {},
|
|
446
|
-
ledgerLineage
|
|
447
|
-
},
|
|
448
|
-
...containment === void 0 ? {} : { containment }
|
|
449
|
-
};
|
|
450
|
-
}
|
|
451
|
-
async function removalContainment(executor, address) {
|
|
452
|
-
return readPgRemovalEffectsClosure({
|
|
453
|
-
executor,
|
|
454
|
-
root: address,
|
|
455
|
-
isManaged: (candidate) => managed(executor, candidate)
|
|
456
|
-
});
|
|
457
|
-
}
|
|
458
|
-
async function executeGeneratorPlan(input) {
|
|
459
|
-
const validation = input.manifest ? { ok: true, manifest: input.manifest } : validateNormalizedManagedStepManifest(
|
|
460
|
-
input.plan?.steps ?? []
|
|
461
|
-
);
|
|
462
|
-
if (!validation.ok)
|
|
463
|
-
return { outcome: "execution-failed", detail: validation.detail };
|
|
464
|
-
const manifest = validation.manifest;
|
|
465
|
-
const approval = input.approval ?? {
|
|
466
|
-
approvals: (input.accepts ?? []).map((value) => ({ class: value }))
|
|
467
|
-
};
|
|
468
|
-
const completedStepKeys = [];
|
|
469
|
-
let destructiveRefusal;
|
|
470
|
-
let destructiveClosureReplanRequired = false;
|
|
471
|
-
const partial = (detail) => completedStepKeys.length === 0 ? { outcome: "execution-failed", detail } : {
|
|
472
|
-
outcome: "partially-applied",
|
|
473
|
-
detail,
|
|
474
|
-
completedStepKeys,
|
|
475
|
-
notStartedStepKeys: managedSteps(manifest).filter((step) => !completedStepKeys.includes(step.stepKey)).map((step) => step.stepKey)
|
|
476
|
-
};
|
|
477
|
-
try {
|
|
478
|
-
const database = await databaseId(input.pool);
|
|
479
|
-
const executionId = `dbsp.generator.execution.${randomUUID()}`;
|
|
480
|
-
await input.recordAttempt(executionId);
|
|
481
|
-
const steps = managedSteps(manifest);
|
|
482
|
-
for (const step of steps) {
|
|
483
|
-
if (step.lifecycle?.kind === "adoption-refused")
|
|
484
|
-
return {
|
|
485
|
-
outcome: "adoption-refused",
|
|
486
|
-
detail: `declared adoption for ${step.address?.name ?? step.stepKey} refuses live shape mismatch`
|
|
487
|
-
};
|
|
488
|
-
const lifecycle = step.lifecycle;
|
|
489
|
-
if (lifecycle?.kind !== "adoption") continue;
|
|
490
|
-
const address = step.address;
|
|
491
|
-
if (!address || !step.expectedDeclaration || !step.expectedCatalogueIdentity)
|
|
492
|
-
return {
|
|
493
|
-
outcome: "execution-failed",
|
|
494
|
-
detail: `adoption step ${step.stepKey} has incomplete normalized material`
|
|
495
|
-
};
|
|
496
|
-
const preflight = await preflightPgDeclaredAdoption({
|
|
497
|
-
executor: input.pool,
|
|
498
|
-
home: home(address),
|
|
499
|
-
address,
|
|
500
|
-
declaration: step.expectedDeclaration,
|
|
501
|
-
expectedCatalogueIdentity: step.expectedCatalogueIdentity,
|
|
502
|
-
shapeMatches: () => adoptionShapeMatches(input.pool, input.schema, lifecycle.shape)
|
|
503
|
-
});
|
|
504
|
-
if (preflight.outcome !== "ready" && preflight.outcome !== "no-op")
|
|
505
|
-
return preflight.outcome === "adoption-refused" ? { outcome: "adoption-refused", detail: preflight.detail } : { outcome: "execution-failed", detail: preflight.detail };
|
|
506
|
-
}
|
|
507
|
-
const replacementSelectors = steps.filter((step) => step.selection?.kind === "replacement").map((step) => step.selection?.selector).filter((selector) => selector !== void 0);
|
|
508
|
-
if (replacementSelectors.length > 0 && !input.replaces?.length)
|
|
509
|
-
return {
|
|
510
|
-
outcome: "destructive-authority-refused",
|
|
511
|
-
detail: "replacement requires a named --replace selector from the reviewed plan",
|
|
512
|
-
refusal: { withheldAuthority: "destructive declaration authority" }
|
|
513
|
-
};
|
|
514
|
-
for (const selector of input.replaces ?? [])
|
|
515
|
-
if (!replacementSelectors.some(
|
|
516
|
-
(reviewed) => matchesReviewedReplacementSelector(reviewed, selector)
|
|
517
|
-
))
|
|
518
|
-
return {
|
|
519
|
-
outcome: "destructive-authority-refused",
|
|
520
|
-
detail: `replacement ${selector} was not requested by the reviewed plan`,
|
|
521
|
-
refusal: { withheldAuthority: "destructive declaration authority" }
|
|
522
|
-
};
|
|
523
|
-
if (replacementSelectors.some(
|
|
524
|
-
(reviewed) => !input.replaces?.some(
|
|
525
|
-
(provided) => matchesReviewedReplacementSelector(reviewed, provided)
|
|
526
|
-
)
|
|
527
|
-
))
|
|
528
|
-
return {
|
|
529
|
-
outcome: "selection-incomplete",
|
|
530
|
-
detail: "replacement selection does not cover every reviewed replacement"
|
|
531
|
-
};
|
|
532
|
-
for (const step of steps) {
|
|
533
|
-
if (step.lifecycle?.kind === "adoption-refused") continue;
|
|
534
|
-
if (step.lifecycle?.kind === "adoption") {
|
|
535
|
-
const lifecycle = step.lifecycle;
|
|
536
|
-
const address2 = step.address;
|
|
537
|
-
if (!address2 || !step.expectedDeclaration || !step.expectedCatalogueIdentity)
|
|
538
|
-
return {
|
|
539
|
-
outcome: "execution-failed",
|
|
540
|
-
detail: `adoption step ${step.stepKey} has incomplete normalized material`
|
|
541
|
-
};
|
|
542
|
-
const adopted = await executePgDeclaredAdoption({
|
|
543
|
-
executor: input.pool,
|
|
544
|
-
run: input.run,
|
|
545
|
-
manifest,
|
|
546
|
-
recomputedPlanDigest: input.planDigest,
|
|
547
|
-
approval,
|
|
548
|
-
executionId,
|
|
549
|
-
step,
|
|
550
|
-
home: home(address2),
|
|
551
|
-
address: address2,
|
|
552
|
-
declaration: step.expectedDeclaration,
|
|
553
|
-
expectedCatalogueIdentity: step.expectedCatalogueIdentity,
|
|
554
|
-
shapeMatches: () => adoptionShapeMatches(input.pool, input.schema, lifecycle.shape),
|
|
555
|
-
...input.observer === void 0 ? {} : { observer: input.observer }
|
|
556
|
-
});
|
|
557
|
-
if (adopted.outcome === "completed" || adopted.outcome === "no-op") {
|
|
558
|
-
completedStepKeys.push(step.stepKey);
|
|
559
|
-
continue;
|
|
560
|
-
}
|
|
561
|
-
if (adopted.outcome === "adoption-refused")
|
|
562
|
-
return { outcome: "adoption-refused", detail: adopted.detail };
|
|
563
|
-
if (adopted.outcome === "recovery-required") return adopted;
|
|
564
|
-
if (adopted.outcome === "transport-ambiguous") return adopted;
|
|
565
|
-
return { outcome: "execution-failed", detail: adopted.detail };
|
|
566
|
-
}
|
|
567
|
-
if (step.lifecycle?.kind === "readdress") {
|
|
568
|
-
const result = await executePgPersistedTableReaddress({
|
|
569
|
-
executor: input.pool,
|
|
570
|
-
run: input.run,
|
|
571
|
-
manifest,
|
|
572
|
-
recomputedPlanDigest: input.planDigest,
|
|
573
|
-
approval,
|
|
574
|
-
executionId,
|
|
575
|
-
step,
|
|
576
|
-
database,
|
|
577
|
-
targetSchema: input.schema,
|
|
578
|
-
...input.observer === void 0 ? {} : { observer: input.observer }
|
|
579
|
-
});
|
|
580
|
-
if (result.outcome === "completed" || result.outcome === "no-op") {
|
|
581
|
-
completedStepKeys.push(step.stepKey);
|
|
582
|
-
continue;
|
|
583
|
-
}
|
|
584
|
-
return result;
|
|
585
|
-
}
|
|
586
|
-
if (step.statementBundle.statements.length === 0) {
|
|
587
|
-
completedStepKeys.push(step.stepKey);
|
|
588
|
-
continue;
|
|
589
|
-
}
|
|
590
|
-
const address = step.address ?? step.closure?.root;
|
|
591
|
-
if (!address)
|
|
592
|
-
return {
|
|
593
|
-
outcome: "execution-failed",
|
|
594
|
-
detail: `managed step ${step.stepKey} has no address`
|
|
595
|
-
};
|
|
596
|
-
const plannedClaimKey = step.plannedClaimKeys[0];
|
|
597
|
-
if (!plannedClaimKey)
|
|
598
|
-
return {
|
|
599
|
-
outcome: "execution-failed",
|
|
600
|
-
detail: `managed step ${step.stepKey} has no planned claim key`
|
|
601
|
-
};
|
|
602
|
-
if (step.classification === "non-destructive" && step.requiresVacancy && await alreadyAppliedCreation(input.pool, address)) {
|
|
603
|
-
completedStepKeys.push(step.stepKey);
|
|
604
|
-
continue;
|
|
605
|
-
}
|
|
606
|
-
const claimKind = step.claimKind;
|
|
607
|
-
const rootClaimId = outcomeClaimId(executionId, plannedClaimKey, address);
|
|
608
|
-
const claim = {
|
|
609
|
-
claimId: rootClaimId,
|
|
610
|
-
claimSpecies: "sql-bearing",
|
|
611
|
-
executionId,
|
|
612
|
-
plannedClaimKey,
|
|
613
|
-
claimGroupId: rootClaimId,
|
|
614
|
-
rootClaimId,
|
|
615
|
-
address,
|
|
616
|
-
claimKind,
|
|
617
|
-
statementBundle: step.statementBundle,
|
|
618
|
-
requiresVacancy: step.requiresVacancy
|
|
619
|
-
};
|
|
620
|
-
const baseReservation = {
|
|
621
|
-
address,
|
|
622
|
-
claimKind,
|
|
623
|
-
executionId,
|
|
624
|
-
rootClaimId: claim.claimId,
|
|
625
|
-
homeLedger: home(address)
|
|
626
|
-
};
|
|
627
|
-
if (step.classification === "non-destructive") {
|
|
628
|
-
const result = await executePgAdmittedOperation(input.pool, {
|
|
629
|
-
run: input.run,
|
|
630
|
-
approval,
|
|
631
|
-
manifest,
|
|
632
|
-
recomputedPlanDigest: input.planDigest,
|
|
633
|
-
operation: {
|
|
634
|
-
kind: "single-outcome",
|
|
635
|
-
request: {
|
|
636
|
-
plan: claim,
|
|
637
|
-
reservations: [baseReservation],
|
|
638
|
-
resolution: {
|
|
639
|
-
eventId: outcomeClaimEventId(claim.claimId, "observed"),
|
|
640
|
-
eventKind: "observed"
|
|
641
|
-
},
|
|
642
|
-
// Transactional DDL is visible only on the admitted session until
|
|
643
|
-
// its terminal ledger fact commits with it.
|
|
644
|
-
readBack: async (session) => readGeneratedPostcondition(session, step, address),
|
|
645
|
-
recordCatalogueIdentity: true,
|
|
646
|
-
...input.observer === void 0 ? {} : { observer: input.observer },
|
|
647
|
-
vacancy: async (executor) => await readPgCatalogueIdentity(executor, address) ? {
|
|
648
|
-
kind: "occupied",
|
|
649
|
-
reason: `creation claim ${claim.claimId} refuses occupied live address ${address.name}`
|
|
650
|
-
} : { kind: "vacant" }
|
|
651
|
-
}
|
|
652
|
-
}
|
|
653
|
-
});
|
|
654
|
-
if (result.kind === "outcome-recovery-required")
|
|
655
|
-
return {
|
|
656
|
-
outcome: "recovery-required",
|
|
657
|
-
claimId: result.claimId,
|
|
658
|
-
detail: `claim ${result.claimId} remains open and requires recovery: ${result.reason}`
|
|
659
|
-
};
|
|
660
|
-
if (result.kind === "outcome-transport-ambiguous")
|
|
661
|
-
return { outcome: "transport-ambiguous", detail: result.reason };
|
|
662
|
-
if ("reason" in result) return partial(result.reason);
|
|
663
|
-
completedStepKeys.push(step.stepKey);
|
|
664
|
-
continue;
|
|
665
|
-
}
|
|
666
|
-
const containment = step.classification === "removal" ? await removalContainment(input.pool, address) : void 0;
|
|
667
|
-
const closureMembers = /* @__PURE__ */ new Map();
|
|
668
|
-
for (const member of step.closure?.members ?? []) {
|
|
669
|
-
closureMembers.set(ledgerAddressKey(member.address), {
|
|
670
|
-
address: member.address,
|
|
671
|
-
plannedClaimKey: member.plannedClaimKey
|
|
672
|
-
});
|
|
673
|
-
}
|
|
674
|
-
if (containment?.kind === "all-contained-or-managed") {
|
|
675
|
-
for (const effect of containment.effects) {
|
|
676
|
-
if (effect.internalOwned) continue;
|
|
677
|
-
if (!containedBy(address, effect.address)) continue;
|
|
678
|
-
if (!await managed(input.pool, effect.address)) continue;
|
|
679
|
-
const key = ledgerAddressKey(effect.address);
|
|
680
|
-
if (closureMembers.has(key)) continue;
|
|
681
|
-
closureMembers.set(key, {
|
|
682
|
-
address: effect.address,
|
|
683
|
-
plannedClaimKey: `closure:${effect.address.kind}:${effect.address.name}`
|
|
684
|
-
});
|
|
685
|
-
}
|
|
686
|
-
}
|
|
687
|
-
const containedClaims = [];
|
|
688
|
-
for (const member of closureMembers.values()) {
|
|
689
|
-
const effect = member.address;
|
|
690
|
-
const childClaimId = outcomeClaimId(
|
|
691
|
-
executionId,
|
|
692
|
-
plannedClaimKey,
|
|
693
|
-
effect,
|
|
694
|
-
member.plannedClaimKey
|
|
695
|
-
);
|
|
696
|
-
const childReservation = {
|
|
697
|
-
...baseReservation,
|
|
698
|
-
address: effect,
|
|
699
|
-
rootClaimId: claim.claimId
|
|
700
|
-
};
|
|
701
|
-
containedClaims.push({
|
|
702
|
-
plan: {
|
|
703
|
-
...claim,
|
|
704
|
-
claimId: childClaimId,
|
|
705
|
-
claimSpecies: "cascade-covered",
|
|
706
|
-
address: effect,
|
|
707
|
-
plannedClaimKey: member.plannedClaimKey,
|
|
708
|
-
// Each member contributes only its terminal absence fact; SQL is
|
|
709
|
-
// exclusively carried by the destructive root manifest claim.
|
|
710
|
-
statementBundle: { statements: [] }
|
|
711
|
-
},
|
|
712
|
-
reservation: childReservation
|
|
713
|
-
});
|
|
714
|
-
}
|
|
715
|
-
const admitRequest = {
|
|
716
|
-
plan: claim,
|
|
717
|
-
reservations: [baseReservation],
|
|
718
|
-
members: containedClaims.map(({ plan, reservation }) => ({
|
|
719
|
-
plan,
|
|
720
|
-
reservations: [reservation]
|
|
721
|
-
})),
|
|
722
|
-
destructiveDecision: async (executor) => {
|
|
723
|
-
const lockedAuthority = await destructiveEvidence({
|
|
724
|
-
executor,
|
|
725
|
-
address,
|
|
726
|
-
classification: step.classification,
|
|
727
|
-
selection: step.selection,
|
|
728
|
-
planDigest: input.planDigest,
|
|
729
|
-
approval
|
|
730
|
-
});
|
|
731
|
-
if (step.classification === "removal" && (containment?.kind !== "all-contained-or-managed" || lockedAuthority.containment?.kind !== "all-contained-or-managed" || containment.closureDigest !== lockedAuthority.containment.closureDigest)) {
|
|
732
|
-
destructiveClosureReplanRequired = true;
|
|
733
|
-
const decision2 = decideDestructiveDecision(
|
|
734
|
-
{ kind: "removal", address },
|
|
735
|
-
{ ...lockedAuthority.evidence, containment: "undecidable" }
|
|
736
|
-
);
|
|
737
|
-
if (decision2.kind === "destructive-decision-refused")
|
|
738
|
-
destructiveRefusal = {
|
|
739
|
-
withheldAuthority: withheldDestructiveAuthority({
|
|
740
|
-
...lockedAuthority.evidence,
|
|
741
|
-
containment: "undecidable"
|
|
742
|
-
}) ?? "destructive containment authority"
|
|
743
|
-
};
|
|
744
|
-
return decision2;
|
|
745
|
-
}
|
|
746
|
-
const decision = decideDestructiveDecision(
|
|
747
|
-
{
|
|
748
|
-
kind: step.classification === "removal" ? "removal" : "data-destructive",
|
|
749
|
-
address,
|
|
750
|
-
...step.classification === "removal" && lockedAuthority.containment?.kind === "all-contained-or-managed" ? { closureDigest: lockedAuthority.containment.closureDigest } : {}
|
|
751
|
-
},
|
|
752
|
-
lockedAuthority.evidence
|
|
753
|
-
);
|
|
754
|
-
if (decision.kind === "destructive-decision-refused")
|
|
755
|
-
destructiveRefusal = {
|
|
756
|
-
withheldAuthority: withheldDestructiveAuthority(lockedAuthority.evidence) ?? "destructive authority"
|
|
757
|
-
};
|
|
758
|
-
return decision;
|
|
759
|
-
},
|
|
760
|
-
...input.observer === void 0 ? {} : { observer: input.observer }
|
|
761
|
-
};
|
|
762
|
-
const executed = await executePgAdmittedOperation(input.pool, {
|
|
763
|
-
run: input.run,
|
|
764
|
-
approval,
|
|
765
|
-
manifest,
|
|
766
|
-
recomputedPlanDigest: input.planDigest,
|
|
767
|
-
operation: {
|
|
768
|
-
kind: "destructive-outcome",
|
|
769
|
-
request: admitRequest,
|
|
770
|
-
readBackAndResolve: async (session) => {
|
|
771
|
-
const declarationReadBack = step.classification === "data-destructive" && step.expectedDeclaration !== void 0 ? await withGeneratedPostconditionSession(
|
|
772
|
-
{
|
|
773
|
-
connect: async () => ({
|
|
774
|
-
query: session.query.bind(session),
|
|
775
|
-
release: () => void 0
|
|
776
|
-
})
|
|
777
|
-
},
|
|
778
|
-
(proof) => {
|
|
779
|
-
const postcondition = generatedPostcondition(
|
|
780
|
-
step,
|
|
781
|
-
address
|
|
782
|
-
);
|
|
783
|
-
if (postcondition.postconditionVersion !== 3)
|
|
784
|
-
throw new Error(
|
|
785
|
-
"generated postcondition decoder returned no supported version"
|
|
786
|
-
);
|
|
787
|
-
return readGeneratedV3Postcondition(
|
|
788
|
-
proof,
|
|
789
|
-
postcondition,
|
|
790
|
-
address
|
|
791
|
-
);
|
|
792
|
-
}
|
|
793
|
-
) : void 0;
|
|
794
|
-
const live = step.classification === "removal" ? await readPgCatalogueIdentity(session, address) : void 0;
|
|
795
|
-
const terminalCatalogueIdentity = step.classification === "removal" ? live?.catalogueIdentity : declarationReadBack?.catalogueIdentity;
|
|
796
|
-
const survivors = step.classification === "removal" && live ? [address] : [];
|
|
797
|
-
const childLives = await Promise.all(
|
|
798
|
-
containedClaims.map(async (child) => ({
|
|
799
|
-
child,
|
|
800
|
-
live: await readPgCatalogueIdentity(
|
|
801
|
-
session,
|
|
802
|
-
child.plan.address
|
|
803
|
-
)
|
|
804
|
-
}))
|
|
805
|
-
);
|
|
806
|
-
for (const { child, live: childLive } of childLives)
|
|
807
|
-
if (childLive) survivors.push(child.plan.address);
|
|
808
|
-
if (survivors.length > 0) {
|
|
809
|
-
const survivorNames = survivors.map(
|
|
810
|
-
(survivor) => `${survivor.kind} ${survivor.schema ?? "<database>"}.${survivor.name}`
|
|
811
|
-
);
|
|
812
|
-
const survivorValue = { survivors: survivorNames };
|
|
813
|
-
const survivorObservation = {
|
|
814
|
-
value: survivorValue,
|
|
815
|
-
digest: canonicalJsonDigest(survivorValue)
|
|
816
|
-
};
|
|
817
|
-
return {
|
|
818
|
-
rootClaimId: claim.claimId,
|
|
819
|
-
members: [
|
|
820
|
-
{
|
|
821
|
-
target: home(address),
|
|
822
|
-
member: {
|
|
823
|
-
eventId: outcomeClaimEventId(
|
|
824
|
-
claim.claimId,
|
|
825
|
-
"indeterminate"
|
|
826
|
-
),
|
|
827
|
-
executionId,
|
|
828
|
-
plannedClaimKey: claim.plannedClaimKey,
|
|
829
|
-
claimGroupId: claim.claimGroupId,
|
|
830
|
-
rootClaimId: claim.rootClaimId,
|
|
831
|
-
address,
|
|
832
|
-
eventKind: "indeterminate",
|
|
833
|
-
observed: survivorObservation
|
|
834
|
-
}
|
|
835
|
-
},
|
|
836
|
-
...containedClaims.map((child) => ({
|
|
837
|
-
target: home(child.plan.address),
|
|
838
|
-
member: {
|
|
839
|
-
eventId: outcomeClaimEventId(
|
|
840
|
-
child.plan.claimId,
|
|
841
|
-
"indeterminate"
|
|
842
|
-
),
|
|
843
|
-
executionId,
|
|
844
|
-
plannedClaimKey: child.plan.plannedClaimKey,
|
|
845
|
-
claimGroupId: claim.claimId,
|
|
846
|
-
rootClaimId: claim.claimId,
|
|
847
|
-
address: child.plan.address,
|
|
848
|
-
eventKind: "indeterminate",
|
|
849
|
-
observed: survivorObservation
|
|
850
|
-
}
|
|
851
|
-
}))
|
|
852
|
-
],
|
|
853
|
-
reservations: [
|
|
854
|
-
baseReservation,
|
|
855
|
-
...containedClaims.map(({ reservation }) => reservation)
|
|
856
|
-
]
|
|
857
|
-
};
|
|
858
|
-
}
|
|
859
|
-
const terminals = [
|
|
860
|
-
{
|
|
861
|
-
target: home(address),
|
|
862
|
-
member: {
|
|
863
|
-
eventId: outcomeClaimEventId(
|
|
864
|
-
claim.claimId,
|
|
865
|
-
step.classification === "removal" ? "absent" : "observed"
|
|
866
|
-
),
|
|
867
|
-
executionId,
|
|
868
|
-
plannedClaimKey: claim.plannedClaimKey,
|
|
869
|
-
claimGroupId: claim.claimGroupId,
|
|
870
|
-
rootClaimId: claim.rootClaimId,
|
|
871
|
-
address,
|
|
872
|
-
eventKind: step.classification === "removal" ? "absent" : "observed",
|
|
873
|
-
// The append protocol reads this address's terminal on its
|
|
874
|
-
// pinned transaction session after the post-DDL read-back.
|
|
875
|
-
...terminalCatalogueIdentity === void 0 ? {} : { catalogueIdentity: terminalCatalogueIdentity },
|
|
876
|
-
...step.classification === "removal" ? {} : {
|
|
877
|
-
observed: declarationReadBack?.observed ?? observed(address)
|
|
878
|
-
}
|
|
879
|
-
}
|
|
880
|
-
}
|
|
881
|
-
];
|
|
882
|
-
for (const child of containedClaims) {
|
|
883
|
-
terminals.push({
|
|
884
|
-
target: home(child.plan.address),
|
|
885
|
-
member: {
|
|
886
|
-
eventId: outcomeClaimEventId(child.plan.claimId, "absent"),
|
|
887
|
-
executionId,
|
|
888
|
-
plannedClaimKey: child.plan.plannedClaimKey,
|
|
889
|
-
claimGroupId: claim.claimId,
|
|
890
|
-
rootClaimId: claim.claimId,
|
|
891
|
-
address: child.plan.address,
|
|
892
|
-
eventKind: "absent",
|
|
893
|
-
predecessor: child.plan.claimId
|
|
894
|
-
}
|
|
895
|
-
});
|
|
896
|
-
}
|
|
897
|
-
return {
|
|
898
|
-
rootClaimId: claim.claimId,
|
|
899
|
-
members: terminals,
|
|
900
|
-
reservations: [
|
|
901
|
-
baseReservation,
|
|
902
|
-
...containedClaims.map(({ reservation }) => reservation)
|
|
903
|
-
]
|
|
904
|
-
};
|
|
905
|
-
}
|
|
906
|
-
}
|
|
907
|
-
});
|
|
908
|
-
if (executed.kind === "outcome-recovery-required")
|
|
909
|
-
return {
|
|
910
|
-
outcome: "recovery-required",
|
|
911
|
-
claimId: executed.claimId,
|
|
912
|
-
detail: `claim ${executed.claimId} remains open and requires recovery: ${executed.reason}`
|
|
913
|
-
};
|
|
914
|
-
if (executed.kind === "outcome-transport-ambiguous")
|
|
915
|
-
return { outcome: "transport-ambiguous", detail: executed.reason };
|
|
916
|
-
if (executed.kind === "outcome-protocol-pending")
|
|
917
|
-
return partial(
|
|
918
|
-
`destructive claim ${claim.claimId} remains pending after executing: ${executed.reason}`
|
|
919
|
-
);
|
|
920
|
-
if (executed.kind !== "executed-destructive-outcome")
|
|
921
|
-
return {
|
|
922
|
-
outcome: "destructive-authority-refused",
|
|
923
|
-
detail: destructiveClosureReplanRequired ? `${executed.reason}; destructive closure changed under lock; replan required` : executed.reason,
|
|
924
|
-
...destructiveRefusal === void 0 && withheldDestructiveAuthorityFromReason(executed.reason) === void 0 ? {} : {
|
|
925
|
-
refusal: destructiveRefusal ?? {
|
|
926
|
-
withheldAuthority: withheldDestructiveAuthorityFromReason(executed.reason) ?? "destructive authority"
|
|
927
|
-
}
|
|
928
|
-
}
|
|
929
|
-
};
|
|
930
|
-
completedStepKeys.push(step.stepKey);
|
|
931
|
-
}
|
|
932
|
-
return { outcome: "completed" };
|
|
933
|
-
} catch (error) {
|
|
934
|
-
return partial(error instanceof Error ? error.message : String(error));
|
|
935
|
-
}
|
|
936
|
-
}
|
|
937
|
-
|
|
938
65
|
// src/commands/generator-plan.ts
|
|
939
|
-
import { randomUUID
|
|
66
|
+
import { randomUUID } from "crypto";
|
|
940
67
|
import {
|
|
941
68
|
assertDeclarableChangeKind,
|
|
942
69
|
classifyGeneratedMutation,
|
|
943
70
|
comparePgsqlDatabaseSchema,
|
|
944
|
-
|
|
71
|
+
createPgPhysicalModel,
|
|
72
|
+
createPgsqlAdapter,
|
|
945
73
|
createPgsqlGeneratedManagedStep,
|
|
946
74
|
createPgTransitionLessor,
|
|
947
75
|
createPgTransitionRunPersister,
|
|
948
76
|
generatedPostconditionForChange,
|
|
949
77
|
generateMigrationSQL,
|
|
950
|
-
|
|
78
|
+
ReferencedKeyRemovalError,
|
|
79
|
+
readPgCatalogueIdentity,
|
|
951
80
|
renderPgTableReaddressStatements
|
|
952
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";
|
|
953
90
|
import {
|
|
954
91
|
acquireTransitionLease,
|
|
955
|
-
|
|
956
|
-
canonicalJsonDigest as canonicalJsonDigest2,
|
|
92
|
+
canonicalJsonDigest,
|
|
957
93
|
transitionPlanDigest,
|
|
958
|
-
validateNormalizedManagedStepManifest
|
|
94
|
+
validateNormalizedManagedStepManifest
|
|
959
95
|
} from "@dbsp/core";
|
|
960
|
-
function
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
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
|
+
};
|
|
965
106
|
}
|
|
966
107
|
function replacementStatements(table, schema) {
|
|
967
108
|
const quote = (value) => `"${value.replaceAll('"', '""')}"`;
|
|
@@ -985,7 +126,7 @@ function replacementStatements(table, schema) {
|
|
|
985
126
|
};
|
|
986
127
|
return {
|
|
987
128
|
retireStatements: [`DROP TABLE ${quote(schema)}.${quote(table.name)}`],
|
|
988
|
-
createStatements:
|
|
129
|
+
createStatements: generateMigrationSQLForPhysicalDiff(createDiff, {
|
|
989
130
|
includeDestructive: true,
|
|
990
131
|
schemaName: schema
|
|
991
132
|
})
|
|
@@ -1104,6 +245,65 @@ function changeTargetsDeclaredTable(change, table) {
|
|
|
1104
245
|
"alter_sequence"
|
|
1105
246
|
])).has(change.kind);
|
|
1106
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
|
+
}
|
|
1107
307
|
function requiredAdoptionIdentity(identities, table) {
|
|
1108
308
|
const identity = identities.get(table);
|
|
1109
309
|
if (!identity)
|
|
@@ -1111,7 +311,7 @@ function requiredAdoptionIdentity(identities, table) {
|
|
|
1111
311
|
return identity;
|
|
1112
312
|
}
|
|
1113
313
|
function asDurableGeneratorPlan(material, steps) {
|
|
1114
|
-
const validation =
|
|
314
|
+
const validation = validateNormalizedManagedStepManifest(steps);
|
|
1115
315
|
if (!validation.ok)
|
|
1116
316
|
throw new Error(
|
|
1117
317
|
`generator planning refuses invalid managed-step manifest: ${validation.detail}`
|
|
@@ -1146,7 +346,7 @@ function render(plan, planDigest) {
|
|
|
1146
346
|
]
|
|
1147
347
|
].join("\n");
|
|
1148
348
|
}
|
|
1149
|
-
async function
|
|
349
|
+
async function databaseId(pool) {
|
|
1150
350
|
const result = await pool.query("SELECT current_database() AS database_id");
|
|
1151
351
|
const value = result.rows[0]?.database_id;
|
|
1152
352
|
if (typeof value !== "string" || value.length === 0)
|
|
@@ -1157,19 +357,39 @@ async function databaseId2(pool) {
|
|
|
1157
357
|
}
|
|
1158
358
|
async function runGeneratorPlan(input) {
|
|
1159
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
|
+
}
|
|
1160
383
|
const { pool } = await createDbConnection(input.db);
|
|
1161
384
|
try {
|
|
1162
|
-
const schema = input.schema ?? "public";
|
|
1163
385
|
const diff = await comparePgsqlDatabaseSchema(
|
|
1164
|
-
|
|
1165
|
-
|
|
386
|
+
createPgsqlAdapter(pool),
|
|
387
|
+
physical,
|
|
1166
388
|
{
|
|
1167
|
-
schema,
|
|
1168
389
|
// `apply --schema` owns one schema. Extensions are database-scoped,
|
|
1169
390
|
// so an extension not declared by this schema must not become a
|
|
1170
391
|
// schema-plan removal merely because it is installed in the database.
|
|
1171
|
-
ignoreUnmanagedExtensions: true
|
|
1172
|
-
...loaded.dbCasing ? { dbCasing: loaded.dbCasing } : {}
|
|
392
|
+
ignoreUnmanagedExtensions: true
|
|
1173
393
|
}
|
|
1174
394
|
);
|
|
1175
395
|
const declaredLifecycleWork = [...loaded.model.tables.values()].some(
|
|
@@ -1198,39 +418,57 @@ async function runGeneratorPlan(input) {
|
|
|
1198
418
|
planDigest: null
|
|
1199
419
|
};
|
|
1200
420
|
}
|
|
1201
|
-
const
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
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);
|
|
1209
436
|
const adoptionIdentities = /* @__PURE__ */ new Map();
|
|
1210
437
|
for (const table of loaded.model.tables.values()) {
|
|
1211
|
-
|
|
1212
|
-
|
|
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, {
|
|
1213
442
|
engine: "postgresql",
|
|
1214
443
|
database,
|
|
1215
444
|
schema: input.schema ?? "public",
|
|
1216
445
|
kind: "table",
|
|
1217
|
-
name:
|
|
446
|
+
name: physicalName
|
|
1218
447
|
});
|
|
1219
448
|
if (!live?.catalogueIdentity)
|
|
1220
449
|
throw new Error(
|
|
1221
|
-
`declared adoption for ${
|
|
450
|
+
`declared adoption for ${physicalName} refuses absent live identity`
|
|
1222
451
|
);
|
|
1223
|
-
adoptionIdentities.set(
|
|
452
|
+
adoptionIdentities.set(physicalName, live.catalogueIdentity);
|
|
1224
453
|
}
|
|
1225
454
|
const executableDiff = {
|
|
1226
455
|
...diff,
|
|
1227
456
|
changes: diff.changes.filter(
|
|
1228
457
|
(change) => ![...loaded.model.tables.values()].some(
|
|
1229
|
-
(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
|
+
)
|
|
1230
462
|
)
|
|
1231
463
|
)
|
|
1232
464
|
};
|
|
1233
|
-
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) => ({
|
|
1234
472
|
kind: change.kind,
|
|
1235
473
|
table: change.table,
|
|
1236
474
|
...change.column ? { column: change.column } : {},
|
|
@@ -1241,45 +479,54 @@ async function runGeneratorPlan(input) {
|
|
|
1241
479
|
schema,
|
|
1242
480
|
change.meta.readdress
|
|
1243
481
|
) : generateMigrationSQL(
|
|
1244
|
-
{ ...executableDiff, changes: [change] },
|
|
1245
|
-
{ includeDestructive: true
|
|
482
|
+
{ ...executableDiff, physical: diff.physical, changes: [change] },
|
|
483
|
+
{ includeDestructive: true }
|
|
1246
484
|
),
|
|
1247
485
|
...change.kind === "readdress_table" && change.meta?.readdress ? { readdress: change.meta.readdress } : {}
|
|
1248
486
|
}));
|
|
1249
487
|
const material = {
|
|
1250
488
|
kind: "schema-differ-generator",
|
|
1251
489
|
planningSchema: schema,
|
|
490
|
+
...dbCasing === "preserve" ? {} : { dbCasing },
|
|
1252
491
|
changes: [
|
|
1253
492
|
...ordinaryChanges,
|
|
1254
|
-
...[...loaded.model.tables.values()].
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
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
|
+
}),
|
|
1283
530
|
...[...loaded.model.tables.values()].filter((table) => table.replace === true).map((table) => ({
|
|
1284
531
|
kind: "replace_table",
|
|
1285
532
|
table: table.name,
|
|
@@ -1291,7 +538,7 @@ async function runGeneratorPlan(input) {
|
|
|
1291
538
|
]
|
|
1292
539
|
};
|
|
1293
540
|
const ordinaryStepSources = [];
|
|
1294
|
-
for (const [changeIndex, change] of
|
|
541
|
+
for (const [changeIndex, change] of phaseOrderedChanges.entries()) {
|
|
1295
542
|
const generated = ordinaryChanges[changeIndex];
|
|
1296
543
|
if (!generated)
|
|
1297
544
|
throw new Error(
|
|
@@ -1408,18 +655,27 @@ async function runGeneratorPlan(input) {
|
|
|
1408
655
|
continue;
|
|
1409
656
|
}
|
|
1410
657
|
if (change.kind === "adopt_table" && change.adoption) {
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
}
|
|
1422
|
-
|
|
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
|
+
});
|
|
1423
679
|
continue;
|
|
1424
680
|
}
|
|
1425
681
|
lifecycleSteps.push(
|
|
@@ -1456,13 +712,13 @@ async function runGeneratorPlan(input) {
|
|
|
1456
712
|
plan
|
|
1457
713
|
);
|
|
1458
714
|
const run = {
|
|
1459
|
-
runId: `dbsp-generator-${
|
|
715
|
+
runId: `dbsp-generator-${randomUUID()}`,
|
|
1460
716
|
planDigest,
|
|
1461
|
-
targetContextDigest:
|
|
1462
|
-
database: await
|
|
717
|
+
targetContextDigest: canonicalJsonDigest({
|
|
718
|
+
database: await databaseId(pool),
|
|
1463
719
|
schema
|
|
1464
720
|
}),
|
|
1465
|
-
databaseId: await
|
|
721
|
+
databaseId: await databaseId(pool),
|
|
1466
722
|
coreVersion: "schema-differ-generator-v1",
|
|
1467
723
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1468
724
|
replayability: material.changes.some(
|
|
@@ -1501,20 +757,25 @@ async function runGeneratorPlan(input) {
|
|
|
1501
757
|
import {
|
|
1502
758
|
acquirePgTransitionClient,
|
|
1503
759
|
createPgExecutionContract,
|
|
1504
|
-
|
|
760
|
+
createPgPhysicalModel as createPgPhysicalModel2,
|
|
761
|
+
createPgsqlAdapter as createPgsqlAdapter2,
|
|
1505
762
|
createPgTransitionLessor as createPgTransitionLessor2,
|
|
1506
763
|
createPgTransitionPack,
|
|
1507
764
|
createPgTransitionRunPersister as createPgTransitionRunPersister2,
|
|
1508
765
|
ensureTransitionJournal,
|
|
1509
766
|
escapeDiagnosticText,
|
|
1510
|
-
getNamingPluginForDbCasing,
|
|
1511
767
|
PgExecutionContractDerivationError,
|
|
1512
768
|
pgTargetIdentityMismatch,
|
|
1513
769
|
readPgExecutionTargetFromClient,
|
|
1514
770
|
readPgObservationContextFromLessor
|
|
1515
771
|
} from "@dbsp/adapter-pgsql";
|
|
772
|
+
import {
|
|
773
|
+
PgTransitionRunPersistenceIndeterminateError,
|
|
774
|
+
planPgTransitionRun
|
|
775
|
+
} from "@dbsp/adapter-pgsql/internal";
|
|
1516
776
|
import {
|
|
1517
777
|
acquireTransitionLease as acquireTransitionLease2,
|
|
778
|
+
assertCanonicalizableJson,
|
|
1518
779
|
bindDeclarationSet,
|
|
1519
780
|
bindExecutionContract,
|
|
1520
781
|
createComparator,
|
|
@@ -1525,6 +786,7 @@ import {
|
|
|
1525
786
|
validateDeclarationModel
|
|
1526
787
|
} from "@dbsp/core";
|
|
1527
788
|
import { Command } from "commander";
|
|
789
|
+
var planPhysicalPgTransitionRun = planPgTransitionRun;
|
|
1528
790
|
var PlanPersistenceIndeterminateError = class extends Error {
|
|
1529
791
|
runId;
|
|
1530
792
|
result;
|
|
@@ -1574,6 +836,15 @@ var PlanOperationAndCleanupError = class extends AggregateError {
|
|
|
1574
836
|
cleanupError;
|
|
1575
837
|
};
|
|
1576
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
|
+
}
|
|
1577
848
|
function equivalenceContext(context) {
|
|
1578
849
|
return {
|
|
1579
850
|
engine: context.engine,
|
|
@@ -1654,20 +925,20 @@ var defaultPlanDeps = {
|
|
|
1654
925
|
async loadCurrent(pool, schema, expectedTargetIdentity) {
|
|
1655
926
|
const lease = await acquirePgTransitionClient(pool);
|
|
1656
927
|
try {
|
|
1657
|
-
const
|
|
928
|
+
const observed = await readPgExecutionTargetFromClient(
|
|
1658
929
|
lease.client,
|
|
1659
930
|
[schema ?? "public"]
|
|
1660
931
|
);
|
|
1661
932
|
const mismatch = pgTargetIdentityMismatch(
|
|
1662
933
|
expectedTargetIdentity,
|
|
1663
|
-
|
|
934
|
+
observed.identity
|
|
1664
935
|
);
|
|
1665
936
|
if (mismatch) {
|
|
1666
937
|
throw new Error(
|
|
1667
938
|
`PostgreSQL introspection target identity does not match the captured target: ${mismatch}`
|
|
1668
939
|
);
|
|
1669
940
|
}
|
|
1670
|
-
return await
|
|
941
|
+
return await createPgsqlAdapter2(lease.client, {
|
|
1671
942
|
borrowedClient: true
|
|
1672
943
|
}).introspect(schema === void 0 ? {} : { schema });
|
|
1673
944
|
} finally {
|
|
@@ -1682,18 +953,14 @@ var defaultPlanDeps = {
|
|
|
1682
953
|
expectedTargetIdentity
|
|
1683
954
|
);
|
|
1684
955
|
},
|
|
1685
|
-
createPlanner(
|
|
1686
|
-
const
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
})
|
|
1690
|
-
]);
|
|
1691
|
-
const comparator = createComparator(registry2);
|
|
1692
|
-
const prover = createProver(registry2);
|
|
956
|
+
createPlanner() {
|
|
957
|
+
const registry = createPackRegistry([createPgTransitionPack()]);
|
|
958
|
+
const comparator = createComparator(registry);
|
|
959
|
+
const prover = createProver(registry);
|
|
1693
960
|
return {
|
|
1694
961
|
compare: (desired, current, context) => comparator.compare(desired, current, equivalenceContext(context)),
|
|
1695
962
|
prove: (compare, pool, context) => prover.prove(compare, createPgTransitionLessor2(pool), context),
|
|
1696
|
-
render: (plan, context) => renderProvenPlanSql(
|
|
963
|
+
render: (plan, context) => renderProvenPlanSql(registry, plan, context)
|
|
1697
964
|
};
|
|
1698
965
|
},
|
|
1699
966
|
async persist(pool, run, plan) {
|
|
@@ -1728,9 +995,9 @@ function executionContractBlockedAssessment(error) {
|
|
|
1728
995
|
]
|
|
1729
996
|
};
|
|
1730
997
|
}
|
|
1731
|
-
function renderProvenPlanSql(
|
|
998
|
+
function renderProvenPlanSql(registry, plan, context) {
|
|
1732
999
|
const statements = plan.steps.map((step) => {
|
|
1733
|
-
const resolution =
|
|
1000
|
+
const resolution = registry.resolveOperation(step.operation);
|
|
1734
1001
|
if (!resolution.ok || !resolution.semantics.renderPlanSql) {
|
|
1735
1002
|
throw new Error(
|
|
1736
1003
|
`no SQL renderer is registered for proven operation ${step.operation.operationKind.name}`
|
|
@@ -1750,126 +1017,170 @@ async function runPlan(options, overrides) {
|
|
|
1750
1017
|
throw new Error(`unsupported plan format ${format}; expected sql or json`);
|
|
1751
1018
|
}
|
|
1752
1019
|
const loaded = await deps.loadSchema(options.schemaFile);
|
|
1753
|
-
|
|
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);
|
|
1754
1028
|
const connection = await deps.createDbConnection(options.db);
|
|
1755
1029
|
const { pool } = connection;
|
|
1756
1030
|
let result;
|
|
1031
|
+
let renderedSql;
|
|
1757
1032
|
let operationError;
|
|
1758
|
-
let failed = false;
|
|
1759
|
-
try {
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
const prove = await planner.prove(compare, pool, context);
|
|
1778
|
-
if (prove.kind === "proven") {
|
|
1779
|
-
let executionContract;
|
|
1780
|
-
try {
|
|
1781
|
-
executionContract = await deps.buildExecutionContract(
|
|
1782
|
-
pool,
|
|
1783
|
-
options.schema,
|
|
1784
|
-
prove.plan,
|
|
1785
|
-
targetIdentity
|
|
1786
|
-
);
|
|
1787
|
-
} catch (error) {
|
|
1788
|
-
if (!(error instanceof PgExecutionContractDerivationError)) throw error;
|
|
1033
|
+
let failed = false;
|
|
1034
|
+
try {
|
|
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");
|
|
1789
1052
|
result = {
|
|
1790
|
-
compareKind: compare.kind,
|
|
1791
|
-
proveKind: "
|
|
1792
|
-
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,
|
|
1793
1067
|
persisted: false,
|
|
1794
1068
|
runId: null,
|
|
1795
1069
|
planDigest: null
|
|
1796
1070
|
};
|
|
1797
1071
|
}
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
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, {
|
|
1808
1120
|
engine: context.engine,
|
|
1809
1121
|
database: context.databaseId,
|
|
1810
|
-
schema:
|
|
1811
|
-
}
|
|
1812
|
-
getNamingPluginForDbCasing(loaded.dbCasing ?? "preserve")
|
|
1813
|
-
)
|
|
1814
|
-
);
|
|
1815
|
-
const run = createTransitionRunMetadata(durablePlan);
|
|
1816
|
-
const proofContext = prove.plan.observations.find(
|
|
1817
|
-
(observation) => observation.role === "evidence"
|
|
1818
|
-
)?.context;
|
|
1819
|
-
if (!proofContext) {
|
|
1820
|
-
throw new Error(
|
|
1821
|
-
"internal error: minted proven plan has no evidence observation context"
|
|
1122
|
+
schema: physical.schema
|
|
1123
|
+
})
|
|
1822
1124
|
);
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
{
|
|
1831
|
-
compareKind: compare.kind,
|
|
1832
|
-
proveKind: prove.kind,
|
|
1833
|
-
assessment: prove.assessment,
|
|
1834
|
-
persisted: "indeterminate",
|
|
1835
|
-
runId: run.runId,
|
|
1836
|
-
planDigest: run.planDigest
|
|
1837
|
-
},
|
|
1838
|
-
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"
|
|
1839
1132
|
);
|
|
1840
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
|
+
};
|
|
1841
1162
|
}
|
|
1163
|
+
}
|
|
1164
|
+
if (prove.kind === "no-drift") {
|
|
1165
|
+
result = {
|
|
1166
|
+
compareKind: compare.kind,
|
|
1167
|
+
proveKind: prove.kind,
|
|
1168
|
+
assessment: prove.assessment,
|
|
1169
|
+
persisted: false,
|
|
1170
|
+
runId: null,
|
|
1171
|
+
planDigest: null
|
|
1172
|
+
};
|
|
1173
|
+
} else if (prove.kind !== "proven") {
|
|
1842
1174
|
result = {
|
|
1843
1175
|
compareKind: compare.kind,
|
|
1844
1176
|
proveKind: prove.kind,
|
|
1845
1177
|
assessment: prove.assessment,
|
|
1846
|
-
persisted:
|
|
1847
|
-
runId:
|
|
1848
|
-
planDigest:
|
|
1849
|
-
plan: durablePlan,
|
|
1850
|
-
sql
|
|
1178
|
+
persisted: false,
|
|
1179
|
+
runId: null,
|
|
1180
|
+
planDigest: null
|
|
1851
1181
|
};
|
|
1852
1182
|
}
|
|
1853
1183
|
}
|
|
1854
|
-
if (prove.kind === "no-drift") {
|
|
1855
|
-
result = {
|
|
1856
|
-
compareKind: compare.kind,
|
|
1857
|
-
proveKind: prove.kind,
|
|
1858
|
-
assessment: prove.assessment,
|
|
1859
|
-
persisted: false,
|
|
1860
|
-
runId: null,
|
|
1861
|
-
planDigest: null
|
|
1862
|
-
};
|
|
1863
|
-
} else if (prove.kind !== "proven") {
|
|
1864
|
-
result = {
|
|
1865
|
-
compareKind: compare.kind,
|
|
1866
|
-
proveKind: prove.kind,
|
|
1867
|
-
assessment: prove.assessment,
|
|
1868
|
-
persisted: false,
|
|
1869
|
-
runId: null,
|
|
1870
|
-
planDigest: null
|
|
1871
|
-
};
|
|
1872
|
-
}
|
|
1873
1184
|
} catch (error) {
|
|
1874
1185
|
failed = true;
|
|
1875
1186
|
operationError = error;
|
|
@@ -1881,6 +1192,20 @@ async function runPlan(options, overrides) {
|
|
|
1881
1192
|
cleanup = { kind: "failed", error };
|
|
1882
1193
|
}
|
|
1883
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
|
+
}
|
|
1884
1209
|
if (operationError instanceof PlanPersistenceIndeterminateError) {
|
|
1885
1210
|
throw new PlanPersistenceIndeterminateError(
|
|
1886
1211
|
operationError.result,
|
|
@@ -2059,7 +1384,7 @@ function generatorPlanLedgerAddresses(steps) {
|
|
|
2059
1384
|
const addresses = /* @__PURE__ */ new Map();
|
|
2060
1385
|
for (const step of steps) {
|
|
2061
1386
|
if (step.address !== void 0) {
|
|
2062
|
-
addresses.set(
|
|
1387
|
+
addresses.set(ledgerAddressKey(step.address), step.address);
|
|
2063
1388
|
continue;
|
|
2064
1389
|
}
|
|
2065
1390
|
const closure = step.closure;
|
|
@@ -2070,7 +1395,7 @@ function generatorPlanLedgerAddresses(steps) {
|
|
|
2070
1395
|
...closure.members.map((member) => member.address)
|
|
2071
1396
|
];
|
|
2072
1397
|
for (const address of candidates)
|
|
2073
|
-
addresses.set(
|
|
1398
|
+
addresses.set(ledgerAddressKey(address), address);
|
|
2074
1399
|
}
|
|
2075
1400
|
return [...addresses.values()];
|
|
2076
1401
|
}
|
|
@@ -2080,21 +1405,21 @@ async function generatorRunHasLedgerEvidence(session, journal, steps) {
|
|
|
2080
1405
|
const addresses = generatorPlanLedgerAddresses(steps);
|
|
2081
1406
|
const homes = /* @__PURE__ */ new Map();
|
|
2082
1407
|
for (const address of addresses) {
|
|
2083
|
-
const
|
|
2084
|
-
homes.set(`${
|
|
1408
|
+
const home = ledgerHomeForGeneratorAddress(address);
|
|
1409
|
+
homes.set(`${home.scope}:${home.schema ?? ""}`, home);
|
|
2085
1410
|
}
|
|
2086
|
-
for (const
|
|
1411
|
+
for (const home of homes.values()) {
|
|
2087
1412
|
for (const executionId of executionIds) {
|
|
2088
1413
|
const reservations = await readPgLedgerReservationsForExecution(
|
|
2089
1414
|
session,
|
|
2090
|
-
|
|
1415
|
+
home,
|
|
2091
1416
|
executionId
|
|
2092
1417
|
);
|
|
2093
1418
|
if (reservations.length > 0) return true;
|
|
2094
1419
|
}
|
|
2095
1420
|
}
|
|
2096
1421
|
for (const address of addresses) {
|
|
2097
|
-
const chain = await
|
|
1422
|
+
const chain = await readPgLedgerAddressChain(
|
|
2098
1423
|
session,
|
|
2099
1424
|
ledgerHomeForGeneratorAddress(address),
|
|
2100
1425
|
address
|
|
@@ -2160,19 +1485,19 @@ async function withPoolCleanupReported(result, close) {
|
|
|
2160
1485
|
function compareCodeUnits(left, right) {
|
|
2161
1486
|
return left < right ? -1 : left > right ? 1 : 0;
|
|
2162
1487
|
}
|
|
2163
|
-
function
|
|
1488
|
+
function canonicalJson(value) {
|
|
2164
1489
|
if (value === null || typeof value === "string" || typeof value === "boolean" || typeof value === "number") {
|
|
2165
1490
|
if (typeof value === "number" && !Number.isFinite(value))
|
|
2166
1491
|
throw new Error("policy values must be finite JSON values");
|
|
2167
1492
|
return JSON.stringify(value);
|
|
2168
1493
|
}
|
|
2169
|
-
if (Array.isArray(value)) return `[${value.map(
|
|
1494
|
+
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
|
|
2170
1495
|
if (typeof value !== "object")
|
|
2171
1496
|
throw new Error(
|
|
2172
1497
|
"policy values must be JSON values (undefined is not allowed)"
|
|
2173
1498
|
);
|
|
2174
1499
|
const record2 = value;
|
|
2175
|
-
return `{${Object.keys(record2).sort().map((key) => `${JSON.stringify(key)}:${
|
|
1500
|
+
return `{${Object.keys(record2).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(record2[key])}`).join(",")}}`;
|
|
2176
1501
|
}
|
|
2177
1502
|
function nonEmptyString(value, path) {
|
|
2178
1503
|
if (typeof value !== "string" || value.length === 0 || value.trim() !== value)
|
|
@@ -2232,7 +1557,7 @@ function validateResourceAddress(value, path) {
|
|
|
2232
1557
|
`${path}.catalogueIdentity.value`
|
|
2233
1558
|
);
|
|
2234
1559
|
try {
|
|
2235
|
-
|
|
1560
|
+
canonicalJson(identityValue);
|
|
2236
1561
|
} catch (error) {
|
|
2237
1562
|
throw new Error(
|
|
2238
1563
|
`${path}.catalogueIdentity.value must be a canonicalizable JSON object: ${errorDetail(error)}`
|
|
@@ -2298,15 +1623,15 @@ function validateAssumptionAcceptance(value, path = "acceptance") {
|
|
|
2298
1623
|
}
|
|
2299
1624
|
return candidate;
|
|
2300
1625
|
}
|
|
2301
|
-
function normaliseAcceptance(
|
|
2302
|
-
const validated = validateAssumptionAcceptance(
|
|
1626
|
+
function normaliseAcceptance(acceptance) {
|
|
1627
|
+
const validated = validateAssumptionAcceptance(acceptance);
|
|
2303
1628
|
const withinScope = validated.withinScope ? [
|
|
2304
1629
|
...new Map(
|
|
2305
|
-
validated.withinScope.map((item) => [
|
|
1630
|
+
validated.withinScope.map((item) => [canonicalJson(item), item])
|
|
2306
1631
|
)
|
|
2307
|
-
].sort(([left], [right]) => compareCodeUnits(left, right)).map(([, item]) => JSON.parse(
|
|
1632
|
+
].sort(([left], [right]) => compareCodeUnits(left, right)).map(([, item]) => JSON.parse(canonicalJson(item))) : void 0;
|
|
2308
1633
|
return JSON.parse(
|
|
2309
|
-
|
|
1634
|
+
canonicalJson({
|
|
2310
1635
|
class: validated.class,
|
|
2311
1636
|
...validated.fromTrustRoot ? { fromTrustRoot: validated.fromTrustRoot } : {},
|
|
2312
1637
|
...withinScope === void 0 ? {} : { withinScope }
|
|
@@ -2318,7 +1643,7 @@ function canonicalApplyPolicy(acceptances) {
|
|
|
2318
1643
|
...new Map(
|
|
2319
1644
|
acceptances.map((item) => {
|
|
2320
1645
|
const normalized = normaliseAcceptance(item);
|
|
2321
|
-
return [
|
|
1646
|
+
return [canonicalJson(normalized), normalized];
|
|
2322
1647
|
})
|
|
2323
1648
|
)
|
|
2324
1649
|
].sort(([left], [right]) => compareCodeUnits(left, right)).map(([, item]) => item);
|
|
@@ -2328,7 +1653,7 @@ function authorizationDigest(runId, planDigest, policy, grants, actor, authorize
|
|
|
2328
1653
|
if (Number.isNaN(authorizedInstant.getTime()))
|
|
2329
1654
|
throw new Error("authorization timestamp must be a valid instant");
|
|
2330
1655
|
return createHash("sha256").update(
|
|
2331
|
-
|
|
1656
|
+
canonicalJson({
|
|
2332
1657
|
runId,
|
|
2333
1658
|
planDigest,
|
|
2334
1659
|
policy: canonicalApplyPolicy(policy),
|
|
@@ -2341,7 +1666,7 @@ function authorizationDigest(runId, planDigest, policy, grants, actor, authorize
|
|
|
2341
1666
|
}
|
|
2342
1667
|
function hasReusableAuthorization(authorizations, runId, planDigest, policy, grants) {
|
|
2343
1668
|
return authorizations?.some(
|
|
2344
|
-
(item) =>
|
|
1669
|
+
(item) => canonicalJson(item.policy) === canonicalJson(policy) && canonicalJson(item.grants) === canonicalJson(grants) && item.digest === authorizationDigest(
|
|
2345
1670
|
runId,
|
|
2346
1671
|
planDigest,
|
|
2347
1672
|
policy,
|
|
@@ -2351,6 +1676,13 @@ function hasReusableAuthorization(authorizations, runId, planDigest, policy, gra
|
|
|
2351
1676
|
)
|
|
2352
1677
|
) ?? false;
|
|
2353
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
|
+
}
|
|
2354
1686
|
async function effectiveApplyPolicy(options) {
|
|
2355
1687
|
let fromFile = [];
|
|
2356
1688
|
if (options.acceptPolicy) {
|
|
@@ -2375,9 +1707,6 @@ async function effectiveApplyPolicy(options) {
|
|
|
2375
1707
|
)
|
|
2376
1708
|
};
|
|
2377
1709
|
}
|
|
2378
|
-
function registry() {
|
|
2379
|
-
return createPackRegistry2([createPgTransitionPack2({})]);
|
|
2380
|
-
}
|
|
2381
1710
|
var APPLY_OUTCOME_CONTRACT = [
|
|
2382
1711
|
["completed", 0, "all planned steps completed"],
|
|
2383
1712
|
["operation-failed-not-applied", 10, "operation failed before application"],
|
|
@@ -2676,13 +2005,6 @@ detail: ${escapeDiagnosticText3(execution.detail)}`;
|
|
|
2676
2005
|
].join("\n");
|
|
2677
2006
|
}
|
|
2678
2007
|
}
|
|
2679
|
-
function acceptanceMatches(assumption, acceptance2) {
|
|
2680
|
-
return acceptance2.class === assumption.class && (!acceptance2.fromTrustRoot || canonicalJson3(acceptance2.fromTrustRoot) === canonicalJson3(assumption.asserter)) && (assumption.scope.length === 0 ? !acceptance2.withinScope || acceptance2.withinScope.length === 0 : !acceptance2.withinScope || acceptance2.withinScope.length === 0 || assumption.scope.every(
|
|
2681
|
-
(resource) => acceptance2.withinScope?.some(
|
|
2682
|
-
(selector) => selectorMatchesResource(selector, resource)
|
|
2683
|
-
)
|
|
2684
|
-
));
|
|
2685
|
-
}
|
|
2686
2008
|
function recordedPlanRefusal() {
|
|
2687
2009
|
return { ...REFUSAL_VOCABULARY2["ERR-10"], state: "recorded-plan" };
|
|
2688
2010
|
}
|
|
@@ -2884,7 +2206,15 @@ async function runApplyInternal(runId, options, pool) {
|
|
|
2884
2206
|
throw new Error(
|
|
2885
2207
|
"persisted run no longer contains a generator plan"
|
|
2886
2208
|
);
|
|
2887
|
-
|
|
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;
|
|
2888
2218
|
if (!recordedSchema)
|
|
2889
2219
|
throw new Error(
|
|
2890
2220
|
"persisted generator run has no recorded planning schema and is non-resumable"
|
|
@@ -2899,7 +2229,7 @@ async function runApplyInternal(runId, options, pool) {
|
|
|
2899
2229
|
}
|
|
2900
2230
|
if (actualDigest !== current.run.planDigest || actualDigest !== expectedPlanDigest)
|
|
2901
2231
|
throw new RecordedPlanDigestMismatchError();
|
|
2902
|
-
const manifest =
|
|
2232
|
+
const manifest = validateNormalizedManagedStepManifest2(
|
|
2903
2233
|
current.plan.steps
|
|
2904
2234
|
);
|
|
2905
2235
|
if (!manifest.ok)
|
|
@@ -2947,6 +2277,18 @@ async function runApplyInternal(runId, options, pool) {
|
|
|
2947
2277
|
await journalLease.release();
|
|
2948
2278
|
}
|
|
2949
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
|
+
},
|
|
2950
2292
|
...options.replace === void 0 ? {} : { replaces: options.replace },
|
|
2951
2293
|
runId
|
|
2952
2294
|
});
|
|
@@ -2974,88 +2316,61 @@ async function runApplyInternal(runId, options, pool) {
|
|
|
2974
2316
|
);
|
|
2975
2317
|
}
|
|
2976
2318
|
} else {
|
|
2977
|
-
const
|
|
2319
|
+
const applied = await applyPgTransitionRun(
|
|
2978
2320
|
owned,
|
|
2979
2321
|
runId,
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
}
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
prepareExecutionSession: async (session, contract, plan) => {
|
|
2994
|
-
const currency = await validatePgManagedLedgerCurrency(
|
|
2995
|
-
session,
|
|
2996
|
-
plan
|
|
2997
|
-
);
|
|
2998
|
-
if (currency)
|
|
2999
|
-
return {
|
|
3000
|
-
ok: false,
|
|
3001
|
-
kind: "refused",
|
|
3002
|
-
detail: currency
|
|
3003
|
-
};
|
|
3004
|
-
return preparePgExecutionSession(session, contract, plan);
|
|
3005
|
-
},
|
|
3006
|
-
policy,
|
|
3007
|
-
target,
|
|
3008
|
-
authorize: async (run, plan, session) => {
|
|
3009
|
-
const current = await loadOnTarget(target, run.runId);
|
|
3010
|
-
const grants = plan.assumptions.map((assumption) => ({
|
|
3011
|
-
assumptionId: assumption.id,
|
|
3012
|
-
grant: policy.accepts.findIndex(
|
|
3013
|
-
(grant) => acceptanceMatches(assumption, grant)
|
|
3014
|
-
)
|
|
3015
|
-
}));
|
|
3016
|
-
if (hasReusableAuthorization(
|
|
3017
|
-
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],
|
|
3018
2335
|
run.runId,
|
|
3019
2336
|
transitionPlanDigest2(plan),
|
|
3020
2337
|
policy.accepts,
|
|
3021
2338
|
grants
|
|
3022
|
-
)
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
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(
|
|
3027
2349
|
run.runId,
|
|
3028
2350
|
transitionPlanDigest2(plan),
|
|
3029
2351
|
policy.accepts,
|
|
3030
2352
|
grants,
|
|
3031
2353
|
actor,
|
|
3032
2354
|
authorizedAt
|
|
3033
|
-
)
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
digest,
|
|
3039
|
-
actor,
|
|
3040
|
-
authorizedAt
|
|
3041
|
-
};
|
|
3042
|
-
await appendTransitionAuthorization(session, record2);
|
|
3043
|
-
}
|
|
3044
|
-
});
|
|
2355
|
+
),
|
|
2356
|
+
actor,
|
|
2357
|
+
authorizedAt
|
|
2358
|
+
};
|
|
2359
|
+
}
|
|
3045
2360
|
}
|
|
3046
2361
|
);
|
|
3047
|
-
if (
|
|
2362
|
+
if (applied.kind === "busy") result = { outcome: "run-busy", runId };
|
|
3048
2363
|
else {
|
|
3049
|
-
const outcome = outcomeForApplyResult(
|
|
2364
|
+
const outcome = outcomeForApplyResult(applied.result);
|
|
3050
2365
|
const preAppendRefusal = applyPreAppendRefusal(
|
|
3051
2366
|
outcome,
|
|
3052
2367
|
persisted.plan,
|
|
3053
|
-
|
|
2368
|
+
applied.result
|
|
3054
2369
|
);
|
|
3055
2370
|
result = {
|
|
3056
2371
|
outcome,
|
|
3057
2372
|
runId,
|
|
3058
|
-
result:
|
|
2373
|
+
result: applied.result,
|
|
3059
2374
|
...preAppendRefusal === void 0 ? {} : { refusal: preAppendRefusal }
|
|
3060
2375
|
};
|
|
3061
2376
|
}
|
|
@@ -3250,13 +2565,19 @@ var generateCommand = new Command3("generate").description("Generate code from s
|
|
|
3250
2565
|
case "ddl": {
|
|
3251
2566
|
const dbCasing = mapCasingToDbCasing(options.casing) ?? schema.dbCasing ?? "snake_case";
|
|
3252
2567
|
const casingLabel = formatGenerateCasingLabel(dbCasing);
|
|
3253
|
-
const { createPgsqlCompileOnlyAdapter } = await import("@dbsp/adapter-pgsql");
|
|
2568
|
+
const { createPgPhysicalModel: createPgPhysicalModel6, createPgsqlCompileOnlyAdapter } = await import("@dbsp/adapter-pgsql");
|
|
3254
2569
|
const adapter = createPgsqlCompileOnlyAdapter({
|
|
3255
2570
|
dbCasing,
|
|
3256
2571
|
...options.schemaName ? { schemaName: options.schemaName } : {}
|
|
3257
2572
|
});
|
|
3258
2573
|
{
|
|
3259
|
-
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, {
|
|
3260
2581
|
...options.drop !== void 0 && {
|
|
3261
2582
|
includeDropStatements: options.drop
|
|
3262
2583
|
},
|
|
@@ -3321,16 +2642,16 @@ function formatGenerateCasingLabel(dbCasing) {
|
|
|
3321
2642
|
}
|
|
3322
2643
|
|
|
3323
2644
|
// src/commands/inspect.ts
|
|
3324
|
-
import { isDeepStrictEqual
|
|
2645
|
+
import { isDeepStrictEqual } from "util";
|
|
3325
2646
|
import {
|
|
3326
2647
|
createPgTransitionLessor as createPgTransitionLessor4,
|
|
3327
2648
|
DBSP_LEDGER_EVENT_TABLE,
|
|
3328
2649
|
escapeDiagnosticText as escapeDiagnosticText4,
|
|
3329
|
-
readPgCatalogueIdentity as
|
|
3330
|
-
readPgLedgerAddressChain as
|
|
2650
|
+
readPgCatalogueIdentity as readPgCatalogueIdentity2,
|
|
2651
|
+
readPgLedgerAddressChain as readPgLedgerAddressChain2,
|
|
3331
2652
|
readPgLedgerMarker
|
|
3332
2653
|
} from "@dbsp/adapter-pgsql";
|
|
3333
|
-
import { acquireTransitionLease as acquireTransitionLease4, projectLedgerChain
|
|
2654
|
+
import { acquireTransitionLease as acquireTransitionLease4, projectLedgerChain } from "@dbsp/core";
|
|
3334
2655
|
import {
|
|
3335
2656
|
canonicalResourceParent
|
|
3336
2657
|
} from "@dbsp/types";
|
|
@@ -3376,7 +2697,7 @@ function inspectLiveDrift(projection, live) {
|
|
|
3376
2697
|
kind: "catalogue-drift",
|
|
3377
2698
|
detail: "ledger records a present catalogue identity but the object is absent"
|
|
3378
2699
|
};
|
|
3379
|
-
return
|
|
2700
|
+
return isDeepStrictEqual(terminal.catalogueIdentity, live.catalogueIdentity) ? { kind: "matches-ledger" } : {
|
|
3380
2701
|
kind: "catalogue-drift",
|
|
3381
2702
|
detail: "live catalogue identity differs from the ledger terminal"
|
|
3382
2703
|
};
|
|
@@ -3499,8 +2820,8 @@ async function runInspect(selector, options) {
|
|
|
3499
2820
|
);
|
|
3500
2821
|
let projection;
|
|
3501
2822
|
try {
|
|
3502
|
-
projection =
|
|
3503
|
-
await
|
|
2823
|
+
projection = projectLedgerChain(
|
|
2824
|
+
await readPgLedgerAddressChain2(lease.session, ledger, address)
|
|
3504
2825
|
);
|
|
3505
2826
|
} catch (error) {
|
|
3506
2827
|
return {
|
|
@@ -3519,7 +2840,7 @@ async function runInspect(selector, options) {
|
|
|
3519
2840
|
};
|
|
3520
2841
|
}
|
|
3521
2842
|
try {
|
|
3522
|
-
const live = await
|
|
2843
|
+
const live = await readPgCatalogueIdentity2(lease.session, address);
|
|
3523
2844
|
const refusal = inspectRefusal(projection);
|
|
3524
2845
|
const openReaddress = projection?.kind === "projected-ledger-chain" && projection.openClaim?.kind === "readdress-intent" && projection.openClaim.event.pairId ? {
|
|
3525
2846
|
pairId: projection.openClaim.event.pairId,
|
|
@@ -3600,69 +2921,409 @@ function reportWarnings(warnings) {
|
|
|
3600
2921
|
console.log(` \u26A0\uFE0F ${warning}`);
|
|
3601
2922
|
}
|
|
3602
2923
|
}
|
|
3603
|
-
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(
|
|
3604
|
-
"--exclude <patterns>",
|
|
3605
|
-
"Tables to exclude (comma-separated glob patterns)",
|
|
3606
|
-
"_migrations,_prisma*,pg_*"
|
|
3607
|
-
).option("--include <patterns>", "Tables to include (comma-separated)").option("--no-db-type-comments", "Omit original DB type comments").option(
|
|
3608
|
-
"--db-casing <casing>",
|
|
3609
|
-
"Database column casing (snake_case \u2192 camelCase in generated code)",
|
|
3610
|
-
"snake_case"
|
|
3611
|
-
).action(
|
|
3612
|
-
async (options) => {
|
|
3613
|
-
const redactedUrl = redactDbUrl(options.db);
|
|
3614
|
-
console.log(`\u{1F50D} Introspecting database: ${redactedUrl}`);
|
|
3615
|
-
console.log(` Schema: ${options.schemaName}`);
|
|
3616
|
-
if (options.exclude) {
|
|
3617
|
-
console.log(` Excluding: ${options.exclude}`);
|
|
3618
|
-
}
|
|
3619
|
-
console.log("");
|
|
3620
|
-
try {
|
|
3621
|
-
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);
|
|
3622
3215
|
try {
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
const includePatterns = options.include ? options.include.split(",").map((s) => s.trim()) : void 0;
|
|
3626
|
-
const model = await introspect(pool, {
|
|
3627
|
-
schema: options.schemaName,
|
|
3628
|
-
...excludePatterns ? { exclude: excludePatterns } : {},
|
|
3629
|
-
...includePatterns ? { include: includePatterns } : {}
|
|
3630
|
-
});
|
|
3631
|
-
const tableCount = model.tables.size;
|
|
3632
|
-
const relationCount = model.relations.size;
|
|
3633
|
-
const hierarchyCount = model.hierarchies?.length ?? 0;
|
|
3634
|
-
console.log(
|
|
3635
|
-
`\u{1F4CA} Found ${tableCount} tables, ${relationCount} relations, ${hierarchyCount} hierarchies`
|
|
3636
|
-
);
|
|
3637
|
-
const codegenOptions = {
|
|
3638
|
-
sourceUrl: options.db,
|
|
3639
|
-
includeDbTypeComments: options.dbTypeComments,
|
|
3640
|
-
introspectedAt: model.introspectedAt,
|
|
3641
|
-
dbCasing: options.dbCasing
|
|
3642
|
-
};
|
|
3643
|
-
const generated = generateSchemaFileWithDiagnostics(
|
|
3644
|
-
model,
|
|
3645
|
-
codegenOptions
|
|
3646
|
-
);
|
|
3647
|
-
const warnings = uniqueWarnings(generated.warnings);
|
|
3648
|
-
if (warnings.length > 0) {
|
|
3649
|
-
reportWarnings(warnings);
|
|
3650
|
-
}
|
|
3651
|
-
console.log("");
|
|
3652
|
-
const outPath = resolve2(process.cwd(), options.out);
|
|
3653
|
-
mkdirSync2(dirname2(outPath), { recursive: true });
|
|
3654
|
-
writeFileSync2(outPath, generated.code, "utf-8");
|
|
3655
|
-
console.log(`\u2705 Generated schema: ${outPath}`);
|
|
3656
|
-
console.log(` Tables: ${tableCount}`);
|
|
3657
|
-
console.log(` Relations: ${relationCount}`);
|
|
3658
|
-
} finally {
|
|
3659
|
-
await pool.end();
|
|
3216
|
+
sensitive.add(decodeURIComponent(password));
|
|
3217
|
+
} catch {
|
|
3660
3218
|
}
|
|
3661
|
-
} catch (error) {
|
|
3662
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
3663
|
-
console.error(`\u274C ${message}`);
|
|
3664
|
-
process.exit(1);
|
|
3665
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;
|
|
3666
3327
|
}
|
|
3667
3328
|
);
|
|
3668
3329
|
|
|
@@ -3671,12 +3332,12 @@ import { createHash as createHash2 } from "crypto";
|
|
|
3671
3332
|
import { mkdtemp, open, rename, rm } from "fs/promises";
|
|
3672
3333
|
import { dirname as dirname3, join } from "path";
|
|
3673
3334
|
import {
|
|
3674
|
-
|
|
3675
|
-
|
|
3335
|
+
createPgPhysicalModel as createPgPhysicalModel4,
|
|
3336
|
+
escapeDiagnosticText as escapeDiagnosticText6,
|
|
3676
3337
|
runPgReinitializePreflight
|
|
3677
3338
|
} from "@dbsp/adapter-pgsql";
|
|
3678
3339
|
import { declarationSetFromModel as declarationSetFromModel2 } from "@dbsp/core";
|
|
3679
|
-
import { Command as
|
|
3340
|
+
import { Command as Command7 } from "commander";
|
|
3680
3341
|
var defaultDeps = { createDbConnection, loadSchema };
|
|
3681
3342
|
var nodeAdoptionFileSystem = {
|
|
3682
3343
|
mkdtemp,
|
|
@@ -3726,13 +3387,9 @@ async function databaseName(pool) {
|
|
|
3726
3387
|
throw new Error("current database could not be read");
|
|
3727
3388
|
return name;
|
|
3728
3389
|
}
|
|
3729
|
-
function declarationsForScopes(model, database, scopes
|
|
3390
|
+
function declarationsForScopes(model, database, scopes) {
|
|
3730
3391
|
const declarations = scopes.flatMap(
|
|
3731
|
-
(schema) => declarationSetFromModel2(
|
|
3732
|
-
model,
|
|
3733
|
-
{ engine: "postgresql", database, schema },
|
|
3734
|
-
naming
|
|
3735
|
-
).declarations
|
|
3392
|
+
(schema) => declarationSetFromModel2(model, { engine: "postgresql", database, schema }).declarations
|
|
3736
3393
|
);
|
|
3737
3394
|
const seen = /* @__PURE__ */ new Set();
|
|
3738
3395
|
const uniqueDeclarations = declarations.filter((declaration) => {
|
|
@@ -3767,19 +3424,25 @@ async function runPreflight(options, deps = defaultDeps) {
|
|
|
3767
3424
|
throw new Error("dbsp preflight requires --reinitialize");
|
|
3768
3425
|
if (!options.out)
|
|
3769
3426
|
throw new Error("dbsp preflight --reinitialize requires --out <file>");
|
|
3770
|
-
|
|
3427
|
+
const physicalSchema = options.scopes[0];
|
|
3428
|
+
if (physicalSchema === void 0)
|
|
3771
3429
|
throw new Error(
|
|
3772
3430
|
"dbsp preflight requires at least one explicit --scope <schema>"
|
|
3773
3431
|
);
|
|
3774
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
|
+
});
|
|
3775
3439
|
const connection = await deps.createDbConnection(options.db);
|
|
3776
3440
|
try {
|
|
3777
3441
|
const database = await databaseName(connection.pool);
|
|
3778
3442
|
const declarations = declarationsForScopes(
|
|
3779
|
-
|
|
3443
|
+
physical.model,
|
|
3780
3444
|
database,
|
|
3781
|
-
options.scopes
|
|
3782
|
-
getNamingPluginForDbCasing2(loaded.dbCasing ?? "preserve")
|
|
3445
|
+
options.scopes
|
|
3783
3446
|
);
|
|
3784
3447
|
return await runPgReinitializePreflight({
|
|
3785
3448
|
pool: connection.pool,
|
|
@@ -3794,18 +3457,18 @@ async function runPreflight(options, deps = defaultDeps) {
|
|
|
3794
3457
|
function formatScope(scope) {
|
|
3795
3458
|
const name = scope.ledger.scope === "database" ? "dbsp_meta" : scope.ledger.schema ?? "unknown";
|
|
3796
3459
|
if (scope.outcome === "failed")
|
|
3797
|
-
return `${
|
|
3798
|
-
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)}`;
|
|
3799
3462
|
}
|
|
3800
3463
|
function formatReinitializeSplit(scopes) {
|
|
3801
3464
|
const names = (predicate) => scopes.filter(predicate).map(
|
|
3802
|
-
(scope) =>
|
|
3465
|
+
(scope) => escapeDiagnosticText6(
|
|
3803
3466
|
scope.ledger.scope === "database" ? "dbsp_meta" : scope.ledger.schema ?? "unknown"
|
|
3804
3467
|
)
|
|
3805
3468
|
).join(",") || "none";
|
|
3806
3469
|
return `reinitialize-split: changed=${names((scope) => scope.outcome === "current")}; failed=${names((scope) => scope.outcome === "failed")}; not-attempted=${names((scope) => scope.outcome === "not-attempted")}`;
|
|
3807
3470
|
}
|
|
3808
|
-
var preflightCommand = new
|
|
3471
|
+
var preflightCommand = new Command7("preflight").description(
|
|
3809
3472
|
"Run the separately privileged managed-ledger reinitialize cutover"
|
|
3810
3473
|
).requiredOption("-d, --db <url>", "Database connection URL (required)").requiredOption(
|
|
3811
3474
|
"--reinitialize",
|
|
@@ -3830,7 +3493,7 @@ var preflightCommand = new Command6("preflight").description(
|
|
|
3830
3493
|
);
|
|
3831
3494
|
} catch (error) {
|
|
3832
3495
|
console.error(
|
|
3833
|
-
|
|
3496
|
+
escapeDiagnosticText6(
|
|
3834
3497
|
error instanceof Error ? error.message : String(error)
|
|
3835
3498
|
)
|
|
3836
3499
|
);
|
|
@@ -3839,33 +3502,12 @@ var preflightCommand = new Command6("preflight").description(
|
|
|
3839
3502
|
});
|
|
3840
3503
|
|
|
3841
3504
|
// src/commands/reconcile.ts
|
|
3842
|
-
import { createHash as createHash3 } from "crypto";
|
|
3843
3505
|
import {
|
|
3844
|
-
|
|
3845
|
-
|
|
3846
|
-
|
|
3847
|
-
isPgDatabaseReadOnlyError,
|
|
3848
|
-
readPgLedgerAddressChain as readPgLedgerAddressChain4,
|
|
3849
|
-
readPgLedgerReservationsForExecution as readPgLedgerReservationsForExecution2,
|
|
3850
|
-
readPgLedgerScopeCurrency as readPgLedgerScopeCurrency2,
|
|
3851
|
-
readTransitionJournal as readTransitionJournal2,
|
|
3852
|
-
readVerifiedPgLedgerReservationsForPair,
|
|
3853
|
-
recoverPgReaddressPair,
|
|
3854
|
-
withPgTransitionRunLock as withPgTransitionRunLock2
|
|
3506
|
+
escapeDiagnosticText as escapeDiagnosticText7,
|
|
3507
|
+
PgReconcileTransitionRunError,
|
|
3508
|
+
reconcilePgTransitionRun
|
|
3855
3509
|
} from "@dbsp/adapter-pgsql";
|
|
3856
|
-
import {
|
|
3857
|
-
import {
|
|
3858
|
-
acquireExclusiveTransitionLease as acquireExclusiveTransitionLease2,
|
|
3859
|
-
assumptionAccepted,
|
|
3860
|
-
canonicalJson as canonicalJson4,
|
|
3861
|
-
canonicalJsonDigest as canonicalJsonDigest3,
|
|
3862
|
-
outcomeClaimId as outcomeClaimId2,
|
|
3863
|
-
projectLedgerChain as projectLedgerChain3,
|
|
3864
|
-
resourceScopeCovers,
|
|
3865
|
-
transitionPlanDigest as transitionPlanDigest3
|
|
3866
|
-
} from "@dbsp/core";
|
|
3867
|
-
import { sameLedgerAddress } from "@dbsp/types";
|
|
3868
|
-
import { Command as Command7 } from "commander";
|
|
3510
|
+
import { Command as Command8 } from "commander";
|
|
3869
3511
|
function pgSqlState(error) {
|
|
3870
3512
|
if (typeof error !== "object" || error === null || !("code" in error))
|
|
3871
3513
|
return void 0;
|
|
@@ -3892,170 +3534,120 @@ function classifyReconcileFailure(error, stage) {
|
|
|
3892
3534
|
return "catalogue";
|
|
3893
3535
|
}
|
|
3894
3536
|
function isUnresolvedRecoveryOutcome(report) {
|
|
3895
|
-
return
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
);
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
|
|
3914
|
-
};
|
|
3915
|
-
}
|
|
3916
|
-
function recoveryReport(address, result) {
|
|
3917
|
-
if (result.kind === "outcome-recovery-appended") {
|
|
3918
|
-
const indeterminate = result.classification.resolution.eventKind === "indeterminate";
|
|
3919
|
-
return {
|
|
3920
|
-
address,
|
|
3921
|
-
outcome: indeterminate ? "indeterminate-appended" : result.append.kind === "already-appended-outcome-resolution" ? "already-appended" : "appended",
|
|
3922
|
-
reason: result.classification.resolution.reason
|
|
3923
|
-
};
|
|
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" });
|
|
3924
3556
|
}
|
|
3925
|
-
if (
|
|
3926
|
-
|
|
3927
|
-
|
|
3928
|
-
|
|
3929
|
-
|
|
3930
|
-
|
|
3931
|
-
|
|
3932
|
-
|
|
3933
|
-
...result.reasonCode === "catalogue-unavailable" ? {
|
|
3934
|
-
refusal: preAppendRefusalFor("ERR-09", {
|
|
3935
|
-
address,
|
|
3936
|
-
state: "unknown"
|
|
3937
|
-
})
|
|
3938
|
-
} : {}
|
|
3939
|
-
};
|
|
3940
|
-
if (result.kind === "outcome-recovery-blocked")
|
|
3941
|
-
return { address, outcome: "blocked", reason: result.reason };
|
|
3942
|
-
if (result.kind === "outcome-recovery-malformed-chain")
|
|
3943
|
-
return {
|
|
3944
|
-
address,
|
|
3945
|
-
outcome: "malformed-chain",
|
|
3946
|
-
reason: result.reason,
|
|
3947
|
-
failureCause: "malformed-journal",
|
|
3948
|
-
refusal: preAppendRefusalFor("ERR-08", { address, state: "unknown" })
|
|
3949
|
-
};
|
|
3950
|
-
if (result.kind === "outcome-transport-ambiguous")
|
|
3951
|
-
return {
|
|
3952
|
-
address,
|
|
3953
|
-
outcome: "transport-ambiguous",
|
|
3954
|
-
reason: result.reason,
|
|
3955
|
-
failureCause: "transport"
|
|
3956
|
-
};
|
|
3957
|
-
return { address, outcome: "protocol-refused", reason: result.reason };
|
|
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" }
|
|
3564
|
+
);
|
|
3958
3565
|
}
|
|
3959
|
-
function
|
|
3960
|
-
|
|
3961
|
-
|
|
3962
|
-
|
|
3963
|
-
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
|
|
3968
|
-
|
|
3969
|
-
|
|
3970
|
-
|
|
3971
|
-
|
|
3972
|
-
|
|
3973
|
-
|
|
3974
|
-
|
|
3975
|
-
|
|
3976
|
-
|
|
3977
|
-
|
|
3978
|
-
|
|
3979
|
-
|
|
3980
|
-
|
|
3981
|
-
|
|
3982
|
-
};
|
|
3983
|
-
return {
|
|
3984
|
-
address: first.address,
|
|
3985
|
-
outcome: "pending",
|
|
3986
|
-
reason: result.reason,
|
|
3987
|
-
pairId: result.pairId
|
|
3988
|
-
};
|
|
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
|
+
}));
|
|
3989
3589
|
}
|
|
3990
3590
|
function unresolvedRecoveryDetail(reports) {
|
|
3991
3591
|
const unresolved = reports.filter(isUnresolvedRecoveryOutcome);
|
|
3992
3592
|
if (unresolved.length === 0) return void 0;
|
|
3993
3593
|
return unresolved.map(
|
|
3994
|
-
(report) => `${
|
|
3594
|
+
(report) => `${escapeDiagnosticText7(report.address.name)}: ${escapeDiagnosticText7(report.reason ?? report.outcome)}`
|
|
3995
3595
|
).join("; ");
|
|
3996
3596
|
}
|
|
3997
3597
|
function formatReconcileHuman(result) {
|
|
3998
|
-
const line = `${
|
|
3598
|
+
const line = `${escapeDiagnosticText7(result.outcome)}: ${escapeDiagnosticText7(result.runId)}`;
|
|
3999
3599
|
const base = result.refusal ? formatPreAppendRefusalHuman(line, result.refusal) : line;
|
|
4000
3600
|
if (result.outcome === "reconcile-completed" || !result.recovery) return base;
|
|
4001
3601
|
const unresolved = result.recovery.filter(isUnresolvedRecoveryOutcome);
|
|
4002
3602
|
return unresolved.length === 0 ? base : [
|
|
4003
3603
|
base,
|
|
4004
3604
|
...unresolved.map(
|
|
4005
|
-
(report) => `${
|
|
3605
|
+
(report) => `${escapeDiagnosticText7(report.address.name)}: ${escapeDiagnosticText7(report.outcome)}${report.reason ? `: ${escapeDiagnosticText7(report.reason)}` : ""}`
|
|
4006
3606
|
)
|
|
4007
3607
|
].join("\n");
|
|
4008
3608
|
}
|
|
4009
|
-
function
|
|
4010
|
-
|
|
4011
|
-
|
|
4012
|
-
|
|
4013
|
-
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
|
|
4020
|
-
|
|
4021
|
-
|
|
4022
|
-
|
|
4023
|
-
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
|
|
4033
|
-
);
|
|
4034
|
-
|
|
4035
|
-
|
|
4036
|
-
const
|
|
4037
|
-
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
|
|
3609
|
+
function mapAdapterResult(result) {
|
|
3610
|
+
if (result.kind === "busy")
|
|
3611
|
+
return {
|
|
3612
|
+
outcome: "reconcile-run-unavailable",
|
|
3613
|
+
runId: result.runId,
|
|
3614
|
+
addresses: []
|
|
3615
|
+
};
|
|
3616
|
+
if (result.kind === "selection-unavailable")
|
|
3617
|
+
return {
|
|
3618
|
+
outcome: "reconcile-claim-selection-unavailable",
|
|
3619
|
+
runId: result.runId,
|
|
3620
|
+
addresses: result.addresses,
|
|
3621
|
+
...result.detail === void 0 ? {} : { detail: result.detail }
|
|
3622
|
+
};
|
|
3623
|
+
if (result.kind === "database-read-only") {
|
|
3624
|
+
const refusal = refusalForSelectedIssue(result.selectedIssue);
|
|
3625
|
+
return {
|
|
3626
|
+
outcome: "database-read-only",
|
|
3627
|
+
runId: result.runId,
|
|
3628
|
+
addresses: result.addresses,
|
|
3629
|
+
detail: result.selectedIssue.reason,
|
|
3630
|
+
...refusal === void 0 ? {} : { refusal }
|
|
3631
|
+
};
|
|
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);
|
|
3637
|
+
return {
|
|
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 }
|
|
3644
|
+
};
|
|
3645
|
+
}
|
|
4045
3646
|
return {
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
|
|
4050
|
-
plannedClaimKey: input.plannedClaimKey,
|
|
4051
|
-
admittedBundleDigest: bundleDigest,
|
|
4052
|
-
persistedBundleDigest: bundleDigest,
|
|
4053
|
-
recordedPreState: input.stableStateBeforeClaim,
|
|
4054
|
-
externalDdlExclusion: {
|
|
4055
|
-
planDigest: input.journal.run.planDigest,
|
|
4056
|
-
address: input.row.address,
|
|
4057
|
-
trustRoot: JSON.stringify(assumption.asserter)
|
|
4058
|
-
}
|
|
3647
|
+
outcome: "reconcile-completed",
|
|
3648
|
+
runId: result.runId,
|
|
3649
|
+
addresses: result.addresses,
|
|
3650
|
+
...recovery === void 0 ? {} : { recovery }
|
|
4059
3651
|
};
|
|
4060
3652
|
}
|
|
4061
3653
|
async function runReconcile(runId, options, pool) {
|
|
@@ -4070,303 +3662,28 @@ async function runReconcile(runId, options, pool) {
|
|
|
4070
3662
|
failureCause: classifyReconcileFailure(error, "reconcile")
|
|
4071
3663
|
};
|
|
4072
3664
|
}
|
|
4073
|
-
let stage = "reconcile";
|
|
4074
3665
|
try {
|
|
4075
|
-
|
|
4076
|
-
owned,
|
|
4077
|
-
runId,
|
|
4078
|
-
async (target) => {
|
|
4079
|
-
const lease = await acquireExclusiveTransitionLease2(target);
|
|
4080
|
-
try {
|
|
4081
|
-
stage = "journal";
|
|
4082
|
-
const journal = await readTransitionJournal2(lease.session, runId, {
|
|
4083
|
-
ensure: false
|
|
4084
|
-
});
|
|
4085
|
-
stage = "reconcile";
|
|
4086
|
-
const material = journal.plan.steps.map((step) => {
|
|
4087
|
-
const generatedStep = step;
|
|
4088
|
-
const generated = generatedStep.address;
|
|
4089
|
-
return step.managedClaim ?? (generated === void 0 ? void 0 : {
|
|
4090
|
-
address: generated,
|
|
4091
|
-
...generatedStep.plannedClaimKeys?.[0] === void 0 ? {} : {
|
|
4092
|
-
plannedClaimKey: generatedStep.plannedClaimKeys[0]
|
|
4093
|
-
}
|
|
4094
|
-
});
|
|
4095
|
-
}).filter(
|
|
4096
|
-
(claim) => claim !== void 0
|
|
4097
|
-
);
|
|
4098
|
-
try {
|
|
4099
|
-
await assertPgDatabaseWritable(lease.session);
|
|
4100
|
-
} catch (error) {
|
|
4101
|
-
if (isPgDatabaseReadOnlyError(error))
|
|
4102
|
-
return {
|
|
4103
|
-
kind: "database-read-only",
|
|
4104
|
-
detail: error.message,
|
|
4105
|
-
addresses: material.map((claim) => claim.address)
|
|
4106
|
-
};
|
|
4107
|
-
throw error;
|
|
4108
|
-
}
|
|
4109
|
-
if (material.length === 0)
|
|
4110
|
-
return {
|
|
4111
|
-
kind: "selection-unavailable",
|
|
4112
|
-
addresses: journal.plan.declarations?.declarations.map(
|
|
4113
|
-
(declaration) => declaration.address
|
|
4114
|
-
) ?? []
|
|
4115
|
-
};
|
|
4116
|
-
const homes = /* @__PURE__ */ new Map();
|
|
4117
|
-
for (const claim of material) {
|
|
4118
|
-
const home2 = ledgerHome(claim.address);
|
|
4119
|
-
homes.set(`${home2.scope}:${home2.schema ?? ""}`, home2);
|
|
4120
|
-
}
|
|
4121
|
-
for (const home2 of homes.values()) {
|
|
4122
|
-
const currency = await readPgLedgerScopeCurrency2(
|
|
4123
|
-
lease.session,
|
|
4124
|
-
home2
|
|
4125
|
-
);
|
|
4126
|
-
if (currency.kind === "current") continue;
|
|
4127
|
-
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`;
|
|
4128
|
-
const affected = material.filter((claim) => {
|
|
4129
|
-
const claimHome = ledgerHome(claim.address);
|
|
4130
|
-
return claimHome.scope === home2.scope && claimHome.schema === home2.schema;
|
|
4131
|
-
});
|
|
4132
|
-
return {
|
|
4133
|
-
kind: "unresolved",
|
|
4134
|
-
addresses: affected.map((claim) => claim.address),
|
|
4135
|
-
detail,
|
|
4136
|
-
recovery: affected.map((claim) => ({
|
|
4137
|
-
address: claim.address,
|
|
4138
|
-
outcome: "blocked",
|
|
4139
|
-
reason: detail,
|
|
4140
|
-
refusal: preAppendRefusalFor("ERR-03", {
|
|
4141
|
-
address: claim.address,
|
|
4142
|
-
state: "unknown"
|
|
4143
|
-
})
|
|
4144
|
-
}))
|
|
4145
|
-
};
|
|
4146
|
-
}
|
|
4147
|
-
const executionIds = executionIdsForRun(journal);
|
|
4148
|
-
const reservations = (await Promise.all(
|
|
4149
|
-
[...homes.values()].flatMap(
|
|
4150
|
-
(home2) => executionIds.map(
|
|
4151
|
-
(executionId) => readPgLedgerReservationsForExecution2(
|
|
4152
|
-
lease.session,
|
|
4153
|
-
home2,
|
|
4154
|
-
executionId
|
|
4155
|
-
)
|
|
4156
|
-
)
|
|
4157
|
-
)
|
|
4158
|
-
)).flat();
|
|
4159
|
-
if (reservations.length === 0)
|
|
4160
|
-
return {
|
|
4161
|
-
kind: "selection-unavailable",
|
|
4162
|
-
addresses: material.map((x) => x.address)
|
|
4163
|
-
};
|
|
4164
|
-
const readdressPairs = /* @__PURE__ */ new Map();
|
|
4165
|
-
const byRoot = /* @__PURE__ */ new Map();
|
|
4166
|
-
for (const row of reservations) {
|
|
4167
|
-
if (row.claimKind === "readdress-intent" && row.pairId !== void 0) {
|
|
4168
|
-
const pairKey = `${row.executionId}:${row.pairId}`;
|
|
4169
|
-
readdressPairs.set(pairKey, [
|
|
4170
|
-
...readdressPairs.get(pairKey) ?? [],
|
|
4171
|
-
row
|
|
4172
|
-
]);
|
|
4173
|
-
continue;
|
|
4174
|
-
}
|
|
4175
|
-
byRoot.set(row.rootClaimId, [
|
|
4176
|
-
...byRoot.get(row.rootClaimId) ?? [],
|
|
4177
|
-
row
|
|
4178
|
-
]);
|
|
4179
|
-
}
|
|
4180
|
-
const recovery = [];
|
|
4181
|
-
for (const [_pairKey, pairRows] of readdressPairs) {
|
|
4182
|
-
const pairId = pairRows[0]?.pairId;
|
|
4183
|
-
const executionId = pairRows[0]?.executionId;
|
|
4184
|
-
if (!pairId || !executionId) continue;
|
|
4185
|
-
const readPairInSelectedHomes = readVerifiedPgLedgerReservationsForPair;
|
|
4186
|
-
const closure = await readPairInSelectedHomes(
|
|
4187
|
-
lease.session,
|
|
4188
|
-
pairId,
|
|
4189
|
-
[...homes.values()]
|
|
4190
|
-
);
|
|
4191
|
-
const recovered = await recoverPgReaddressPair(lease.session, {
|
|
4192
|
-
pairId,
|
|
4193
|
-
executionId,
|
|
4194
|
-
reservations: closure
|
|
4195
|
-
});
|
|
4196
|
-
recovery.push(readdressRecoveryReport(closure, recovered));
|
|
4197
|
-
}
|
|
4198
|
-
for (const rows of byRoot.values()) {
|
|
4199
|
-
const rootClaimId = rows[0]?.rootClaimId;
|
|
4200
|
-
if (!rootClaimId) continue;
|
|
4201
|
-
if (rows.some(
|
|
4202
|
-
(row) => !executionIds.includes(row.executionId) || row.rootClaimId !== rootClaimId
|
|
4203
|
-
))
|
|
4204
|
-
return {
|
|
4205
|
-
kind: "selection-unavailable",
|
|
4206
|
-
addresses: rows.map((row) => row.address),
|
|
4207
|
-
detail: `reservation disagreement for root claim ${rootClaimId}`
|
|
4208
|
-
};
|
|
4209
|
-
const rootCandidates = [];
|
|
4210
|
-
for (const row of rows) {
|
|
4211
|
-
stage = "catalogue";
|
|
4212
|
-
const chain = await readPgLedgerAddressChain4(
|
|
4213
|
-
lease.session,
|
|
4214
|
-
ledgerHome(row.address),
|
|
4215
|
-
row.address
|
|
4216
|
-
);
|
|
4217
|
-
stage = "reconcile";
|
|
4218
|
-
const projection = projectLedgerChain3(chain);
|
|
4219
|
-
if (projection.kind !== "projected-ledger-chain" || projection.openClaim === void 0)
|
|
4220
|
-
continue;
|
|
4221
|
-
const open2 = projection.openClaim.event;
|
|
4222
|
-
const openRoot = open2.rootClaimId ?? open2.eventId;
|
|
4223
|
-
if (open2.executionId !== row.executionId || openRoot !== rootClaimId)
|
|
4224
|
-
return {
|
|
4225
|
-
kind: "selection-unavailable",
|
|
4226
|
-
addresses: rows.map((item) => item.address),
|
|
4227
|
-
detail: `open chain member disagrees with reservation root ${rootClaimId}`
|
|
4228
|
-
};
|
|
4229
|
-
if (open2.eventId === rootClaimId)
|
|
4230
|
-
rootCandidates.push({
|
|
4231
|
-
row,
|
|
4232
|
-
stableStateBeforeClaim: projection.openClaim.stableStateBeforeClaim,
|
|
4233
|
-
...open2.plannedClaimKey === void 0 ? {} : { plannedClaimKey: open2.plannedClaimKey }
|
|
4234
|
-
});
|
|
4235
|
-
}
|
|
4236
|
-
if (rootCandidates.length !== 1)
|
|
4237
|
-
return {
|
|
4238
|
-
kind: "selection-unavailable",
|
|
4239
|
-
addresses: rows.map((row) => row.address),
|
|
4240
|
-
detail: `execution ${runId} has ${rootCandidates.length} open root members for ${rootClaimId}`
|
|
4241
|
-
};
|
|
4242
|
-
const selected = rootCandidates[0];
|
|
4243
|
-
if (!selected) continue;
|
|
4244
|
-
if (!selected.plannedClaimKey || !material.some(
|
|
4245
|
-
(claim) => claim.plannedClaimKey === selected.plannedClaimKey
|
|
4246
|
-
))
|
|
4247
|
-
return {
|
|
4248
|
-
kind: "selection-unavailable",
|
|
4249
|
-
addresses: rows.map((row) => row.address),
|
|
4250
|
-
detail: `open root ${rootClaimId} has no matching persisted managed step`
|
|
4251
|
-
};
|
|
4252
|
-
const step = journal.plan.steps.find(
|
|
4253
|
-
(candidate) => candidate.managedClaim?.plannedClaimKey === selected.plannedClaimKey || "plannedClaimKeys" in candidate && Array.isArray(candidate.plannedClaimKeys) && candidate.plannedClaimKeys.includes(
|
|
4254
|
-
selected.plannedClaimKey
|
|
4255
|
-
)
|
|
4256
|
-
);
|
|
4257
|
-
const operationReadBack = step?.operation?.operationKind.name === "CreateUniqueIndexConcurrently" ? async (executor, _address, identity) => {
|
|
4258
|
-
await assertCreateUniqueIndexConcurrentlyRecoveryNotInvalid(
|
|
4259
|
-
executor,
|
|
4260
|
-
step.operation
|
|
4261
|
-
);
|
|
4262
|
-
return {
|
|
4263
|
-
observed: recoveryPayload(identity),
|
|
4264
|
-
effect: "unverifiable"
|
|
4265
|
-
};
|
|
4266
|
-
} : void 0;
|
|
4267
|
-
const indeterminateEvidence = recoveryEvidenceForClaim({
|
|
4268
|
-
journal,
|
|
4269
|
-
row: selected.row,
|
|
4270
|
-
plannedClaimKey: selected.plannedClaimKey,
|
|
4271
|
-
stableStateBeforeClaim: selected.stableStateBeforeClaim
|
|
4272
|
-
});
|
|
4273
|
-
for (const row of rows) {
|
|
4274
|
-
const isRoot = sameLedgerAddress(
|
|
4275
|
-
row.address,
|
|
4276
|
-
selected.row.address
|
|
4277
|
-
);
|
|
4278
|
-
const recovered = await recoverPgOutcomeClaim(owned, {
|
|
4279
|
-
address: row.address,
|
|
4280
|
-
reservations: [row],
|
|
4281
|
-
resolutionEventId: isRoot ? `${rootClaimId}:reconcile:${runId}` : `${rootClaimId}:reconcile:${runId}:${row.address.kind}:${row.address.name}`,
|
|
4282
|
-
acceptedExternalDdlExclusion: isRoot && indeterminateEvidence !== void 0,
|
|
4283
|
-
resolveIndeterminate: true,
|
|
4284
|
-
readBack: async (_executor, _address, identity) => recoveryPayload(identity),
|
|
4285
|
-
...isRoot && indeterminateEvidence !== void 0 ? { indeterminateEvidence } : {},
|
|
4286
|
-
...isRoot && operationReadBack !== void 0 ? { operationReadBack } : {}
|
|
4287
|
-
});
|
|
4288
|
-
recovery.push(recoveryReport(row.address, recovered));
|
|
4289
|
-
}
|
|
4290
|
-
}
|
|
4291
|
-
const unresolved = unresolvedRecoveryDetail(recovery);
|
|
4292
|
-
if (unresolved)
|
|
4293
|
-
return {
|
|
4294
|
-
kind: "unresolved",
|
|
4295
|
-
addresses: reservations.map((item) => item.address),
|
|
4296
|
-
detail: unresolved,
|
|
4297
|
-
recovery
|
|
4298
|
-
};
|
|
4299
|
-
return {
|
|
4300
|
-
kind: "completed",
|
|
4301
|
-
addresses: reservations.map((item) => item.address),
|
|
4302
|
-
recovery
|
|
4303
|
-
};
|
|
4304
|
-
} finally {
|
|
4305
|
-
await lease.release();
|
|
4306
|
-
}
|
|
4307
|
-
}
|
|
4308
|
-
);
|
|
4309
|
-
if (locked.kind === "busy")
|
|
4310
|
-
return { outcome: "reconcile-run-unavailable", runId, addresses: [] };
|
|
4311
|
-
if (locked.value.kind === "selection-unavailable")
|
|
4312
|
-
return {
|
|
4313
|
-
outcome: "reconcile-claim-selection-unavailable",
|
|
4314
|
-
runId,
|
|
4315
|
-
addresses: locked.value.addresses,
|
|
4316
|
-
...locked.value.detail ? { detail: locked.value.detail } : {}
|
|
4317
|
-
};
|
|
4318
|
-
if (locked.value.kind === "database-read-only") {
|
|
4319
|
-
const address = locked.value.addresses[0];
|
|
4320
|
-
return {
|
|
4321
|
-
outcome: "database-read-only",
|
|
4322
|
-
runId,
|
|
4323
|
-
addresses: locked.value.addresses,
|
|
4324
|
-
detail: locked.value.detail,
|
|
4325
|
-
...address === void 0 ? {} : {
|
|
4326
|
-
refusal: preAppendRefusalFor("ERR-07", {
|
|
4327
|
-
address,
|
|
4328
|
-
state: "unknown"
|
|
4329
|
-
})
|
|
4330
|
-
}
|
|
4331
|
-
};
|
|
4332
|
-
}
|
|
4333
|
-
if (locked.value.kind === "unresolved") {
|
|
4334
|
-
const address = locked.value.addresses[0];
|
|
4335
|
-
const markerRefusal = address !== void 0 && locked.value.detail?.includes("run dbsp preflight --reinitialize") ? preAppendRefusalFor("ERR-03", { address, state: "unknown" }) : void 0;
|
|
4336
|
-
const recoveryRefusal = locked.value.recovery?.find(
|
|
4337
|
-
(report) => report.refusal !== void 0
|
|
4338
|
-
)?.refusal;
|
|
4339
|
-
const refusal = markerRefusal ?? recoveryRefusal;
|
|
4340
|
-
return {
|
|
4341
|
-
outcome: "reconcile-unresolved",
|
|
4342
|
-
runId,
|
|
4343
|
-
addresses: locked.value.addresses,
|
|
4344
|
-
...locked.value.detail ? { detail: locked.value.detail } : {},
|
|
4345
|
-
...locked.value.recovery ? { recovery: locked.value.recovery } : {},
|
|
4346
|
-
...refusal === void 0 ? {} : { refusal }
|
|
4347
|
-
};
|
|
4348
|
-
}
|
|
4349
|
-
return {
|
|
4350
|
-
outcome: "reconcile-completed",
|
|
4351
|
-
runId,
|
|
4352
|
-
addresses: locked.value.addresses,
|
|
4353
|
-
...locked.value.recovery ? { recovery: locked.value.recovery } : {}
|
|
4354
|
-
};
|
|
3666
|
+
return mapAdapterResult(await reconcilePgTransitionRun(owned, runId));
|
|
4355
3667
|
} catch (error) {
|
|
3668
|
+
const typed = error instanceof PgReconcileTransitionRunError ? error : void 0;
|
|
3669
|
+
const cause = typed?.originalCause ?? error;
|
|
4356
3670
|
return {
|
|
4357
3671
|
outcome: "reconcile-run-unavailable",
|
|
4358
3672
|
runId,
|
|
4359
3673
|
addresses: [],
|
|
4360
|
-
detail:
|
|
4361
|
-
|
|
3674
|
+
detail: escapeDiagnosticText7(
|
|
3675
|
+
cause instanceof Error ? cause.message : String(cause)
|
|
4362
3676
|
),
|
|
4363
|
-
failureCause: classifyReconcileFailure(
|
|
3677
|
+
failureCause: classifyReconcileFailure(
|
|
3678
|
+
cause,
|
|
3679
|
+
typed?.stage ?? "reconcile"
|
|
3680
|
+
)
|
|
4364
3681
|
};
|
|
4365
3682
|
} finally {
|
|
4366
3683
|
if (pool === void 0) await owned.end();
|
|
4367
3684
|
}
|
|
4368
3685
|
}
|
|
4369
|
-
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) => {
|
|
4370
3687
|
const result = await runReconcile(runId, options);
|
|
4371
3688
|
if (options.format === "json") printCliJson(result);
|
|
4372
3689
|
else console.log(formatReconcileHuman(result));
|
|
@@ -4375,25 +3692,25 @@ var reconcileCommand = new Command7("reconcile").description("Resolve this run\u
|
|
|
4375
3692
|
|
|
4376
3693
|
// src/commands/recover.ts
|
|
4377
3694
|
import {
|
|
4378
|
-
assertPgDatabaseWritable
|
|
4379
|
-
createPgTransitionPack as
|
|
4380
|
-
isPgDatabaseReadOnlyError
|
|
3695
|
+
assertPgDatabaseWritable,
|
|
3696
|
+
createPgTransitionPack as createPgTransitionPack2,
|
|
3697
|
+
isPgDatabaseReadOnlyError,
|
|
4381
3698
|
preparePgRecoveryAdmission,
|
|
4382
|
-
readPgLedgerScopeCurrency
|
|
3699
|
+
readPgLedgerScopeCurrency,
|
|
4383
3700
|
readPgObservationContextFromLessor as readPgObservationContextFromLessor2,
|
|
4384
|
-
readTransitionJournal as
|
|
4385
|
-
withPgTransitionRunLock as
|
|
3701
|
+
readTransitionJournal as readTransitionJournal2,
|
|
3702
|
+
withPgTransitionRunLock as withPgTransitionRunLock2
|
|
4386
3703
|
} from "@dbsp/adapter-pgsql";
|
|
4387
3704
|
import {
|
|
4388
3705
|
acquireTransitionTargetLease,
|
|
4389
|
-
assumptionAccepted
|
|
4390
|
-
createApplier
|
|
4391
|
-
createPackRegistry as
|
|
3706
|
+
assumptionAccepted,
|
|
3707
|
+
createApplier,
|
|
3708
|
+
createPackRegistry as createPackRegistry2,
|
|
4392
3709
|
loadVerifiedRecoveryJournal,
|
|
4393
|
-
transitionPlanDigest as
|
|
3710
|
+
transitionPlanDigest as transitionPlanDigest3,
|
|
4394
3711
|
validateTransitionRelationalInvariants
|
|
4395
3712
|
} from "@dbsp/core";
|
|
4396
|
-
import { Command as
|
|
3713
|
+
import { Command as Command9 } from "commander";
|
|
4397
3714
|
var RECOVER_OUTCOME_CONTRACT = [
|
|
4398
3715
|
["completed", 0, "all attempted steps were durably reconciled"],
|
|
4399
3716
|
["recovery-resume-required", 40, "remaining work requires a new proof"],
|
|
@@ -4482,7 +3799,7 @@ function validateRecoveryAuthorization(journal) {
|
|
|
4482
3799
|
if (journal.plan.assumptions.every((assumption) => {
|
|
4483
3800
|
const grant = grants.get(assumption.id);
|
|
4484
3801
|
const acceptedGrant = grant === void 0 ? void 0 : accepts[grant];
|
|
4485
|
-
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, {
|
|
4486
3803
|
accepts: [acceptedGrant]
|
|
4487
3804
|
});
|
|
4488
3805
|
}))
|
|
@@ -4493,7 +3810,7 @@ function validateRecoveryAuthorization(journal) {
|
|
|
4493
3810
|
return { ok: false, outcome: "recovery-authorization-invalid" };
|
|
4494
3811
|
}
|
|
4495
3812
|
function recoveryPolicyForJournal(loaded, runId) {
|
|
4496
|
-
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)
|
|
4497
3814
|
return { ok: false, outcome: "recovery-plan-invalid" };
|
|
4498
3815
|
return validateRecoveryAuthorization(loaded);
|
|
4499
3816
|
}
|
|
@@ -4521,12 +3838,12 @@ function outcomeForRecoveryResult(result) {
|
|
|
4521
3838
|
async function load(target, runId) {
|
|
4522
3839
|
const lease = await acquireTransitionTargetLease(target);
|
|
4523
3840
|
try {
|
|
4524
|
-
return await
|
|
3841
|
+
return await readTransitionJournal2(lease.session, runId, { ensure: false });
|
|
4525
3842
|
} finally {
|
|
4526
3843
|
await lease.release();
|
|
4527
3844
|
}
|
|
4528
3845
|
}
|
|
4529
|
-
function
|
|
3846
|
+
function ledgerHome(address) {
|
|
4530
3847
|
if (address.scope === "database") return { scope: "database" };
|
|
4531
3848
|
if (!address.schema)
|
|
4532
3849
|
throw new Error(
|
|
@@ -4544,24 +3861,24 @@ function recoveryAddresses(journal) {
|
|
|
4544
3861
|
return address === void 0 ? [] : [address];
|
|
4545
3862
|
});
|
|
4546
3863
|
}
|
|
4547
|
-
function formatLedgerHome(
|
|
4548
|
-
return
|
|
3864
|
+
function formatLedgerHome(home) {
|
|
3865
|
+
return home.scope === "database" ? "database" : `schema ${home.schema}`;
|
|
4549
3866
|
}
|
|
4550
3867
|
async function recoverMarkerRefusal(target, journal) {
|
|
4551
3868
|
const addresses = recoveryAddresses(journal);
|
|
4552
3869
|
const homes = [];
|
|
4553
3870
|
for (const address of addresses) {
|
|
4554
|
-
const
|
|
4555
|
-
if (homes.some((candidate) => sameLedgerHome(candidate.home,
|
|
3871
|
+
const home = ledgerHome(address);
|
|
3872
|
+
if (homes.some((candidate) => sameLedgerHome(candidate.home, home)))
|
|
4556
3873
|
continue;
|
|
4557
|
-
homes.push({ address, home
|
|
3874
|
+
homes.push({ address, home });
|
|
4558
3875
|
}
|
|
4559
3876
|
const lease = await acquireTransitionTargetLease(target);
|
|
4560
3877
|
try {
|
|
4561
|
-
for (const { address, home
|
|
4562
|
-
const currency = await
|
|
3878
|
+
for (const { address, home } of homes) {
|
|
3879
|
+
const currency = await readPgLedgerScopeCurrency(lease.session, home);
|
|
4563
3880
|
if (currency.kind === "current") continue;
|
|
4564
|
-
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`;
|
|
4565
3882
|
return {
|
|
4566
3883
|
detail,
|
|
4567
3884
|
refusal: preAppendRefusalFor("ERR-03", {
|
|
@@ -4581,7 +3898,7 @@ async function runRecover(runId, options, pool) {
|
|
|
4581
3898
|
const owned = pool === void 0 ? (await createDbConnection(options.db)).pool : pool;
|
|
4582
3899
|
let result;
|
|
4583
3900
|
try {
|
|
4584
|
-
const locked = await
|
|
3901
|
+
const locked = await withPgTransitionRunLock2(
|
|
4585
3902
|
owned,
|
|
4586
3903
|
runId,
|
|
4587
3904
|
async (target) => {
|
|
@@ -4604,9 +3921,9 @@ async function runRecover(runId, options, pool) {
|
|
|
4604
3921
|
}
|
|
4605
3922
|
const writabilityLease = await acquireTransitionTargetLease(target);
|
|
4606
3923
|
try {
|
|
4607
|
-
await
|
|
3924
|
+
await assertPgDatabaseWritable(writabilityLease.session);
|
|
4608
3925
|
} catch (error) {
|
|
4609
|
-
if (
|
|
3926
|
+
if (isPgDatabaseReadOnlyError(error))
|
|
4610
3927
|
return {
|
|
4611
3928
|
outcome: "database-read-only",
|
|
4612
3929
|
detail: error.message
|
|
@@ -4627,8 +3944,8 @@ async function runRecover(runId, options, pool) {
|
|
|
4627
3944
|
const authorization = loaded.journal.events.length === 0 ? void 0 : recoveryPolicyForJournal(loaded.journal, runId);
|
|
4628
3945
|
if (authorization && !authorization.ok)
|
|
4629
3946
|
return { outcome: authorization.outcome };
|
|
4630
|
-
const result2 = await
|
|
4631
|
-
|
|
3947
|
+
const result2 = await createApplier(
|
|
3948
|
+
createPackRegistry2([createPgTransitionPack2({})]),
|
|
4632
3949
|
{ persist: async () => void 0 }
|
|
4633
3950
|
).resume(
|
|
4634
3951
|
loaded.journal,
|
|
@@ -4662,7 +3979,7 @@ async function runRecover(runId, options, pool) {
|
|
|
4662
3979
|
}
|
|
4663
3980
|
return result;
|
|
4664
3981
|
}
|
|
4665
|
-
var recoverCommand = new
|
|
3982
|
+
var recoverCommand = new Command9("recover").description(
|
|
4666
3983
|
"Observe and classify an attempted durable run; performs no planned target DDL and does not re-authorize assumptions"
|
|
4667
3984
|
).argument("<run-id>", "Durable run identifier").requiredOption("-d, --db <url>", "Database connection URL (required)").requiredOption(
|
|
4668
3985
|
"--plan-digest <sha>",
|
|
@@ -4698,11 +4015,11 @@ ${RECOVER_OUTCOME_CONTRACT.map(([outcome, code, description]) => ` ${outcome} -
|
|
|
4698
4015
|
// src/commands/release.ts
|
|
4699
4016
|
import {
|
|
4700
4017
|
createPgTransitionLessor as createPgTransitionLessor5,
|
|
4701
|
-
escapeDiagnosticText as
|
|
4018
|
+
escapeDiagnosticText as escapeDiagnosticText8,
|
|
4702
4019
|
releasePgManagedAddress
|
|
4703
4020
|
} from "@dbsp/adapter-pgsql";
|
|
4704
4021
|
import { acquireTransitionLease as acquireTransitionLease5 } from "@dbsp/core";
|
|
4705
|
-
import { Command as
|
|
4022
|
+
import { Command as Command10 } from "commander";
|
|
4706
4023
|
async function runRelease(selector, options) {
|
|
4707
4024
|
const { pool } = await createDbConnection(options.db);
|
|
4708
4025
|
const schema = options.schema ?? "public";
|
|
@@ -4737,18 +4054,18 @@ async function runRelease(selector, options) {
|
|
|
4737
4054
|
function formatReleaseHuman(result) {
|
|
4738
4055
|
if (result.outcome === "released") return "released";
|
|
4739
4056
|
if (result.outcome === "database-read-only")
|
|
4740
|
-
return `database-read-only: ${
|
|
4057
|
+
return `database-read-only: ${escapeDiagnosticText8(result.detail)}`;
|
|
4741
4058
|
if (result.outcome === "release-unavailable")
|
|
4742
|
-
return `release-unavailable: ${
|
|
4059
|
+
return `release-unavailable: ${escapeDiagnosticText8(result.detail)}`;
|
|
4743
4060
|
return [
|
|
4744
|
-
`release-refused: ${
|
|
4745
|
-
`address: ${
|
|
4746
|
-
`state: ${
|
|
4747
|
-
`withheld authority: ${
|
|
4748
|
-
`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)}`
|
|
4749
4066
|
].join("\n");
|
|
4750
4067
|
}
|
|
4751
|
-
var releaseCommand = new
|
|
4068
|
+
var releaseCommand = new Command10("release").description(
|
|
4752
4069
|
"End management for one address without touching the database object"
|
|
4753
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(
|
|
4754
4071
|
"--parent <kind:name>",
|
|
@@ -4766,15 +4083,15 @@ var releaseCommand = new Command9("release").description(
|
|
|
4766
4083
|
const detail = error instanceof Error ? error.message : String(error);
|
|
4767
4084
|
if (options.format === "json")
|
|
4768
4085
|
printCliJson({ outcome: "release-refused", detail });
|
|
4769
|
-
else console.error(`release-refused: ${
|
|
4086
|
+
else console.error(`release-refused: ${escapeDiagnosticText8(detail)}`);
|
|
4770
4087
|
process.exitCode = 1;
|
|
4771
4088
|
}
|
|
4772
4089
|
});
|
|
4773
4090
|
|
|
4774
4091
|
// src/commands/repl.ts
|
|
4775
4092
|
import { readFileSync } from "fs";
|
|
4776
|
-
import { Command as
|
|
4777
|
-
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(
|
|
4778
4095
|
"-d, --db <url>",
|
|
4779
4096
|
"PostgreSQL connection URL for execution mode (e.g., postgres://localhost/mydb)"
|
|
4780
4097
|
).option("-e, --eval <query>", "Execute a single query and exit (batch mode)").option(
|
|
@@ -4896,10 +4213,11 @@ function mapReplCasingOption(casing) {
|
|
|
4896
4213
|
// src/commands/verify.ts
|
|
4897
4214
|
import {
|
|
4898
4215
|
comparePgsqlDatabaseSchema as comparePgsqlDatabaseSchema2,
|
|
4899
|
-
|
|
4900
|
-
|
|
4216
|
+
createPgPhysicalModel as createPgPhysicalModel5,
|
|
4217
|
+
createPgsqlAdapter as createPgsqlAdapter3,
|
|
4218
|
+
escapeDiagnosticText as escapeDiagnosticText9
|
|
4901
4219
|
} from "@dbsp/adapter-pgsql";
|
|
4902
|
-
import { Command as
|
|
4220
|
+
import { Command as Command12 } from "commander";
|
|
4903
4221
|
|
|
4904
4222
|
// src/verifier.ts
|
|
4905
4223
|
var CHANGE_TO_DRIFT = {
|
|
@@ -4929,6 +4247,10 @@ var CHANGE_TO_DRIFT = {
|
|
|
4929
4247
|
// Column enhancements
|
|
4930
4248
|
alter_column_collation: { type: "collation_mismatch", severity: "warning" },
|
|
4931
4249
|
alter_column_identity: { type: "identity_mismatch", severity: "warning" },
|
|
4250
|
+
alter_column_auto_increment: {
|
|
4251
|
+
type: "auto_increment_mismatch",
|
|
4252
|
+
severity: "error"
|
|
4253
|
+
},
|
|
4932
4254
|
// Comments
|
|
4933
4255
|
add_comment: { type: "comment_mismatch", severity: "info" },
|
|
4934
4256
|
drop_comment: { type: "comment_mismatch", severity: "info" },
|
|
@@ -5041,7 +4363,7 @@ function formatVerifyResult(result) {
|
|
|
5041
4363
|
}
|
|
5042
4364
|
|
|
5043
4365
|
// src/commands/verify.ts
|
|
5044
|
-
var verifyCommand = new
|
|
4366
|
+
var verifyCommand = new Command12("verify").description("Compare schema vs real database (drift detection)").option(
|
|
5045
4367
|
"-s, --schema <path>",
|
|
5046
4368
|
"Path to schema file (default: dbsp.schema.ts)"
|
|
5047
4369
|
).requiredOption("-d, --db <url>", "Database connection URL (required)").option("--schema-name <name>", "Database schema name (default: public)").option("--json", "Output as JSON").action(
|
|
@@ -5059,14 +4381,18 @@ var verifyCommand = new Command11("verify").description("Compare schema vs real
|
|
|
5059
4381
|
try {
|
|
5060
4382
|
const loaded = await loadSchema(schemaPath);
|
|
5061
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
|
+
});
|
|
5062
4390
|
const { pool } = await createDbConnection(options.db);
|
|
5063
4391
|
try {
|
|
5064
|
-
const adapter =
|
|
5065
|
-
const diff = await comparePgsqlDatabaseSchema2(adapter,
|
|
5066
|
-
...options.schemaName ? { schema: options.schemaName } : {},
|
|
5067
|
-
...loaded.dbCasing !== void 0 ? { dbCasing: loaded.dbCasing } : {},
|
|
4392
|
+
const adapter = createPgsqlAdapter3(pool);
|
|
4393
|
+
const diff = await comparePgsqlDatabaseSchema2(adapter, physical, {
|
|
5068
4394
|
onExpressionCanonicalizationWarning: (warning) => console.warn(
|
|
5069
|
-
`\u26A0\uFE0F [${warning.kind} ${
|
|
4395
|
+
`\u26A0\uFE0F [${warning.kind} ${escapeDiagnosticText9(warning.table)}.${escapeDiagnosticText9(warning.name)}] ${warning.message}`
|
|
5070
4396
|
)
|
|
5071
4397
|
});
|
|
5072
4398
|
const dbModel = await adapter.introspect({
|
|
@@ -5103,12 +4429,13 @@ var verifyCommand = new Command11("verify").description("Compare schema vs real
|
|
|
5103
4429
|
);
|
|
5104
4430
|
|
|
5105
4431
|
// src/index.ts
|
|
5106
|
-
var program = new
|
|
4432
|
+
var program = new Command13();
|
|
5107
4433
|
var packageJson = createRequire(import.meta.url)("../package.json");
|
|
5108
4434
|
var jsonFormatCommands = /* @__PURE__ */ new Set([
|
|
5109
4435
|
"apply",
|
|
5110
4436
|
"inspect",
|
|
5111
4437
|
"plan",
|
|
4438
|
+
"migrate",
|
|
5112
4439
|
"recover",
|
|
5113
4440
|
"reconcile",
|
|
5114
4441
|
"release"
|
|
@@ -5136,6 +4463,7 @@ program.name("dbsp").description("Schema-first code generation for db-semantic-p
|
|
|
5136
4463
|
program.addCommand(generateCommand);
|
|
5137
4464
|
program.addCommand(introspectCommand);
|
|
5138
4465
|
program.addCommand(planCommand);
|
|
4466
|
+
program.addCommand(migrateCommand);
|
|
5139
4467
|
program.addCommand(preflightCommand);
|
|
5140
4468
|
program.addCommand(applyCommand);
|
|
5141
4469
|
program.addCommand(inspectCommand);
|