@substrat-run/adapter-cloudflare 0.175.0 → 0.177.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/control-plane-do.d.ts +7 -0
- package/dist/control-plane-do.d.ts.map +1 -1
- package/dist/control-plane-do.js +16 -1
- package/dist/control-plane-do.js.map +1 -1
- package/dist/host.d.ts +28 -2
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +84 -24
- package/dist/host.js.map +1 -1
- package/dist/scope-do.d.ts.map +1 -1
- package/dist/scope-do.js +121 -33
- 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, 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';
|
|
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, OUTBOX_PRUNED_DDL, outboxSubscribersOf, pruneDrainedOutbox, denialListQuery, refusalListQuery, mapRefusalRow, denialSummaryQuery, denialTotalsQuery, DENIAL_WINDOW_QUERY, mapDenialRow, mapDenialSummaryBuckets, PermissionDenied, assertImpersonationAdmitted, assertImpersonationWrites, assertModuleEnqueueableKind, impersonationStampOf, createAtomic, NotSearchable, isSearchIndexTable, searchIndexPlans, NotListable, listIndexPlans, moduleMigrations, REGISTERED_MIGRATIONS, registeredMigrationsOf, 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';
|
|
@@ -387,6 +387,7 @@ const KERNEL_DDL = `
|
|
|
387
387
|
-- #1686: where a copied scope's data came from, and the event id its own events start above.
|
|
388
388
|
-- Shared with the other adapter from @substrat-run/kernel; the column comments are there.
|
|
389
389
|
${COPY_ORIGIN_DDL}
|
|
390
|
+
${OUTBOX_PRUNED_DDL}
|
|
390
391
|
-- #119: the kernel's authorization for one archive/trash move, read by the derived update
|
|
391
392
|
-- trigger. Shared with the other adapter from @substrat-run/kernel; the comments are there.
|
|
392
393
|
${ENTITY_STATE_MOVES_DDL}
|
|
@@ -442,16 +443,22 @@ const STAGE_IN_PROGRESS = 'a different staged load is in progress; retry after i
|
|
|
442
443
|
const STAGE_MAX_AGE_MS = 6 * 3_600_000 + 3 * SCOPE_COPY_LEASE_MS;
|
|
443
444
|
/**
|
|
444
445
|
* 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.
|
|
446
|
+
* `_substrat_stage_tables` (each staged table's name, DDL and columns), `_substrat_stage_objects`
|
|
447
|
+
* (below) and one `_substrat_stage_rows_<n>` per staged table. They hold a carry's pages until the
|
|
448
|
+
* load commits from them, so they are this store's, never its data: no dump carries them, and a
|
|
449
|
+
* load's drops leave them to the commit that reads them.
|
|
449
450
|
*/
|
|
450
451
|
const STAGE_GLOB = '_substrat_stage*';
|
|
451
452
|
const STAGE_TABLES_DDL = [
|
|
452
453
|
`CREATE TABLE _substrat_stage (stage_id TEXT NOT NULL, next_seq INTEGER NOT NULL)`,
|
|
453
454
|
`CREATE TABLE _substrat_stage_tables (ord INTEGER PRIMARY KEY, name TEXT NOT NULL UNIQUE, ddl TEXT NOT NULL, columns TEXT NOT NULL)`,
|
|
454
455
|
];
|
|
456
|
+
/**
|
|
457
|
+
* #2820: each staged table's own indexes and triggers, for a table whose pages carried them. Created
|
|
458
|
+
* by every page if missing, so a stage opened before it existed still takes pages, and commits with
|
|
459
|
+
* none: as a dump written before objects were carried.
|
|
460
|
+
*/
|
|
461
|
+
const STAGE_OBJECTS_DDL = `CREATE TABLE IF NOT EXISTS _substrat_stage_objects (name TEXT PRIMARY KEY, objects TEXT NOT NULL)`;
|
|
455
462
|
const stageRowsTable = (ord) => `_substrat_stage_rows_${ord}`;
|
|
456
463
|
/** How many staged rows a commit reads at a time. */
|
|
457
464
|
const STAGE_READ_BATCH = 500;
|
|
@@ -892,6 +899,14 @@ options = {}) {
|
|
|
892
899
|
noteDeclaredKeys(declaredListed, m.manifest);
|
|
893
900
|
}
|
|
894
901
|
return class ScopeDO extends DurableObject {
|
|
902
|
+
/**
|
|
903
|
+
* #2351: the migrations this class registers, read by the control plane, which evaluates the
|
|
904
|
+
* bundle in an isolate with no bindings and calls this before it stores the version. The same
|
|
905
|
+
* list `registerModule` keeps for each module (`moduleMigrations`).
|
|
906
|
+
*/
|
|
907
|
+
static [REGISTERED_MIGRATIONS]() {
|
|
908
|
+
return registeredMigrationsOf(modules);
|
|
909
|
+
}
|
|
895
910
|
sql;
|
|
896
911
|
continuationOrder = 0;
|
|
897
912
|
/** DO storage KV is private to the adapter; neither module SQL nor dumps can read it. */
|
|
@@ -944,6 +959,8 @@ options = {}) {
|
|
|
944
959
|
purgeSchedules = new Map();
|
|
945
960
|
/** #116: the operations that declared `idempotency: false` — refusals, not participants. */
|
|
946
961
|
operationIdempotencyOptOut = new Set();
|
|
962
|
+
/** #2821: the operations that declared `impersonation: 'refused'` — no session may invoke them. */
|
|
963
|
+
operationImpersonationRefused = new Set();
|
|
947
964
|
/** #2387: the operations held to a `ctx.check` — the pure adapter's set, from the same kernel read. */
|
|
948
965
|
operationCheckHeld = new Set();
|
|
949
966
|
modules = new Map();
|
|
@@ -1222,6 +1239,7 @@ options = {}) {
|
|
|
1222
1239
|
// #116: same rule again for an opt-out — one on an unbound name reads as a
|
|
1223
1240
|
// deliberate exclusion of an operation that is not there.
|
|
1224
1241
|
const declaredOptOuts = registration.operations?.idempotencyOptOuts ?? [];
|
|
1242
|
+
const impersonationRefused = registration.operations?.impersonationRefused ?? [];
|
|
1225
1243
|
const unboundOptOuts = declaredOptOuts.filter((name) => !ownOps.has(name));
|
|
1226
1244
|
if (unboundOptOuts.length > 0) {
|
|
1227
1245
|
throw new Error(`${manifest.id} declares idempotency: false for unbound operation(s): ` +
|
|
@@ -1244,6 +1262,9 @@ options = {}) {
|
|
|
1244
1262
|
if (declaredOptOuts.includes(name) && this.operations.has(name)) {
|
|
1245
1263
|
this.operationIdempotencyOptOut.add(name);
|
|
1246
1264
|
}
|
|
1265
|
+
if (impersonationRefused.includes(name) && this.operations.has(name)) {
|
|
1266
|
+
this.operationImpersonationRefused.add(name);
|
|
1267
|
+
}
|
|
1247
1268
|
const target = trashTargets.get(name);
|
|
1248
1269
|
if (target && this.operations.has(name))
|
|
1249
1270
|
this.operationTarget.set(name, target);
|
|
@@ -1807,6 +1828,20 @@ options = {}) {
|
|
|
1807
1828
|
return drained;
|
|
1808
1829
|
});
|
|
1809
1830
|
}
|
|
1831
|
+
/**
|
|
1832
|
+
* The retention prune (#1334): the kernel's `pruneDrainedOutbox`, on the queue and in one
|
|
1833
|
+
* transaction, so the rows, their deliveries and the pruned-through floor go together.
|
|
1834
|
+
*/
|
|
1835
|
+
async pruneDrainedEvents(drainedBefore, limit, executors, due = true) {
|
|
1836
|
+
return this.queue.enqueue(() => this.revision.transactionSync(() => pruneDrainedOutbox(this.switchSql(), {
|
|
1837
|
+
drainedBefore,
|
|
1838
|
+
limit,
|
|
1839
|
+
due,
|
|
1840
|
+
// Executors run on the coordinator, which names them; the consumers are this DO's own.
|
|
1841
|
+
subscribers: [...outboxSubscribersOf(this.modules.values(), []), ...executors],
|
|
1842
|
+
exportTypes: this.crossVertical.exportTypes(),
|
|
1843
|
+
})));
|
|
1844
|
+
}
|
|
1810
1845
|
/**
|
|
1811
1846
|
* Reopen rows stamped strictly before `drainedBefore`, so the drain ships them again
|
|
1812
1847
|
* (#1334). The kernel contract says why the instant is required; this is the mechanism.
|
|
@@ -2266,6 +2301,8 @@ options = {}) {
|
|
|
2266
2301
|
// else. Naming the code once here is what lets those patterns go.
|
|
2267
2302
|
if (!handler)
|
|
2268
2303
|
throw substratError('not_found', `unknown operation: ${operation}`);
|
|
2304
|
+
// #2821: before the parse, the guards and the handler — and under read-only too.
|
|
2305
|
+
assertImpersonationAdmitted(impersonation, operation, this.operationImpersonationRefused);
|
|
2269
2306
|
// #304 entitlement gate, scope-local path: fail closed on the projected view exactly as
|
|
2270
2307
|
// the coordinator fails closed against the CP. Only active once entitlements have been
|
|
2271
2308
|
// projected (the `entitlements_enforced` marker) — before that the scope trusts upstream,
|
|
@@ -4364,11 +4401,12 @@ options = {}) {
|
|
|
4364
4401
|
return await this.queue.enqueue(async () => {
|
|
4365
4402
|
const liveSince = this.liveHighWaterMark();
|
|
4366
4403
|
const now = instant.parse(new Date().toISOString());
|
|
4367
|
-
let answer = { ok: true, unbound: [] };
|
|
4404
|
+
let answer = { ok: true, unbound: [], revokedLinks: [] };
|
|
4368
4405
|
await this.revision.transaction(async () => {
|
|
4369
4406
|
answer = await unbindEntityHolderBounded({
|
|
4370
4407
|
sql: doSpineSql(this.sql),
|
|
4371
4408
|
now,
|
|
4409
|
+
caller: by,
|
|
4372
4410
|
allowed: allowedFor(this.checker, by, { tenantId, scopeId }),
|
|
4373
4411
|
emit: (event) => kernelEmit(this.operationContext(by, tenantId, scopeId, undefined, undefined, undefined, undefined, undefined, ENTITY_UNBIND_OPERATION, undefined, [], now), event),
|
|
4374
4412
|
}, entity);
|
|
@@ -5488,6 +5526,7 @@ options = {}) {
|
|
|
5488
5526
|
* with the scope's identity after the K-3 check (host.ts admin.exportScope).
|
|
5489
5527
|
*/
|
|
5490
5528
|
exportDump() {
|
|
5529
|
+
const objects = this.dumpObjects();
|
|
5491
5530
|
return this.dumpTableDefs().map(({ name, sql }) => {
|
|
5492
5531
|
// Raw positional rows, cells as-is (blobs kept as bytes, not nulled like a UI
|
|
5493
5532
|
// read) so the dump reloads faithfully. The name is from the live schema.
|
|
@@ -5495,9 +5534,13 @@ options = {}) {
|
|
|
5495
5534
|
const cursor = this.sql.exec(`SELECT * FROM "${name}" WHERE ${where}`, ...bindings);
|
|
5496
5535
|
const columns = cursor.columnNames;
|
|
5497
5536
|
const rows = Array.from(cursor.raw(), (row) => row);
|
|
5498
|
-
return { name, ddl: sql, columns, rows };
|
|
5537
|
+
return { name, ddl: sql, columns, rows, ...objects(name) };
|
|
5499
5538
|
});
|
|
5500
5539
|
}
|
|
5540
|
+
/** #2820: each module table's own indexes and triggers, as a dump carries them beside the table. */
|
|
5541
|
+
dumpObjects() {
|
|
5542
|
+
return dumpObjectsOf(this.sql.exec(DUMP_OBJECTS_QUERY).toArray());
|
|
5543
|
+
}
|
|
5501
5544
|
/**
|
|
5502
5545
|
* The tables a dump carries, by name. The derived search index (#827) is excluded: its shadow
|
|
5503
5546
|
* tables cannot be replayed — this host answers "object name reserved for internal use" — and
|
|
@@ -5537,6 +5580,7 @@ options = {}) {
|
|
|
5537
5580
|
this.sql.exec(`INSERT INTO _substrat_meta (key, value) VALUES (?, ?)`, LOAD_STAMP_KEY, loadStamp);
|
|
5538
5581
|
}
|
|
5539
5582
|
const defs = this.dumpTableDefs();
|
|
5583
|
+
const objects = this.dumpObjects();
|
|
5540
5584
|
if (capture !== undefined) {
|
|
5541
5585
|
if (cursor === null)
|
|
5542
5586
|
this.openCapture(capture, defs);
|
|
@@ -5584,7 +5628,7 @@ options = {}) {
|
|
|
5584
5628
|
if (full || read.length < DUMP_READ_BATCH)
|
|
5585
5629
|
break;
|
|
5586
5630
|
}
|
|
5587
|
-
tables.push({ name, ddl: sql, columns, rows, ...(capture !== undefined && byRowid ? { rowids } : {}) });
|
|
5631
|
+
tables.push({ name, ddl: sql, columns, rows, ...objects(name), ...(capture !== undefined && byRowid ? { rowids } : {}) });
|
|
5588
5632
|
if (full) {
|
|
5589
5633
|
next = encodeDumpCursor({ table: name, after: last ?? 0 });
|
|
5590
5634
|
break;
|
|
@@ -5677,11 +5721,21 @@ options = {}) {
|
|
|
5677
5721
|
return key.map((c) => ({ column: c.name, collation: c.coll }));
|
|
5678
5722
|
});
|
|
5679
5723
|
}
|
|
5680
|
-
/**
|
|
5724
|
+
/**
|
|
5725
|
+
* What a capture's pages read the schema as (#2450): the dumped tables, their explicit indexes and
|
|
5726
|
+
* (#2820) their triggers, which the pages carry as the tables' own objects. The capture's own
|
|
5727
|
+
* triggers are not part of it: they come and go with captures.
|
|
5728
|
+
*/
|
|
5681
5729
|
captureSchema(defs) {
|
|
5682
5730
|
const names = new Set(defs.map((d) => d.name));
|
|
5683
|
-
const
|
|
5684
|
-
|
|
5731
|
+
const objects = this.sql
|
|
5732
|
+
.exec(`SELECT type, tbl_name, name, sql FROM sqlite_master
|
|
5733
|
+
WHERE (type = 'index' OR (type = 'trigger' AND name NOT GLOB '${CAPTURE_GLOB}')) AND sql IS NOT NULL ORDER BY name`)
|
|
5734
|
+
.toArray().filter((o) => names.has(o.tbl_name));
|
|
5735
|
+
const of = (type) => objects.filter((o) => o.type === type).map((o) => [o.name, o.sql]);
|
|
5736
|
+
// Named only when there are some, so a capture opened before triggers were part of it still reads alike.
|
|
5737
|
+
const triggers = of('trigger');
|
|
5738
|
+
return JSON.stringify({ tables: defs, indexes: of('index'), ...(triggers.length ? { triggers } : {}) });
|
|
5685
5739
|
}
|
|
5686
5740
|
/**
|
|
5687
5741
|
* The capture `copyId`, renewed, as its pages read the schema (#2450). Refused when it is gone.
|
|
@@ -6076,7 +6130,7 @@ options = {}) {
|
|
|
6076
6130
|
});
|
|
6077
6131
|
}
|
|
6078
6132
|
async importDump(tables, destScopeId, opts = {}) {
|
|
6079
|
-
return this.loadTables(tables.map((t) => ({ name: t.name, ddl: t.ddl, columns: t.columns, rows: () => t.rows })), destScopeId, opts);
|
|
6133
|
+
return this.loadTables(tables.map((t) => ({ name: t.name, ddl: t.ddl, columns: t.columns, objects: t.objects, rows: () => t.rows })), destScopeId, opts);
|
|
6080
6134
|
}
|
|
6081
6135
|
/**
|
|
6082
6136
|
* The load `importDump` runs, over tables whose rows are read lazily (#2450): from the dump's
|
|
@@ -6084,7 +6138,7 @@ options = {}) {
|
|
|
6084
6138
|
* in memory at once. `stageId` names the staged load the rows come from: checked inside the
|
|
6085
6139
|
* load's transaction, and its tables dropped in it once the rows are in.
|
|
6086
6140
|
*/
|
|
6087
|
-
async loadTables(tables, destScopeId, { switchOff, sourceScopeId, exact, loadStamp, expect, resolveKept, markCopy, provisionedFor, fence, roleRepair, onRolesProjected, projection, keepPermissionFlags, } = {}, stageId) {
|
|
6141
|
+
async loadTables(tables, destScopeId, { switchOff, sourceScopeId, exact, loadStamp, expect, resolveKept, markCopy, provisionedFor, fence, roleRepair, onRolesProjected, projection, keepPermissionFlags, onObjects, } = {}, stageId) {
|
|
6088
6142
|
// The WHOLE drop-then-replay runs under deferred foreign keys, in one transaction.
|
|
6089
6143
|
//
|
|
6090
6144
|
// Two distinct FK hazards, and both need the deferral — the second is why it cannot
|
|
@@ -6118,7 +6172,8 @@ options = {}) {
|
|
|
6118
6172
|
// amount of name checking reaches it. There is no prepare step here to compile
|
|
6119
6173
|
// only the first statement, so the text itself has to be the one statement.
|
|
6120
6174
|
// Pure input validation, so it runs before the first DROP: a refused dump touches nothing.
|
|
6121
|
-
assertReplayableDump(replayable, { maxColumns: DO_SQL_LIMITS.columns });
|
|
6175
|
+
const objects = assertReplayableDump(replayable, { maxColumns: DO_SQL_LIMITS.columns });
|
|
6176
|
+
onObjects?.(objects);
|
|
6122
6177
|
// #1869: `exact` vouches for a named source; without one it would silently fall back.
|
|
6123
6178
|
if (exact && !sourceScopeId) {
|
|
6124
6179
|
throw substratError('validation_failed', 'restore refused: `exact` needs the scope the dump came from');
|
|
@@ -6306,6 +6361,10 @@ options = {}) {
|
|
|
6306
6361
|
// derived (#2090). Inside the replay's transaction: a dump whose journal owes a table a
|
|
6307
6362
|
// state column it does not carry rolls the whole load back. A plan whose table this dump
|
|
6308
6363
|
// did not carry is skipped: a restore must not invent one.
|
|
6364
|
+
// #2820: a module's own indexes and triggers went with the dropped tables, and its journal
|
|
6365
|
+
// says the migrations that made them ran. Put back from the dump, after the rows they judge
|
|
6366
|
+
// and before the kernel derives its own.
|
|
6367
|
+
replayDumpObjects(objects, (ddl) => this.sql.exec(blankSqlComments(ddl)));
|
|
6309
6368
|
repairDerivedObjects(doSpineSql(this.sql), (ddl) => this.runScript(ddl), this.derivedPlans(), {
|
|
6310
6369
|
after: 'the restored dump',
|
|
6311
6370
|
absentTable: 'skip',
|
|
@@ -6405,7 +6464,7 @@ options = {}) {
|
|
|
6405
6464
|
* (`changed`, 412) and "this store is a kept copy" (`kept`, 409) from a failure.
|
|
6406
6465
|
*/
|
|
6407
6466
|
async importDumpChecked(tables, destScopeId, opts) {
|
|
6408
|
-
return this.checkedLoad(() => this.importDump(tables, destScopeId, opts));
|
|
6467
|
+
return this.checkedLoad((onObjects) => this.importDump(tables, destScopeId, { ...opts, onObjects }));
|
|
6409
6468
|
}
|
|
6410
6469
|
/**
|
|
6411
6470
|
* #2531: `importDumpChecked` with the role repair judged and projected in the load's own
|
|
@@ -6415,7 +6474,7 @@ options = {}) {
|
|
|
6415
6474
|
* rather than loading with the repair silently dropped.
|
|
6416
6475
|
*/
|
|
6417
6476
|
async importDumpJudged(tables, destScopeId, opts, roleRepair) {
|
|
6418
|
-
return this.judgedLoad((onRolesProjected) => this.checkedLoad(() => this.importDump(tables, destScopeId, { ...opts, roleRepair, onRolesProjected })));
|
|
6477
|
+
return this.judgedLoad((onRolesProjected) => this.checkedLoad((onObjects) => this.importDump(tables, destScopeId, { ...opts, roleRepair, onRolesProjected, onObjects })));
|
|
6419
6478
|
}
|
|
6420
6479
|
/**
|
|
6421
6480
|
* #2555: a control-plane-full restore or import. `importDump`, with this store's permission flags
|
|
@@ -6429,15 +6488,16 @@ options = {}) {
|
|
|
6429
6488
|
importDumpProjected(tables, destScopeId, opts, projection) {
|
|
6430
6489
|
return replyOf(async () => {
|
|
6431
6490
|
try {
|
|
6432
|
-
|
|
6433
|
-
|
|
6434
|
-
|
|
6435
|
-
|
|
6436
|
-
|
|
6437
|
-
|
|
6438
|
-
|
|
6439
|
-
|
|
6440
|
-
};
|
|
6491
|
+
let plan;
|
|
6492
|
+
const switchedOff = await this.importDump(tables, destScopeId, {
|
|
6493
|
+
switchOff: opts.switchOff,
|
|
6494
|
+
sourceScopeId: opts.sourceScopeId,
|
|
6495
|
+
exact: opts.exact,
|
|
6496
|
+
keepPermissionFlags: true,
|
|
6497
|
+
...(projection ? { projection } : {}),
|
|
6498
|
+
onObjects: (judged) => (plan = judged),
|
|
6499
|
+
});
|
|
6500
|
+
return { switchedOff, ...(plan ? { moduleObjects: dumpObjectsAudit(plan) } : {}) };
|
|
6441
6501
|
}
|
|
6442
6502
|
catch (e) {
|
|
6443
6503
|
if (e instanceof Error && roleRefusals.has(e))
|
|
@@ -6508,19 +6568,31 @@ options = {}) {
|
|
|
6508
6568
|
if (seq > head.nextSeq) {
|
|
6509
6569
|
throw substratError('precondition_failed', `stage page ${seq} arrived before page ${head.nextSeq}`);
|
|
6510
6570
|
}
|
|
6571
|
+
// A stage opened before objects were staged takes their table now: its earlier pages
|
|
6572
|
+
// staged none, so a later page that carries them reads as the table having changed.
|
|
6573
|
+
this.sql.exec(STAGE_OBJECTS_DDL);
|
|
6511
6574
|
for (const t of tables) {
|
|
6512
6575
|
const columns = JSON.stringify(t.columns);
|
|
6576
|
+
const objects = t.objects === undefined ? null : JSON.stringify(t.objects);
|
|
6513
6577
|
let held = this.sql
|
|
6514
|
-
.exec(`SELECT ord, ddl, columns FROM _substrat_stage_tables
|
|
6578
|
+
.exec(`SELECT s.ord, s.ddl, s.columns, o.objects FROM _substrat_stage_tables s
|
|
6579
|
+
LEFT JOIN _substrat_stage_objects o ON o.name = s.name WHERE s.name = ?`, t.name)
|
|
6515
6580
|
.toArray()[0];
|
|
6516
6581
|
if (!held) {
|
|
6582
|
+
// #2820: an export carries objects for every module table or for none. A page that
|
|
6583
|
+
// disagrees with the tables already staged was read across a change of version.
|
|
6584
|
+
if (!isSpineTable(t.name) && this.stagedModuleObjects().some((has) => has !== (objects !== null))) {
|
|
6585
|
+
throw substratError('precondition_failed', `table ${t.name} changed between dump pages; nothing more was staged`);
|
|
6586
|
+
}
|
|
6517
6587
|
const ord = Number(this.sql.exec(`SELECT COUNT(*) AS n FROM _substrat_stage_tables`).toArray()[0].n);
|
|
6518
6588
|
this.sql.exec(`INSERT INTO _substrat_stage_tables (ord, name, ddl, columns) VALUES (?, ?, ?, ?)`, ord, t.name, t.ddl, columns);
|
|
6589
|
+
if (objects !== null)
|
|
6590
|
+
this.sql.exec(`INSERT INTO _substrat_stage_objects (name, objects) VALUES (?, ?)`, t.name, objects);
|
|
6519
6591
|
// Untyped columns: no affinity, so a cell is stored exactly as it came.
|
|
6520
6592
|
this.sql.exec(`CREATE TABLE "${stageRowsTable(ord)}" (${t.columns.map((_, i) => `c${i}`).join(', ')})`);
|
|
6521
|
-
held = { ord, ddl: t.ddl, columns };
|
|
6593
|
+
held = { ord, ddl: t.ddl, columns, objects };
|
|
6522
6594
|
}
|
|
6523
|
-
else if (held.ddl !== t.ddl || held.columns !== columns) {
|
|
6595
|
+
else if (held.ddl !== t.ddl || held.columns !== columns || held.objects !== objects) {
|
|
6524
6596
|
throw substratError('precondition_failed', `table ${t.name} changed between dump pages; nothing more was staged`);
|
|
6525
6597
|
}
|
|
6526
6598
|
this.stageRows(held.ord, t);
|
|
@@ -6614,21 +6686,27 @@ options = {}) {
|
|
|
6614
6686
|
*/
|
|
6615
6687
|
async commitStagedDumpChecked(stageId, destScopeId, opts) {
|
|
6616
6688
|
let count = 0;
|
|
6617
|
-
const out = await this.checkedLoad(() => {
|
|
6689
|
+
const out = await this.checkedLoad((onObjects) => {
|
|
6618
6690
|
const head = this.stageHead();
|
|
6619
6691
|
if (head?.stageId !== stageId)
|
|
6620
6692
|
throw substratError('precondition_failed', STAGE_GONE);
|
|
6693
|
+
// Read only: a stage opened before objects were staged has no table for them, and loads none.
|
|
6694
|
+
const withObjects = this.hasTable('_substrat_stage_objects');
|
|
6621
6695
|
const staged = this.sql
|
|
6622
|
-
.exec(
|
|
6696
|
+
.exec(withObjects
|
|
6697
|
+
? `SELECT s.ord, s.name, s.ddl, s.columns, o.objects FROM _substrat_stage_tables s
|
|
6698
|
+
LEFT JOIN _substrat_stage_objects o ON o.name = s.name ORDER BY s.name`
|
|
6699
|
+
: `SELECT ord, name, ddl, columns, NULL AS objects FROM _substrat_stage_tables ORDER BY name`)
|
|
6623
6700
|
.toArray();
|
|
6624
6701
|
const tables = staged.map((t) => ({
|
|
6625
6702
|
name: t.name,
|
|
6626
6703
|
ddl: t.ddl,
|
|
6627
6704
|
columns: JSON.parse(t.columns),
|
|
6705
|
+
...(t.objects === null ? {} : { objects: JSON.parse(t.objects) }),
|
|
6628
6706
|
rows: () => this.stagedRows(t.ord),
|
|
6629
6707
|
}));
|
|
6630
6708
|
count = tables.length;
|
|
6631
|
-
return this.loadTables(tables, destScopeId, opts, stageId);
|
|
6709
|
+
return this.loadTables(tables, destScopeId, { ...opts, onObjects }, stageId);
|
|
6632
6710
|
});
|
|
6633
6711
|
return out.refused === false ? { ...out, tables: count } : out;
|
|
6634
6712
|
}
|
|
@@ -6664,6 +6742,14 @@ options = {}) {
|
|
|
6664
6742
|
after = Number(batch[batch.length - 1][0]);
|
|
6665
6743
|
}
|
|
6666
6744
|
}
|
|
6745
|
+
/** #2820: for each module table staged so far, whether its pages carried its objects. */
|
|
6746
|
+
stagedModuleObjects() {
|
|
6747
|
+
return this.sql
|
|
6748
|
+
.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`)
|
|
6749
|
+
.toArray()
|
|
6750
|
+
.filter((r) => !isSpineTable(r.name))
|
|
6751
|
+
.map((r) => Boolean(r.has));
|
|
6752
|
+
}
|
|
6667
6753
|
/** The staged load this store holds (#2450), or null. */
|
|
6668
6754
|
stageHead() {
|
|
6669
6755
|
const present = this.sql.exec(`SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = '_substrat_stage'`).toArray();
|
|
@@ -6683,7 +6769,9 @@ options = {}) {
|
|
|
6683
6769
|
/** A load's refusals answered as values (#1722): across the RPC a throw carries only its message. */
|
|
6684
6770
|
async checkedLoad(run) {
|
|
6685
6771
|
try {
|
|
6686
|
-
|
|
6772
|
+
let plan;
|
|
6773
|
+
const switchedOff = await run((judged) => (plan = judged));
|
|
6774
|
+
return { refused: false, switchedOff, ...(plan ? { moduleObjects: dumpObjectsAudit(plan) } : {}) };
|
|
6687
6775
|
}
|
|
6688
6776
|
catch (e) {
|
|
6689
6777
|
if (e instanceof Error && fenceRefusals.has(e))
|