@substrat-run/adapter-cloudflare 0.174.0 → 0.176.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/dist/host.d.ts +5 -1
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +8 -3
- package/dist/host.js.map +1 -1
- package/dist/scope-do.d.ts.map +1 -1
- package/dist/scope-do.js +106 -35
- package/dist/scope-do.js.map +1 -1
- package/package.json +9 -9
package/dist/scope-do.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scope-do.d.ts","sourceRoot":"","sources":["../src/scope-do.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,EAuDL,KAAK,QAAQ,
|
|
1
|
+
{"version":3,"file":"scope-do.d.ts","sourceRoot":"","sources":["../src/scope-do.ts"],"names":[],"mappings":"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"}
|
package/dist/scope-do.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { REWIND_REFUSED } from './rewind-refusal.js';
|
|
2
2
|
import { SYSTEM_DOOR_MOVED } from './system-door.js';
|
|
3
3
|
import { DurableObject } from 'cloudflare:workers';
|
|
4
|
-
import { ATTACHMENT_ADDED, ATTACHMENT_REMOVED, CAPTURE_REFUSALS, attachmentRecord, domainEvent, domainEventInput, eventId, instant, objectRef, entityObjectRef, assertModuleEmittableType, assertKernelAuthoredType, toWireFailure, grantRefFromProof, principalId, principalBecomeCapabilityInput, scopeId as scopeIdOf, tenantId as tenantIdOf, platformRequestInput, platformRequestId, MAX_PENDING_PLATFORM_REQUESTS, MAX_PENDING_SWEEP_RUNS, SWEEP_RUNS_KIND, SCOPE_DUMP_PAGE_MAX_BYTES, SCOPE_TABLE_PAGE_MAX, SCOPE_QUERY_ROW_MAX, listLimitOf, pageOf, countedPageOf, requestFingerprint, substratError, errorCodeOf, assertReplayableDump, REDRAIN_BATCH, } from '@substrat-run/contracts';
|
|
5
|
-
import { ulid, DO_SQL_LIMITS, unknownRoleError, createUlid, assertAllowed, ConnectionSealingKeyUnavailableError, noSealingKeyMessage, sealTo, assertReadOnlyQuery, entitlementDenial, platformRequestHistoryQuery, platformRequestOf, PLATFORM_REQUEST_COLUMNS, PLATFORM_REQUEST_REDACTION_SQL, platformRequestRedactionParams, platformRequestRedactionQuery, intentPayloadCarriesSubject, redactSubjectJobRuns, eraseSubjectFromModules, moduleErasurePlan, redactSubjectScopeText, assertRowLimit, assertRowOffset, JOB_RUN_PATCH_SQL, JOB_RUN_CLAIM_SQL, JOB_RUN_RENEW_SQL, JOB_RUN_BEGIN_SQL, JOB_RUN_MISS_SQL, JOB_RUN_MISS_SETTLE_SQL, admissionMissOutcome, JOB_LEASE_EXPIRED_NOTE, JOB_STEP_RECORD_SQL, DELIVERY_ERROR_REDACTION_SQL, REDACTED_DELIVERY_NOTE, seatScopeTuple, delegatedRevokeSql, writeExplicitTupleIn, grantEntityShapeIn, topUpEntityGrantShapes, applyScopeRoleChange, changeScopeRole, revokeScopeRoles, scopeRoleHolders, assertModuleDeclared, noteDeclaredKeys, noteReservedKeys, SCOPE_SEATED_ROLES_SQL, inUnitRoleRefusal, loadedRoleSetRefusedRole, reservedRoleSetRefusedRole, reservedGrantRefusal, reservedRoleRefusal, reservedShapeRefusal, effectiveRoleGrantQuery, switchRecordedOff, switchSystemSchedules, peerGrantsStatus, systemGrantsStatus, systemScheduleState, systemSwitchedOff, denialListQuery, refusalListQuery, mapRefusalRow, denialSummaryQuery, denialTotalsQuery, DENIAL_WINDOW_QUERY, mapDenialRow, mapDenialSummaryBuckets, PermissionDenied, assertImpersonationWrites, assertModuleEnqueueableKind, impersonationStampOf, createAtomic, NotSearchable, isSearchIndexTable, searchIndexPlans, NotListable, listIndexPlans, moduleMigrations, MIGRATION_DIGEST_FENCE_DDL, MIGRATION_DIGEST_MARK_LEGACY, assertJournalDumpCoherent, assertMigrationSql, migrationDivergence, migrationFailedError, migrationSetDivergence, migrationSteps, planMigrations, listQuery, cursorOf, searchLimit, searchMatchExpression, searchQuery, ATTACHMENT_TEXT_DDL, ATTACHMENT_BYTES_SQL, ATTACHMENT_CONDEMNED_DDL, ATTACHMENT_SWEEP_LICENCE_DDL, ATTACHMENT_SWEEP_LICENCE_TABLE, WIRE_NONCE_DDL, WIRE_NONCE_FULL, claimWireNonceIn, isWireNonceTable, isWireNonceWrite, clearOrphanSweepLicence, condemnLicensedOrphanAttachments, consumeOrphanSweepLicence, licensedOrphanIds, recordOrphanSweepLicence, refuseUnlicensedOrphanSweep, refuseCondemnedAttachment, refuseCondemnedInDump, rowlessAttachmentIds, attachmentRecordOfRow, enqueueAttachmentText, reconcileAttachmentText, queueAttachmentTextBackfill, startAttachmentTextBackfill, recordAttachmentText, searchAttachments, IDEMPOTENCY_DDL, REFUSALS_TABLE_DDL, REFUSALS_INDEX, refusalInsert, refusalOf, markGuardRefusal, REFUSALS_REBUILD, refusalsAdmitGuards, assertIdempotencyKey, assertPermissionKey, idempotencyLookupQuery, idempotencyPruneStatement, idempotencyRecordStatement, idempotencyOptedOutMessage, replayFor, entityVersionQuery, entityVersionOf, assertIfMatch, OUTBOX_ENTITY_INDEX, SCHEDULE_STATE_DDL, SCHEDULE_STATE_REBUILD, scheduleStateHasKind, VERTICAL_EVENTS_DDL, EXPORT_HOPS_SQL, emptyImportResult, IMPORT_CURSORS_SQL, IMPORT_CURSOR_OF_SQL, OUTBOX_MARK_SQL, exportedSinceQuery, emittedSinceQuery, emittedReportOf, EMITTED_REPORT_CAP, IMPORT_CURSOR_ADVANCE_SQL, IMPORT_RECORD_SQL, moveImportCursor, CrossVerticalRegistry, exportReadPlan, exportReadQuery, planExportBatch, withheldNote, JOB_RUN_DDL, JOB_RUN_DUE_AT, jobRunListLimit, SYSTEM_DOOR_WAIT, ancestorsWithin, } from '@substrat-run/kernel';
|
|
4
|
+
import { ATTACHMENT_ADDED, ATTACHMENT_REMOVED, CAPTURE_REFUSALS, attachmentRecord, domainEvent, domainEventInput, eventId, instant, objectRef, entityObjectRef, assertModuleEmittableType, assertKernelAuthoredType, toWireFailure, grantRefFromProof, principalId, principalBecomeCapabilityInput, scopeId as scopeIdOf, tenantId as tenantIdOf, platformRequestInput, platformRequestId, MAX_PENDING_PLATFORM_REQUESTS, MAX_PENDING_SWEEP_RUNS, SWEEP_RUNS_KIND, SCOPE_DUMP_PAGE_MAX_BYTES, SCOPE_TABLE_PAGE_MAX, SCOPE_QUERY_ROW_MAX, listLimitOf, pageOf, countedPageOf, requestFingerprint, substratError, errorCodeOf, assertReplayableDump, DUMP_OBJECTS_QUERY, dumpObjectsOf, replayDumpObjects, dumpObjectsAudit, REDRAIN_BATCH, } from '@substrat-run/contracts';
|
|
5
|
+
import { ulid, DO_SQL_LIMITS, unknownRoleError, createUlid, assertAllowed, ConnectionSealingKeyUnavailableError, noSealingKeyMessage, sealTo, assertReadOnlyQuery, entitlementDenial, platformRequestHistoryQuery, platformRequestOf, PLATFORM_REQUEST_COLUMNS, PLATFORM_REQUEST_REDACTION_SQL, platformRequestRedactionParams, platformRequestRedactionQuery, intentPayloadCarriesSubject, redactSubjectJobRuns, eraseSubjectFromModules, moduleErasurePlan, redactSubjectScopeText, assertRowLimit, assertRowOffset, JOB_RUN_PATCH_SQL, JOB_RUN_CLAIM_SQL, JOB_RUN_RENEW_SQL, JOB_RUN_BEGIN_SQL, JOB_RUN_MISS_SQL, JOB_RUN_MISS_SETTLE_SQL, admissionMissOutcome, JOB_LEASE_EXPIRED_NOTE, JOB_STEP_RECORD_SQL, DELIVERY_ERROR_REDACTION_SQL, REDACTED_DELIVERY_NOTE, seatScopeTuple, delegatedRevokeSql, writeExplicitTupleIn, grantEntityShapeIn, topUpEntityGrantShapes, applyScopeRoleChange, changeScopeRole, revokeScopeRoles, scopeRoleHolders, assertModuleDeclared, noteDeclaredKeys, noteReservedKeys, SCOPE_SEATED_ROLES_SQL, inUnitRoleRefusal, loadedRoleSetRefusedRole, reservedRoleSetRefusedRole, reservedGrantRefusal, reservedRoleRefusal, reservedShapeRefusal, effectiveRoleGrantQuery, switchRecordedOff, switchSystemSchedules, peerGrantsStatus, systemGrantsStatus, systemScheduleState, systemSwitchedOff, denialListQuery, refusalListQuery, mapRefusalRow, denialSummaryQuery, denialTotalsQuery, DENIAL_WINDOW_QUERY, mapDenialRow, mapDenialSummaryBuckets, PermissionDenied, assertImpersonationAdmitted, assertImpersonationWrites, assertModuleEnqueueableKind, impersonationStampOf, createAtomic, NotSearchable, isSearchIndexTable, searchIndexPlans, NotListable, listIndexPlans, moduleMigrations, MIGRATION_DIGEST_FENCE_DDL, MIGRATION_DIGEST_MARK_LEGACY, assertJournalDumpCoherent, assertMigrationSql, migrationDivergence, migrationFailedError, migrationSetDivergence, migrationSteps, planMigrations, listQuery, cursorOf, searchLimit, searchMatchExpression, searchQuery, ATTACHMENT_TEXT_DDL, ATTACHMENT_BYTES_SQL, ATTACHMENT_CONDEMNED_DDL, ATTACHMENT_SWEEP_LICENCE_DDL, ATTACHMENT_SWEEP_LICENCE_TABLE, WIRE_NONCE_DDL, WIRE_NONCE_FULL, claimWireNonceIn, isWireNonceTable, isWireNonceWrite, clearOrphanSweepLicence, condemnLicensedOrphanAttachments, consumeOrphanSweepLicence, licensedOrphanIds, recordOrphanSweepLicence, refuseUnlicensedOrphanSweep, refuseCondemnedAttachment, refuseCondemnedInDump, rowlessAttachmentIds, attachmentRecordOfRow, enqueueAttachmentText, reconcileAttachmentText, queueAttachmentTextBackfill, startAttachmentTextBackfill, recordAttachmentText, searchAttachments, IDEMPOTENCY_DDL, REFUSALS_TABLE_DDL, REFUSALS_INDEX, refusalInsert, refusalOf, markGuardRefusal, REFUSALS_REBUILD, refusalsAdmitGuards, assertIdempotencyKey, assertPermissionKey, idempotencyLookupQuery, idempotencyPruneStatement, idempotencyRecordStatement, idempotencyOptedOutMessage, replayFor, entityVersionQuery, entityVersionOf, assertIfMatch, OUTBOX_ENTITY_INDEX, SCHEDULE_STATE_DDL, SCHEDULE_STATE_REBUILD, scheduleStateHasKind, VERTICAL_EVENTS_DDL, EXPORT_HOPS_SQL, emptyImportResult, IMPORT_CURSORS_SQL, IMPORT_CURSOR_OF_SQL, OUTBOX_MARK_SQL, exportedSinceQuery, emittedSinceQuery, emittedReportOf, EMITTED_REPORT_CAP, noteTransitionInto, withTransitions, IMPORT_CURSOR_ADVANCE_SQL, IMPORT_RECORD_SQL, moveImportCursor, CrossVerticalRegistry, exportReadPlan, exportReadQuery, planExportBatch, withheldNote, JOB_RUN_DDL, JOB_RUN_DUE_AT, jobRunListLimit, SYSTEM_DOOR_WAIT, ancestorsWithin, } from '@substrat-run/kernel';
|
|
6
6
|
import { isUpgradeRequest, readSubscription, LIVE_DECIDE_ATTEMPTS, LIVE_FANOUT_LIMIT, LIVE_PRINCIPAL_HEADER, LIVE_SCOPE_HEADER, LIVE_SUBSCRIBE_PATH, LIVE_TENANT_HEADER, LIVE_WITHIN_HEADER, LIVE_EXPIRES_HEADER, LIVE_CLOSE, LIVE_SOCKETS_PER_PRINCIPAL, decodeLiveWithin, liveInstant, liveRefusal, liveRefusalStatus, } from './live-reads.js';
|
|
7
7
|
import { replyOf } from './do-reply.js';
|
|
8
8
|
import { OperationQueue } from './serialization.js';
|
|
@@ -442,16 +442,22 @@ const STAGE_IN_PROGRESS = 'a different staged load is in progress; retry after i
|
|
|
442
442
|
const STAGE_MAX_AGE_MS = 6 * 3_600_000 + 3 * SCOPE_COPY_LEASE_MS;
|
|
443
443
|
/**
|
|
444
444
|
* A staged load's own tables (#2450): `_substrat_stage` (which load, and the next page it takes),
|
|
445
|
-
* `_substrat_stage_tables` (each staged table's name, DDL and columns)
|
|
446
|
-
* `_substrat_stage_rows_<n>` per staged table. They hold a carry's pages until the
|
|
447
|
-
* from them, so they are this store's, never its data: no dump carries them, and a
|
|
448
|
-
* leave them to the commit that reads them.
|
|
445
|
+
* `_substrat_stage_tables` (each staged table's name, DDL and columns), `_substrat_stage_objects`
|
|
446
|
+
* (below) and one `_substrat_stage_rows_<n>` per staged table. They hold a carry's pages until the
|
|
447
|
+
* load commits from them, so they are this store's, never its data: no dump carries them, and a
|
|
448
|
+
* load's drops leave them to the commit that reads them.
|
|
449
449
|
*/
|
|
450
450
|
const STAGE_GLOB = '_substrat_stage*';
|
|
451
451
|
const STAGE_TABLES_DDL = [
|
|
452
452
|
`CREATE TABLE _substrat_stage (stage_id TEXT NOT NULL, next_seq INTEGER NOT NULL)`,
|
|
453
453
|
`CREATE TABLE _substrat_stage_tables (ord INTEGER PRIMARY KEY, name TEXT NOT NULL UNIQUE, ddl TEXT NOT NULL, columns TEXT NOT NULL)`,
|
|
454
454
|
];
|
|
455
|
+
/**
|
|
456
|
+
* #2820: each staged table's own indexes and triggers, for a table whose pages carried them. Created
|
|
457
|
+
* by every page if missing, so a stage opened before it existed still takes pages, and commits with
|
|
458
|
+
* none: as a dump written before objects were carried.
|
|
459
|
+
*/
|
|
460
|
+
const STAGE_OBJECTS_DDL = `CREATE TABLE IF NOT EXISTS _substrat_stage_objects (name TEXT PRIMARY KEY, objects TEXT NOT NULL)`;
|
|
455
461
|
const stageRowsTable = (ord) => `_substrat_stage_rows_${ord}`;
|
|
456
462
|
/** How many staged rows a commit reads at a time. */
|
|
457
463
|
const STAGE_READ_BATCH = 500;
|
|
@@ -944,6 +950,8 @@ options = {}) {
|
|
|
944
950
|
purgeSchedules = new Map();
|
|
945
951
|
/** #116: the operations that declared `idempotency: false` — refusals, not participants. */
|
|
946
952
|
operationIdempotencyOptOut = new Set();
|
|
953
|
+
/** #2821: the operations that declared `impersonation: 'refused'` — no session may invoke them. */
|
|
954
|
+
operationImpersonationRefused = new Set();
|
|
947
955
|
/** #2387: the operations held to a `ctx.check` — the pure adapter's set, from the same kernel read. */
|
|
948
956
|
operationCheckHeld = new Set();
|
|
949
957
|
modules = new Map();
|
|
@@ -1222,6 +1230,7 @@ options = {}) {
|
|
|
1222
1230
|
// #116: same rule again for an opt-out — one on an unbound name reads as a
|
|
1223
1231
|
// deliberate exclusion of an operation that is not there.
|
|
1224
1232
|
const declaredOptOuts = registration.operations?.idempotencyOptOuts ?? [];
|
|
1233
|
+
const impersonationRefused = registration.operations?.impersonationRefused ?? [];
|
|
1225
1234
|
const unboundOptOuts = declaredOptOuts.filter((name) => !ownOps.has(name));
|
|
1226
1235
|
if (unboundOptOuts.length > 0) {
|
|
1227
1236
|
throw new Error(`${manifest.id} declares idempotency: false for unbound operation(s): ` +
|
|
@@ -1244,6 +1253,9 @@ options = {}) {
|
|
|
1244
1253
|
if (declaredOptOuts.includes(name) && this.operations.has(name)) {
|
|
1245
1254
|
this.operationIdempotencyOptOut.add(name);
|
|
1246
1255
|
}
|
|
1256
|
+
if (impersonationRefused.includes(name) && this.operations.has(name)) {
|
|
1257
|
+
this.operationImpersonationRefused.add(name);
|
|
1258
|
+
}
|
|
1247
1259
|
const target = trashTargets.get(name);
|
|
1248
1260
|
if (target && this.operations.has(name))
|
|
1249
1261
|
this.operationTarget.set(name, target);
|
|
@@ -2266,6 +2278,8 @@ options = {}) {
|
|
|
2266
2278
|
// else. Naming the code once here is what lets those patterns go.
|
|
2267
2279
|
if (!handler)
|
|
2268
2280
|
throw substratError('not_found', `unknown operation: ${operation}`);
|
|
2281
|
+
// #2821: before the parse, the guards and the handler — and under read-only too.
|
|
2282
|
+
assertImpersonationAdmitted(impersonation, operation, this.operationImpersonationRefused);
|
|
2269
2283
|
// #304 entitlement gate, scope-local path: fail closed on the projected view exactly as
|
|
2270
2284
|
// the coordinator fails closed against the CP. Only active once entitlements have been
|
|
2271
2285
|
// projected (the `entitlements_enforced` marker) — before that the scope trusts upstream,
|
|
@@ -2409,7 +2423,12 @@ options = {}) {
|
|
|
2409
2423
|
// transaction, reported only after commit — a rolled-back intent is no signal.
|
|
2410
2424
|
// The envelope return (below) is the DO↔coordinator wire for it; both sides
|
|
2411
2425
|
// live in this package and deploy as one script, so the shape never skews.
|
|
2412
|
-
|
|
2426
|
+
// #2762: and the lifecycle moves `ctx.noteTransition` notes, when the caller asked for the
|
|
2427
|
+
// report they ride on. `ctx.atomic` restores both.
|
|
2428
|
+
const signals = {
|
|
2429
|
+
platformRequests: 0,
|
|
2430
|
+
...(invokeOptions?.onEmitted !== undefined ? { transitions: [] } : {}),
|
|
2431
|
+
};
|
|
2413
2432
|
/**
|
|
2414
2433
|
* K-42: how a read-only session is made to BE read-only.
|
|
2415
2434
|
*
|
|
@@ -2560,7 +2579,7 @@ options = {}) {
|
|
|
2560
2579
|
let emitted;
|
|
2561
2580
|
if (emittedMark !== null && !replayed) {
|
|
2562
2581
|
const q = emittedSinceQuery(emittedMark, EMITTED_REPORT_CAP);
|
|
2563
|
-
emitted = emittedReportOf(this.sql.exec(q.sql, ...q.params).toArray());
|
|
2582
|
+
emitted = withTransitions(emittedReportOf(this.sql.exec(q.sql, ...q.params).toArray()), signals.transitions);
|
|
2564
2583
|
}
|
|
2565
2584
|
if (!replayed)
|
|
2566
2585
|
await this.settleCommitted(tenantId, scopeId, liveSince, this.invocationId);
|
|
@@ -4359,11 +4378,12 @@ options = {}) {
|
|
|
4359
4378
|
return await this.queue.enqueue(async () => {
|
|
4360
4379
|
const liveSince = this.liveHighWaterMark();
|
|
4361
4380
|
const now = instant.parse(new Date().toISOString());
|
|
4362
|
-
let answer = { ok: true, unbound: [] };
|
|
4381
|
+
let answer = { ok: true, unbound: [], revokedLinks: [] };
|
|
4363
4382
|
await this.revision.transaction(async () => {
|
|
4364
4383
|
answer = await unbindEntityHolderBounded({
|
|
4365
4384
|
sql: doSpineSql(this.sql),
|
|
4366
4385
|
now,
|
|
4386
|
+
caller: by,
|
|
4367
4387
|
allowed: allowedFor(this.checker, by, { tenantId, scopeId }),
|
|
4368
4388
|
emit: (event) => kernelEmit(this.operationContext(by, tenantId, scopeId, undefined, undefined, undefined, undefined, undefined, ENTITY_UNBIND_OPERATION, undefined, [], now), event),
|
|
4369
4389
|
}, entity);
|
|
@@ -5483,6 +5503,7 @@ options = {}) {
|
|
|
5483
5503
|
* with the scope's identity after the K-3 check (host.ts admin.exportScope).
|
|
5484
5504
|
*/
|
|
5485
5505
|
exportDump() {
|
|
5506
|
+
const objects = this.dumpObjects();
|
|
5486
5507
|
return this.dumpTableDefs().map(({ name, sql }) => {
|
|
5487
5508
|
// Raw positional rows, cells as-is (blobs kept as bytes, not nulled like a UI
|
|
5488
5509
|
// read) so the dump reloads faithfully. The name is from the live schema.
|
|
@@ -5490,9 +5511,13 @@ options = {}) {
|
|
|
5490
5511
|
const cursor = this.sql.exec(`SELECT * FROM "${name}" WHERE ${where}`, ...bindings);
|
|
5491
5512
|
const columns = cursor.columnNames;
|
|
5492
5513
|
const rows = Array.from(cursor.raw(), (row) => row);
|
|
5493
|
-
return { name, ddl: sql, columns, rows };
|
|
5514
|
+
return { name, ddl: sql, columns, rows, ...objects(name) };
|
|
5494
5515
|
});
|
|
5495
5516
|
}
|
|
5517
|
+
/** #2820: each module table's own indexes and triggers, as a dump carries them beside the table. */
|
|
5518
|
+
dumpObjects() {
|
|
5519
|
+
return dumpObjectsOf(this.sql.exec(DUMP_OBJECTS_QUERY).toArray());
|
|
5520
|
+
}
|
|
5496
5521
|
/**
|
|
5497
5522
|
* The tables a dump carries, by name. The derived search index (#827) is excluded: its shadow
|
|
5498
5523
|
* tables cannot be replayed — this host answers "object name reserved for internal use" — and
|
|
@@ -5532,6 +5557,7 @@ options = {}) {
|
|
|
5532
5557
|
this.sql.exec(`INSERT INTO _substrat_meta (key, value) VALUES (?, ?)`, LOAD_STAMP_KEY, loadStamp);
|
|
5533
5558
|
}
|
|
5534
5559
|
const defs = this.dumpTableDefs();
|
|
5560
|
+
const objects = this.dumpObjects();
|
|
5535
5561
|
if (capture !== undefined) {
|
|
5536
5562
|
if (cursor === null)
|
|
5537
5563
|
this.openCapture(capture, defs);
|
|
@@ -5579,7 +5605,7 @@ options = {}) {
|
|
|
5579
5605
|
if (full || read.length < DUMP_READ_BATCH)
|
|
5580
5606
|
break;
|
|
5581
5607
|
}
|
|
5582
|
-
tables.push({ name, ddl: sql, columns, rows, ...(capture !== undefined && byRowid ? { rowids } : {}) });
|
|
5608
|
+
tables.push({ name, ddl: sql, columns, rows, ...objects(name), ...(capture !== undefined && byRowid ? { rowids } : {}) });
|
|
5583
5609
|
if (full) {
|
|
5584
5610
|
next = encodeDumpCursor({ table: name, after: last ?? 0 });
|
|
5585
5611
|
break;
|
|
@@ -5672,11 +5698,21 @@ options = {}) {
|
|
|
5672
5698
|
return key.map((c) => ({ column: c.name, collation: c.coll }));
|
|
5673
5699
|
});
|
|
5674
5700
|
}
|
|
5675
|
-
/**
|
|
5701
|
+
/**
|
|
5702
|
+
* What a capture's pages read the schema as (#2450): the dumped tables, their explicit indexes and
|
|
5703
|
+
* (#2820) their triggers, which the pages carry as the tables' own objects. The capture's own
|
|
5704
|
+
* triggers are not part of it: they come and go with captures.
|
|
5705
|
+
*/
|
|
5676
5706
|
captureSchema(defs) {
|
|
5677
5707
|
const names = new Set(defs.map((d) => d.name));
|
|
5678
|
-
const
|
|
5679
|
-
|
|
5708
|
+
const objects = this.sql
|
|
5709
|
+
.exec(`SELECT type, tbl_name, name, sql FROM sqlite_master
|
|
5710
|
+
WHERE (type = 'index' OR (type = 'trigger' AND name NOT GLOB '${CAPTURE_GLOB}')) AND sql IS NOT NULL ORDER BY name`)
|
|
5711
|
+
.toArray().filter((o) => names.has(o.tbl_name));
|
|
5712
|
+
const of = (type) => objects.filter((o) => o.type === type).map((o) => [o.name, o.sql]);
|
|
5713
|
+
// Named only when there are some, so a capture opened before triggers were part of it still reads alike.
|
|
5714
|
+
const triggers = of('trigger');
|
|
5715
|
+
return JSON.stringify({ tables: defs, indexes: of('index'), ...(triggers.length ? { triggers } : {}) });
|
|
5680
5716
|
}
|
|
5681
5717
|
/**
|
|
5682
5718
|
* The capture `copyId`, renewed, as its pages read the schema (#2450). Refused when it is gone.
|
|
@@ -6071,7 +6107,7 @@ options = {}) {
|
|
|
6071
6107
|
});
|
|
6072
6108
|
}
|
|
6073
6109
|
async importDump(tables, destScopeId, opts = {}) {
|
|
6074
|
-
return this.loadTables(tables.map((t) => ({ name: t.name, ddl: t.ddl, columns: t.columns, rows: () => t.rows })), destScopeId, opts);
|
|
6110
|
+
return this.loadTables(tables.map((t) => ({ name: t.name, ddl: t.ddl, columns: t.columns, objects: t.objects, rows: () => t.rows })), destScopeId, opts);
|
|
6075
6111
|
}
|
|
6076
6112
|
/**
|
|
6077
6113
|
* The load `importDump` runs, over tables whose rows are read lazily (#2450): from the dump's
|
|
@@ -6079,7 +6115,7 @@ options = {}) {
|
|
|
6079
6115
|
* in memory at once. `stageId` names the staged load the rows come from: checked inside the
|
|
6080
6116
|
* load's transaction, and its tables dropped in it once the rows are in.
|
|
6081
6117
|
*/
|
|
6082
|
-
async loadTables(tables, destScopeId, { switchOff, sourceScopeId, exact, loadStamp, expect, resolveKept, markCopy, provisionedFor, fence, roleRepair, onRolesProjected, projection, keepPermissionFlags, } = {}, stageId) {
|
|
6118
|
+
async loadTables(tables, destScopeId, { switchOff, sourceScopeId, exact, loadStamp, expect, resolveKept, markCopy, provisionedFor, fence, roleRepair, onRolesProjected, projection, keepPermissionFlags, onObjects, } = {}, stageId) {
|
|
6083
6119
|
// The WHOLE drop-then-replay runs under deferred foreign keys, in one transaction.
|
|
6084
6120
|
//
|
|
6085
6121
|
// Two distinct FK hazards, and both need the deferral — the second is why it cannot
|
|
@@ -6113,7 +6149,8 @@ options = {}) {
|
|
|
6113
6149
|
// amount of name checking reaches it. There is no prepare step here to compile
|
|
6114
6150
|
// only the first statement, so the text itself has to be the one statement.
|
|
6115
6151
|
// Pure input validation, so it runs before the first DROP: a refused dump touches nothing.
|
|
6116
|
-
assertReplayableDump(replayable, { maxColumns: DO_SQL_LIMITS.columns });
|
|
6152
|
+
const objects = assertReplayableDump(replayable, { maxColumns: DO_SQL_LIMITS.columns });
|
|
6153
|
+
onObjects?.(objects);
|
|
6117
6154
|
// #1869: `exact` vouches for a named source; without one it would silently fall back.
|
|
6118
6155
|
if (exact && !sourceScopeId) {
|
|
6119
6156
|
throw substratError('validation_failed', 'restore refused: `exact` needs the scope the dump came from');
|
|
@@ -6301,6 +6338,10 @@ options = {}) {
|
|
|
6301
6338
|
// derived (#2090). Inside the replay's transaction: a dump whose journal owes a table a
|
|
6302
6339
|
// state column it does not carry rolls the whole load back. A plan whose table this dump
|
|
6303
6340
|
// did not carry is skipped: a restore must not invent one.
|
|
6341
|
+
// #2820: a module's own indexes and triggers went with the dropped tables, and its journal
|
|
6342
|
+
// says the migrations that made them ran. Put back from the dump, after the rows they judge
|
|
6343
|
+
// and before the kernel derives its own.
|
|
6344
|
+
replayDumpObjects(objects, (ddl) => this.sql.exec(blankSqlComments(ddl)));
|
|
6304
6345
|
repairDerivedObjects(doSpineSql(this.sql), (ddl) => this.runScript(ddl), this.derivedPlans(), {
|
|
6305
6346
|
after: 'the restored dump',
|
|
6306
6347
|
absentTable: 'skip',
|
|
@@ -6400,7 +6441,7 @@ options = {}) {
|
|
|
6400
6441
|
* (`changed`, 412) and "this store is a kept copy" (`kept`, 409) from a failure.
|
|
6401
6442
|
*/
|
|
6402
6443
|
async importDumpChecked(tables, destScopeId, opts) {
|
|
6403
|
-
return this.checkedLoad(() => this.importDump(tables, destScopeId, opts));
|
|
6444
|
+
return this.checkedLoad((onObjects) => this.importDump(tables, destScopeId, { ...opts, onObjects }));
|
|
6404
6445
|
}
|
|
6405
6446
|
/**
|
|
6406
6447
|
* #2531: `importDumpChecked` with the role repair judged and projected in the load's own
|
|
@@ -6410,7 +6451,7 @@ options = {}) {
|
|
|
6410
6451
|
* rather than loading with the repair silently dropped.
|
|
6411
6452
|
*/
|
|
6412
6453
|
async importDumpJudged(tables, destScopeId, opts, roleRepair) {
|
|
6413
|
-
return this.judgedLoad((onRolesProjected) => this.checkedLoad(() => this.importDump(tables, destScopeId, { ...opts, roleRepair, onRolesProjected })));
|
|
6454
|
+
return this.judgedLoad((onRolesProjected) => this.checkedLoad((onObjects) => this.importDump(tables, destScopeId, { ...opts, roleRepair, onRolesProjected, onObjects })));
|
|
6414
6455
|
}
|
|
6415
6456
|
/**
|
|
6416
6457
|
* #2555: a control-plane-full restore or import. `importDump`, with this store's permission flags
|
|
@@ -6424,15 +6465,16 @@ options = {}) {
|
|
|
6424
6465
|
importDumpProjected(tables, destScopeId, opts, projection) {
|
|
6425
6466
|
return replyOf(async () => {
|
|
6426
6467
|
try {
|
|
6427
|
-
|
|
6428
|
-
|
|
6429
|
-
|
|
6430
|
-
|
|
6431
|
-
|
|
6432
|
-
|
|
6433
|
-
|
|
6434
|
-
|
|
6435
|
-
};
|
|
6468
|
+
let plan;
|
|
6469
|
+
const switchedOff = await this.importDump(tables, destScopeId, {
|
|
6470
|
+
switchOff: opts.switchOff,
|
|
6471
|
+
sourceScopeId: opts.sourceScopeId,
|
|
6472
|
+
exact: opts.exact,
|
|
6473
|
+
keepPermissionFlags: true,
|
|
6474
|
+
...(projection ? { projection } : {}),
|
|
6475
|
+
onObjects: (judged) => (plan = judged),
|
|
6476
|
+
});
|
|
6477
|
+
return { switchedOff, ...(plan ? { moduleObjects: dumpObjectsAudit(plan) } : {}) };
|
|
6436
6478
|
}
|
|
6437
6479
|
catch (e) {
|
|
6438
6480
|
if (e instanceof Error && roleRefusals.has(e))
|
|
@@ -6503,19 +6545,31 @@ options = {}) {
|
|
|
6503
6545
|
if (seq > head.nextSeq) {
|
|
6504
6546
|
throw substratError('precondition_failed', `stage page ${seq} arrived before page ${head.nextSeq}`);
|
|
6505
6547
|
}
|
|
6548
|
+
// A stage opened before objects were staged takes their table now: its earlier pages
|
|
6549
|
+
// staged none, so a later page that carries them reads as the table having changed.
|
|
6550
|
+
this.sql.exec(STAGE_OBJECTS_DDL);
|
|
6506
6551
|
for (const t of tables) {
|
|
6507
6552
|
const columns = JSON.stringify(t.columns);
|
|
6553
|
+
const objects = t.objects === undefined ? null : JSON.stringify(t.objects);
|
|
6508
6554
|
let held = this.sql
|
|
6509
|
-
.exec(`SELECT ord, ddl, columns FROM _substrat_stage_tables
|
|
6555
|
+
.exec(`SELECT s.ord, s.ddl, s.columns, o.objects FROM _substrat_stage_tables s
|
|
6556
|
+
LEFT JOIN _substrat_stage_objects o ON o.name = s.name WHERE s.name = ?`, t.name)
|
|
6510
6557
|
.toArray()[0];
|
|
6511
6558
|
if (!held) {
|
|
6559
|
+
// #2820: an export carries objects for every module table or for none. A page that
|
|
6560
|
+
// disagrees with the tables already staged was read across a change of version.
|
|
6561
|
+
if (!isSpineTable(t.name) && this.stagedModuleObjects().some((has) => has !== (objects !== null))) {
|
|
6562
|
+
throw substratError('precondition_failed', `table ${t.name} changed between dump pages; nothing more was staged`);
|
|
6563
|
+
}
|
|
6512
6564
|
const ord = Number(this.sql.exec(`SELECT COUNT(*) AS n FROM _substrat_stage_tables`).toArray()[0].n);
|
|
6513
6565
|
this.sql.exec(`INSERT INTO _substrat_stage_tables (ord, name, ddl, columns) VALUES (?, ?, ?, ?)`, ord, t.name, t.ddl, columns);
|
|
6566
|
+
if (objects !== null)
|
|
6567
|
+
this.sql.exec(`INSERT INTO _substrat_stage_objects (name, objects) VALUES (?, ?)`, t.name, objects);
|
|
6514
6568
|
// Untyped columns: no affinity, so a cell is stored exactly as it came.
|
|
6515
6569
|
this.sql.exec(`CREATE TABLE "${stageRowsTable(ord)}" (${t.columns.map((_, i) => `c${i}`).join(', ')})`);
|
|
6516
|
-
held = { ord, ddl: t.ddl, columns };
|
|
6570
|
+
held = { ord, ddl: t.ddl, columns, objects };
|
|
6517
6571
|
}
|
|
6518
|
-
else if (held.ddl !== t.ddl || held.columns !== columns) {
|
|
6572
|
+
else if (held.ddl !== t.ddl || held.columns !== columns || held.objects !== objects) {
|
|
6519
6573
|
throw substratError('precondition_failed', `table ${t.name} changed between dump pages; nothing more was staged`);
|
|
6520
6574
|
}
|
|
6521
6575
|
this.stageRows(held.ord, t);
|
|
@@ -6609,21 +6663,27 @@ options = {}) {
|
|
|
6609
6663
|
*/
|
|
6610
6664
|
async commitStagedDumpChecked(stageId, destScopeId, opts) {
|
|
6611
6665
|
let count = 0;
|
|
6612
|
-
const out = await this.checkedLoad(() => {
|
|
6666
|
+
const out = await this.checkedLoad((onObjects) => {
|
|
6613
6667
|
const head = this.stageHead();
|
|
6614
6668
|
if (head?.stageId !== stageId)
|
|
6615
6669
|
throw substratError('precondition_failed', STAGE_GONE);
|
|
6670
|
+
// Read only: a stage opened before objects were staged has no table for them, and loads none.
|
|
6671
|
+
const withObjects = this.hasTable('_substrat_stage_objects');
|
|
6616
6672
|
const staged = this.sql
|
|
6617
|
-
.exec(
|
|
6673
|
+
.exec(withObjects
|
|
6674
|
+
? `SELECT s.ord, s.name, s.ddl, s.columns, o.objects FROM _substrat_stage_tables s
|
|
6675
|
+
LEFT JOIN _substrat_stage_objects o ON o.name = s.name ORDER BY s.name`
|
|
6676
|
+
: `SELECT ord, name, ddl, columns, NULL AS objects FROM _substrat_stage_tables ORDER BY name`)
|
|
6618
6677
|
.toArray();
|
|
6619
6678
|
const tables = staged.map((t) => ({
|
|
6620
6679
|
name: t.name,
|
|
6621
6680
|
ddl: t.ddl,
|
|
6622
6681
|
columns: JSON.parse(t.columns),
|
|
6682
|
+
...(t.objects === null ? {} : { objects: JSON.parse(t.objects) }),
|
|
6623
6683
|
rows: () => this.stagedRows(t.ord),
|
|
6624
6684
|
}));
|
|
6625
6685
|
count = tables.length;
|
|
6626
|
-
return this.loadTables(tables, destScopeId, opts, stageId);
|
|
6686
|
+
return this.loadTables(tables, destScopeId, { ...opts, onObjects }, stageId);
|
|
6627
6687
|
});
|
|
6628
6688
|
return out.refused === false ? { ...out, tables: count } : out;
|
|
6629
6689
|
}
|
|
@@ -6659,6 +6719,14 @@ options = {}) {
|
|
|
6659
6719
|
after = Number(batch[batch.length - 1][0]);
|
|
6660
6720
|
}
|
|
6661
6721
|
}
|
|
6722
|
+
/** #2820: for each module table staged so far, whether its pages carried its objects. */
|
|
6723
|
+
stagedModuleObjects() {
|
|
6724
|
+
return this.sql
|
|
6725
|
+
.exec(`SELECT s.name, o.name IS NOT NULL AS has FROM _substrat_stage_tables s LEFT JOIN _substrat_stage_objects o ON o.name = s.name`)
|
|
6726
|
+
.toArray()
|
|
6727
|
+
.filter((r) => !isSpineTable(r.name))
|
|
6728
|
+
.map((r) => Boolean(r.has));
|
|
6729
|
+
}
|
|
6662
6730
|
/** The staged load this store holds (#2450), or null. */
|
|
6663
6731
|
stageHead() {
|
|
6664
6732
|
const present = this.sql.exec(`SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = '_substrat_stage'`).toArray();
|
|
@@ -6678,7 +6746,9 @@ options = {}) {
|
|
|
6678
6746
|
/** A load's refusals answered as values (#1722): across the RPC a throw carries only its message. */
|
|
6679
6747
|
async checkedLoad(run) {
|
|
6680
6748
|
try {
|
|
6681
|
-
|
|
6749
|
+
let plan;
|
|
6750
|
+
const switchedOff = await run((judged) => (plan = judged));
|
|
6751
|
+
return { refused: false, switchedOff, ...(plan ? { moduleObjects: dumpObjectsAudit(plan) } : {}) };
|
|
6682
6752
|
}
|
|
6683
6753
|
catch (e) {
|
|
6684
6754
|
if (e instanceof Error && fenceRefusals.has(e))
|
|
@@ -7531,6 +7601,7 @@ options = {}) {
|
|
|
7531
7601
|
sql.exec(r.sql, ...r.params);
|
|
7532
7602
|
},
|
|
7533
7603
|
atomic: createAtomic(runSub, { passed, signals }),
|
|
7604
|
+
noteTransition: noteTransitionInto(signals),
|
|
7534
7605
|
// #1672: mint / revoke / list, written once in the kernel — the pure adapter hands
|
|
7535
7606
|
// the same function the same four things. The raw spine seam (the kernel's own write
|
|
7536
7607
|
// to `_substrat_capabilities`), the operation's OWN check, and `ctx.emit`. A
|