@xnetjs/data 0.0.3 → 0.1.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/account-MWNCH4GJ.js +12 -0
- package/dist/account-ledger-R2GAFJL6.js +38 -0
- package/dist/auth/index.d.ts +11 -4
- package/dist/auth/index.js +1 -1
- package/dist/budget-NN7P35XP.js +12 -0
- package/dist/canvas-6WYBNH53.js +10 -0
- package/dist/channel-AT3LQF4A.js +12 -0
- package/dist/chat-message-DSVO72B3.js +10 -0
- package/dist/{chunk-J6EDPPH4.js → chunk-2ZNJWIQG.js} +2 -2
- package/dist/{chunk-5FHJE7RC.js → chunk-3J3HILXO.js} +2 -2
- package/dist/{chunk-EZA5UT4Y.js → chunk-3RUDVTMH.js} +6 -6
- package/dist/{chunk-YEQ6LLUY.js → chunk-4EYKGTSQ.js} +2 -2
- package/dist/{chunk-L66WXPTW.js → chunk-53F4PRNC.js} +2 -2
- package/dist/{chunk-LGD72FVD.js → chunk-5EXHNXPL.js} +2 -2
- package/dist/{chunk-4U4KBCDN.js → chunk-5HC5NO57.js} +2 -2
- package/dist/{chunk-YWN27MLD.js → chunk-6KLGA6IR.js} +2 -2
- package/dist/{chunk-37267RSF.js → chunk-6OMG4I7M.js} +2 -2
- package/dist/{chunk-DOZJXPQ6.js → chunk-BQLQSWKZ.js} +2 -2
- package/dist/{chunk-V6HZ3LF3.js → chunk-BY5O6LNC.js} +1 -1
- package/dist/{chunk-64AM6JPM.js → chunk-DCTRX6II.js} +1 -1
- package/dist/{chunk-SPFKSTPT.js → chunk-DWEXKD6E.js} +2 -2
- package/dist/{chunk-O64EK5FK.js → chunk-EBNMV2VO.js} +11 -2
- package/dist/{chunk-J4IVJA5M.js → chunk-FVEACSKE.js} +2 -2
- package/dist/{chunk-XWA3CJQ6.js → chunk-GU6THOAB.js} +2 -2
- package/dist/{chunk-XTUGZSYQ.js → chunk-H4GA4BBK.js} +2 -2
- package/dist/{chunk-GR3NDMBY.js → chunk-IJ44LR7N.js} +2 -2
- package/dist/{chunk-E6S557NC.js → chunk-J4ANIXIV.js} +2 -2
- package/dist/{chunk-EIQX7INF.js → chunk-JDFNMFOZ.js} +2 -2
- package/dist/{chunk-PSUJEKVV.js → chunk-K7DOZWWT.js} +244 -155
- package/dist/{chunk-UDUPQKRB.js → chunk-KQAT4XBL.js} +2 -2
- package/dist/chunk-KQUALW4O.js +183 -0
- package/dist/{chunk-KPVBKKZS.js → chunk-LYSWLCOI.js} +2 -2
- package/dist/{chunk-YXZGYLKV.js → chunk-MBTUO3ZL.js} +2 -2
- package/dist/{chunk-7GMJOAPF.js → chunk-NI4FHG2K.js} +2 -2
- package/dist/{chunk-6OPZJ7MG.js → chunk-OCMSAKWV.js} +2 -2
- package/dist/{chunk-IP4BEVVD.js → chunk-OGJCRNGE.js} +2 -2
- package/dist/{chunk-6CN53PON.js → chunk-OPPHF3TF.js} +3 -3
- package/dist/{chunk-77UDLVQY.js → chunk-OSAWNZVM.js} +1 -1
- package/dist/{chunk-GWYVY2K4.js → chunk-PNESGUH5.js} +2 -2
- package/dist/{chunk-RSSZXCLL.js → chunk-Q3IEGH4B.js} +2 -2
- package/dist/{chunk-2DSDH2ZH.js → chunk-QJW5LDP4.js} +2 -2
- package/dist/{chunk-JESSJ5CZ.js → chunk-RL64OJJ5.js} +13 -3
- package/dist/{chunk-S4B5OPZU.js → chunk-S5RP5RKY.js} +1 -0
- package/dist/{chunk-4UQTURCD.js → chunk-S6U6TCMN.js} +2 -2
- package/dist/{chunk-C5IRSEEZ.js → chunk-SVNGSZZA.js} +2 -2
- package/dist/{chunk-ZICIXDEE.js → chunk-T5AZAOG5.js} +1 -1
- package/dist/{chunk-IJ675GJR.js → chunk-TCTZW4A6.js} +2 -2
- package/dist/{chunk-54Y7QRZN.js → chunk-U64CW73O.js} +2 -2
- package/dist/{chunk-QVZXXJSP.js → chunk-UDWKWTAX.js} +485 -81
- package/dist/{chunk-3RB2FMMO.js → chunk-UQM3G5A2.js} +2 -2
- package/dist/{chunk-ENTHVNOC.js → chunk-VQ7JHB67.js} +2 -2
- package/dist/{chunk-JO6Y4E5V.js → chunk-W7EBL7ZU.js} +2 -2
- package/dist/{chunk-WUR3WWQM.js → chunk-WEPK7SZF.js} +2 -2
- package/dist/{chunk-3JKW6TDE.js → chunk-XMSKJ5PV.js} +2 -2
- package/dist/{chunk-WMSCEBFH.js → chunk-XROI44ZP.js} +2 -2
- package/dist/{chunk-CHZUSBSB.js → chunk-Y3S5SRVM.js} +2 -2
- package/dist/{chunk-KTJLFJEQ.js → chunk-YEFKQYKA.js} +2 -2
- package/dist/{chunk-DTPSTMUD.js → chunk-YSUIXD2Y.js} +2 -2
- package/dist/{chunk-ZMCO3O2Z.js → chunk-ZCOFZY5M.js} +2 -2
- package/dist/{chunk-U6CBYDVZ.js → chunk-ZLU4Y3O2.js} +2 -2
- package/dist/{chunk-337E3ZOB.js → chunk-ZPZ7XDA6.js} +2 -2
- package/dist/{chunk-6GB7PBCR.js → chunk-ZZ6TWKGS.js} +2 -2
- package/dist/{clone-DpbVKLUu.d.ts → clone-C0Jhk2UN.d.ts} +2 -2
- package/dist/comment-QSWYAQVS.js +10 -0
- package/dist/{crm-AAJXYIJY.js → crm-L5RYAAF3.js} +5 -5
- package/dist/dashboard-JMKTTRA7.js +10 -0
- package/dist/database/index.d.ts +3 -3
- package/dist/database/index.js +11 -11
- package/dist/database-GIXSPZJW.js +10 -0
- package/dist/database-field-3EPAMZUR.js +10 -0
- package/dist/database-row-6Q6TRVE4.js +10 -0
- package/dist/database-select-option-BBBJFLLH.js +10 -0
- package/dist/database-view-5PLSOBI2.js +10 -0
- package/dist/experiment-DOVOZ34Y.js +10 -0
- package/dist/{external-item-D654PKLI.js → external-item-P2E5X7BZ.js} +5 -5
- package/dist/external-reference-7BSFF6SA.js +8 -0
- package/dist/feed-2IXHHP6O.js +12 -0
- package/dist/feed-item-DPE6LN7I.js +12 -0
- package/dist/{folder-ONKSWHUT.js → folder-6KM3KYD5.js} +3 -3
- package/dist/{game-BF7DP5JU.js → game-5AK5XWBR.js} +5 -5
- package/dist/grant-WTQSNT5A.js +8 -0
- package/dist/{grant-expiration-cleaner-DBiNTWbD.d.ts → grant-expiration-cleaner-CY6BcmlR.d.ts} +2 -2
- package/dist/import-batch-IXBJLQPJ.js +12 -0
- package/dist/{inbox-state-AN6FPCIY.js → inbox-state-E5NSVOGP.js} +3 -3
- package/dist/index.d.ts +7 -7
- package/dist/index.js +101 -55
- package/dist/map-RG5Y3CPC.js +10 -0
- package/dist/media-asset-QTCGLIZA.js +8 -0
- package/dist/{memory-NV6EVJBE.js → memory-PNRTMBZO.js} +3 -3
- package/dist/metric-65SVOX5C.js +10 -0
- package/dist/milestone-XEO6ZGDG.js +12 -0
- package/dist/{moderation-QSUVBCNS.js → moderation-FFHETMRB.js} +3 -3
- package/dist/observation-2Y3QKUXM.js +10 -0
- package/dist/page-2V27JGKJ.js +10 -0
- package/dist/posting-GBOSZCMZ.js +12 -0
- package/dist/profile-BLLDWIDK.js +8 -0
- package/dist/project-CQL5LN26.js +10 -0
- package/dist/{query-ast-CWo6_30h.d.ts → query-ast-C3m6kZYv.d.ts} +1 -1
- package/dist/reaction-T2DTJBBV.js +10 -0
- package/dist/{registry-CQoN_p8U.d.ts → registry-DunPv__d.d.ts} +1 -1
- package/dist/saved-view-4R2DBCL3.js +10 -0
- package/dist/schema/index.d.ts +320 -13
- package/dist/schema/index.js +98 -52
- package/dist/{schema-extension-5BUXRDPB.js → schema-extension-LNRF2YRD.js} +5 -5
- package/dist/{space-PZ737CIS.js → space-IJU24PBH.js} +6 -6
- package/dist/{space-membership-N35NRX6F.js → space-membership-DEUMM7NV.js} +3 -3
- package/dist/store/index.d.ts +131 -4
- package/dist/store/index.js +5 -5
- package/dist/{store-CvgiydME.d.ts → store-DtIDjk7c.d.ts} +18 -1
- package/dist/{system-Q5I6STDC.js → system-ILHCZOVO.js} +3 -3
- package/dist/{tag-C677GKQW.js → tag-SKCHM3U5.js} +3 -3
- package/dist/{task-WQLMPN5U.js → task-7AOBP7WC.js} +5 -5
- package/dist/task-view-NUGKELJP.js +10 -0
- package/dist/transaction-ZDSEGDIR.js +12 -0
- package/dist/transcription-PSCLDAGX.js +12 -0
- package/dist/{types-Def-5Nka.d.ts → types-S-BJAKyK.d.ts} +53 -2
- package/dist/user-widget-RLY2N46A.js +8 -0
- package/package.json +7 -7
- package/dist/account-BZHETMC4.js +0 -12
- package/dist/budget-WR2Q4YH3.js +0 -12
- package/dist/canvas-HFMMVQLD.js +0 -10
- package/dist/channel-DXDYJ3ZH.js +0 -12
- package/dist/chat-message-QUUQGXEW.js +0 -10
- package/dist/comment-Y2Y3QYYI.js +0 -10
- package/dist/dashboard-3DC6B4BJ.js +0 -10
- package/dist/database-DRGL5UYS.js +0 -10
- package/dist/database-field-F47FY3XX.js +0 -10
- package/dist/database-row-77OXW2NF.js +0 -10
- package/dist/database-select-option-6S3TTA7A.js +0 -10
- package/dist/database-view-YW7DQG5X.js +0 -10
- package/dist/experiment-TJOAL3OH.js +0 -10
- package/dist/external-reference-K5SUJIQN.js +0 -8
- package/dist/feed-L23XTNDI.js +0 -12
- package/dist/feed-item-GSO242J6.js +0 -12
- package/dist/grant-NNKMWXPX.js +0 -8
- package/dist/import-batch-JMESZ4VI.js +0 -12
- package/dist/map-AUV65YS3.js +0 -10
- package/dist/media-asset-4I5YPR4R.js +0 -8
- package/dist/metric-X7W55DLC.js +0 -10
- package/dist/milestone-JJGOGK4R.js +0 -12
- package/dist/observation-UZSXXGBC.js +0 -10
- package/dist/page-NI7XODS6.js +0 -10
- package/dist/posting-FZOYVGH2.js +0 -12
- package/dist/profile-42A43GES.js +0 -8
- package/dist/project-G2CTLDXY.js +0 -10
- package/dist/reaction-VAQBVDUD.js +0 -10
- package/dist/saved-view-AOQHGHNM.js +0 -10
- package/dist/task-view-T5S32V4L.js +0 -10
- package/dist/transaction-U3ITRGJP.js +0 -12
- package/dist/transcription-F3T23LLO.js +0 -12
- package/dist/user-widget-4SJJFPG5.js +0 -8
|
@@ -3,15 +3,16 @@ import {
|
|
|
3
3
|
getNodeQuerySearchTokens,
|
|
4
4
|
withoutNodeQueryMaterializedView,
|
|
5
5
|
withoutNodeQueryPagination
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-S5RP5RKY.js";
|
|
7
7
|
import {
|
|
8
|
+
SYSTEM_SCHEMA_BASE_IRIS,
|
|
8
9
|
isSystemNamespaceResource,
|
|
9
10
|
isSystemSchemaIri
|
|
10
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-BY5O6LNC.js";
|
|
11
12
|
import {
|
|
12
13
|
createNodeId,
|
|
13
14
|
getBaseSchemaIRI
|
|
14
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-EBNMV2VO.js";
|
|
15
16
|
|
|
16
17
|
// src/store/query-ast.ts
|
|
17
18
|
var QUERY_AST_VERSION = 1;
|
|
@@ -1142,6 +1143,9 @@ var NodeStore = class {
|
|
|
1142
1143
|
this.onUnauthorizedRemoteChange = options.onUnauthorizedRemoteChange;
|
|
1143
1144
|
this.auth = options.auth;
|
|
1144
1145
|
this.telemetry = options.telemetry;
|
|
1146
|
+
if (this.authEvaluator && this.storage.setNodeReadAuthorizer) {
|
|
1147
|
+
this.storage.setNodeReadAuthorizer((nodes) => this.filterReadableNodes(nodes));
|
|
1148
|
+
}
|
|
1145
1149
|
}
|
|
1146
1150
|
/**
|
|
1147
1151
|
* Initialize the store by loading the last Lamport time from storage.
|
|
@@ -1569,6 +1573,18 @@ var NodeStore = class {
|
|
|
1569
1573
|
}
|
|
1570
1574
|
};
|
|
1571
1575
|
}
|
|
1576
|
+
if (this.storage.queryNodes && !this.nodeContentCipher && this.authEvaluator && !descriptor.nodeId && descriptor.materializedView !== void 0 && this.materializedAuthSupported()) {
|
|
1577
|
+
const authFingerprint = await this.authFingerprint();
|
|
1578
|
+
const result2 = await this.storage.queryNodes({ ...descriptor, authFingerprint });
|
|
1579
|
+
return {
|
|
1580
|
+
nodes: result2.nodes,
|
|
1581
|
+
totalCount: result2.totalCount,
|
|
1582
|
+
plan: {
|
|
1583
|
+
...result2.plan,
|
|
1584
|
+
durationMs: Date.now() - start
|
|
1585
|
+
}
|
|
1586
|
+
};
|
|
1587
|
+
}
|
|
1572
1588
|
if (this.storage.queryNodes && !this.nodeContentCipher && this.authEvaluator && !descriptor.nodeId && descriptor.materializedView === void 0) {
|
|
1573
1589
|
const candidates = await this.storage.queryNodes(withoutNodeQueryPagination(descriptor));
|
|
1574
1590
|
const readable2 = await this.filterReadableNodes(candidates.nodes);
|
|
@@ -2926,6 +2942,31 @@ var NodeStore = class {
|
|
|
2926
2942
|
const decisions = await Promise.all(nodes.map((node) => this.canReadNode(node)));
|
|
2927
2943
|
return nodes.filter((_, index) => decisions[index]);
|
|
2928
2944
|
}
|
|
2945
|
+
/**
|
|
2946
|
+
* Whether storage can safely materialize a view under read authorization
|
|
2947
|
+
* (exploration 0226): it must both accept the read authorizer (to authorize
|
|
2948
|
+
* the id list at refresh) and expose a reload-stable authorization-state
|
|
2949
|
+
* version (to fingerprint cache validity). When either is missing the store
|
|
2950
|
+
* falls back to the authorize-then-paginate path, which never caches.
|
|
2951
|
+
*/
|
|
2952
|
+
materializedAuthSupported() {
|
|
2953
|
+
return Boolean(this.storage.setNodeReadAuthorizer && this.storage.getAuthorizationStateVersion);
|
|
2954
|
+
}
|
|
2955
|
+
/**
|
|
2956
|
+
* A reload-stable fingerprint of the viewer's authorization context, folded
|
|
2957
|
+
* from the subject DID and the control-plane (grant) state version. Stamped
|
|
2958
|
+
* onto a materialized view so any grant change forces an `'authz-changed'`
|
|
2959
|
+
* refresh — the cache can never serve rows a revoked viewer may no longer
|
|
2960
|
+
* read. Returns `undefined` when authorization is off (materialized views run
|
|
2961
|
+
* untouched, as in the trusted single-user case).
|
|
2962
|
+
*/
|
|
2963
|
+
async authFingerprint() {
|
|
2964
|
+
if (!this.authEvaluator || !this.storage.getAuthorizationStateVersion) {
|
|
2965
|
+
return void 0;
|
|
2966
|
+
}
|
|
2967
|
+
const version = await this.storage.getAuthorizationStateVersion();
|
|
2968
|
+
return `v1:${this.authorDID}:${version.count}:${version.maxUpdatedAt}`;
|
|
2969
|
+
}
|
|
2929
2970
|
applyListPagination(nodes, options) {
|
|
2930
2971
|
const offset = options?.offset ?? 0;
|
|
2931
2972
|
const limit = options?.limit ?? nodes.length;
|
|
@@ -3374,6 +3415,13 @@ var DEFAULT_QUERY_VERIFICATION = {
|
|
|
3374
3415
|
var QUERY_TELEMETRY_FLUSH_THRESHOLD = 50;
|
|
3375
3416
|
var SQLITE_BIND_PARAMETER_BATCH_SIZE = 900;
|
|
3376
3417
|
var SQLITE_HYDRATE_NODE_BATCH_SIZE = Math.floor(SQLITE_BIND_PARAMETER_BATCH_SIZE / 2);
|
|
3418
|
+
var SQL_HYDRATE_ARITY_BUCKETS = [1, 10, 50, 150, SQLITE_HYDRATE_NODE_BATCH_SIZE];
|
|
3419
|
+
var SQL_IN_ARITY_BUCKETS = [10, 50, 300, SQLITE_BIND_PARAMETER_BATCH_SIZE];
|
|
3420
|
+
function padToArityBucket(items, buckets) {
|
|
3421
|
+
const size = buckets.find((bucket) => bucket >= items.length) ?? items.length;
|
|
3422
|
+
if (size === items.length) return items;
|
|
3423
|
+
return [...items, ...Array(size - items.length).fill(null)];
|
|
3424
|
+
}
|
|
3377
3425
|
function getMaterializedQueryRefreshReason(input) {
|
|
3378
3426
|
if (!input.cached) {
|
|
3379
3427
|
return "missing";
|
|
@@ -3384,6 +3432,9 @@ function getMaterializedQueryRefreshReason(input) {
|
|
|
3384
3432
|
if (input.cached.descriptor_hash !== input.descriptorHash) {
|
|
3385
3433
|
return "descriptor-changed";
|
|
3386
3434
|
}
|
|
3435
|
+
if ((input.cached.auth_fingerprint ?? null) !== input.authFingerprint) {
|
|
3436
|
+
return "authz-changed";
|
|
3437
|
+
}
|
|
3387
3438
|
if (input.cached.invalidated_at !== null) {
|
|
3388
3439
|
return "invalidated";
|
|
3389
3440
|
}
|
|
@@ -3407,12 +3458,18 @@ var SQLiteNodeStorageAdapter = class {
|
|
|
3407
3458
|
...options.queryVerification
|
|
3408
3459
|
};
|
|
3409
3460
|
this.queryDiagnostics = options.queryDiagnostics ?? false;
|
|
3461
|
+
this.aggregatedHydration = options.aggregatedHydration ?? true;
|
|
3462
|
+
this.scheduleMaintenance = options.scheduleMaintenance;
|
|
3410
3463
|
}
|
|
3411
3464
|
// Prepared statement cache for hot paths
|
|
3412
3465
|
stmtCache = /* @__PURE__ */ new Map();
|
|
3413
3466
|
adaptiveIndexing;
|
|
3414
3467
|
queryVerification;
|
|
3415
3468
|
queryDiagnostics;
|
|
3469
|
+
/** Hydrate one row per node via json_group_object (0264 Wave 2 flag). */
|
|
3470
|
+
aggregatedHydration;
|
|
3471
|
+
/** Idle scheduler for maintenance work (adaptive index creation, 0264). */
|
|
3472
|
+
scheduleMaintenance;
|
|
3416
3473
|
pendingQueryTelemetry = /* @__PURE__ */ new Map();
|
|
3417
3474
|
pendingQueryTelemetryHits = 0;
|
|
3418
3475
|
adaptiveIndexBudgetColumnsReady = false;
|
|
@@ -3420,6 +3477,15 @@ var SQLiteNodeStorageAdapter = class {
|
|
|
3420
3477
|
spatialTablesState = "unknown";
|
|
3421
3478
|
fullTextSearchTablesState = "unknown";
|
|
3422
3479
|
writeQueue = Promise.resolve();
|
|
3480
|
+
/**
|
|
3481
|
+
* Read-authorization filter applied before persisting a materialized view's
|
|
3482
|
+
* id list (exploration 0226). Wired by `NodeStore` when an auth evaluator is
|
|
3483
|
+
* present; `undefined` means materialized views are computed unauthorized
|
|
3484
|
+
* (the trusted single-user case).
|
|
3485
|
+
*/
|
|
3486
|
+
nodeReadAuthorizer;
|
|
3487
|
+
/** One-time guard: ensure the `auth_fingerprint` column exists on upgraded DBs. */
|
|
3488
|
+
materializationColumnsReady = false;
|
|
3423
3489
|
getSQLiteAdapter() {
|
|
3424
3490
|
return this.db;
|
|
3425
3491
|
}
|
|
@@ -3574,6 +3640,66 @@ var SQLiteNodeStorageAdapter = class {
|
|
|
3574
3640
|
);
|
|
3575
3641
|
return rows.map((row) => this.deserializeChange(row));
|
|
3576
3642
|
}
|
|
3643
|
+
/**
|
|
3644
|
+
* Compact the local change log (exploration 0254 / F3): delete only
|
|
3645
|
+
* *superseded* history so the OPFS file shrinks (fast cold open) and the
|
|
3646
|
+
* outbound-resync slice shrinks (cheap first sync), without affecting reads,
|
|
3647
|
+
* outbound sync, hash-chain chaining, or convergence with peers that never
|
|
3648
|
+
* compacted.
|
|
3649
|
+
*
|
|
3650
|
+
* A row is deleted iff ALL of:
|
|
3651
|
+
* - `lamport_time < wsafe` — below the confirmed-durable floor, so the hub
|
|
3652
|
+
* holds it and it is not part of the unconfirmed outbound tail (K1);
|
|
3653
|
+
* - it is NOT a node's hash-chain tip — we keep every row at a node's
|
|
3654
|
+
* `MAX(lamport_time)` (the whole tie group), so `getLastChange()` still
|
|
3655
|
+
* returns a real tip and the next write's `parentHash`/hash/signature are
|
|
3656
|
+
* byte-identical to an uncompacted peer's (K2);
|
|
3657
|
+
* - it backs NO currently-winning LWW value — its `(node_id, lamport_time,
|
|
3658
|
+
* author)` is not the provenance of any live `node_properties` row (K3).
|
|
3659
|
+
*
|
|
3660
|
+
* K3 is the load-bearing safety net: because every live projection value keeps
|
|
3661
|
+
* its backing row, the retained log still materialises to — and can re-push —
|
|
3662
|
+
* the exact current state, so a stranded/rolled-back peer never loses live
|
|
3663
|
+
* data. Reads are unaffected (they read the materialized `nodes`/
|
|
3664
|
+
* `node_properties`, never the log). Only rows are deleted, never rewritten
|
|
3665
|
+
* (the hash + signature are immutable and unforgeable). Runs chunked inside
|
|
3666
|
+
* the write lane, yields between chunks, and never throws.
|
|
3667
|
+
*/
|
|
3668
|
+
async pruneSupersededChanges(wsafe, options = {}) {
|
|
3669
|
+
const chunk = Math.max(1, Math.floor(options.chunk ?? 5e3));
|
|
3670
|
+
const maxRows = Math.max(0, Math.floor(options.maxRows ?? 25e4));
|
|
3671
|
+
if (!Number.isFinite(wsafe) || wsafe <= 0 || maxRows === 0) return { deleted: 0 };
|
|
3672
|
+
let deleted = 0;
|
|
3673
|
+
for (; ; ) {
|
|
3674
|
+
const remaining = maxRows - deleted;
|
|
3675
|
+
const limit = Math.min(chunk, remaining);
|
|
3676
|
+
if (limit <= 0) break;
|
|
3677
|
+
const affected = await this.enqueueWrite(async () => {
|
|
3678
|
+
const result = await this.db.run(
|
|
3679
|
+
`DELETE FROM changes WHERE hash IN (
|
|
3680
|
+
SELECT c.hash FROM changes c
|
|
3681
|
+
WHERE c.lamport_time < ?
|
|
3682
|
+
AND c.lamport_time < (
|
|
3683
|
+
SELECT MAX(t.lamport_time) FROM changes t WHERE t.node_id = c.node_id
|
|
3684
|
+
)
|
|
3685
|
+
AND NOT EXISTS (
|
|
3686
|
+
SELECT 1 FROM node_properties p
|
|
3687
|
+
WHERE p.node_id = c.node_id
|
|
3688
|
+
AND p.lamport_time = c.lamport_time
|
|
3689
|
+
AND p.updated_by = c.author
|
|
3690
|
+
)
|
|
3691
|
+
LIMIT ?
|
|
3692
|
+
)`,
|
|
3693
|
+
[wsafe, limit]
|
|
3694
|
+
);
|
|
3695
|
+
return result.changes ?? 0;
|
|
3696
|
+
});
|
|
3697
|
+
deleted += affected;
|
|
3698
|
+
if (affected < limit) break;
|
|
3699
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
3700
|
+
}
|
|
3701
|
+
return { deleted };
|
|
3702
|
+
}
|
|
3577
3703
|
async getChangeByHash(hash) {
|
|
3578
3704
|
const row = await this.db.queryOne(
|
|
3579
3705
|
`SELECT hash, node_id, payload, lamport_time,
|
|
@@ -3599,7 +3725,8 @@ var SQLiteNodeStorageAdapter = class {
|
|
|
3599
3725
|
const changes = /* @__PURE__ */ new Map();
|
|
3600
3726
|
if (uniqueIds.length === 0) return changes;
|
|
3601
3727
|
for (const batch of chunkItems(uniqueIds, SQLITE_BIND_PARAMETER_BATCH_SIZE)) {
|
|
3602
|
-
const
|
|
3728
|
+
const padded = padToArityBucket(batch, SQL_IN_ARITY_BUCKETS);
|
|
3729
|
+
const placeholders = padded.map(() => "?").join(", ");
|
|
3603
3730
|
const rows = await this.db.query(
|
|
3604
3731
|
`SELECT hash, node_id, payload, lamport_time,
|
|
3605
3732
|
lamport_peer as lamport_author, wall_time,
|
|
@@ -3613,7 +3740,7 @@ var SQLiteNodeStorageAdapter = class {
|
|
|
3613
3740
|
ORDER BY c2.lamport_time DESC
|
|
3614
3741
|
LIMIT 1
|
|
3615
3742
|
)`,
|
|
3616
|
-
|
|
3743
|
+
padded
|
|
3617
3744
|
);
|
|
3618
3745
|
rows.forEach((row) => {
|
|
3619
3746
|
changes.set(row.node_id, this.deserializeChange(row));
|
|
@@ -3628,57 +3755,24 @@ var SQLiteNodeStorageAdapter = class {
|
|
|
3628
3755
|
}
|
|
3629
3756
|
// ─── Materialized State Operations ────────────────────────────────────────
|
|
3630
3757
|
async getNode(id) {
|
|
3631
|
-
const
|
|
3632
|
-
|
|
3633
|
-
const propRows = await this.db.query(
|
|
3634
|
-
`SELECT * FROM node_properties WHERE node_id = ?`,
|
|
3635
|
-
[id]
|
|
3636
|
-
);
|
|
3637
|
-
const properties = {};
|
|
3638
|
-
const timestamps = {};
|
|
3639
|
-
let updatedBy = nodeRow.created_by;
|
|
3640
|
-
for (const prop of propRows) {
|
|
3641
|
-
properties[prop.property_key] = this.deserializeValue(prop.value);
|
|
3642
|
-
timestamps[prop.property_key] = {
|
|
3643
|
-
lamport: prop.lamport_time,
|
|
3644
|
-
author: prop.updated_by,
|
|
3645
|
-
wallTime: prop.updated_at
|
|
3646
|
-
};
|
|
3647
|
-
if (prop.updated_at >= nodeRow.updated_at) {
|
|
3648
|
-
updatedBy = prop.updated_by;
|
|
3649
|
-
}
|
|
3650
|
-
}
|
|
3651
|
-
return {
|
|
3652
|
-
id: nodeRow.id,
|
|
3653
|
-
schemaId: nodeRow.schema_id,
|
|
3654
|
-
properties,
|
|
3655
|
-
timestamps,
|
|
3656
|
-
deleted: nodeRow.deleted_at !== null,
|
|
3657
|
-
deletedAt: nodeRow.deleted_at ? { lamport: 0, author: "", wallTime: nodeRow.deleted_at } : void 0,
|
|
3658
|
-
createdAt: nodeRow.created_at,
|
|
3659
|
-
createdBy: nodeRow.created_by,
|
|
3660
|
-
updatedAt: nodeRow.updated_at,
|
|
3661
|
-
updatedBy
|
|
3662
|
-
};
|
|
3758
|
+
const nodes = await this.hydrateNodesByIds([id]);
|
|
3759
|
+
return nodes[0] ?? null;
|
|
3663
3760
|
}
|
|
3664
3761
|
async getNodes(ids) {
|
|
3665
3762
|
const uniqueIds = Array.from(new Set(ids));
|
|
3666
3763
|
if (uniqueIds.length === 0) return [];
|
|
3667
|
-
|
|
3668
|
-
for (const batch of chunkItems(uniqueIds, SQLITE_HYDRATE_NODE_BATCH_SIZE)) {
|
|
3669
|
-
nodes.push(...await this.hydrateNodesByIds(batch));
|
|
3670
|
-
}
|
|
3671
|
-
return nodes;
|
|
3764
|
+
return this.hydrateNodesByIds(uniqueIds);
|
|
3672
3765
|
}
|
|
3673
3766
|
async getExistingNodeIds(ids) {
|
|
3674
3767
|
const uniqueIds = Array.from(new Set(ids));
|
|
3675
3768
|
if (uniqueIds.length === 0) return [];
|
|
3676
3769
|
const existingIds = /* @__PURE__ */ new Set();
|
|
3677
3770
|
for (const batch of chunkItems(uniqueIds, SQLITE_BIND_PARAMETER_BATCH_SIZE)) {
|
|
3678
|
-
const
|
|
3771
|
+
const padded = padToArityBucket(batch, SQL_IN_ARITY_BUCKETS);
|
|
3772
|
+
const placeholders = padded.map(() => "?").join(", ");
|
|
3679
3773
|
const rows = await this.db.query(
|
|
3680
3774
|
`SELECT id FROM nodes WHERE id IN (${placeholders})`,
|
|
3681
|
-
|
|
3775
|
+
padded
|
|
3682
3776
|
);
|
|
3683
3777
|
rows.forEach((row) => existingIds.add(row.id));
|
|
3684
3778
|
}
|
|
@@ -3882,15 +3976,27 @@ var SQLiteNodeStorageAdapter = class {
|
|
|
3882
3976
|
this.debugQueryPlan(descriptor, result2);
|
|
3883
3977
|
return result2;
|
|
3884
3978
|
}
|
|
3885
|
-
const [queryDiagnostics,
|
|
3979
|
+
const [queryDiagnostics, mainQuery] = await Promise.all([
|
|
3886
3980
|
this.isQueryDiagnosticsEnabled() ? this.collectCompiledQueryDiagnostics(compiled) : Promise.resolve({}),
|
|
3887
|
-
timeQuery(this.db, compiled.sql, compiled.params)
|
|
3981
|
+
compiled.fused ? timeQuery(this.db, compiled.fused.sql, compiled.fused.params) : timeQuery(this.db, compiled.sql, compiled.params)
|
|
3888
3982
|
]);
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
|
|
3983
|
+
let candidates;
|
|
3984
|
+
let candidateCount;
|
|
3985
|
+
let fusedExactCount;
|
|
3986
|
+
if (compiled.fused) {
|
|
3987
|
+
const rows = mainQuery.result;
|
|
3988
|
+
candidates = this.aggregatedHydration ? this.hydrateAggregatedRows(rows) : this.hydrateJoinedRows(rows);
|
|
3989
|
+
candidateCount = candidates.length;
|
|
3990
|
+
if (compiled.fused.includesExactCount && rows.length > 0) {
|
|
3991
|
+
fusedExactCount = Number(rows[0].total_count ?? 0);
|
|
3992
|
+
}
|
|
3993
|
+
} else {
|
|
3994
|
+
const ids = mainQuery.result.map((row) => row.id);
|
|
3995
|
+
candidates = await this.hydrateNodesByIds(ids);
|
|
3996
|
+
candidateCount = ids.length;
|
|
3997
|
+
}
|
|
3892
3998
|
const nodes = applyNodeQueryDescriptor(candidates, compiled.postFilterDescriptor);
|
|
3893
|
-
const totalCount = compiled.sqlPagination ? descriptor.count === "exact" ? await this.countCompiledNodeQuery(descriptor, spatialPlan, fullTextSearchPlan) : void 0 : applyNodeQueryDescriptor(
|
|
3999
|
+
const totalCount = compiled.sqlPagination ? descriptor.count === "exact" ? fusedExactCount ?? (candidateCount === 0 && (descriptor.offset ?? 0) === 0 ? 0 : await this.countCompiledNodeQuery(descriptor, spatialPlan, fullTextSearchPlan)) : void 0 : applyNodeQueryDescriptor(
|
|
3894
4000
|
candidates,
|
|
3895
4001
|
withoutNodeQueryPagination(compiled.postFilterDescriptor)
|
|
3896
4002
|
).length;
|
|
@@ -3903,14 +4009,14 @@ var SQLiteNodeStorageAdapter = class {
|
|
|
3903
4009
|
totalCount,
|
|
3904
4010
|
plan: {
|
|
3905
4011
|
strategy: "storage-query",
|
|
3906
|
-
candidateNodeCount:
|
|
4012
|
+
candidateNodeCount: candidateCount,
|
|
3907
4013
|
hydratedNodeCount: candidates.length,
|
|
3908
4014
|
returnedNodeCount: nodes.length,
|
|
3909
4015
|
durationMs: Date.now() - start,
|
|
3910
|
-
sql: compiled.sql,
|
|
3911
|
-
params: compiled.params,
|
|
4016
|
+
sql: compiled.fused ? compiled.fused.sql : compiled.sql,
|
|
4017
|
+
params: compiled.fused ? compiled.fused.params : compiled.params,
|
|
3912
4018
|
postFilterReason: compiled.postFilterReason,
|
|
3913
|
-
candidateQueryDurationMs:
|
|
4019
|
+
candidateQueryDurationMs: mainQuery.durationMs,
|
|
3914
4020
|
...candidateAccelerators.length > 0 ? {
|
|
3915
4021
|
candidateAccelerators
|
|
3916
4022
|
} : {},
|
|
@@ -3959,6 +4065,21 @@ var SQLiteNodeStorageAdapter = class {
|
|
|
3959
4065
|
);
|
|
3960
4066
|
return row ? parseInt(row.value, 10) || 0 : 0;
|
|
3961
4067
|
}
|
|
4068
|
+
/**
|
|
4069
|
+
* The lowest confirmed-durable sync cursor across every node-change room
|
|
4070
|
+
* (`MIN` over the persisted `nodeSync:hwm:*` marks), or `null` when no room has
|
|
4071
|
+
* ever confirmed. This is the safe compaction floor (exploration 0254): the
|
|
4072
|
+
* hub durably holds every change at or below it. Returns `null` — never 0 — so
|
|
4073
|
+
* a workspace that has never synced is left untouched.
|
|
4074
|
+
*/
|
|
4075
|
+
async getMinConfirmedSyncCursor() {
|
|
4076
|
+
const row = await this.db.queryOne(
|
|
4077
|
+
`SELECT MIN(CAST(value AS INTEGER)) AS min_value
|
|
4078
|
+
FROM sync_state WHERE key LIKE 'nodeSync:hwm:%'`
|
|
4079
|
+
);
|
|
4080
|
+
const value = row?.min_value;
|
|
4081
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : null;
|
|
4082
|
+
}
|
|
3962
4083
|
async setSyncCursor(room, lamport) {
|
|
3963
4084
|
await this.enqueueWrite(async () => {
|
|
3964
4085
|
await this.db.run(
|
|
@@ -3969,6 +4090,27 @@ var SQLiteNodeStorageAdapter = class {
|
|
|
3969
4090
|
);
|
|
3970
4091
|
});
|
|
3971
4092
|
}
|
|
4093
|
+
// ─── App State (generic K/V, FK-free) ─────────────────────────────────────
|
|
4094
|
+
/** Namespace app-state keys so they can't collide with sync cursors. */
|
|
4095
|
+
appStateKey(key) {
|
|
4096
|
+
return `app:${key}`;
|
|
4097
|
+
}
|
|
4098
|
+
async getAppState(key) {
|
|
4099
|
+
const row = await this.db.queryOne(
|
|
4100
|
+
`SELECT value FROM sync_state WHERE key = ?`,
|
|
4101
|
+
[this.appStateKey(key)]
|
|
4102
|
+
);
|
|
4103
|
+
return row ? row.value : null;
|
|
4104
|
+
}
|
|
4105
|
+
async setAppState(key, value) {
|
|
4106
|
+
await this.enqueueWrite(async () => {
|
|
4107
|
+
await this.db.run(
|
|
4108
|
+
`INSERT INTO sync_state (key, value) VALUES (?, ?)
|
|
4109
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value`,
|
|
4110
|
+
[this.appStateKey(key), value]
|
|
4111
|
+
);
|
|
4112
|
+
});
|
|
4113
|
+
}
|
|
3972
4114
|
// ─── Document Content (Yjs) ───────────────────────────────────────────────
|
|
3973
4115
|
async getDocumentContent(nodeId) {
|
|
3974
4116
|
const row = await this.db.queryOne(
|
|
@@ -4697,19 +4839,10 @@ var SQLiteNodeStorageAdapter = class {
|
|
|
4697
4839
|
}
|
|
4698
4840
|
return Array.from(nodeMap.values());
|
|
4699
4841
|
}
|
|
4700
|
-
|
|
4701
|
-
|
|
4702
|
-
|
|
4703
|
-
|
|
4704
|
-
if (ids.length > SQLITE_HYDRATE_NODE_BATCH_SIZE) {
|
|
4705
|
-
const nodes = [];
|
|
4706
|
-
for (const batch of chunkItems(ids, SQLITE_HYDRATE_NODE_BATCH_SIZE)) {
|
|
4707
|
-
nodes.push(...await this.hydrateNodesByIds(batch));
|
|
4708
|
-
}
|
|
4709
|
-
return nodes;
|
|
4710
|
-
}
|
|
4711
|
-
const values = ids.map(() => "(?, ?)").join(", ");
|
|
4712
|
-
const params = ids.flatMap((id, ordinal) => [id, ordinal]);
|
|
4842
|
+
buildHydrateChunkQuery(ids) {
|
|
4843
|
+
const padded = padToArityBucket(ids, SQL_HYDRATE_ARITY_BUCKETS);
|
|
4844
|
+
const values = padded.map(() => "(?, ?)").join(", ");
|
|
4845
|
+
const params = padded.flatMap((id, ordinal) => [id, ordinal]);
|
|
4713
4846
|
const sql = `
|
|
4714
4847
|
WITH wanted(id, ordinal) AS (
|
|
4715
4848
|
VALUES ${values}
|
|
@@ -4732,14 +4865,123 @@ var SQLiteNodeStorageAdapter = class {
|
|
|
4732
4865
|
LEFT JOIN node_properties p ON p.node_id = n.id
|
|
4733
4866
|
ORDER BY wanted.ordinal ASC, p.property_key ASC
|
|
4734
4867
|
`;
|
|
4735
|
-
|
|
4736
|
-
return this.hydrateJoinedRows(rows);
|
|
4868
|
+
return { sql, params };
|
|
4737
4869
|
}
|
|
4870
|
+
async hydrateNodesByIds(ids) {
|
|
4871
|
+
if (ids.length === 0) {
|
|
4872
|
+
return [];
|
|
4873
|
+
}
|
|
4874
|
+
const aggregated = this.aggregatedHydration;
|
|
4875
|
+
const chunks = ids.length > SQLITE_HYDRATE_NODE_BATCH_SIZE ? chunkItems(ids, SQLITE_HYDRATE_NODE_BATCH_SIZE) : [ids];
|
|
4876
|
+
const reads = chunks.map(
|
|
4877
|
+
(chunk) => aggregated ? this.buildAggregatedHydrateChunkQuery(chunk) : this.buildHydrateChunkQuery(chunk)
|
|
4878
|
+
);
|
|
4879
|
+
const parse = (rows) => aggregated ? this.hydrateAggregatedRows(rows) : this.hydrateJoinedRows(rows);
|
|
4880
|
+
if (reads.length > 1 && typeof this.db.queryBatch === "function") {
|
|
4881
|
+
const results = await this.db.queryBatch(reads);
|
|
4882
|
+
const nodes2 = [];
|
|
4883
|
+
for (const rows of results) {
|
|
4884
|
+
nodes2.push(...parse(rows));
|
|
4885
|
+
}
|
|
4886
|
+
return nodes2;
|
|
4887
|
+
}
|
|
4888
|
+
const nodes = [];
|
|
4889
|
+
for (const read of reads) {
|
|
4890
|
+
const rows = await this.db.query(read.sql, read.params);
|
|
4891
|
+
nodes.push(...parse(rows));
|
|
4892
|
+
}
|
|
4893
|
+
return nodes;
|
|
4894
|
+
}
|
|
4895
|
+
/**
|
|
4896
|
+
* Aggregated hydrate (exploration 0264, Wave 2): collapse the EAV rows to
|
|
4897
|
+
* ONE row per node inside SQL via `json_group_object`, so the boundary
|
|
4898
|
+
* ships N rows instead of N × properties. `value` is stored as JSON text
|
|
4899
|
+
* in a BLOB — `json(CAST(… AS TEXT))` re-emits it as real JSON inside the
|
|
4900
|
+
* aggregate (without the cast/wrap it would double-encode as a string).
|
|
4901
|
+
*/
|
|
4902
|
+
buildAggregatedHydrateChunkQuery(ids) {
|
|
4903
|
+
const padded = padToArityBucket(ids, SQL_HYDRATE_ARITY_BUCKETS);
|
|
4904
|
+
const values = padded.map(() => "(?, ?)").join(", ");
|
|
4905
|
+
const params = padded.flatMap((id, ordinal) => [id, ordinal]);
|
|
4906
|
+
const sql = `
|
|
4907
|
+
WITH wanted(id, ordinal) AS (
|
|
4908
|
+
VALUES ${values}
|
|
4909
|
+
)
|
|
4910
|
+
SELECT
|
|
4911
|
+
n.id,
|
|
4912
|
+
n.schema_id,
|
|
4913
|
+
n.created_at,
|
|
4914
|
+
n.updated_at,
|
|
4915
|
+
n.created_by,
|
|
4916
|
+
n.deleted_at,
|
|
4917
|
+
wanted.ordinal,
|
|
4918
|
+
json_group_object(p.property_key, json(CAST(p.value AS TEXT)))
|
|
4919
|
+
FILTER (WHERE p.property_key IS NOT NULL) AS props_json,
|
|
4920
|
+
json_group_object(
|
|
4921
|
+
p.property_key,
|
|
4922
|
+
json_object('l', p.lamport_time, 'b', p.updated_by, 'w', p.updated_at)
|
|
4923
|
+
) FILTER (WHERE p.property_key IS NOT NULL) AS meta_json
|
|
4924
|
+
FROM wanted
|
|
4925
|
+
JOIN nodes n ON n.id = wanted.id
|
|
4926
|
+
LEFT JOIN node_properties p ON p.node_id = n.id
|
|
4927
|
+
GROUP BY n.id
|
|
4928
|
+
ORDER BY wanted.ordinal ASC
|
|
4929
|
+
`;
|
|
4930
|
+
return { sql, params };
|
|
4931
|
+
}
|
|
4932
|
+
/** Parse one-row-per-node aggregated hydrate results into NodeStates. */
|
|
4933
|
+
hydrateAggregatedRows(rows) {
|
|
4934
|
+
const nodes = [];
|
|
4935
|
+
for (const row of rows) {
|
|
4936
|
+
const properties = row.props_json ? JSON.parse(row.props_json) : {};
|
|
4937
|
+
const meta = row.meta_json ? JSON.parse(row.meta_json) : {};
|
|
4938
|
+
const timestamps = {};
|
|
4939
|
+
let updatedBy = row.created_by;
|
|
4940
|
+
for (const [key, entry] of Object.entries(meta)) {
|
|
4941
|
+
timestamps[key] = {
|
|
4942
|
+
lamport: entry.l ?? 0,
|
|
4943
|
+
author: entry.b ?? "",
|
|
4944
|
+
wallTime: entry.w ?? 0
|
|
4945
|
+
};
|
|
4946
|
+
if ((entry.w ?? 0) >= row.updated_at) {
|
|
4947
|
+
updatedBy = entry.b ?? row.created_by;
|
|
4948
|
+
}
|
|
4949
|
+
}
|
|
4950
|
+
nodes.push({
|
|
4951
|
+
id: row.id,
|
|
4952
|
+
schemaId: row.schema_id,
|
|
4953
|
+
properties,
|
|
4954
|
+
timestamps,
|
|
4955
|
+
deleted: row.deleted_at !== null,
|
|
4956
|
+
deletedAt: row.deleted_at ? { lamport: 0, author: "", wallTime: row.deleted_at } : void 0,
|
|
4957
|
+
createdAt: row.created_at,
|
|
4958
|
+
createdBy: row.created_by,
|
|
4959
|
+
updatedAt: row.updated_at,
|
|
4960
|
+
updatedBy
|
|
4961
|
+
});
|
|
4962
|
+
}
|
|
4963
|
+
return nodes;
|
|
4964
|
+
}
|
|
4965
|
+
/**
|
|
4966
|
+
* Read (or refresh) a materialized view.
|
|
4967
|
+
*
|
|
4968
|
+
* A materialized view is a purely LOCAL, DERIVED cache: an ordered list of
|
|
4969
|
+
* node ids (membership + order) plus bookkeeping. It is NEVER part of the
|
|
4970
|
+
* change log and is NEVER synced — peers each derive their own. It therefore
|
|
4971
|
+
* carries no authority and can be rebuilt or dropped at any time (a wiped
|
|
4972
|
+
* cache only costs a recompute, never data). Content is re-hydrated from the
|
|
4973
|
+
* live `nodes`/`node_properties` tables on every read, so the cache exposes
|
|
4974
|
+
* nothing the underlying tables don't already, and a write to the schema
|
|
4975
|
+
* (`invalidated_at`) or a grant change (`auth_fingerprint`) forces a
|
|
4976
|
+
* re-materialization. See exploration 0226.
|
|
4977
|
+
*/
|
|
4738
4978
|
async queryMaterializedView(descriptor, start) {
|
|
4739
4979
|
const materializedView = descriptor.materializedView;
|
|
4740
4980
|
if (!materializedView) {
|
|
4741
4981
|
throw new Error("Materialized view query requires descriptor.materializedView");
|
|
4742
4982
|
}
|
|
4983
|
+
await this.ensureMaterializationColumns();
|
|
4984
|
+
const authFingerprint = descriptor.authFingerprint ?? null;
|
|
4743
4985
|
const baseDescriptor = withoutNodeQueryMaterializedView(withoutNodeQueryPagination(descriptor));
|
|
4744
4986
|
const descriptorJson = this.stringifyStable(baseDescriptor);
|
|
4745
4987
|
const descriptorHash = this.hashScalarValue(descriptorJson);
|
|
@@ -4748,10 +4990,11 @@ var SQLiteNodeStorageAdapter = class {
|
|
|
4748
4990
|
const refreshReason = getMaterializedQueryRefreshReason({
|
|
4749
4991
|
cached,
|
|
4750
4992
|
descriptorHash,
|
|
4993
|
+
authFingerprint,
|
|
4751
4994
|
cacheExpired,
|
|
4752
4995
|
forceRefresh: materializedView.forceRefresh ?? false
|
|
4753
4996
|
});
|
|
4754
|
-
const canUseCache = cached !== null && cached.descriptor_hash === descriptorHash && cached.invalidated_at === null && !cacheExpired && !materializedView.forceRefresh;
|
|
4997
|
+
const canUseCache = cached !== null && cached.descriptor_hash === descriptorHash && (cached.auth_fingerprint ?? null) === authFingerprint && cached.invalidated_at === null && !cacheExpired && !materializedView.forceRefresh;
|
|
4755
4998
|
const readPlan = canUseCache ? {
|
|
4756
4999
|
viewId: materializedView.viewId,
|
|
4757
5000
|
descriptorHash,
|
|
@@ -4764,6 +5007,7 @@ var SQLiteNodeStorageAdapter = class {
|
|
|
4764
5007
|
descriptor: baseDescriptor,
|
|
4765
5008
|
descriptorHash,
|
|
4766
5009
|
descriptorJson,
|
|
5010
|
+
authFingerprint,
|
|
4767
5011
|
refreshReason: refreshReason ?? "missing",
|
|
4768
5012
|
invalidatedAt: cached?.invalidated_at ?? null
|
|
4769
5013
|
});
|
|
@@ -4780,14 +5024,63 @@ var SQLiteNodeStorageAdapter = class {
|
|
|
4780
5024
|
descriptor_json,
|
|
4781
5025
|
generated_at,
|
|
4782
5026
|
invalidated_at,
|
|
4783
|
-
row_count
|
|
5027
|
+
row_count,
|
|
5028
|
+
auth_fingerprint
|
|
4784
5029
|
FROM node_query_materializations
|
|
4785
5030
|
WHERE view_id = ?`,
|
|
4786
5031
|
[viewId]
|
|
4787
5032
|
);
|
|
4788
5033
|
}
|
|
5034
|
+
/**
|
|
5035
|
+
* Ensure the `auth_fingerprint` column exists. Fresh databases get it from
|
|
5036
|
+
* the DDL; databases upgraded in place (the runtime applies the full DDL with
|
|
5037
|
+
* `CREATE TABLE IF NOT EXISTS`, which cannot add a column) need this guard.
|
|
5038
|
+
* Idempotent and memoized (exploration 0226).
|
|
5039
|
+
*/
|
|
5040
|
+
async ensureMaterializationColumns() {
|
|
5041
|
+
if (this.materializationColumnsReady) return;
|
|
5042
|
+
try {
|
|
5043
|
+
const columns = await this.db.query(
|
|
5044
|
+
`PRAGMA table_info(node_query_materializations)`
|
|
5045
|
+
);
|
|
5046
|
+
const hasColumn = columns.some((column) => column.name === "auth_fingerprint");
|
|
5047
|
+
if (!hasColumn) {
|
|
5048
|
+
await this.db.run(
|
|
5049
|
+
"ALTER TABLE node_query_materializations ADD COLUMN auth_fingerprint TEXT"
|
|
5050
|
+
);
|
|
5051
|
+
}
|
|
5052
|
+
} catch {
|
|
5053
|
+
}
|
|
5054
|
+
this.materializationColumnsReady = true;
|
|
5055
|
+
}
|
|
5056
|
+
setNodeReadAuthorizer(authorizer) {
|
|
5057
|
+
this.nodeReadAuthorizer = authorizer;
|
|
5058
|
+
}
|
|
5059
|
+
/**
|
|
5060
|
+
* Reload-stable version of the authorization-relevant control-plane state:
|
|
5061
|
+
* grants (`Grant` schema) and per-subject `/sys/authz/` resources. Ordinary
|
|
5062
|
+
* data writes do not touch these, so a database's materialized views survive
|
|
5063
|
+
* reloads and edits, but any grant change shifts `count`/`maxUpdatedAt` and
|
|
5064
|
+
* forces an `'authz-changed'` refresh (exploration 0226).
|
|
5065
|
+
*/
|
|
5066
|
+
async getAuthorizationStateVersion() {
|
|
5067
|
+
const grantIri = SYSTEM_SCHEMA_BASE_IRIS.Grant;
|
|
5068
|
+
const row = await this.db.queryOne(
|
|
5069
|
+
`SELECT COUNT(*) AS count, COALESCE(MAX(updated_at), 0) AS max_updated_at
|
|
5070
|
+
FROM nodes
|
|
5071
|
+
WHERE schema_id = ?
|
|
5072
|
+
OR schema_id LIKE ?
|
|
5073
|
+
OR id LIKE 'xnet://%/sys/authz/%'`,
|
|
5074
|
+
[grantIri, `${grantIri}@%`]
|
|
5075
|
+
);
|
|
5076
|
+
return {
|
|
5077
|
+
count: row?.count ?? 0,
|
|
5078
|
+
maxUpdatedAt: row?.max_updated_at ?? 0
|
|
5079
|
+
};
|
|
5080
|
+
}
|
|
4789
5081
|
async refreshMaterializedView(input) {
|
|
4790
5082
|
const refreshed = await this.queryNodes(input.descriptor);
|
|
5083
|
+
const authorizedNodes = this.nodeReadAuthorizer ? await this.nodeReadAuthorizer(refreshed.nodes) : refreshed.nodes;
|
|
4791
5084
|
const generatedAt = Date.now();
|
|
4792
5085
|
await this.enqueueWrite(async () => {
|
|
4793
5086
|
await this.db.beginTransaction();
|
|
@@ -4804,26 +5097,29 @@ var SQLiteNodeStorageAdapter = class {
|
|
|
4804
5097
|
descriptor_json,
|
|
4805
5098
|
generated_at,
|
|
4806
5099
|
invalidated_at,
|
|
4807
|
-
row_count
|
|
5100
|
+
row_count,
|
|
5101
|
+
auth_fingerprint
|
|
4808
5102
|
)
|
|
4809
|
-
VALUES (?, ?, ?, ?, ?, NULL, ?)
|
|
5103
|
+
VALUES (?, ?, ?, ?, ?, NULL, ?, ?)
|
|
4810
5104
|
ON CONFLICT(view_id) DO UPDATE SET
|
|
4811
5105
|
descriptor_hash = excluded.descriptor_hash,
|
|
4812
5106
|
schema_id = excluded.schema_id,
|
|
4813
5107
|
descriptor_json = excluded.descriptor_json,
|
|
4814
5108
|
generated_at = excluded.generated_at,
|
|
4815
5109
|
invalidated_at = NULL,
|
|
4816
|
-
row_count = excluded.row_count
|
|
5110
|
+
row_count = excluded.row_count,
|
|
5111
|
+
auth_fingerprint = excluded.auth_fingerprint`,
|
|
4817
5112
|
[
|
|
4818
5113
|
input.viewId,
|
|
4819
5114
|
input.descriptorHash,
|
|
4820
5115
|
input.descriptor.schemaId,
|
|
4821
5116
|
input.descriptorJson,
|
|
4822
5117
|
generatedAt,
|
|
4823
|
-
|
|
5118
|
+
authorizedNodes.length,
|
|
5119
|
+
input.authFingerprint
|
|
4824
5120
|
]
|
|
4825
5121
|
);
|
|
4826
|
-
for (const [ordinal, node] of
|
|
5122
|
+
for (const [ordinal, node] of authorizedNodes.entries()) {
|
|
4827
5123
|
await this.db.run(
|
|
4828
5124
|
`INSERT INTO node_query_materialized_ids (view_id, ordinal, node_id)
|
|
4829
5125
|
VALUES (?, ?, ?)`,
|
|
@@ -4841,7 +5137,7 @@ var SQLiteNodeStorageAdapter = class {
|
|
|
4841
5137
|
descriptorHash: input.descriptorHash,
|
|
4842
5138
|
generatedAt,
|
|
4843
5139
|
invalidatedAt: input.invalidatedAt,
|
|
4844
|
-
rowCount:
|
|
5140
|
+
rowCount: authorizedNodes.length,
|
|
4845
5141
|
cacheHit: false,
|
|
4846
5142
|
refreshReason: input.refreshReason
|
|
4847
5143
|
};
|
|
@@ -5359,6 +5655,23 @@ CREATE INDEX IF NOT EXISTS idx_node_spatial_ids_schema
|
|
|
5359
5655
|
if (!stats || !this.shouldCreateAdaptiveIndexes(stats)) {
|
|
5360
5656
|
return { descriptorHash, adaptiveIndexNames: [] };
|
|
5361
5657
|
}
|
|
5658
|
+
if (this.scheduleMaintenance) {
|
|
5659
|
+
const input = {
|
|
5660
|
+
descriptorHash,
|
|
5661
|
+
schemaId: descriptor.schemaId,
|
|
5662
|
+
hints: adaptiveIndexHints,
|
|
5663
|
+
now
|
|
5664
|
+
};
|
|
5665
|
+
this.scheduleMaintenance(
|
|
5666
|
+
() => this.ensureAdaptiveIndexes(input).then(
|
|
5667
|
+
() => void 0,
|
|
5668
|
+
(err) => {
|
|
5669
|
+
console.warn("[SQLiteNodeStorageAdapter] deferred adaptive index skipped:", err);
|
|
5670
|
+
}
|
|
5671
|
+
)
|
|
5672
|
+
);
|
|
5673
|
+
return { descriptorHash, adaptiveIndexNames: [] };
|
|
5674
|
+
}
|
|
5362
5675
|
const adaptiveIndexNames = await this.ensureAdaptiveIndexes({
|
|
5363
5676
|
descriptorHash,
|
|
5364
5677
|
schemaId: descriptor.schemaId,
|
|
@@ -5484,6 +5797,9 @@ CREATE INDEX IF NOT EXISTS idx_node_spatial_ids_schema
|
|
|
5484
5797
|
if (!this.queryVerification.enabled) {
|
|
5485
5798
|
return { strategy: "skipped", reason: "disabled" };
|
|
5486
5799
|
}
|
|
5800
|
+
if (this.nodeReadAuthorizer) {
|
|
5801
|
+
return { strategy: "skipped", reason: "authorized-materialization" };
|
|
5802
|
+
}
|
|
5487
5803
|
const candidateScopeCount = descriptor.nodeId ? 1 : await this.countNodes({
|
|
5488
5804
|
schemaId: descriptor.schemaId,
|
|
5489
5805
|
includeDeleted: descriptor.includeDeleted
|
|
@@ -5880,17 +6196,37 @@ WHERE schema_id = ${this.quoteSqlLiteral(schemaId)}
|
|
|
5880
6196
|
const hasPropertySort = Object.keys(descriptor.orderBy ?? {}).some(
|
|
5881
6197
|
(key) => key !== "createdAt" && key !== "updatedAt"
|
|
5882
6198
|
);
|
|
6199
|
+
const propertySort = this.resolvePropertySortPushdown(descriptor, hasPropertySort);
|
|
5883
6200
|
const hasSqlCandidateBenefit = scalarWhere.length > 0 || spatialPlan !== null || fullTextSearchPlan !== null || !descriptor.spatial || this.hasOnlySystemOrdering(descriptor.orderBy);
|
|
5884
|
-
if (!hasSqlCandidateBenefit) {
|
|
6201
|
+
if (!hasSqlCandidateBenefit && !propertySort) {
|
|
5885
6202
|
return null;
|
|
5886
6203
|
}
|
|
5887
6204
|
return this.compileSqlQuery(descriptor, {
|
|
5888
6205
|
whereEntries: scalarWhere,
|
|
5889
|
-
canUseSqlPagination: !hasPropertySort && !descriptor.spatial && !descriptor.search,
|
|
6206
|
+
canUseSqlPagination: (!hasPropertySort || propertySort !== null) && !descriptor.spatial && !descriptor.search,
|
|
5890
6207
|
spatialPlan,
|
|
5891
|
-
fullTextSearchPlan
|
|
6208
|
+
fullTextSearchPlan,
|
|
6209
|
+
propertySort
|
|
5892
6210
|
});
|
|
5893
6211
|
}
|
|
6212
|
+
/**
|
|
6213
|
+
* A custom-property sort can push down when it is the descriptor's ONLY
|
|
6214
|
+
* order key and no cursor/spatial/search accelerator is in play. Gated on
|
|
6215
|
+
* the adaptive-indexing flag: the typed partial indexes on
|
|
6216
|
+
* `node_property_scalars` make the join+order cheap, and the flag keeps
|
|
6217
|
+
* the behavioural change opt-in while it soaks (exploration 0264).
|
|
6218
|
+
*/
|
|
6219
|
+
resolvePropertySortPushdown(descriptor, hasPropertySort) {
|
|
6220
|
+
if (!hasPropertySort || !this.adaptiveIndexing.enabled) return null;
|
|
6221
|
+
if (descriptor.spatial || descriptor.search || descriptor.after !== void 0) return null;
|
|
6222
|
+
const entries = Object.entries(descriptor.orderBy ?? {}).filter(
|
|
6223
|
+
(entry) => entry[1] === "asc" || entry[1] === "desc"
|
|
6224
|
+
);
|
|
6225
|
+
if (entries.length !== 1) return null;
|
|
6226
|
+
const [key, direction] = entries[0];
|
|
6227
|
+
if (key === "createdAt" || key === "updatedAt") return null;
|
|
6228
|
+
return { key, direction };
|
|
6229
|
+
}
|
|
5894
6230
|
compileSqlQuery(descriptor, options) {
|
|
5895
6231
|
const joins = [];
|
|
5896
6232
|
const where = ["n.schema_id = ?"];
|
|
@@ -5945,7 +6281,17 @@ WHERE schema_id = ${this.quoteSqlLiteral(schemaId)}
|
|
|
5945
6281
|
options.spatialPlan.bounds.maxY
|
|
5946
6282
|
);
|
|
5947
6283
|
}
|
|
5948
|
-
|
|
6284
|
+
if (options.propertySort) {
|
|
6285
|
+
const schemaId = this.quoteSqlLiteral(descriptor.schemaId);
|
|
6286
|
+
const propertyKey = this.quoteSqlLiteral(options.propertySort.key);
|
|
6287
|
+
joins.push(
|
|
6288
|
+
`LEFT JOIN node_property_scalars sortp
|
|
6289
|
+
ON sortp.node_id = n.id
|
|
6290
|
+
AND sortp.schema_id = ${schemaId}
|
|
6291
|
+
AND sortp.property_key = ${propertyKey}`
|
|
6292
|
+
);
|
|
6293
|
+
}
|
|
6294
|
+
const orderBy = options.propertySort ? this.buildPropertySortOrderBy(options.propertySort.direction) : this.buildSqlOrderBy(descriptor.orderBy);
|
|
5949
6295
|
const useSqlPagination = options.canUseSqlPagination && descriptor.after === void 0 && (descriptor.limit !== void 0 || (descriptor.offset ?? 0) > 0);
|
|
5950
6296
|
let sql = `
|
|
5951
6297
|
SELECT n.id
|
|
@@ -5954,13 +6300,59 @@ WHERE schema_id = ${this.quoteSqlLiteral(schemaId)}
|
|
|
5954
6300
|
WHERE ${where.join(" AND ")}
|
|
5955
6301
|
ORDER BY ${orderBy}
|
|
5956
6302
|
`;
|
|
6303
|
+
let fused;
|
|
5957
6304
|
if (useSqlPagination) {
|
|
5958
6305
|
sql += "\nLIMIT ? OFFSET ?";
|
|
5959
6306
|
whereParams.push(descriptor.limit ?? -1, descriptor.offset ?? 0);
|
|
6307
|
+
const includesExactCount = descriptor.count === "exact";
|
|
6308
|
+
const countColumn = includesExactCount ? ",\n COUNT(*) OVER () AS total_count" : "";
|
|
6309
|
+
const countSelect = includesExactCount ? "c.total_count," : "";
|
|
6310
|
+
const candidatesCte = `
|
|
6311
|
+
WITH candidates AS (
|
|
6312
|
+
SELECT
|
|
6313
|
+
n.id, n.schema_id, n.created_at, n.updated_at, n.created_by, n.deleted_at,
|
|
6314
|
+
ROW_NUMBER() OVER (ORDER BY ${orderBy}) AS ordinal${countColumn}
|
|
6315
|
+
FROM nodes n
|
|
6316
|
+
${joins.join("\n")}
|
|
6317
|
+
WHERE ${where.join(" AND ")}
|
|
6318
|
+
ORDER BY ${orderBy}
|
|
6319
|
+
LIMIT ? OFFSET ?
|
|
6320
|
+
)`;
|
|
6321
|
+
const fusedSql = this.aggregatedHydration ? `${candidatesCte}
|
|
6322
|
+
SELECT
|
|
6323
|
+
c.id, c.schema_id, c.created_at, c.updated_at, c.created_by, c.deleted_at,
|
|
6324
|
+
${countSelect}
|
|
6325
|
+
c.ordinal,
|
|
6326
|
+
json_group_object(p.property_key, json(CAST(p.value AS TEXT)))
|
|
6327
|
+
FILTER (WHERE p.property_key IS NOT NULL) AS props_json,
|
|
6328
|
+
json_group_object(
|
|
6329
|
+
p.property_key,
|
|
6330
|
+
json_object('l', p.lamport_time, 'b', p.updated_by, 'w', p.updated_at)
|
|
6331
|
+
) FILTER (WHERE p.property_key IS NOT NULL) AS meta_json
|
|
6332
|
+
FROM candidates c
|
|
6333
|
+
LEFT JOIN node_properties p ON p.node_id = c.id
|
|
6334
|
+
GROUP BY c.id
|
|
6335
|
+
ORDER BY c.ordinal ASC
|
|
6336
|
+
` : `${candidatesCte}
|
|
6337
|
+
SELECT
|
|
6338
|
+
c.id, c.schema_id, c.created_at, c.updated_at, c.created_by, c.deleted_at,
|
|
6339
|
+
${countSelect}
|
|
6340
|
+
p.property_key, p.value, p.lamport_time, p.updated_by, p.updated_at AS prop_updated_at,
|
|
6341
|
+
c.ordinal
|
|
6342
|
+
FROM candidates c
|
|
6343
|
+
LEFT JOIN node_properties p ON p.node_id = c.id
|
|
6344
|
+
ORDER BY c.ordinal ASC, p.property_key ASC
|
|
6345
|
+
`;
|
|
6346
|
+
fused = {
|
|
6347
|
+
sql: fusedSql,
|
|
6348
|
+
params: [...whereParams],
|
|
6349
|
+
includesExactCount
|
|
6350
|
+
};
|
|
5960
6351
|
}
|
|
5961
6352
|
return {
|
|
5962
6353
|
sql,
|
|
5963
6354
|
params: whereParams,
|
|
6355
|
+
fused,
|
|
5964
6356
|
postFilterDescriptor: useSqlPagination ? withoutNodeQueryPagination(descriptor) : descriptor,
|
|
5965
6357
|
postFilterReason: this.getCompiledPostFilterReason({
|
|
5966
6358
|
useSqlPagination,
|
|
@@ -6009,6 +6401,18 @@ WHERE schema_id = ${this.quoteSqlLiteral(schemaId)}
|
|
|
6009
6401
|
return;
|
|
6010
6402
|
}
|
|
6011
6403
|
}
|
|
6404
|
+
/**
|
|
6405
|
+
* ORDER BY for a pushed-down custom-property sort (0264). Mirrors the JS
|
|
6406
|
+
* comparator in `applyNodeQueryDescriptor`: missing properties sort last
|
|
6407
|
+
* ascending / first descending; typed value columns carry the order (for a
|
|
6408
|
+
* homogeneous property exactly one is non-NULL). `n.id` breaks ties so the
|
|
6409
|
+
* page boundary is a total order.
|
|
6410
|
+
*/
|
|
6411
|
+
buildPropertySortOrderBy(direction) {
|
|
6412
|
+
const dir = direction.toUpperCase();
|
|
6413
|
+
const nullsDir = direction === "asc" ? "ASC" : "DESC";
|
|
6414
|
+
return `(sortp.node_id IS NULL) ${nullsDir}, sortp.value_number ${dir}, sortp.value_boolean ${dir}, sortp.value_text ${dir}, n.id ASC`;
|
|
6415
|
+
}
|
|
6012
6416
|
buildSqlOrderBy(orderBy) {
|
|
6013
6417
|
const entries = Object.entries(orderBy ?? {}).filter(
|
|
6014
6418
|
(entry) => entry[1] === "asc" || entry[1] === "desc"
|