@dbsp/cli 3.0.0 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +507 -322
- package/dist/index.js.map +1 -1
- package/package.json +8 -8
package/dist/index.js
CHANGED
|
@@ -19,7 +19,7 @@ import { Command as Command12, CommanderError } from "commander";
|
|
|
19
19
|
|
|
20
20
|
// src/commands/apply.ts
|
|
21
21
|
import { AsyncLocalStorage } from "async_hooks";
|
|
22
|
-
import { createHash
|
|
22
|
+
import { createHash } from "crypto";
|
|
23
23
|
import { readFile } from "fs/promises";
|
|
24
24
|
import { createInterface } from "readline/promises";
|
|
25
25
|
import {
|
|
@@ -29,6 +29,8 @@ import {
|
|
|
29
29
|
createPgTransitionPack as createPgTransitionPack2,
|
|
30
30
|
escapeDiagnosticText as escapeDiagnosticText3,
|
|
31
31
|
preparePgExecutionSession,
|
|
32
|
+
readPgLedgerAddressChain as readPgLedgerAddressChain2,
|
|
33
|
+
readPgLedgerReservationsForExecution,
|
|
32
34
|
readTransitionJournal,
|
|
33
35
|
TransitionRunIdentityMismatchError,
|
|
34
36
|
validatePgManagedLedgerCurrency,
|
|
@@ -45,7 +47,7 @@ import {
|
|
|
45
47
|
validateNormalizedManagedStepManifest as validateNormalizedManagedStepManifest3
|
|
46
48
|
} from "@dbsp/core";
|
|
47
49
|
import { mintDurablyLoadedRun } from "@dbsp/core/internal";
|
|
48
|
-
import { REFUSAL_VOCABULARY as REFUSAL_VOCABULARY2 } from "@dbsp/types";
|
|
50
|
+
import { ledgerAddressKey as ledgerAddressKey2, REFUSAL_VOCABULARY as REFUSAL_VOCABULARY2 } from "@dbsp/types";
|
|
49
51
|
import { Command as Command2 } from "commander";
|
|
50
52
|
|
|
51
53
|
// src/utils/output.ts
|
|
@@ -59,13 +61,29 @@ function printCliJson(value) {
|
|
|
59
61
|
console.log(serializeCliJson(value));
|
|
60
62
|
}
|
|
61
63
|
|
|
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
|
+
|
|
62
79
|
// src/commands/generator-execution.ts
|
|
63
|
-
import {
|
|
80
|
+
import { randomUUID } from "crypto";
|
|
64
81
|
import { isDeepStrictEqual } from "util";
|
|
65
82
|
import {
|
|
83
|
+
assertGeneratedPostconditionSession,
|
|
66
84
|
compareSchemata,
|
|
67
85
|
createPgsqlAdapter,
|
|
68
|
-
|
|
86
|
+
decodeGeneratedPostconditionPayload,
|
|
69
87
|
executePgAdmittedOperation,
|
|
70
88
|
executePgDeclaredAdoption,
|
|
71
89
|
executePgPersistedTableReaddress,
|
|
@@ -73,9 +91,18 @@ import {
|
|
|
73
91
|
readPgCatalogueIdentity,
|
|
74
92
|
readPgLedgerAddressChain,
|
|
75
93
|
readPgLedgerScopeCurrency,
|
|
76
|
-
readPgRemovalEffectsClosure
|
|
94
|
+
readPgRemovalEffectsClosure,
|
|
95
|
+
toGeneratedPostconditionBindingAddress,
|
|
96
|
+
verifyGeneratedCheckPostcondition,
|
|
97
|
+
verifyGeneratedColumnPostcondition,
|
|
98
|
+
verifyGeneratedIdentityPostcondition,
|
|
99
|
+
verifyGeneratedIndexPostcondition,
|
|
100
|
+
verifyGeneratedTablePostcondition,
|
|
101
|
+
withGeneratedPostconditionSession
|
|
77
102
|
} from "@dbsp/adapter-pgsql";
|
|
78
103
|
import {
|
|
104
|
+
canonicalJson,
|
|
105
|
+
canonicalJsonDigest,
|
|
79
106
|
outcomeClaimEventId,
|
|
80
107
|
outcomeClaimId,
|
|
81
108
|
projectLedgerChain,
|
|
@@ -194,261 +221,180 @@ function observed(address) {
|
|
|
194
221
|
digest: `generator:${address.kind}:${address.name}`
|
|
195
222
|
};
|
|
196
223
|
}
|
|
197
|
-
function normalizedDefinition(value) {
|
|
198
|
-
return value.replaceAll('"', "").replace(/\s+/gu, " ").replace(/;$/u, "").trim().toLowerCase();
|
|
199
|
-
}
|
|
200
224
|
function generatedPayload(value) {
|
|
225
|
+
const encoded = canonicalJson(value);
|
|
226
|
+
const normalized = JSON.parse(encoded);
|
|
201
227
|
return {
|
|
202
|
-
value,
|
|
203
|
-
digest:
|
|
228
|
+
value: normalized,
|
|
229
|
+
digest: canonicalJsonDigest(normalized),
|
|
230
|
+
payloadKind: "generated-structural-observation"
|
|
204
231
|
};
|
|
205
232
|
}
|
|
206
|
-
function
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
+
};
|
|
224
261
|
}
|
|
225
262
|
function generatedPostcondition(step, address) {
|
|
226
|
-
const
|
|
227
|
-
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
263
|
+
const declaration = step.expectedDeclaration;
|
|
264
|
+
if (!declaration?.value || typeof declaration.value !== "object" || Array.isArray(declaration.value))
|
|
228
265
|
throw new Error(
|
|
229
|
-
`generated ${address.kind} step ${step.stepKey} has no
|
|
266
|
+
`generated ${address.kind} step ${step.stepKey} has no decodable generated declaration`
|
|
230
267
|
);
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
);
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
const nullable = row.is_not_null === true ? false : row.is_not_null === false ? true : void 0;
|
|
254
|
-
const actualDefault = row.column_default == null ? void 0 : String(row.column_default);
|
|
255
|
-
const actualCollation = row.collation_name == null ? null : String(row.collation_name);
|
|
256
|
-
const actualIdentity = row.identity_kind === "a" ? "always" : row.identity_kind === "d" ? "byDefault" : null;
|
|
257
|
-
if (expected.type !== void 0 && !dbTypesEqual(type, expected.type))
|
|
258
|
-
throw new Error(
|
|
259
|
-
`generated column ${address.name} type postcondition differs`
|
|
260
|
-
);
|
|
261
|
-
if (expected.nullable !== void 0 && nullable !== expected.nullable)
|
|
262
|
-
throw new Error(
|
|
263
|
-
`generated column ${address.name} nullability postcondition differs`
|
|
264
|
-
);
|
|
265
|
-
if (expected.hasDefault === true && (actualDefault === void 0 || expected.default === void 0 || actualDefault !== expected.default))
|
|
266
|
-
throw new Error(
|
|
267
|
-
`generated column ${address.name} default postcondition differs`
|
|
268
|
-
);
|
|
269
|
-
if (expected.hasDefault === false && actualDefault !== void 0)
|
|
270
|
-
throw new Error(
|
|
271
|
-
`generated column ${address.name} default postcondition differs`
|
|
272
|
-
);
|
|
273
|
-
if (expected.collation !== void 0 && actualCollation !== expected.collation)
|
|
274
|
-
throw new Error(
|
|
275
|
-
`generated column ${address.name} collation postcondition differs`
|
|
276
|
-
);
|
|
277
|
-
if (expected.identity !== void 0 && actualIdentity !== expected.identity)
|
|
278
|
-
throw new Error(
|
|
279
|
-
`generated column ${address.name} identity postcondition differs`
|
|
280
|
-
);
|
|
281
|
-
return generatedPayload({
|
|
282
|
-
kind: "column",
|
|
283
|
-
type,
|
|
284
|
-
nullable,
|
|
285
|
-
default: actualDefault
|
|
286
|
-
});
|
|
287
|
-
}
|
|
288
|
-
if (address.kind === "constraint" && parent && address.schema) {
|
|
289
|
-
const postcondition = generatedPostcondition(step, address);
|
|
290
|
-
if (postcondition.kind !== "constraint")
|
|
291
|
-
throw new Error(
|
|
292
|
-
`generated constraint ${address.name} has a non-constraint structural postcondition`
|
|
293
|
-
);
|
|
294
|
-
const expected = postcondition.constraint;
|
|
295
|
-
const row = (await executor.query(
|
|
296
|
-
`SELECT constraint_item.contype AS constraint_type, pg_catalog.pg_get_constraintdef(constraint_item.oid, true) AS constraint_definition, ARRAY(SELECT attribute.attname::text FROM unnest(constraint_item.conkey) WITH ORDINALITY AS key_column(attnum, position) JOIN pg_catalog.pg_attribute attribute ON attribute.attrelid = constraint_item.conrelid AND attribute.attnum = key_column.attnum ORDER BY key_column.position) AS key_columns, referenced_namespace.nspname AS referenced_schema, referenced_relation.relname AS referenced_table, ARRAY(SELECT attribute.attname::text FROM unnest(constraint_item.confkey) WITH ORDINALITY AS key_column(attnum, position) JOIN pg_catalog.pg_attribute attribute ON attribute.attrelid = constraint_item.confrelid AND attribute.attnum = key_column.attnum ORDER BY key_column.position) AS referenced_columns, constraint_item.confdeltype AS on_delete, constraint_item.confupdtype AS on_update, constraint_item.condeferrable AS is_deferrable, constraint_item.condeferred AS is_deferred, constraint_item.convalidated AS is_validated FROM pg_catalog.pg_constraint constraint_item JOIN pg_catalog.pg_class relation ON relation.oid = constraint_item.conrelid JOIN pg_catalog.pg_namespace namespace ON namespace.oid = relation.relnamespace LEFT JOIN pg_catalog.pg_class referenced_relation ON referenced_relation.oid = constraint_item.confrelid LEFT JOIN pg_catalog.pg_namespace referenced_namespace ON referenced_namespace.oid = referenced_relation.relnamespace WHERE namespace.nspname = $1 AND relation.relname = $2 AND constraint_item.conname = $3`,
|
|
297
|
-
[address.schema, parent, address.name]
|
|
298
|
-
)).rows[0];
|
|
299
|
-
if (!row) throw new Error(`generated constraint ${address.name} is absent`);
|
|
300
|
-
const definition = String(row.constraint_definition ?? "");
|
|
301
|
-
if (String(row.constraint_type) !== expected.type)
|
|
302
|
-
throw new Error(
|
|
303
|
-
`generated constraint ${address.name} postcondition differs`
|
|
304
|
-
);
|
|
305
|
-
if ((expected.type === "p" || expected.type === "u") && JSON.stringify(textList(row.key_columns)) !== JSON.stringify(expected.columns))
|
|
306
|
-
throw new Error(
|
|
307
|
-
`generated constraint ${address.name} postcondition differs`
|
|
308
|
-
);
|
|
309
|
-
if (expected.type === "c") {
|
|
310
|
-
if (normalizedDefinition(definition) !== normalizedDefinition(expected.definition) || row.is_validated === true === expected.notValid)
|
|
311
|
-
throw new Error(
|
|
312
|
-
`generated constraint ${address.name} postcondition differs`
|
|
313
|
-
);
|
|
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
|
+
};
|
|
314
290
|
}
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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
|
+
};
|
|
320
311
|
}
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
if (address.kind === "index" && parent && address.schema) {
|
|
328
|
-
const postcondition = generatedPostcondition(step, address);
|
|
329
|
-
if (postcondition.kind !== "index")
|
|
330
|
-
throw new Error(
|
|
331
|
-
`generated index ${address.name} has a non-index structural postcondition`
|
|
312
|
+
case "constraint": {
|
|
313
|
+
return identityObserved(
|
|
314
|
+
executor,
|
|
315
|
+
postcondition,
|
|
316
|
+
bindingAddress,
|
|
317
|
+
"constraint"
|
|
332
318
|
);
|
|
333
|
-
const row = (await executor.query(
|
|
334
|
-
`SELECT index_relation.relname AS index_name, index_meta.indisunique AS is_unique, pg_catalog.pg_get_indexdef(index_meta.indexrelid, 0, true) AS index_definition FROM pg_catalog.pg_index index_meta JOIN pg_catalog.pg_class relation ON relation.oid = index_meta.indrelid JOIN pg_catalog.pg_namespace namespace ON namespace.oid = relation.relnamespace JOIN pg_catalog.pg_class index_relation ON index_relation.oid = index_meta.indexrelid WHERE namespace.nspname = $1 AND relation.relname = $2 AND index_relation.relname = $3`,
|
|
335
|
-
[address.schema, parent, address.name]
|
|
336
|
-
)).rows[0];
|
|
337
|
-
if (!row) throw new Error(`generated index ${address.name} is absent`);
|
|
338
|
-
const definition = String(row.index_definition ?? "");
|
|
339
|
-
if (normalizedDefinition(definition) !== normalizedDefinition(postcondition.definition))
|
|
340
|
-
throw new Error(`generated index ${address.name} postcondition differs`);
|
|
341
|
-
return generatedPayload({
|
|
342
|
-
kind: "index",
|
|
343
|
-
unique: row.is_unique === true,
|
|
344
|
-
definition
|
|
345
|
-
});
|
|
346
|
-
}
|
|
347
|
-
if (address.kind === "table" && address.schema) {
|
|
348
|
-
const postcondition = generatedPostcondition(step, address);
|
|
349
|
-
if (postcondition.kind !== "table")
|
|
350
|
-
throw new Error(
|
|
351
|
-
`generated table ${address.name} has a non-table structural postcondition`
|
|
352
|
-
);
|
|
353
|
-
const rows = (await executor.query(
|
|
354
|
-
`SELECT attribute.attname AS column_name, pg_catalog.format_type(attribute.atttypid, attribute.atttypmod) AS column_type, attribute.attnotnull AS is_not_null, pg_catalog.pg_get_expr(default_value.adbin, default_value.adrelid) AS column_default FROM pg_catalog.pg_attribute attribute JOIN pg_catalog.pg_class relation ON relation.oid = attribute.attrelid JOIN pg_catalog.pg_namespace namespace ON namespace.oid = relation.relnamespace LEFT JOIN pg_catalog.pg_attrdef default_value ON default_value.adrelid = attribute.attrelid AND default_value.adnum = attribute.attnum WHERE namespace.nspname = $1 AND relation.relname = $2 AND attribute.attnum > 0 AND NOT attribute.attisdropped ORDER BY attribute.attnum`,
|
|
355
|
-
[address.schema, address.name]
|
|
356
|
-
)).rows;
|
|
357
|
-
const expected = postcondition.columns;
|
|
358
|
-
if (expected.length === 0 || rows.length !== expected.length)
|
|
359
|
-
throw new Error(
|
|
360
|
-
`generated table ${address.name} column postcondition differs: expected ${JSON.stringify(expected)}, live ${JSON.stringify(rows)}`
|
|
361
|
-
);
|
|
362
|
-
for (const specification of expected) {
|
|
363
|
-
const actual = rows.find((row) => row.column_name === specification.name);
|
|
364
|
-
if (!actual || specification.type !== void 0 && !dbTypesEqual(
|
|
365
|
-
String(actual.column_type ?? ""),
|
|
366
|
-
specification.type
|
|
367
|
-
) || specification.nullable !== void 0 && actual.is_not_null !== true !== specification.nullable || specification.hasDefault === true && (specification.default === void 0 || actual.column_default == null || String(actual.column_default) !== specification.default) || specification.hasDefault === false && actual.column_default != null)
|
|
368
|
-
throw new Error(
|
|
369
|
-
`generated table ${address.name} column postcondition differs: expected ${JSON.stringify(specification)}, live ${JSON.stringify(actual)}`
|
|
370
|
-
);
|
|
371
319
|
}
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
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"
|
|
387
364
|
);
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
return generatedPayload({ kind: "enum", labels });
|
|
396
|
-
}
|
|
397
|
-
if (address.kind === "sequence" && address.schema) {
|
|
398
|
-
const postcondition = generatedPostcondition(step, address);
|
|
399
|
-
if (postcondition.kind !== "sequence")
|
|
400
|
-
throw new Error(
|
|
401
|
-
`generated sequence ${address.name} has a non-sequence structural postcondition`
|
|
365
|
+
}
|
|
366
|
+
case "extension": {
|
|
367
|
+
return identityObserved(
|
|
368
|
+
executor,
|
|
369
|
+
postcondition,
|
|
370
|
+
bindingAddress,
|
|
371
|
+
"extension"
|
|
402
372
|
);
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
if (!row) throw new Error(`generated sequence ${address.name} is absent`);
|
|
408
|
-
const actual = {
|
|
409
|
-
startValue: String(row.start_value),
|
|
410
|
-
incrementBy: String(row.increment_by),
|
|
411
|
-
minValue: String(row.min_value),
|
|
412
|
-
maxValue: String(row.max_value),
|
|
413
|
-
cycle: row.cycle === true
|
|
414
|
-
};
|
|
415
|
-
for (const key of [
|
|
416
|
-
"startValue",
|
|
417
|
-
"incrementBy",
|
|
418
|
-
"minValue",
|
|
419
|
-
"maxValue",
|
|
420
|
-
"cycle"
|
|
421
|
-
])
|
|
422
|
-
if (postcondition[key] !== void 0 && postcondition[key] !== actual[key])
|
|
373
|
+
}
|
|
374
|
+
case "absent": {
|
|
375
|
+
const live = await readPgCatalogueIdentity(executor, address);
|
|
376
|
+
if (live)
|
|
423
377
|
throw new Error(
|
|
424
|
-
`generated
|
|
378
|
+
`generated ${address.kind} absence postcondition differs: ${address.name} is still present`
|
|
425
379
|
);
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
if (postcondition.kind !== "extension")
|
|
431
|
-
throw new Error(
|
|
432
|
-
`generated extension ${address.name} has a non-extension structural postcondition`
|
|
433
|
-
);
|
|
434
|
-
const row = (await executor.query(
|
|
435
|
-
`SELECT extension.extversion AS version FROM pg_catalog.pg_extension extension WHERE extension.extname = $1`,
|
|
436
|
-
[address.name]
|
|
437
|
-
)).rows[0];
|
|
438
|
-
if (!row)
|
|
439
|
-
throw new Error(
|
|
440
|
-
`generated extension ${address.name} version postcondition differs`
|
|
441
|
-
);
|
|
442
|
-
if (postcondition.version !== void 0 && String(row.version) !== postcondition.version)
|
|
443
|
-
throw new Error(
|
|
444
|
-
`generated extension ${address.name} version postcondition differs`
|
|
445
|
-
);
|
|
446
|
-
return generatedPayload({
|
|
447
|
-
kind: "extension",
|
|
448
|
-
version: String(row.version)
|
|
449
|
-
});
|
|
380
|
+
return { observed: generatedPayload({ kind: "absent" }) };
|
|
381
|
+
}
|
|
382
|
+
default:
|
|
383
|
+
throw new Error("generated v3 postcondition has no declarable read-back");
|
|
450
384
|
}
|
|
451
|
-
|
|
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
|
+
);
|
|
452
398
|
}
|
|
453
399
|
function containedBy(root, candidate) {
|
|
454
400
|
for (let parent = candidate.parent; parent; parent = parent.parent) {
|
|
@@ -531,7 +477,7 @@ async function executeGeneratorPlan(input) {
|
|
|
531
477
|
try {
|
|
532
478
|
const database = await databaseId(input.pool);
|
|
533
479
|
const executionId = `dbsp.generator.execution.${randomUUID()}`;
|
|
534
|
-
await input.recordAttempt
|
|
480
|
+
await input.recordAttempt(executionId);
|
|
535
481
|
const steps = managedSteps(manifest);
|
|
536
482
|
for (const step of steps) {
|
|
537
483
|
if (step.lifecycle?.kind === "adoption-refused")
|
|
@@ -599,6 +545,7 @@ async function executeGeneratorPlan(input) {
|
|
|
599
545
|
manifest,
|
|
600
546
|
recomputedPlanDigest: input.planDigest,
|
|
601
547
|
approval,
|
|
548
|
+
executionId,
|
|
602
549
|
step,
|
|
603
550
|
home: home(address2),
|
|
604
551
|
address: address2,
|
|
@@ -611,7 +558,11 @@ async function executeGeneratorPlan(input) {
|
|
|
611
558
|
completedStepKeys.push(step.stepKey);
|
|
612
559
|
continue;
|
|
613
560
|
}
|
|
614
|
-
|
|
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 };
|
|
615
566
|
}
|
|
616
567
|
if (step.lifecycle?.kind === "readdress") {
|
|
617
568
|
const result = await executePgPersistedTableReaddress({
|
|
@@ -620,6 +571,7 @@ async function executeGeneratorPlan(input) {
|
|
|
620
571
|
manifest,
|
|
621
572
|
recomputedPlanDigest: input.planDigest,
|
|
622
573
|
approval,
|
|
574
|
+
executionId,
|
|
623
575
|
step,
|
|
624
576
|
database,
|
|
625
577
|
targetSchema: input.schema,
|
|
@@ -705,6 +657,8 @@ async function executeGeneratorPlan(input) {
|
|
|
705
657
|
claimId: result.claimId,
|
|
706
658
|
detail: `claim ${result.claimId} remains open and requires recovery: ${result.reason}`
|
|
707
659
|
};
|
|
660
|
+
if (result.kind === "outcome-transport-ambiguous")
|
|
661
|
+
return { outcome: "transport-ambiguous", detail: result.reason };
|
|
708
662
|
if ("reason" in result) return partial(result.reason);
|
|
709
663
|
completedStepKeys.push(step.stepKey);
|
|
710
664
|
continue;
|
|
@@ -814,7 +768,31 @@ async function executeGeneratorPlan(input) {
|
|
|
814
768
|
kind: "destructive-outcome",
|
|
815
769
|
request: admitRequest,
|
|
816
770
|
readBackAndResolve: async (session) => {
|
|
817
|
-
const
|
|
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;
|
|
818
796
|
const survivors = step.classification === "removal" && live ? [address] : [];
|
|
819
797
|
const childLives = await Promise.all(
|
|
820
798
|
containedClaims.map(async (child) => ({
|
|
@@ -831,9 +809,10 @@ async function executeGeneratorPlan(input) {
|
|
|
831
809
|
const survivorNames = survivors.map(
|
|
832
810
|
(survivor) => `${survivor.kind} ${survivor.schema ?? "<database>"}.${survivor.name}`
|
|
833
811
|
);
|
|
812
|
+
const survivorValue = { survivors: survivorNames };
|
|
834
813
|
const survivorObservation = {
|
|
835
|
-
value:
|
|
836
|
-
digest:
|
|
814
|
+
value: survivorValue,
|
|
815
|
+
digest: canonicalJsonDigest(survivorValue)
|
|
837
816
|
};
|
|
838
817
|
return {
|
|
839
818
|
rootClaimId: claim.claimId,
|
|
@@ -893,8 +872,10 @@ async function executeGeneratorPlan(input) {
|
|
|
893
872
|
eventKind: step.classification === "removal" ? "absent" : "observed",
|
|
894
873
|
// The append protocol reads this address's terminal on its
|
|
895
874
|
// pinned transaction session after the post-DDL read-back.
|
|
896
|
-
...
|
|
897
|
-
...step.classification === "removal" ? {} : {
|
|
875
|
+
...terminalCatalogueIdentity === void 0 ? {} : { catalogueIdentity: terminalCatalogueIdentity },
|
|
876
|
+
...step.classification === "removal" ? {} : {
|
|
877
|
+
observed: declarationReadBack?.observed ?? observed(address)
|
|
878
|
+
}
|
|
898
879
|
}
|
|
899
880
|
}
|
|
900
881
|
];
|
|
@@ -931,10 +912,7 @@ async function executeGeneratorPlan(input) {
|
|
|
931
912
|
detail: `claim ${executed.claimId} remains open and requires recovery: ${executed.reason}`
|
|
932
913
|
};
|
|
933
914
|
if (executed.kind === "outcome-transport-ambiguous")
|
|
934
|
-
return {
|
|
935
|
-
outcome: "execution-failed",
|
|
936
|
-
detail: executed.reason
|
|
937
|
-
};
|
|
915
|
+
return { outcome: "transport-ambiguous", detail: executed.reason };
|
|
938
916
|
if (executed.kind === "outcome-protocol-pending")
|
|
939
917
|
return partial(
|
|
940
918
|
`destructive claim ${claim.claimId} remains pending after executing: ${executed.reason}`
|
|
@@ -958,7 +936,7 @@ async function executeGeneratorPlan(input) {
|
|
|
958
936
|
}
|
|
959
937
|
|
|
960
938
|
// src/commands/generator-plan.ts
|
|
961
|
-
import {
|
|
939
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
962
940
|
import {
|
|
963
941
|
assertDeclarableChangeKind,
|
|
964
942
|
classifyGeneratedMutation,
|
|
@@ -974,17 +952,16 @@ import {
|
|
|
974
952
|
} from "@dbsp/adapter-pgsql";
|
|
975
953
|
import {
|
|
976
954
|
acquireTransitionLease,
|
|
955
|
+
canonicalJson as canonicalJson2,
|
|
956
|
+
canonicalJsonDigest as canonicalJsonDigest2,
|
|
977
957
|
transitionPlanDigest,
|
|
978
958
|
validateNormalizedManagedStepManifest as validateNormalizedManagedStepManifest2
|
|
979
959
|
} from "@dbsp/core";
|
|
980
|
-
function digest(value) {
|
|
981
|
-
return createHash2("sha256").update(JSON.stringify(value)).digest("hex");
|
|
982
|
-
}
|
|
983
960
|
function adoptionDeclaration(table) {
|
|
984
961
|
const value = JSON.parse(
|
|
985
|
-
|
|
962
|
+
canonicalJson2({ kind: "table", name: table.name, shape: table })
|
|
986
963
|
);
|
|
987
|
-
return { value, digest:
|
|
964
|
+
return { value, digest: canonicalJsonDigest2(value) };
|
|
988
965
|
}
|
|
989
966
|
function replacementStatements(table, schema) {
|
|
990
967
|
const quote = (value) => `"${value.replaceAll('"', '""')}"`;
|
|
@@ -1481,7 +1458,10 @@ async function runGeneratorPlan(input) {
|
|
|
1481
1458
|
const run = {
|
|
1482
1459
|
runId: `dbsp-generator-${randomUUID2()}`,
|
|
1483
1460
|
planDigest,
|
|
1484
|
-
targetContextDigest:
|
|
1461
|
+
targetContextDigest: canonicalJsonDigest2({
|
|
1462
|
+
database: await databaseId2(pool),
|
|
1463
|
+
schema
|
|
1464
|
+
}),
|
|
1485
1465
|
databaseId: await databaseId2(pool),
|
|
1486
1466
|
coreVersion: "schema-differ-generator-v1",
|
|
1487
1467
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -2067,6 +2047,72 @@ function generatorRunHasPriorStepEvents(loaded) {
|
|
|
2067
2047
|
return !(typeof executionId === "string" && event.stepId === `dbsp.generator.attempt:${executionId}` && event.operationRef === "dbsp.generator.attempt");
|
|
2068
2048
|
});
|
|
2069
2049
|
}
|
|
2050
|
+
function ledgerHomeForGeneratorAddress(address) {
|
|
2051
|
+
if (address.scope === "database") return { scope: "database" };
|
|
2052
|
+
if (!address.schema)
|
|
2053
|
+
throw new Error(
|
|
2054
|
+
`schema-scoped generated address ${address.name} has no schema ledger`
|
|
2055
|
+
);
|
|
2056
|
+
return { scope: "schema", schema: address.schema };
|
|
2057
|
+
}
|
|
2058
|
+
function generatorPlanLedgerAddresses(steps) {
|
|
2059
|
+
const addresses = /* @__PURE__ */ new Map();
|
|
2060
|
+
for (const step of steps) {
|
|
2061
|
+
if (step.address !== void 0) {
|
|
2062
|
+
addresses.set(ledgerAddressKey2(step.address), step.address);
|
|
2063
|
+
continue;
|
|
2064
|
+
}
|
|
2065
|
+
const closure = step.closure;
|
|
2066
|
+
if (!closure)
|
|
2067
|
+
throw new Error(`generated step ${step.stepKey} has no ledger address`);
|
|
2068
|
+
const candidates = [
|
|
2069
|
+
closure.root,
|
|
2070
|
+
...closure.members.map((member) => member.address)
|
|
2071
|
+
];
|
|
2072
|
+
for (const address of candidates)
|
|
2073
|
+
addresses.set(ledgerAddressKey2(address), address);
|
|
2074
|
+
}
|
|
2075
|
+
return [...addresses.values()];
|
|
2076
|
+
}
|
|
2077
|
+
async function generatorRunHasLedgerEvidence(session, journal, steps) {
|
|
2078
|
+
const executionIds = executionIdsForRun(journal);
|
|
2079
|
+
const executionIdSet = new Set(executionIds);
|
|
2080
|
+
const addresses = generatorPlanLedgerAddresses(steps);
|
|
2081
|
+
const homes = /* @__PURE__ */ new Map();
|
|
2082
|
+
for (const address of addresses) {
|
|
2083
|
+
const home2 = ledgerHomeForGeneratorAddress(address);
|
|
2084
|
+
homes.set(`${home2.scope}:${home2.schema ?? ""}`, home2);
|
|
2085
|
+
}
|
|
2086
|
+
for (const home2 of homes.values()) {
|
|
2087
|
+
for (const executionId of executionIds) {
|
|
2088
|
+
const reservations = await readPgLedgerReservationsForExecution(
|
|
2089
|
+
session,
|
|
2090
|
+
home2,
|
|
2091
|
+
executionId
|
|
2092
|
+
);
|
|
2093
|
+
if (reservations.length > 0) return true;
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
2096
|
+
for (const address of addresses) {
|
|
2097
|
+
const chain = await readPgLedgerAddressChain2(
|
|
2098
|
+
session,
|
|
2099
|
+
ledgerHomeForGeneratorAddress(address),
|
|
2100
|
+
address
|
|
2101
|
+
);
|
|
2102
|
+
if (chain.events.some((event) => executionIdSet.has(event.executionId ?? "")))
|
|
2103
|
+
return true;
|
|
2104
|
+
}
|
|
2105
|
+
return false;
|
|
2106
|
+
}
|
|
2107
|
+
async function generatorRunRequiresRecovery(target, journal, steps) {
|
|
2108
|
+
if (generatorRunHasPriorStepEvents(journal)) return true;
|
|
2109
|
+
const lease = await acquireExclusiveTransitionLease(target);
|
|
2110
|
+
try {
|
|
2111
|
+
return await generatorRunHasLedgerEvidence(lease.session, journal, steps);
|
|
2112
|
+
} finally {
|
|
2113
|
+
await lease.release();
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2070
2116
|
var GENERATOR_ATTEMPT_OPERATION = {
|
|
2071
2117
|
ref: "dbsp.generator.attempt",
|
|
2072
2118
|
operationKind: {
|
|
@@ -2114,19 +2160,19 @@ async function withPoolCleanupReported(result, close) {
|
|
|
2114
2160
|
function compareCodeUnits(left, right) {
|
|
2115
2161
|
return left < right ? -1 : left > right ? 1 : 0;
|
|
2116
2162
|
}
|
|
2117
|
-
function
|
|
2163
|
+
function canonicalJson3(value) {
|
|
2118
2164
|
if (value === null || typeof value === "string" || typeof value === "boolean" || typeof value === "number") {
|
|
2119
2165
|
if (typeof value === "number" && !Number.isFinite(value))
|
|
2120
2166
|
throw new Error("policy values must be finite JSON values");
|
|
2121
2167
|
return JSON.stringify(value);
|
|
2122
2168
|
}
|
|
2123
|
-
if (Array.isArray(value)) return `[${value.map(
|
|
2169
|
+
if (Array.isArray(value)) return `[${value.map(canonicalJson3).join(",")}]`;
|
|
2124
2170
|
if (typeof value !== "object")
|
|
2125
2171
|
throw new Error(
|
|
2126
2172
|
"policy values must be JSON values (undefined is not allowed)"
|
|
2127
2173
|
);
|
|
2128
2174
|
const record2 = value;
|
|
2129
|
-
return `{${Object.keys(record2).sort().map((key) => `${JSON.stringify(key)}:${
|
|
2175
|
+
return `{${Object.keys(record2).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson3(record2[key])}`).join(",")}}`;
|
|
2130
2176
|
}
|
|
2131
2177
|
function nonEmptyString(value, path) {
|
|
2132
2178
|
if (typeof value !== "string" || value.length === 0 || value.trim() !== value)
|
|
@@ -2186,7 +2232,7 @@ function validateResourceAddress(value, path) {
|
|
|
2186
2232
|
`${path}.catalogueIdentity.value`
|
|
2187
2233
|
);
|
|
2188
2234
|
try {
|
|
2189
|
-
|
|
2235
|
+
canonicalJson3(identityValue);
|
|
2190
2236
|
} catch (error) {
|
|
2191
2237
|
throw new Error(
|
|
2192
2238
|
`${path}.catalogueIdentity.value must be a canonicalizable JSON object: ${errorDetail(error)}`
|
|
@@ -2256,11 +2302,11 @@ function normaliseAcceptance(acceptance2) {
|
|
|
2256
2302
|
const validated = validateAssumptionAcceptance(acceptance2);
|
|
2257
2303
|
const withinScope = validated.withinScope ? [
|
|
2258
2304
|
...new Map(
|
|
2259
|
-
validated.withinScope.map((item) => [
|
|
2305
|
+
validated.withinScope.map((item) => [canonicalJson3(item), item])
|
|
2260
2306
|
)
|
|
2261
|
-
].sort(([left], [right]) => compareCodeUnits(left, right)).map(([, item]) => JSON.parse(
|
|
2307
|
+
].sort(([left], [right]) => compareCodeUnits(left, right)).map(([, item]) => JSON.parse(canonicalJson3(item))) : void 0;
|
|
2262
2308
|
return JSON.parse(
|
|
2263
|
-
|
|
2309
|
+
canonicalJson3({
|
|
2264
2310
|
class: validated.class,
|
|
2265
2311
|
...validated.fromTrustRoot ? { fromTrustRoot: validated.fromTrustRoot } : {},
|
|
2266
2312
|
...withinScope === void 0 ? {} : { withinScope }
|
|
@@ -2272,7 +2318,7 @@ function canonicalApplyPolicy(acceptances) {
|
|
|
2272
2318
|
...new Map(
|
|
2273
2319
|
acceptances.map((item) => {
|
|
2274
2320
|
const normalized = normaliseAcceptance(item);
|
|
2275
|
-
return [
|
|
2321
|
+
return [canonicalJson3(normalized), normalized];
|
|
2276
2322
|
})
|
|
2277
2323
|
)
|
|
2278
2324
|
].sort(([left], [right]) => compareCodeUnits(left, right)).map(([, item]) => item);
|
|
@@ -2281,8 +2327,8 @@ function authorizationDigest(runId, planDigest, policy, grants, actor, authorize
|
|
|
2281
2327
|
const authorizedInstant = new Date(authorizedAt);
|
|
2282
2328
|
if (Number.isNaN(authorizedInstant.getTime()))
|
|
2283
2329
|
throw new Error("authorization timestamp must be a valid instant");
|
|
2284
|
-
return
|
|
2285
|
-
|
|
2330
|
+
return createHash("sha256").update(
|
|
2331
|
+
canonicalJson3({
|
|
2286
2332
|
runId,
|
|
2287
2333
|
planDigest,
|
|
2288
2334
|
policy: canonicalApplyPolicy(policy),
|
|
@@ -2295,7 +2341,7 @@ function authorizationDigest(runId, planDigest, policy, grants, actor, authorize
|
|
|
2295
2341
|
}
|
|
2296
2342
|
function hasReusableAuthorization(authorizations, runId, planDigest, policy, grants) {
|
|
2297
2343
|
return authorizations?.some(
|
|
2298
|
-
(item) =>
|
|
2344
|
+
(item) => canonicalJson3(item.policy) === canonicalJson3(policy) && canonicalJson3(item.grants) === canonicalJson3(grants) && item.digest === authorizationDigest(
|
|
2299
2345
|
runId,
|
|
2300
2346
|
planDigest,
|
|
2301
2347
|
policy,
|
|
@@ -2370,7 +2416,7 @@ var APPLY_OUTCOME_CONTRACT = [
|
|
|
2370
2416
|
[
|
|
2371
2417
|
"prior-step-events-refusal",
|
|
2372
2418
|
19,
|
|
2373
|
-
"attempted runs must be classified by
|
|
2419
|
+
"attempted runs with prior generator step-attempt events must be classified by dbsp reconcile --db <database> <run-id>"
|
|
2374
2420
|
],
|
|
2375
2421
|
[
|
|
2376
2422
|
"compatibility-refusal",
|
|
@@ -2402,7 +2448,12 @@ var APPLY_OUTCOME_CONTRACT = [
|
|
|
2402
2448
|
[
|
|
2403
2449
|
"recovery-required",
|
|
2404
2450
|
64,
|
|
2405
|
-
"an admitted
|
|
2451
|
+
"an admitted claim remains open; run dbsp reconcile --db <database> <run-id>"
|
|
2452
|
+
],
|
|
2453
|
+
[
|
|
2454
|
+
"transport-ambiguous",
|
|
2455
|
+
65,
|
|
2456
|
+
"an admitted operation has an ambiguous transport outcome; reconcile the run before retrying"
|
|
2406
2457
|
],
|
|
2407
2458
|
["database-read-only", 34, "target cannot accept managed writes"],
|
|
2408
2459
|
[
|
|
@@ -2455,6 +2506,7 @@ function exitCodeForApplyOutcome(outcome) {
|
|
|
2455
2506
|
return applyExitCodes.get(outcome);
|
|
2456
2507
|
}
|
|
2457
2508
|
function outcomeForApplyResult(result) {
|
|
2509
|
+
if (result.unresolvedOutcome) return result.unresolvedOutcome.kind;
|
|
2458
2510
|
if (result.durableOutcome && isApplyExecutionOutcome(result.durableOutcome))
|
|
2459
2511
|
return result.durableOutcome;
|
|
2460
2512
|
if (result.assessment.lifecycle === "partially-applied")
|
|
@@ -2475,8 +2527,90 @@ function outcomeForApplyResult(result) {
|
|
|
2475
2527
|
return "outcome-unknown";
|
|
2476
2528
|
return "context-mismatch";
|
|
2477
2529
|
}
|
|
2530
|
+
function generatorApplyCommandResult(execution, runId, refusal) {
|
|
2531
|
+
const fields = refusal === void 0 ? {} : { refusal };
|
|
2532
|
+
switch (execution.outcome) {
|
|
2533
|
+
case "completed":
|
|
2534
|
+
return {
|
|
2535
|
+
outcome: execution.outcome,
|
|
2536
|
+
runId,
|
|
2537
|
+
result: execution,
|
|
2538
|
+
...fields
|
|
2539
|
+
};
|
|
2540
|
+
case "partially-applied":
|
|
2541
|
+
return {
|
|
2542
|
+
outcome: execution.outcome,
|
|
2543
|
+
runId,
|
|
2544
|
+
result: execution,
|
|
2545
|
+
...fields
|
|
2546
|
+
};
|
|
2547
|
+
case "selection-incomplete":
|
|
2548
|
+
return {
|
|
2549
|
+
outcome: execution.outcome,
|
|
2550
|
+
runId,
|
|
2551
|
+
result: execution,
|
|
2552
|
+
...fields
|
|
2553
|
+
};
|
|
2554
|
+
case "adoption-refused":
|
|
2555
|
+
return {
|
|
2556
|
+
outcome: execution.outcome,
|
|
2557
|
+
runId,
|
|
2558
|
+
result: execution,
|
|
2559
|
+
...fields
|
|
2560
|
+
};
|
|
2561
|
+
case "readdress-unsupported":
|
|
2562
|
+
return {
|
|
2563
|
+
outcome: execution.outcome,
|
|
2564
|
+
runId,
|
|
2565
|
+
result: execution,
|
|
2566
|
+
...fields
|
|
2567
|
+
};
|
|
2568
|
+
case "readdress-refused":
|
|
2569
|
+
return {
|
|
2570
|
+
outcome: execution.outcome,
|
|
2571
|
+
runId,
|
|
2572
|
+
result: execution,
|
|
2573
|
+
...fields
|
|
2574
|
+
};
|
|
2575
|
+
case "destructive-authority-refused":
|
|
2576
|
+
return {
|
|
2577
|
+
outcome: execution.outcome,
|
|
2578
|
+
runId,
|
|
2579
|
+
result: execution,
|
|
2580
|
+
...fields
|
|
2581
|
+
};
|
|
2582
|
+
case "prior-step-events-refusal":
|
|
2583
|
+
return {
|
|
2584
|
+
outcome: execution.outcome,
|
|
2585
|
+
runId,
|
|
2586
|
+
result: execution,
|
|
2587
|
+
...fields
|
|
2588
|
+
};
|
|
2589
|
+
case "recovery-required":
|
|
2590
|
+
return {
|
|
2591
|
+
outcome: execution.outcome,
|
|
2592
|
+
runId,
|
|
2593
|
+
result: execution,
|
|
2594
|
+
...fields
|
|
2595
|
+
};
|
|
2596
|
+
case "transport-ambiguous":
|
|
2597
|
+
return {
|
|
2598
|
+
outcome: execution.outcome,
|
|
2599
|
+
runId,
|
|
2600
|
+
result: execution,
|
|
2601
|
+
...fields
|
|
2602
|
+
};
|
|
2603
|
+
case "execution-failed":
|
|
2604
|
+
return {
|
|
2605
|
+
outcome: execution.outcome,
|
|
2606
|
+
runId,
|
|
2607
|
+
result: execution,
|
|
2608
|
+
...fields
|
|
2609
|
+
};
|
|
2610
|
+
}
|
|
2611
|
+
}
|
|
2478
2612
|
function formatApplyHuman(result) {
|
|
2479
|
-
const line = `${result.outcome}: ${result.runId}`;
|
|
2613
|
+
const line = `${escapeDiagnosticText3(result.outcome)}: ${escapeDiagnosticText3(result.runId)}`;
|
|
2480
2614
|
if (result.refusal) {
|
|
2481
2615
|
if ("address" in result.refusal)
|
|
2482
2616
|
return formatPreAppendRefusalHuman(line, result.refusal);
|
|
@@ -2488,13 +2622,62 @@ function formatApplyHuman(result) {
|
|
|
2488
2622
|
`resolving command: ${escapeDiagnosticText3(result.refusal.resolvingCommand)}`
|
|
2489
2623
|
].join("\n");
|
|
2490
2624
|
}
|
|
2491
|
-
if (result
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2625
|
+
if (!("result" in result)) return line;
|
|
2626
|
+
if ("assessment" in result.result) {
|
|
2627
|
+
if (result.result.unresolvedOutcome) {
|
|
2628
|
+
const unresolved = result.result.unresolvedOutcome;
|
|
2629
|
+
return unresolved.kind === "recovery-required" ? [
|
|
2630
|
+
line,
|
|
2631
|
+
`claim: ${escapeDiagnosticText3(unresolved.claimId)}`,
|
|
2632
|
+
`detail: ${escapeDiagnosticText3(unresolved.detail)}`,
|
|
2633
|
+
`resolving command: dbsp reconcile --db <database> ${escapeDiagnosticText3(result.runId)}`
|
|
2634
|
+
].join("\n") : [
|
|
2635
|
+
line,
|
|
2636
|
+
`detail: ${escapeDiagnosticText3(unresolved.detail)}`,
|
|
2637
|
+
`resolving command: dbsp reconcile --db <database> ${escapeDiagnosticText3(result.runId)}`
|
|
2638
|
+
].join("\n");
|
|
2639
|
+
}
|
|
2640
|
+
if (result.outcome !== "plan-digest-mismatch") return line;
|
|
2641
|
+
const detail = result.result.assessment.reasons[0]?.detail;
|
|
2642
|
+
return detail ? `${line}
|
|
2643
|
+
${escapeDiagnosticText3(detail)}` : line;
|
|
2644
|
+
}
|
|
2645
|
+
const execution = result.result;
|
|
2646
|
+
switch (execution.outcome) {
|
|
2647
|
+
case "completed":
|
|
2648
|
+
return line;
|
|
2649
|
+
case "recovery-required":
|
|
2650
|
+
return [
|
|
2651
|
+
line,
|
|
2652
|
+
`claim: ${escapeDiagnosticText3(execution.claimId)}`,
|
|
2653
|
+
`detail: ${escapeDiagnosticText3(execution.detail)}`,
|
|
2654
|
+
`resolving command: dbsp reconcile --db <database> ${escapeDiagnosticText3(result.runId)}`
|
|
2655
|
+
].join("\n");
|
|
2656
|
+
case "transport-ambiguous":
|
|
2657
|
+
return [
|
|
2658
|
+
line,
|
|
2659
|
+
`detail: ${escapeDiagnosticText3(execution.detail)}`,
|
|
2660
|
+
`resolving command: dbsp reconcile --db <database> ${escapeDiagnosticText3(result.runId)}`
|
|
2661
|
+
].join("\n");
|
|
2662
|
+
case "partially-applied":
|
|
2663
|
+
case "selection-incomplete":
|
|
2664
|
+
case "adoption-refused":
|
|
2665
|
+
case "readdress-unsupported":
|
|
2666
|
+
case "readdress-refused":
|
|
2667
|
+
case "destructive-authority-refused":
|
|
2668
|
+
case "execution-failed":
|
|
2669
|
+
return `${line}
|
|
2670
|
+
detail: ${escapeDiagnosticText3(execution.detail)}`;
|
|
2671
|
+
case "prior-step-events-refusal":
|
|
2672
|
+
return [
|
|
2673
|
+
line,
|
|
2674
|
+
`detail: ${escapeDiagnosticText3(execution.detail)}`,
|
|
2675
|
+
`resolving command: dbsp reconcile --db <database> ${escapeDiagnosticText3(result.runId)}`
|
|
2676
|
+
].join("\n");
|
|
2677
|
+
}
|
|
2495
2678
|
}
|
|
2496
2679
|
function acceptanceMatches(assumption, acceptance2) {
|
|
2497
|
-
return acceptance2.class === assumption.class && (!acceptance2.fromTrustRoot ||
|
|
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(
|
|
2498
2681
|
(resource) => acceptance2.withinScope?.some(
|
|
2499
2682
|
(selector) => selectorMatchesResource(selector, resource)
|
|
2500
2683
|
)
|
|
@@ -2730,10 +2913,14 @@ async function runApplyInternal(runId, options, pool) {
|
|
|
2730
2913
|
throw new RecordedPlanDigestMismatchError(
|
|
2731
2914
|
`persisted generator manifest is invalid: ${lifecycleError}`
|
|
2732
2915
|
);
|
|
2733
|
-
if (
|
|
2916
|
+
if (await generatorRunRequiresRecovery(
|
|
2917
|
+
target,
|
|
2918
|
+
current,
|
|
2919
|
+
manifest.manifest.steps
|
|
2920
|
+
))
|
|
2734
2921
|
return {
|
|
2735
2922
|
outcome: "prior-step-events-refusal",
|
|
2736
|
-
detail: "run has prior generator step-attempt events; run dbsp
|
|
2923
|
+
detail: "run has prior generator step-attempt events; run dbsp reconcile --db <database> <run-id>"
|
|
2737
2924
|
};
|
|
2738
2925
|
return executeGeneratorPlan({
|
|
2739
2926
|
pool: owned,
|
|
@@ -2780,12 +2967,11 @@ async function runApplyInternal(runId, options, pool) {
|
|
|
2780
2967
|
withheldAuthority
|
|
2781
2968
|
}
|
|
2782
2969
|
};
|
|
2783
|
-
result =
|
|
2784
|
-
|
|
2970
|
+
result = generatorApplyCommandResult(
|
|
2971
|
+
execution,
|
|
2785
2972
|
runId,
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
};
|
|
2973
|
+
preAppendRefusal
|
|
2974
|
+
);
|
|
2789
2975
|
}
|
|
2790
2976
|
} else {
|
|
2791
2977
|
const locked = await withPgTransitionRunLock(
|
|
@@ -2837,7 +3023,7 @@ async function runApplyInternal(runId, options, pool) {
|
|
|
2837
3023
|
return;
|
|
2838
3024
|
const actor = process.env.USER ?? process.env.LOGNAME ?? "unknown-local-actor";
|
|
2839
3025
|
const authorizedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2840
|
-
const
|
|
3026
|
+
const digest = authorizationDigest(
|
|
2841
3027
|
run.runId,
|
|
2842
3028
|
transitionPlanDigest2(plan),
|
|
2843
3029
|
policy.accepts,
|
|
@@ -2849,7 +3035,7 @@ async function runApplyInternal(runId, options, pool) {
|
|
|
2849
3035
|
runId: run.runId,
|
|
2850
3036
|
policy: policy.accepts,
|
|
2851
3037
|
grants,
|
|
2852
|
-
digest
|
|
3038
|
+
digest,
|
|
2853
3039
|
actor,
|
|
2854
3040
|
authorizedAt
|
|
2855
3041
|
};
|
|
@@ -2965,9 +3151,7 @@ var applyCommand = new Command2("apply").description(
|
|
|
2965
3151
|
console.log("confirmation-declined: persisted run was not executed");
|
|
2966
3152
|
else if (result2.outcome === "not-executable") {
|
|
2967
3153
|
} else if (result2.outcome !== "dry-run" && "result" in result2)
|
|
2968
|
-
console.log(
|
|
2969
|
-
"runId" in result2.result ? formatApplyHuman(result2.result) : `${result2.result.outcome}${"detail" in result2.result ? `: ${result2.result.detail}` : ""}`
|
|
2970
|
-
);
|
|
3154
|
+
console.log(formatApplyHuman(result2.result));
|
|
2971
3155
|
}
|
|
2972
3156
|
process.exitCode = exitCode2;
|
|
2973
3157
|
return;
|
|
@@ -3143,7 +3327,7 @@ import {
|
|
|
3143
3327
|
DBSP_LEDGER_EVENT_TABLE,
|
|
3144
3328
|
escapeDiagnosticText as escapeDiagnosticText4,
|
|
3145
3329
|
readPgCatalogueIdentity as readPgCatalogueIdentity3,
|
|
3146
|
-
readPgLedgerAddressChain as
|
|
3330
|
+
readPgLedgerAddressChain as readPgLedgerAddressChain3,
|
|
3147
3331
|
readPgLedgerMarker
|
|
3148
3332
|
} from "@dbsp/adapter-pgsql";
|
|
3149
3333
|
import { acquireTransitionLease as acquireTransitionLease4, projectLedgerChain as projectLedgerChain2 } from "@dbsp/core";
|
|
@@ -3316,7 +3500,7 @@ async function runInspect(selector, options) {
|
|
|
3316
3500
|
let projection;
|
|
3317
3501
|
try {
|
|
3318
3502
|
projection = projectLedgerChain2(
|
|
3319
|
-
await
|
|
3503
|
+
await readPgLedgerAddressChain3(lease.session, ledger, address)
|
|
3320
3504
|
);
|
|
3321
3505
|
} catch (error) {
|
|
3322
3506
|
return {
|
|
@@ -3483,7 +3667,7 @@ var introspectCommand = new Command5("introspect").description("Generate schema.
|
|
|
3483
3667
|
);
|
|
3484
3668
|
|
|
3485
3669
|
// src/commands/preflight.ts
|
|
3486
|
-
import { createHash as
|
|
3670
|
+
import { createHash as createHash2 } from "crypto";
|
|
3487
3671
|
import { mkdtemp, open, rename, rm } from "fs/promises";
|
|
3488
3672
|
import { dirname as dirname3, join } from "path";
|
|
3489
3673
|
import {
|
|
@@ -3568,7 +3752,7 @@ function declarationsForScopes(model, database, scopes, naming) {
|
|
|
3568
3752
|
});
|
|
3569
3753
|
return {
|
|
3570
3754
|
version: 1,
|
|
3571
|
-
digest:
|
|
3755
|
+
digest: createHash2("sha256").update(canonical).digest("hex"),
|
|
3572
3756
|
declarations: uniqueDeclarations
|
|
3573
3757
|
};
|
|
3574
3758
|
}
|
|
@@ -3655,14 +3839,14 @@ var preflightCommand = new Command6("preflight").description(
|
|
|
3655
3839
|
});
|
|
3656
3840
|
|
|
3657
3841
|
// src/commands/reconcile.ts
|
|
3658
|
-
import { createHash as
|
|
3842
|
+
import { createHash as createHash3 } from "crypto";
|
|
3659
3843
|
import {
|
|
3660
3844
|
assertCreateUniqueIndexConcurrentlyRecoveryNotInvalid,
|
|
3661
3845
|
assertPgDatabaseWritable,
|
|
3662
3846
|
escapeDiagnosticText as escapeDiagnosticText6,
|
|
3663
3847
|
isPgDatabaseReadOnlyError,
|
|
3664
|
-
readPgLedgerAddressChain as
|
|
3665
|
-
readPgLedgerReservationsForExecution,
|
|
3848
|
+
readPgLedgerAddressChain as readPgLedgerAddressChain4,
|
|
3849
|
+
readPgLedgerReservationsForExecution as readPgLedgerReservationsForExecution2,
|
|
3666
3850
|
readPgLedgerScopeCurrency as readPgLedgerScopeCurrency2,
|
|
3667
3851
|
readTransitionJournal as readTransitionJournal2,
|
|
3668
3852
|
readVerifiedPgLedgerReservationsForPair,
|
|
@@ -3673,6 +3857,8 @@ import { recoverPgOutcomeClaim } from "@dbsp/adapter-pgsql/internal";
|
|
|
3673
3857
|
import {
|
|
3674
3858
|
acquireExclusiveTransitionLease as acquireExclusiveTransitionLease2,
|
|
3675
3859
|
assumptionAccepted,
|
|
3860
|
+
canonicalJson as canonicalJson4,
|
|
3861
|
+
canonicalJsonDigest as canonicalJsonDigest3,
|
|
3676
3862
|
outcomeClaimId as outcomeClaimId2,
|
|
3677
3863
|
projectLedgerChain as projectLedgerChain3,
|
|
3678
3864
|
resourceScopeCovers,
|
|
@@ -3705,6 +3891,9 @@ function classifyReconcileFailure(error, stage) {
|
|
|
3705
3891
|
if (stage === "journal") return "malformed-journal";
|
|
3706
3892
|
return "catalogue";
|
|
3707
3893
|
}
|
|
3894
|
+
function isUnresolvedRecoveryOutcome(report) {
|
|
3895
|
+
return report.outcome === "pending" || report.outcome === "blocked" || report.outcome === "malformed-chain" || report.outcome === "protocol-refused" || report.outcome === "transport-ambiguous" || report.outcome === "no-open-claim" || report.outcome === "indeterminate-appended" || report.outcome === "indeterminate-pair";
|
|
3896
|
+
}
|
|
3708
3897
|
function ledgerHome(address) {
|
|
3709
3898
|
if (address.scope === "database") return { scope: "database" };
|
|
3710
3899
|
if (!address.schema)
|
|
@@ -3715,18 +3904,21 @@ function ledgerHome(address) {
|
|
|
3715
3904
|
}
|
|
3716
3905
|
function recoveryPayload(identity) {
|
|
3717
3906
|
const value = JSON.parse(
|
|
3718
|
-
|
|
3907
|
+
canonicalJson4({
|
|
3908
|
+
...identity === void 0 ? {} : { catalogueIdentity: identity }
|
|
3909
|
+
})
|
|
3719
3910
|
);
|
|
3720
3911
|
return {
|
|
3721
3912
|
value,
|
|
3722
|
-
digest:
|
|
3913
|
+
digest: canonicalJsonDigest3(value)
|
|
3723
3914
|
};
|
|
3724
3915
|
}
|
|
3725
3916
|
function recoveryReport(address, result) {
|
|
3726
3917
|
if (result.kind === "outcome-recovery-appended") {
|
|
3918
|
+
const indeterminate = result.classification.resolution.eventKind === "indeterminate";
|
|
3727
3919
|
return {
|
|
3728
3920
|
address,
|
|
3729
|
-
outcome: result.append.kind === "already-appended-outcome-resolution" ? "already-appended" : "appended",
|
|
3921
|
+
outcome: indeterminate ? "indeterminate-appended" : result.append.kind === "already-appended-outcome-resolution" ? "already-appended" : "appended",
|
|
3730
3922
|
reason: result.classification.resolution.reason
|
|
3731
3923
|
};
|
|
3732
3924
|
}
|
|
@@ -3796,9 +3988,7 @@ function readdressRecoveryReport(reservations, result) {
|
|
|
3796
3988
|
};
|
|
3797
3989
|
}
|
|
3798
3990
|
function unresolvedRecoveryDetail(reports) {
|
|
3799
|
-
const unresolved = reports.filter(
|
|
3800
|
-
(report) => report.outcome === "pending" || report.outcome === "blocked" || report.outcome === "malformed-chain" || report.outcome === "protocol-refused" || report.outcome === "transport-ambiguous" || report.outcome === "no-open-claim" || report.outcome === "indeterminate-pair"
|
|
3801
|
-
);
|
|
3991
|
+
const unresolved = reports.filter(isUnresolvedRecoveryOutcome);
|
|
3802
3992
|
if (unresolved.length === 0) return void 0;
|
|
3803
3993
|
return unresolved.map(
|
|
3804
3994
|
(report) => `${escapeDiagnosticText6(report.address.name)}: ${escapeDiagnosticText6(report.reason ?? report.outcome)}`
|
|
@@ -3806,23 +3996,18 @@ function unresolvedRecoveryDetail(reports) {
|
|
|
3806
3996
|
}
|
|
3807
3997
|
function formatReconcileHuman(result) {
|
|
3808
3998
|
const line = `${escapeDiagnosticText6(result.outcome)}: ${escapeDiagnosticText6(result.runId)}`;
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
executionIds.add(record2.intent.executionId);
|
|
3819
|
-
}
|
|
3820
|
-
if ("generator" in journal.plan)
|
|
3821
|
-
executionIds.add(`dbsp.generator.execution.${journal.run.runId}`);
|
|
3822
|
-
return [...executionIds];
|
|
3999
|
+
const base = result.refusal ? formatPreAppendRefusalHuman(line, result.refusal) : line;
|
|
4000
|
+
if (result.outcome === "reconcile-completed" || !result.recovery) return base;
|
|
4001
|
+
const unresolved = result.recovery.filter(isUnresolvedRecoveryOutcome);
|
|
4002
|
+
return unresolved.length === 0 ? base : [
|
|
4003
|
+
base,
|
|
4004
|
+
...unresolved.map(
|
|
4005
|
+
(report) => `${escapeDiagnosticText6(report.address.name)}: ${escapeDiagnosticText6(report.outcome)}${report.reason ? `: ${escapeDiagnosticText6(report.reason)}` : ""}`
|
|
4006
|
+
)
|
|
4007
|
+
].join("\n");
|
|
3823
4008
|
}
|
|
3824
4009
|
function statementBundleDigest(statements) {
|
|
3825
|
-
return
|
|
4010
|
+
return createHash3("sha256").update(
|
|
3826
4011
|
JSON.stringify(statements.map(({ ordinal, sql }) => [ordinal, sql]))
|
|
3827
4012
|
).digest("hex");
|
|
3828
4013
|
}
|
|
@@ -3963,7 +4148,7 @@ async function runReconcile(runId, options, pool) {
|
|
|
3963
4148
|
const reservations = (await Promise.all(
|
|
3964
4149
|
[...homes.values()].flatMap(
|
|
3965
4150
|
(home2) => executionIds.map(
|
|
3966
|
-
(executionId) =>
|
|
4151
|
+
(executionId) => readPgLedgerReservationsForExecution2(
|
|
3967
4152
|
lease.session,
|
|
3968
4153
|
home2,
|
|
3969
4154
|
executionId
|
|
@@ -4024,7 +4209,7 @@ async function runReconcile(runId, options, pool) {
|
|
|
4024
4209
|
const rootCandidates = [];
|
|
4025
4210
|
for (const row of rows) {
|
|
4026
4211
|
stage = "catalogue";
|
|
4027
|
-
const chain = await
|
|
4212
|
+
const chain = await readPgLedgerAddressChain4(
|
|
4028
4213
|
lease.session,
|
|
4029
4214
|
ledgerHome(row.address),
|
|
4030
4215
|
row.address
|