@substrat-run/adapter-cloudflare 0.175.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 +98 -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, 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,
|
|
@@ -4364,11 +4378,12 @@ options = {}) {
|
|
|
4364
4378
|
return await this.queue.enqueue(async () => {
|
|
4365
4379
|
const liveSince = this.liveHighWaterMark();
|
|
4366
4380
|
const now = instant.parse(new Date().toISOString());
|
|
4367
|
-
let answer = { ok: true, unbound: [] };
|
|
4381
|
+
let answer = { ok: true, unbound: [], revokedLinks: [] };
|
|
4368
4382
|
await this.revision.transaction(async () => {
|
|
4369
4383
|
answer = await unbindEntityHolderBounded({
|
|
4370
4384
|
sql: doSpineSql(this.sql),
|
|
4371
4385
|
now,
|
|
4386
|
+
caller: by,
|
|
4372
4387
|
allowed: allowedFor(this.checker, by, { tenantId, scopeId }),
|
|
4373
4388
|
emit: (event) => kernelEmit(this.operationContext(by, tenantId, scopeId, undefined, undefined, undefined, undefined, undefined, ENTITY_UNBIND_OPERATION, undefined, [], now), event),
|
|
4374
4389
|
}, entity);
|
|
@@ -5488,6 +5503,7 @@ options = {}) {
|
|
|
5488
5503
|
* with the scope's identity after the K-3 check (host.ts admin.exportScope).
|
|
5489
5504
|
*/
|
|
5490
5505
|
exportDump() {
|
|
5506
|
+
const objects = this.dumpObjects();
|
|
5491
5507
|
return this.dumpTableDefs().map(({ name, sql }) => {
|
|
5492
5508
|
// Raw positional rows, cells as-is (blobs kept as bytes, not nulled like a UI
|
|
5493
5509
|
// read) so the dump reloads faithfully. The name is from the live schema.
|
|
@@ -5495,9 +5511,13 @@ options = {}) {
|
|
|
5495
5511
|
const cursor = this.sql.exec(`SELECT * FROM "${name}" WHERE ${where}`, ...bindings);
|
|
5496
5512
|
const columns = cursor.columnNames;
|
|
5497
5513
|
const rows = Array.from(cursor.raw(), (row) => row);
|
|
5498
|
-
return { name, ddl: sql, columns, rows };
|
|
5514
|
+
return { name, ddl: sql, columns, rows, ...objects(name) };
|
|
5499
5515
|
});
|
|
5500
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
|
+
}
|
|
5501
5521
|
/**
|
|
5502
5522
|
* The tables a dump carries, by name. The derived search index (#827) is excluded: its shadow
|
|
5503
5523
|
* tables cannot be replayed — this host answers "object name reserved for internal use" — and
|
|
@@ -5537,6 +5557,7 @@ options = {}) {
|
|
|
5537
5557
|
this.sql.exec(`INSERT INTO _substrat_meta (key, value) VALUES (?, ?)`, LOAD_STAMP_KEY, loadStamp);
|
|
5538
5558
|
}
|
|
5539
5559
|
const defs = this.dumpTableDefs();
|
|
5560
|
+
const objects = this.dumpObjects();
|
|
5540
5561
|
if (capture !== undefined) {
|
|
5541
5562
|
if (cursor === null)
|
|
5542
5563
|
this.openCapture(capture, defs);
|
|
@@ -5584,7 +5605,7 @@ options = {}) {
|
|
|
5584
5605
|
if (full || read.length < DUMP_READ_BATCH)
|
|
5585
5606
|
break;
|
|
5586
5607
|
}
|
|
5587
|
-
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 } : {}) });
|
|
5588
5609
|
if (full) {
|
|
5589
5610
|
next = encodeDumpCursor({ table: name, after: last ?? 0 });
|
|
5590
5611
|
break;
|
|
@@ -5677,11 +5698,21 @@ options = {}) {
|
|
|
5677
5698
|
return key.map((c) => ({ column: c.name, collation: c.coll }));
|
|
5678
5699
|
});
|
|
5679
5700
|
}
|
|
5680
|
-
/**
|
|
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
|
+
*/
|
|
5681
5706
|
captureSchema(defs) {
|
|
5682
5707
|
const names = new Set(defs.map((d) => d.name));
|
|
5683
|
-
const
|
|
5684
|
-
|
|
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 } : {}) });
|
|
5685
5716
|
}
|
|
5686
5717
|
/**
|
|
5687
5718
|
* The capture `copyId`, renewed, as its pages read the schema (#2450). Refused when it is gone.
|
|
@@ -6076,7 +6107,7 @@ options = {}) {
|
|
|
6076
6107
|
});
|
|
6077
6108
|
}
|
|
6078
6109
|
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);
|
|
6110
|
+
return this.loadTables(tables.map((t) => ({ name: t.name, ddl: t.ddl, columns: t.columns, objects: t.objects, rows: () => t.rows })), destScopeId, opts);
|
|
6080
6111
|
}
|
|
6081
6112
|
/**
|
|
6082
6113
|
* The load `importDump` runs, over tables whose rows are read lazily (#2450): from the dump's
|
|
@@ -6084,7 +6115,7 @@ options = {}) {
|
|
|
6084
6115
|
* in memory at once. `stageId` names the staged load the rows come from: checked inside the
|
|
6085
6116
|
* load's transaction, and its tables dropped in it once the rows are in.
|
|
6086
6117
|
*/
|
|
6087
|
-
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) {
|
|
6088
6119
|
// The WHOLE drop-then-replay runs under deferred foreign keys, in one transaction.
|
|
6089
6120
|
//
|
|
6090
6121
|
// Two distinct FK hazards, and both need the deferral — the second is why it cannot
|
|
@@ -6118,7 +6149,8 @@ options = {}) {
|
|
|
6118
6149
|
// amount of name checking reaches it. There is no prepare step here to compile
|
|
6119
6150
|
// only the first statement, so the text itself has to be the one statement.
|
|
6120
6151
|
// Pure input validation, so it runs before the first DROP: a refused dump touches nothing.
|
|
6121
|
-
assertReplayableDump(replayable, { maxColumns: DO_SQL_LIMITS.columns });
|
|
6152
|
+
const objects = assertReplayableDump(replayable, { maxColumns: DO_SQL_LIMITS.columns });
|
|
6153
|
+
onObjects?.(objects);
|
|
6122
6154
|
// #1869: `exact` vouches for a named source; without one it would silently fall back.
|
|
6123
6155
|
if (exact && !sourceScopeId) {
|
|
6124
6156
|
throw substratError('validation_failed', 'restore refused: `exact` needs the scope the dump came from');
|
|
@@ -6306,6 +6338,10 @@ options = {}) {
|
|
|
6306
6338
|
// derived (#2090). Inside the replay's transaction: a dump whose journal owes a table a
|
|
6307
6339
|
// state column it does not carry rolls the whole load back. A plan whose table this dump
|
|
6308
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)));
|
|
6309
6345
|
repairDerivedObjects(doSpineSql(this.sql), (ddl) => this.runScript(ddl), this.derivedPlans(), {
|
|
6310
6346
|
after: 'the restored dump',
|
|
6311
6347
|
absentTable: 'skip',
|
|
@@ -6405,7 +6441,7 @@ options = {}) {
|
|
|
6405
6441
|
* (`changed`, 412) and "this store is a kept copy" (`kept`, 409) from a failure.
|
|
6406
6442
|
*/
|
|
6407
6443
|
async importDumpChecked(tables, destScopeId, opts) {
|
|
6408
|
-
return this.checkedLoad(() => this.importDump(tables, destScopeId, opts));
|
|
6444
|
+
return this.checkedLoad((onObjects) => this.importDump(tables, destScopeId, { ...opts, onObjects }));
|
|
6409
6445
|
}
|
|
6410
6446
|
/**
|
|
6411
6447
|
* #2531: `importDumpChecked` with the role repair judged and projected in the load's own
|
|
@@ -6415,7 +6451,7 @@ options = {}) {
|
|
|
6415
6451
|
* rather than loading with the repair silently dropped.
|
|
6416
6452
|
*/
|
|
6417
6453
|
async importDumpJudged(tables, destScopeId, opts, roleRepair) {
|
|
6418
|
-
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 })));
|
|
6419
6455
|
}
|
|
6420
6456
|
/**
|
|
6421
6457
|
* #2555: a control-plane-full restore or import. `importDump`, with this store's permission flags
|
|
@@ -6429,15 +6465,16 @@ options = {}) {
|
|
|
6429
6465
|
importDumpProjected(tables, destScopeId, opts, projection) {
|
|
6430
6466
|
return replyOf(async () => {
|
|
6431
6467
|
try {
|
|
6432
|
-
|
|
6433
|
-
|
|
6434
|
-
|
|
6435
|
-
|
|
6436
|
-
|
|
6437
|
-
|
|
6438
|
-
|
|
6439
|
-
|
|
6440
|
-
};
|
|
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) } : {}) };
|
|
6441
6478
|
}
|
|
6442
6479
|
catch (e) {
|
|
6443
6480
|
if (e instanceof Error && roleRefusals.has(e))
|
|
@@ -6508,19 +6545,31 @@ options = {}) {
|
|
|
6508
6545
|
if (seq > head.nextSeq) {
|
|
6509
6546
|
throw substratError('precondition_failed', `stage page ${seq} arrived before page ${head.nextSeq}`);
|
|
6510
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);
|
|
6511
6551
|
for (const t of tables) {
|
|
6512
6552
|
const columns = JSON.stringify(t.columns);
|
|
6553
|
+
const objects = t.objects === undefined ? null : JSON.stringify(t.objects);
|
|
6513
6554
|
let held = this.sql
|
|
6514
|
-
.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)
|
|
6515
6557
|
.toArray()[0];
|
|
6516
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
|
+
}
|
|
6517
6564
|
const ord = Number(this.sql.exec(`SELECT COUNT(*) AS n FROM _substrat_stage_tables`).toArray()[0].n);
|
|
6518
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);
|
|
6519
6568
|
// Untyped columns: no affinity, so a cell is stored exactly as it came.
|
|
6520
6569
|
this.sql.exec(`CREATE TABLE "${stageRowsTable(ord)}" (${t.columns.map((_, i) => `c${i}`).join(', ')})`);
|
|
6521
|
-
held = { ord, ddl: t.ddl, columns };
|
|
6570
|
+
held = { ord, ddl: t.ddl, columns, objects };
|
|
6522
6571
|
}
|
|
6523
|
-
else if (held.ddl !== t.ddl || held.columns !== columns) {
|
|
6572
|
+
else if (held.ddl !== t.ddl || held.columns !== columns || held.objects !== objects) {
|
|
6524
6573
|
throw substratError('precondition_failed', `table ${t.name} changed between dump pages; nothing more was staged`);
|
|
6525
6574
|
}
|
|
6526
6575
|
this.stageRows(held.ord, t);
|
|
@@ -6614,21 +6663,27 @@ options = {}) {
|
|
|
6614
6663
|
*/
|
|
6615
6664
|
async commitStagedDumpChecked(stageId, destScopeId, opts) {
|
|
6616
6665
|
let count = 0;
|
|
6617
|
-
const out = await this.checkedLoad(() => {
|
|
6666
|
+
const out = await this.checkedLoad((onObjects) => {
|
|
6618
6667
|
const head = this.stageHead();
|
|
6619
6668
|
if (head?.stageId !== stageId)
|
|
6620
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');
|
|
6621
6672
|
const staged = this.sql
|
|
6622
|
-
.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`)
|
|
6623
6677
|
.toArray();
|
|
6624
6678
|
const tables = staged.map((t) => ({
|
|
6625
6679
|
name: t.name,
|
|
6626
6680
|
ddl: t.ddl,
|
|
6627
6681
|
columns: JSON.parse(t.columns),
|
|
6682
|
+
...(t.objects === null ? {} : { objects: JSON.parse(t.objects) }),
|
|
6628
6683
|
rows: () => this.stagedRows(t.ord),
|
|
6629
6684
|
}));
|
|
6630
6685
|
count = tables.length;
|
|
6631
|
-
return this.loadTables(tables, destScopeId, opts, stageId);
|
|
6686
|
+
return this.loadTables(tables, destScopeId, { ...opts, onObjects }, stageId);
|
|
6632
6687
|
});
|
|
6633
6688
|
return out.refused === false ? { ...out, tables: count } : out;
|
|
6634
6689
|
}
|
|
@@ -6664,6 +6719,14 @@ options = {}) {
|
|
|
6664
6719
|
after = Number(batch[batch.length - 1][0]);
|
|
6665
6720
|
}
|
|
6666
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
|
+
}
|
|
6667
6730
|
/** The staged load this store holds (#2450), or null. */
|
|
6668
6731
|
stageHead() {
|
|
6669
6732
|
const present = this.sql.exec(`SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = '_substrat_stage'`).toArray();
|
|
@@ -6683,7 +6746,9 @@ options = {}) {
|
|
|
6683
6746
|
/** A load's refusals answered as values (#1722): across the RPC a throw carries only its message. */
|
|
6684
6747
|
async checkedLoad(run) {
|
|
6685
6748
|
try {
|
|
6686
|
-
|
|
6749
|
+
let plan;
|
|
6750
|
+
const switchedOff = await run((judged) => (plan = judged));
|
|
6751
|
+
return { refused: false, switchedOff, ...(plan ? { moduleObjects: dumpObjectsAudit(plan) } : {}) };
|
|
6687
6752
|
}
|
|
6688
6753
|
catch (e) {
|
|
6689
6754
|
if (e instanceof Error && fenceRefusals.has(e))
|