@substrat-run/adapter-cloudflare 0.143.0 → 0.144.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 +7 -1
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +18 -8
- package/dist/host.js.map +1 -1
- package/dist/scope-do.d.ts.map +1 -1
- package/dist/scope-do.js +90 -28
- 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;AA4DnD,OAAO,EA2DL,KAAK,SAAS,EAsBd,KAAK,kBAAkB,EAEvB,KAAK,gBAAgB,EAqGtB,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"scope-do.d.ts","sourceRoot":"","sources":["../src/scope-do.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AA4DnD,OAAO,EA2DL,KAAK,SAAS,EAsBd,KAAK,kBAAkB,EAEvB,KAAK,gBAAgB,EAqGtB,MAAM,sBAAsB,CAAC;AAoD9B,OAAO,EAAW,KAAK,OAAO,EAAE,MAAM,eAAe,CAAC;AAGtD,OAAO,EAkBL,kBAAkB,EAyEnB,MAAM,sBAAsB,CAAC;AA0B9B;;;;;;;;;;;;;GAaG;AAEH,MAAM,WAAW,UAAU;IACzB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,sBAAsB,CAAC;IACvC;;;;;;OAMG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC9B;AAoFD,oFAAoF;AACpF,MAAM,MAAM,aAAa,GAAG,UAAU,GAAG,UAAU,GAAG,SAAS,GAAG,SAAS,CAAC;AA+jB5E;;;GAGG;AACH,MAAM,MAAM,yBAAyB,CAAC,CAAC,IAAI,OAAO,CAAC,CAAC,CAAC,CAAC;AAmBtD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,UAAU,GAAG,SAAS,CAOxD;AAED,gHAAgH;AAChH,OAAO,EAAE,kBAAkB,EAAE,CAAC;AA2B9B,wBAAgB,aAAa,CAC3B,OAAO,EAAE,kBAAkB,EAAE,EAC7B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC,GACxD,KAAK,GAAG,EAAE,kBAAkB,EAAE,GAAG,EAAE,UAAU,KAAK,aAAa,CAAC,UAAU,CAAC,CA0gO7E"}
|
package/dist/scope-do.js
CHANGED
|
@@ -7,7 +7,7 @@ import { isUpgradeRequest, readSubscription, LIVE_DECIDE_ATTEMPTS, LIVE_FANOUT_L
|
|
|
7
7
|
import { replyOf } from './do-reply.js';
|
|
8
8
|
import { OperationQueue } from './serialization.js';
|
|
9
9
|
import { assertNoWorkerdStorage, doScopedSql, doBuiltColumnsOf, doRedactionSql, doSpineSql } from './sql.js';
|
|
10
|
-
import { actorOf, admitPeer, collectPeers, switchPeer, peerSubjectRef, subjectGrantState, assertNoSecret, CAPABILITY_DDL, COPY_ORIGIN_DDL, ENTITY_STATE_MOVES_DDL, SWITCH_FENCES_DDL, TABLE_OWNERS_DDL, assertMigrationLeavesLedgerAlone, recordOwnershipSteps, runMigrationStatements, splitSqlStatements, executableSqlStatements, blankSqlComments, CAPABILITY_EXCHANGE_OPERATION, CAPABILITY_BECOME_MINT_OPERATION, becomeMintCheck, mintBecomeCapabilityAsPrincipal, revokeBecomeCapabilityAsPrincipal, readBecomeLinkStates, capabilityAttachmentWriteRefused, createCapabilityVerbs, createEntityEdgeVerbs, createEntityStateVerbs, createTrashedReads, CONTINUATION_POSITION_CAP, visibleContinuation, searchStateWhere, uncheckedView, addStatePlans, assertNoCallerPurge, isUnreachableParent, PURGE_BATCH, heldPurgePass, lifecycleRefusal, purgeHeldBy, purgeDueOf, purgeOnlyKeysOf, runPurgePass, refuseTrashedTarget, assertBoundOperations, registerTrashTargets, withheldKeysFor, statefulTablesOf, afterMigration, afterRuntimeDdl, derivesAnything, repairDerivedObjects, StateColumnLost, exchangeCapability, guardSecrets, mintBecomeCapability, redactSecrets, redactSecretText, moduleLog, asyncInvocationId, asyncLinePass, resolveCapabilitySession, revokeCapabilityAsPlatform, readCapabilityPage, domainEventOf, facetEvents, readDeadLetters, readLifecycleFlow, readOperationSeries, readHistory, readInvocation, readUndrainedOutbox, walkEventCause, walkEventEffects, } from '@substrat-run/kernel';
|
|
10
|
+
import { actorOf, admitPeer, collectPeers, switchPeer, peerSubjectRef, subjectGrantState, assertNoSecret, CAPABILITY_DDL, COPY_ORIGIN_DDL, ENTITY_STATE_MOVES_DDL, SWITCH_FENCES_DDL, TABLE_OWNERS_DDL, assertMigrationLeavesLedgerAlone, recordOwnershipSteps, runMigrationStatements, splitSqlStatements, executableSqlStatements, blankSqlComments, CAPABILITY_EXCHANGE_OPERATION, CAPABILITY_BECOME_MINT_OPERATION, becomeMintCheck, mintBecomeCapabilityAsPrincipal, revokeBecomeCapabilityAsPrincipal, readBecomeLinkStates, ENTITY_UNBIND_OPERATION, allowedFor, unbindEntityHolderBounded, capabilityAttachmentWriteRefused, createCapabilityVerbs, createEntityEdgeVerbs, createEntityStateVerbs, createTrashedReads, CONTINUATION_POSITION_CAP, visibleContinuation, searchStateWhere, uncheckedView, addStatePlans, assertNoCallerPurge, isUnreachableParent, PURGE_BATCH, heldPurgePass, lifecycleRefusal, purgeHeldBy, purgeDueOf, purgeOnlyKeysOf, runPurgePass, refuseTrashedTarget, assertBoundOperations, registerTrashTargets, withheldKeysFor, statefulTablesOf, afterMigration, afterRuntimeDdl, derivesAnything, repairDerivedObjects, StateColumnLost, exchangeCapability, guardSecrets, mintBecomeCapability, redactSecrets, redactSecretText, moduleLog, asyncInvocationId, asyncLinePass, resolveCapabilitySession, revokeCapabilityAsPlatform, readCapabilityPage, domainEventOf, facetEvents, readDeadLetters, readLifecycleFlow, readOperationSeries, readHistory, readInvocation, readUndrainedOutbox, walkEventCause, walkEventEffects, } from '@substrat-run/kernel';
|
|
11
11
|
import { COPY_RESTORE_FENCE_LAPSED, copyRestoreFenceLapsed } from '@substrat-run/kernel';
|
|
12
12
|
import { createDoTupleChecker, createLocalControlPlaneReader, scopeTupleReader } from './checker.js';
|
|
13
13
|
import { settlePlatformRequestIn } from '@substrat-run/kernel';
|
|
@@ -561,6 +561,15 @@ function toRpcError(err) {
|
|
|
561
561
|
}
|
|
562
562
|
return new Error(String(err));
|
|
563
563
|
}
|
|
564
|
+
/** A throwing RPC kept for a coordinator from before #113: `fn`'s refusal, as `toRpcError` rebuilds it. */
|
|
565
|
+
async function thrownForRpc(fn) {
|
|
566
|
+
try {
|
|
567
|
+
return await fn();
|
|
568
|
+
}
|
|
569
|
+
catch (err) {
|
|
570
|
+
throw toRpcError(err);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
564
573
|
/** The check subject for an attachment gate (#476): the connection when the connector door
|
|
565
574
|
* set one, else the principal — mirrors the invoke path's subject selection. */
|
|
566
575
|
/**
|
|
@@ -2758,8 +2767,42 @@ export function defineScopeDO(modules, bareOps) {
|
|
|
2758
2767
|
.toArray()[0];
|
|
2759
2768
|
return row ? attachmentRecordOfRow(row) : null;
|
|
2760
2769
|
}
|
|
2770
|
+
// The attachment verbs (#473) each have two doors onto one private body. The `…Reply`
|
|
2771
|
+
// door answers a refusal as DATA, so its code and extensions survive the hop (#113); the
|
|
2772
|
+
// plain one throws it as the bare `Error` `toRpcError` rebuilds, and is kept for a
|
|
2773
|
+
// coordinator from before #113.
|
|
2774
|
+
attachmentAdd(...args) {
|
|
2775
|
+
return thrownForRpc(() => this.addAttachment(...args));
|
|
2776
|
+
}
|
|
2777
|
+
attachmentAddReply(...args) {
|
|
2778
|
+
return replyOf(() => this.addAttachment(...args));
|
|
2779
|
+
}
|
|
2780
|
+
attachmentList(...args) {
|
|
2781
|
+
return thrownForRpc(() => this.listAttachments(...args));
|
|
2782
|
+
}
|
|
2783
|
+
attachmentListReply(...args) {
|
|
2784
|
+
return replyOf(() => this.listAttachments(...args));
|
|
2785
|
+
}
|
|
2786
|
+
attachmentAuthorize(...args) {
|
|
2787
|
+
return thrownForRpc(() => this.authorizeAttachment(...args));
|
|
2788
|
+
}
|
|
2789
|
+
attachmentAuthorizeReply(...args) {
|
|
2790
|
+
return replyOf(() => this.authorizeAttachment(...args));
|
|
2791
|
+
}
|
|
2792
|
+
systemAttachmentAuthorize(...args) {
|
|
2793
|
+
return thrownForRpc(() => this.authorizeSystemAttachment(...args));
|
|
2794
|
+
}
|
|
2795
|
+
systemAttachmentAuthorizeReply(...args) {
|
|
2796
|
+
return replyOf(() => this.authorizeSystemAttachment(...args));
|
|
2797
|
+
}
|
|
2798
|
+
attachmentRemove(...args) {
|
|
2799
|
+
return thrownForRpc(() => this.removeAttachment(...args));
|
|
2800
|
+
}
|
|
2801
|
+
attachmentRemoveReply(...args) {
|
|
2802
|
+
return replyOf(() => this.removeAttachment(...args));
|
|
2803
|
+
}
|
|
2761
2804
|
/** Record an uploaded attachment: write gate + row + `attachment.added`, one txn. */
|
|
2762
|
-
async
|
|
2805
|
+
async addAttachment(record, principal, tenantId, scopeId,
|
|
2763
2806
|
// #476: when set, the gate is checked as this CONNECTION (not `principal`, which is
|
|
2764
2807
|
// then only the ctx.principal placeholder the connector door passes).
|
|
2765
2808
|
connectionId) {
|
|
@@ -2795,7 +2838,7 @@ export function defineScopeDO(modules, bareOps) {
|
|
|
2795
2838
|
if (err instanceof PermissionDenied) {
|
|
2796
2839
|
this.recordDenial(attachSubject(principal, connectionId), tenantId, 'attachments.upload', err, null);
|
|
2797
2840
|
}
|
|
2798
|
-
throw
|
|
2841
|
+
throw err;
|
|
2799
2842
|
}
|
|
2800
2843
|
// #1525: null. An attachment RPC carries no invocation, exactly as its denial
|
|
2801
2844
|
// records none — so the deliveries it drains name no call rather than a wrong one.
|
|
@@ -2804,7 +2847,7 @@ export function defineScopeDO(modules, bareOps) {
|
|
|
2804
2847
|
});
|
|
2805
2848
|
}
|
|
2806
2849
|
/** Records for one entity, newest first — read gate first. */
|
|
2807
|
-
async
|
|
2850
|
+
async listAttachments(entity, principal, tenantId, scopeId, connectionId) {
|
|
2808
2851
|
await this.ensureMigrations();
|
|
2809
2852
|
const gate = this.attachmentGate(entity.entityType);
|
|
2810
2853
|
try {
|
|
@@ -2815,7 +2858,7 @@ export function defineScopeDO(modules, bareOps) {
|
|
|
2815
2858
|
if (err instanceof PermissionDenied) {
|
|
2816
2859
|
this.recordDenial(attachSubject(principal, connectionId), tenantId, 'attachments.list', err, null);
|
|
2817
2860
|
}
|
|
2818
|
-
throw
|
|
2861
|
+
throw err;
|
|
2819
2862
|
}
|
|
2820
2863
|
const rows = this.sql
|
|
2821
2864
|
.exec(`SELECT id FROM _substrat_attachments WHERE entity_type = ? AND entity_id = ?
|
|
@@ -2827,7 +2870,7 @@ export function defineScopeDO(modules, bareOps) {
|
|
|
2827
2870
|
* Authorize one attachment (`read` before bytes are served, `write` before a remove is
|
|
2828
2871
|
* attempted elsewhere) and return its record — null for an id this scope does not know.
|
|
2829
2872
|
*/
|
|
2830
|
-
async
|
|
2873
|
+
async authorizeAttachment(attachmentId, mode, principal, tenantId, scopeId, connectionId, forEventId) {
|
|
2831
2874
|
await this.ensureMigrations();
|
|
2832
2875
|
const record = this.attachmentRow(attachmentId);
|
|
2833
2876
|
if (!record)
|
|
@@ -2846,7 +2889,7 @@ export function defineScopeDO(modules, bareOps) {
|
|
|
2846
2889
|
if (err instanceof PermissionDenied) {
|
|
2847
2890
|
this.recordDenial(attachSubject(principal, connectionId), tenantId, 'attachments.open', err, null);
|
|
2848
2891
|
}
|
|
2849
|
-
throw
|
|
2892
|
+
throw err;
|
|
2850
2893
|
}
|
|
2851
2894
|
return record;
|
|
2852
2895
|
}
|
|
@@ -2859,12 +2902,12 @@ export function defineScopeDO(modules, bareOps) {
|
|
|
2859
2902
|
if (err instanceof PermissionDenied) {
|
|
2860
2903
|
this.recordDenial(attachSubject(principal, connectionId), tenantId, 'attachments.open', err, null);
|
|
2861
2904
|
}
|
|
2862
|
-
throw
|
|
2905
|
+
throw err;
|
|
2863
2906
|
}
|
|
2864
2907
|
return record;
|
|
2865
2908
|
}
|
|
2866
2909
|
/** Authorize a job's read as its registered module, never as a person. */
|
|
2867
|
-
async
|
|
2910
|
+
async authorizeSystemAttachment(attachmentId, moduleId, tenantId, scopeId,
|
|
2868
2911
|
/** #1834: the instance the system door's gate read; see `invoke`. */
|
|
2869
2912
|
systemDoorInstance) {
|
|
2870
2913
|
await this.ensureMigrations();
|
|
@@ -2875,10 +2918,10 @@ export function defineScopeDO(modules, bareOps) {
|
|
|
2875
2918
|
catch (err) {
|
|
2876
2919
|
if (err instanceof SystemDoorMovedError)
|
|
2877
2920
|
return SYSTEM_DOOR_MOVED;
|
|
2878
|
-
throw
|
|
2921
|
+
throw err;
|
|
2879
2922
|
}
|
|
2880
2923
|
if (!this.modules.has(moduleId)) {
|
|
2881
|
-
throw
|
|
2924
|
+
throw substratError('not_found', `module not registered in this scope: ${moduleId}`);
|
|
2882
2925
|
}
|
|
2883
2926
|
const record = this.attachmentRow(attachmentId);
|
|
2884
2927
|
if (!record)
|
|
@@ -2892,12 +2935,12 @@ export function defineScopeDO(modules, bareOps) {
|
|
|
2892
2935
|
if (err instanceof PermissionDenied) {
|
|
2893
2936
|
this.recordDenial({ kind: 'system', id: moduleId }, tenantId, 'attachments.open', err, null);
|
|
2894
2937
|
}
|
|
2895
|
-
throw
|
|
2938
|
+
throw err;
|
|
2896
2939
|
}
|
|
2897
2940
|
return record;
|
|
2898
2941
|
}
|
|
2899
2942
|
/** Remove an attachment's metadata fact: write gate + delete + `attachment.removed`. */
|
|
2900
|
-
async
|
|
2943
|
+
async removeAttachment(attachmentId, principal, tenantId, scopeId, connectionId) {
|
|
2901
2944
|
await this.ensureMigrations();
|
|
2902
2945
|
return this.queue.enqueue(async () => {
|
|
2903
2946
|
const record = this.attachmentRow(attachmentId);
|
|
@@ -2924,7 +2967,7 @@ export function defineScopeDO(modules, bareOps) {
|
|
|
2924
2967
|
if (err instanceof PermissionDenied) {
|
|
2925
2968
|
this.recordDenial(attachSubject(principal, connectionId), tenantId, 'attachments.remove', err, null);
|
|
2926
2969
|
}
|
|
2927
|
-
throw
|
|
2970
|
+
throw err;
|
|
2928
2971
|
}
|
|
2929
2972
|
// #1525: null. An attachment RPC carries no invocation, exactly as its denial
|
|
2930
2973
|
// records none — so the deliveries it drains name no call rather than a wrong one.
|
|
@@ -3530,6 +3573,7 @@ export function defineScopeDO(modules, bareOps) {
|
|
|
3530
3573
|
holdings: this.checker.holdings
|
|
3531
3574
|
? (principal) => this.checker.holdings({ kind: 'principal', id: principal }, { tenantId, scopeId })
|
|
3532
3575
|
: undefined,
|
|
3576
|
+
allowed: (principal, permission, entity) => allowedFor(this.checker, principal, { tenantId, scopeId })(permission, entity),
|
|
3533
3577
|
}, secret, mode);
|
|
3534
3578
|
});
|
|
3535
3579
|
if (outcome)
|
|
@@ -3548,7 +3592,7 @@ export function defineScopeDO(modules, bareOps) {
|
|
|
3548
3592
|
const minter = principalId.parse(caller);
|
|
3549
3593
|
const parsed = principalBecomeCapabilityInput.parse(input);
|
|
3550
3594
|
return await this.queue.enqueue(async () => {
|
|
3551
|
-
const checked = await becomeMintCheck({ sql: doSpineSql(this.sql), checker: this.checker }, minter, parsed.principal, { tenantId, scopeId });
|
|
3595
|
+
const checked = await becomeMintCheck({ sql: doSpineSql(this.sql), checker: this.checker }, minter, parsed.principal, { tenantId, scopeId }, parsed.bind);
|
|
3552
3596
|
if (!checked.ok)
|
|
3553
3597
|
return checked;
|
|
3554
3598
|
const liveSince = this.liveHighWaterMark();
|
|
@@ -3565,6 +3609,32 @@ export function defineScopeDO(modules, bareOps) {
|
|
|
3565
3609
|
return { ok: true, minted: minted };
|
|
3566
3610
|
});
|
|
3567
3611
|
}
|
|
3612
|
+
/**
|
|
3613
|
+
* Unbind a binding shape's record (#1686) — its column cleared and the shape its person held
|
|
3614
|
+
* there through it taken back — as `caller`, who must hold the binding's key on the record. The
|
|
3615
|
+
* kernel's bound and the write in ONE queued body and one storage transaction; a refusal writes
|
|
3616
|
+
* nothing. On the spine as `entity.holder-unbound`, the caller its actor.
|
|
3617
|
+
*/
|
|
3618
|
+
async unbindEntityHolderBoundedFor(tenantId, scopeId, caller, entity) {
|
|
3619
|
+
await this.ensureMigrations();
|
|
3620
|
+
const by = principalId.parse(caller);
|
|
3621
|
+
return await this.queue.enqueue(async () => {
|
|
3622
|
+
const liveSince = this.liveHighWaterMark();
|
|
3623
|
+
const now = instant.parse(new Date().toISOString());
|
|
3624
|
+
let answer = { ok: true, unbound: [] };
|
|
3625
|
+
await this.revision.transaction(async () => {
|
|
3626
|
+
answer = await unbindEntityHolderBounded({
|
|
3627
|
+
sql: doSpineSql(this.sql),
|
|
3628
|
+
now,
|
|
3629
|
+
allowed: allowedFor(this.checker, by, { tenantId, scopeId }),
|
|
3630
|
+
emit: (event) => kernelEmit(this.operationContext(by, tenantId, scopeId, undefined, undefined, undefined, undefined, undefined, ENTITY_UNBIND_OPERATION, undefined, [], now), event),
|
|
3631
|
+
}, entity);
|
|
3632
|
+
});
|
|
3633
|
+
if (answer.ok && answer.unbound.length > 0)
|
|
3634
|
+
await this.settleCommitted(tenantId, scopeId, liveSince, null);
|
|
3635
|
+
return answer;
|
|
3636
|
+
});
|
|
3637
|
+
}
|
|
3568
3638
|
/** Where each named `become` link stands (#1686) — a pending-invite list's read. */
|
|
3569
3639
|
async becomeLinkStatesFor(tenantId, scopeId, ids) {
|
|
3570
3640
|
await this.ensureMigrations();
|
|
@@ -4510,12 +4580,7 @@ export function defineScopeDO(modules, bareOps) {
|
|
|
4510
4580
|
/** A bounded page of one table. Unknown table names throw — never queried blind. Kept for a
|
|
4511
4581
|
* coordinator from before #113; the current one calls `introspectTableReply`. */
|
|
4512
4582
|
introspectTable(table, limit, offset) {
|
|
4513
|
-
|
|
4514
|
-
return this.tablePage(table, limit, offset);
|
|
4515
|
-
}
|
|
4516
|
-
catch (e) {
|
|
4517
|
-
throw toRpcError(e);
|
|
4518
|
-
}
|
|
4583
|
+
return thrownForRpc(async () => this.tablePage(table, limit, offset));
|
|
4519
4584
|
}
|
|
4520
4585
|
/** `introspectTable`, its refusal answered as DATA so its code survives the hop (#113). */
|
|
4521
4586
|
introspectTableReply(table, limit, offset) {
|
|
@@ -4547,13 +4612,8 @@ export function defineScopeDO(modules, bareOps) {
|
|
|
4547
4612
|
* at SCOPE_QUERY_ROW_MAX with `truncated` set, never an error.
|
|
4548
4613
|
* Kept for a coordinator from before #113; the current one calls `introspectQueryReply`.
|
|
4549
4614
|
*/
|
|
4550
|
-
|
|
4551
|
-
|
|
4552
|
-
return await this.readOnlyQuery(sql);
|
|
4553
|
-
}
|
|
4554
|
-
catch (e) {
|
|
4555
|
-
throw toRpcError(e);
|
|
4556
|
-
}
|
|
4615
|
+
introspectQuery(sql) {
|
|
4616
|
+
return thrownForRpc(() => this.readOnlyQuery(sql));
|
|
4557
4617
|
}
|
|
4558
4618
|
/** `introspectQuery`, its refusal answered as DATA so its code survives the hop (#113). */
|
|
4559
4619
|
introspectQueryReply(sql) {
|
|
@@ -4711,6 +4771,8 @@ export function defineScopeDO(modules, bareOps) {
|
|
|
4711
4771
|
'ALTER TABLE _substrat_capabilities ADD COLUMN target_digest TEXT',
|
|
4712
4772
|
// #1686: why the kernel revoked a link itself; NULL for every revoke already there.
|
|
4713
4773
|
'ALTER TABLE _substrat_capabilities ADD COLUMN revoked_reason TEXT',
|
|
4774
|
+
// #1686: the action a become carries; NULL on every row already there — none carries one.
|
|
4775
|
+
'ALTER TABLE _substrat_capabilities ADD COLUMN action TEXT',
|
|
4714
4776
|
// #1632: legacy runs retain an unknown subject; no content-based backfill.
|
|
4715
4777
|
'ALTER TABLE _substrat_job_runs ADD COLUMN subject_id TEXT',
|
|
4716
4778
|
// #2034: the lease. NULL = nobody holds the run, which is right for every row already there.
|