document-dataply 0.0.10-alpha.2 → 0.0.10-alpha.4
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/cjs/index.js +1354 -1248
- package/dist/types/core/DocumentFormatter.d.ts +10 -0
- package/dist/types/core/IndexManager.d.ts +68 -0
- package/dist/types/core/MetadataManager.d.ts +18 -0
- package/dist/types/core/MutationManager.d.ts +53 -0
- package/dist/types/core/Optimizer.d.ts +78 -0
- package/dist/types/core/QueryManager.d.ts +73 -0
- package/dist/types/core/documentAPI.d.ts +21 -198
- package/dist/types/types/index.d.ts +5 -0
- package/package.json +1 -1
package/dist/cjs/index.js
CHANGED
|
@@ -95,7 +95,7 @@ var require_cjs = __commonJS({
|
|
|
95
95
|
StringComparator: () => StringComparator,
|
|
96
96
|
SyncMVCCStrategy: () => SyncMVCCStrategy2,
|
|
97
97
|
SyncMVCCTransaction: () => SyncMVCCTransaction2,
|
|
98
|
-
Transaction: () =>
|
|
98
|
+
Transaction: () => Transaction4,
|
|
99
99
|
UnknownPageManager: () => UnknownPageManager,
|
|
100
100
|
ValueComparator: () => ValueComparator2
|
|
101
101
|
});
|
|
@@ -9598,7 +9598,7 @@ var require_cjs = __commonJS({
|
|
|
9598
9598
|
}
|
|
9599
9599
|
}
|
|
9600
9600
|
};
|
|
9601
|
-
var
|
|
9601
|
+
var Transaction4 = class {
|
|
9602
9602
|
/**
|
|
9603
9603
|
* @param id Transaction ID
|
|
9604
9604
|
* @param context Transaction context
|
|
@@ -9997,7 +9997,7 @@ var require_cjs = __commonJS({
|
|
|
9997
9997
|
* @returns Transaction object
|
|
9998
9998
|
*/
|
|
9999
9999
|
createTransaction() {
|
|
10000
|
-
return new
|
|
10000
|
+
return new Transaction4(
|
|
10001
10001
|
++this.txIdCounter,
|
|
10002
10002
|
this.txContext,
|
|
10003
10003
|
this.pfs.getPageStrategy(),
|
|
@@ -10383,88 +10383,17 @@ var require_cjs = __commonJS({
|
|
|
10383
10383
|
var src_exports = {};
|
|
10384
10384
|
__export(src_exports, {
|
|
10385
10385
|
DocumentDataply: () => DocumentDataply,
|
|
10386
|
-
GlobalTransaction: () =>
|
|
10387
|
-
Transaction: () =>
|
|
10386
|
+
GlobalTransaction: () => import_dataply5.GlobalTransaction,
|
|
10387
|
+
Transaction: () => import_dataply5.Transaction
|
|
10388
10388
|
});
|
|
10389
10389
|
module.exports = __toCommonJS(src_exports);
|
|
10390
10390
|
|
|
10391
10391
|
// src/core/documentAPI.ts
|
|
10392
|
-
var
|
|
10393
|
-
var import_dataply3 = __toESM(require_cjs());
|
|
10394
|
-
|
|
10395
|
-
// src/core/bptree/documentStrategy.ts
|
|
10396
|
-
var import_dataply = __toESM(require_cjs());
|
|
10397
|
-
var DocumentSerializeStrategyAsync = class extends import_dataply.SerializeStrategyAsync {
|
|
10398
|
-
constructor(order, api, txContext, treeKey) {
|
|
10399
|
-
super(order);
|
|
10400
|
-
this.api = api;
|
|
10401
|
-
this.txContext = txContext;
|
|
10402
|
-
this.treeKey = treeKey;
|
|
10403
|
-
}
|
|
10404
|
-
/**
|
|
10405
|
-
* readHead에서 할당된 headPk를 캐싱하여
|
|
10406
|
-
* writeHead에서 AsyncLocalStorage 컨텍스트 유실 시에도 사용할 수 있도록 함
|
|
10407
|
-
*/
|
|
10408
|
-
cachedHeadPk = null;
|
|
10409
|
-
async id(isLeaf) {
|
|
10410
|
-
const tx = this.txContext.get();
|
|
10411
|
-
const pk = await this.api.insertAsOverflow("__BPTREE_NODE_PLACEHOLDER__", false, tx);
|
|
10412
|
-
return pk + "";
|
|
10413
|
-
}
|
|
10414
|
-
async read(id) {
|
|
10415
|
-
const tx = this.txContext.get();
|
|
10416
|
-
const row = await this.api.select(Number(id), false, tx);
|
|
10417
|
-
if (row === null || row === "" || row.startsWith("__BPTREE_")) {
|
|
10418
|
-
throw new Error(`Node not found or empty with ID: ${id}`);
|
|
10419
|
-
}
|
|
10420
|
-
return JSON.parse(row);
|
|
10421
|
-
}
|
|
10422
|
-
async write(id, node) {
|
|
10423
|
-
const tx = this.txContext.get();
|
|
10424
|
-
const json = JSON.stringify(node);
|
|
10425
|
-
await this.api.update(+id, json, tx);
|
|
10426
|
-
}
|
|
10427
|
-
async delete(id) {
|
|
10428
|
-
const tx = this.txContext.get();
|
|
10429
|
-
await this.api.delete(+id, false, tx);
|
|
10430
|
-
}
|
|
10431
|
-
async readHead() {
|
|
10432
|
-
const tx = this.txContext.get();
|
|
10433
|
-
const metadata = await this.api.getDocumentInnerMetadata(tx);
|
|
10434
|
-
const indexInfo = metadata.indices[this.treeKey];
|
|
10435
|
-
if (!indexInfo) return null;
|
|
10436
|
-
const headPk = indexInfo[0];
|
|
10437
|
-
if (headPk === -1) {
|
|
10438
|
-
const pk = await this.api.insertAsOverflow("__BPTREE_HEAD_PLACEHOLDER__", false, tx);
|
|
10439
|
-
metadata.indices[this.treeKey][0] = pk;
|
|
10440
|
-
await this.api.updateDocumentInnerMetadata(metadata, tx);
|
|
10441
|
-
this.cachedHeadPk = pk;
|
|
10442
|
-
return null;
|
|
10443
|
-
}
|
|
10444
|
-
this.cachedHeadPk = headPk;
|
|
10445
|
-
const row = await this.api.select(headPk, false, tx);
|
|
10446
|
-
if (row === null || row === "" || row.startsWith("__BPTREE_")) return null;
|
|
10447
|
-
return JSON.parse(row);
|
|
10448
|
-
}
|
|
10449
|
-
async writeHead(head) {
|
|
10450
|
-
const tx = this.txContext.get();
|
|
10451
|
-
let headPk = this.cachedHeadPk;
|
|
10452
|
-
if (headPk === null) {
|
|
10453
|
-
const metadata = await this.api.getDocumentInnerMetadata(tx);
|
|
10454
|
-
const indexInfo = metadata.indices[this.treeKey];
|
|
10455
|
-
if (!indexInfo) {
|
|
10456
|
-
throw new Error(`Index info not found for tree: ${this.treeKey}. Initialization should be handled outside.`);
|
|
10457
|
-
}
|
|
10458
|
-
headPk = indexInfo[0];
|
|
10459
|
-
}
|
|
10460
|
-
const json = JSON.stringify(head);
|
|
10461
|
-
await this.api.update(headPk, json, tx);
|
|
10462
|
-
}
|
|
10463
|
-
};
|
|
10392
|
+
var import_dataply4 = __toESM(require_cjs());
|
|
10464
10393
|
|
|
10465
10394
|
// src/core/bptree/documentComparator.ts
|
|
10466
|
-
var
|
|
10467
|
-
var DocumentValueComparator = class extends
|
|
10395
|
+
var import_dataply = __toESM(require_cjs());
|
|
10396
|
+
var DocumentValueComparator = class extends import_dataply.ValueComparator {
|
|
10468
10397
|
primaryAsc(a, b) {
|
|
10469
10398
|
return this._compareValue(a.v, b.v);
|
|
10470
10399
|
}
|
|
@@ -10505,76 +10434,6 @@ var DocumentValueComparator = class extends import_dataply2.ValueComparator {
|
|
|
10505
10434
|
}
|
|
10506
10435
|
};
|
|
10507
10436
|
|
|
10508
|
-
// src/utils/catchPromise.ts
|
|
10509
|
-
async function catchPromise(promise) {
|
|
10510
|
-
return promise.then((res) => [void 0, res]).catch((reason) => [reason]);
|
|
10511
|
-
}
|
|
10512
|
-
|
|
10513
|
-
// src/utils/heap.ts
|
|
10514
|
-
var BinaryHeap = class {
|
|
10515
|
-
constructor(comparator) {
|
|
10516
|
-
this.comparator = comparator;
|
|
10517
|
-
}
|
|
10518
|
-
heap = [];
|
|
10519
|
-
get size() {
|
|
10520
|
-
return this.heap.length;
|
|
10521
|
-
}
|
|
10522
|
-
peek() {
|
|
10523
|
-
return this.heap[0];
|
|
10524
|
-
}
|
|
10525
|
-
push(value) {
|
|
10526
|
-
this.heap.push(value);
|
|
10527
|
-
this.bubbleUp(this.heap.length - 1);
|
|
10528
|
-
}
|
|
10529
|
-
pop() {
|
|
10530
|
-
if (this.size === 0) return void 0;
|
|
10531
|
-
const top = this.heap[0];
|
|
10532
|
-
const bottom = this.heap.pop();
|
|
10533
|
-
if (this.size > 0) {
|
|
10534
|
-
this.heap[0] = bottom;
|
|
10535
|
-
this.sinkDown(0);
|
|
10536
|
-
}
|
|
10537
|
-
return top;
|
|
10538
|
-
}
|
|
10539
|
-
/**
|
|
10540
|
-
* Replace the root element with a new value and re-heapify.
|
|
10541
|
-
* Faster than pop() followed by push().
|
|
10542
|
-
*/
|
|
10543
|
-
replace(value) {
|
|
10544
|
-
const top = this.heap[0];
|
|
10545
|
-
this.heap[0] = value;
|
|
10546
|
-
this.sinkDown(0);
|
|
10547
|
-
return top;
|
|
10548
|
-
}
|
|
10549
|
-
toArray() {
|
|
10550
|
-
return [...this.heap];
|
|
10551
|
-
}
|
|
10552
|
-
bubbleUp(index) {
|
|
10553
|
-
while (index > 0) {
|
|
10554
|
-
const parentIndex = Math.floor((index - 1) / 2);
|
|
10555
|
-
if (this.comparator(this.heap[index], this.heap[parentIndex]) >= 0) break;
|
|
10556
|
-
[this.heap[index], this.heap[parentIndex]] = [this.heap[parentIndex], this.heap[index]];
|
|
10557
|
-
index = parentIndex;
|
|
10558
|
-
}
|
|
10559
|
-
}
|
|
10560
|
-
sinkDown(index) {
|
|
10561
|
-
while (true) {
|
|
10562
|
-
let smallest = index;
|
|
10563
|
-
const left = 2 * index + 1;
|
|
10564
|
-
const right = 2 * index + 2;
|
|
10565
|
-
if (left < this.size && this.comparator(this.heap[left], this.heap[smallest]) < 0) {
|
|
10566
|
-
smallest = left;
|
|
10567
|
-
}
|
|
10568
|
-
if (right < this.size && this.comparator(this.heap[right], this.heap[smallest]) < 0) {
|
|
10569
|
-
smallest = right;
|
|
10570
|
-
}
|
|
10571
|
-
if (smallest === index) break;
|
|
10572
|
-
[this.heap[index], this.heap[smallest]] = [this.heap[smallest], this.heap[index]];
|
|
10573
|
-
index = smallest;
|
|
10574
|
-
}
|
|
10575
|
-
}
|
|
10576
|
-
};
|
|
10577
|
-
|
|
10578
10437
|
// src/utils/tokenizer.ts
|
|
10579
10438
|
function whitespaceTokenize(text) {
|
|
10580
10439
|
if (typeof text !== "string") return [];
|
|
@@ -10606,343 +10465,992 @@ function tokenize(text, options) {
|
|
|
10606
10465
|
return [];
|
|
10607
10466
|
}
|
|
10608
10467
|
|
|
10609
|
-
// src/core/
|
|
10610
|
-
var
|
|
10611
|
-
|
|
10612
|
-
|
|
10613
|
-
comparator = new DocumentValueComparator();
|
|
10614
|
-
pendingBackfillFields = [];
|
|
10615
|
-
_initialized = false;
|
|
10616
|
-
indexedFields;
|
|
10617
|
-
/**
|
|
10618
|
-
* Registered indices via createIndex() (before init)
|
|
10619
|
-
* Key: index name, Value: index configuration
|
|
10620
|
-
*/
|
|
10621
|
-
pendingCreateIndices = /* @__PURE__ */ new Map();
|
|
10622
|
-
/**
|
|
10623
|
-
* Resolved index configurations after init.
|
|
10624
|
-
* Key: index name, Value: index config (from metadata)
|
|
10625
|
-
*/
|
|
10626
|
-
registeredIndices = /* @__PURE__ */ new Map();
|
|
10627
|
-
/**
|
|
10628
|
-
* Maps field name → index names that cover this field.
|
|
10629
|
-
* Used for query resolution.
|
|
10630
|
-
*/
|
|
10631
|
-
fieldToIndices = /* @__PURE__ */ new Map();
|
|
10632
|
-
operatorConverters = {
|
|
10633
|
-
equal: "primaryEqual",
|
|
10634
|
-
notEqual: "primaryNotEqual",
|
|
10635
|
-
lt: "primaryLt",
|
|
10636
|
-
lte: "primaryLte",
|
|
10637
|
-
gt: "primaryGt",
|
|
10638
|
-
gte: "primaryGte",
|
|
10639
|
-
or: "primaryOr",
|
|
10640
|
-
like: "like"
|
|
10641
|
-
};
|
|
10642
|
-
constructor(file, options) {
|
|
10643
|
-
super(file, options);
|
|
10644
|
-
this.trees = /* @__PURE__ */ new Map();
|
|
10645
|
-
this.indexedFields = /* @__PURE__ */ new Set(["_id"]);
|
|
10646
|
-
this.hook.onceAfter("init", async (tx, isNewlyCreated) => {
|
|
10647
|
-
if (isNewlyCreated) {
|
|
10648
|
-
await this.initializeDocumentFile(tx);
|
|
10649
|
-
}
|
|
10650
|
-
if (!await this.verifyDocumentFile(tx)) {
|
|
10651
|
-
throw new Error("Document metadata verification failed");
|
|
10652
|
-
}
|
|
10653
|
-
const metadata = await this.getDocumentInnerMetadata(tx);
|
|
10654
|
-
const targetIndices = /* @__PURE__ */ new Map([
|
|
10655
|
-
["_id", { type: "btree", fields: ["_id"] }]
|
|
10656
|
-
]);
|
|
10657
|
-
for (const [name, info] of Object.entries(metadata.indices)) {
|
|
10658
|
-
targetIndices.set(name, info[1]);
|
|
10659
|
-
}
|
|
10660
|
-
for (const [name, option] of this.pendingCreateIndices) {
|
|
10661
|
-
const config = this.toIndexMetaConfig(option);
|
|
10662
|
-
targetIndices.set(name, config);
|
|
10663
|
-
}
|
|
10664
|
-
const backfillTargets = [];
|
|
10665
|
-
let isMetadataChanged = false;
|
|
10666
|
-
for (const [indexName, config] of targetIndices) {
|
|
10667
|
-
const existingIndex = metadata.indices[indexName];
|
|
10668
|
-
if (!existingIndex) {
|
|
10669
|
-
metadata.indices[indexName] = [-1, config];
|
|
10670
|
-
isMetadataChanged = true;
|
|
10671
|
-
if (!isNewlyCreated) {
|
|
10672
|
-
backfillTargets.push(indexName);
|
|
10673
|
-
}
|
|
10674
|
-
} else {
|
|
10675
|
-
const [_pk, existingConfig] = existingIndex;
|
|
10676
|
-
if (JSON.stringify(existingConfig) !== JSON.stringify(config)) {
|
|
10677
|
-
metadata.indices[indexName] = [_pk, config];
|
|
10678
|
-
isMetadataChanged = true;
|
|
10679
|
-
if (!isNewlyCreated) {
|
|
10680
|
-
backfillTargets.push(indexName);
|
|
10681
|
-
}
|
|
10682
|
-
}
|
|
10683
|
-
}
|
|
10684
|
-
}
|
|
10685
|
-
if (isMetadataChanged) {
|
|
10686
|
-
await this.updateDocumentInnerMetadata(metadata, tx);
|
|
10687
|
-
}
|
|
10688
|
-
this.indices = metadata.indices;
|
|
10689
|
-
this.registeredIndices = /* @__PURE__ */ new Map();
|
|
10690
|
-
this.fieldToIndices = /* @__PURE__ */ new Map();
|
|
10691
|
-
for (const [indexName, config] of targetIndices) {
|
|
10692
|
-
this.registeredIndices.set(indexName, config);
|
|
10693
|
-
const fields = this.getFieldsFromConfig(config);
|
|
10694
|
-
for (const field of fields) {
|
|
10695
|
-
this.indexedFields.add(field);
|
|
10696
|
-
if (!this.fieldToIndices.has(field)) {
|
|
10697
|
-
this.fieldToIndices.set(field, []);
|
|
10698
|
-
}
|
|
10699
|
-
this.fieldToIndices.get(field).push(indexName);
|
|
10700
|
-
}
|
|
10701
|
-
}
|
|
10702
|
-
for (const indexName of targetIndices.keys()) {
|
|
10703
|
-
if (metadata.indices[indexName]) {
|
|
10704
|
-
const tree = new import_dataply3.BPTreeAsync(
|
|
10705
|
-
new DocumentSerializeStrategyAsync(
|
|
10706
|
-
this.rowTableEngine.order,
|
|
10707
|
-
this,
|
|
10708
|
-
this.txContext,
|
|
10709
|
-
indexName
|
|
10710
|
-
),
|
|
10711
|
-
this.comparator
|
|
10712
|
-
);
|
|
10713
|
-
await tree.init();
|
|
10714
|
-
this.trees.set(indexName, tree);
|
|
10715
|
-
}
|
|
10716
|
-
}
|
|
10717
|
-
this.pendingBackfillFields = backfillTargets;
|
|
10718
|
-
this._initialized = true;
|
|
10719
|
-
return tx;
|
|
10720
|
-
});
|
|
10721
|
-
}
|
|
10722
|
-
/**
|
|
10723
|
-
* Whether the document database has been initialized.
|
|
10724
|
-
*/
|
|
10725
|
-
get isDocInitialized() {
|
|
10726
|
-
return this._initialized;
|
|
10727
|
-
}
|
|
10728
|
-
/**
|
|
10729
|
-
* Register an index. If called before init(), queues it for processing during init.
|
|
10730
|
-
* If called after init(), immediately creates the tree, updates metadata, and backfills.
|
|
10731
|
-
*/
|
|
10732
|
-
async registerIndex(name, option, tx) {
|
|
10733
|
-
if (!this._initialized) {
|
|
10734
|
-
this.pendingCreateIndices.set(name, option);
|
|
10735
|
-
return;
|
|
10736
|
-
}
|
|
10737
|
-
await this.registerIndexRuntime(name, option, tx);
|
|
10468
|
+
// src/core/Optimizer.ts
|
|
10469
|
+
var Optimizer = class {
|
|
10470
|
+
constructor(api) {
|
|
10471
|
+
this.api = api;
|
|
10738
10472
|
}
|
|
10739
10473
|
/**
|
|
10740
|
-
*
|
|
10741
|
-
* Creates the tree, updates metadata, and backfills existing data.
|
|
10474
|
+
* B-Tree 타입 인덱스의 선택도를 평가하고 트리에 부여할 조건을 산출합니다.
|
|
10742
10475
|
*/
|
|
10743
|
-
|
|
10744
|
-
const
|
|
10745
|
-
if (
|
|
10746
|
-
|
|
10747
|
-
|
|
10748
|
-
|
|
10749
|
-
|
|
10750
|
-
|
|
10751
|
-
|
|
10752
|
-
|
|
10753
|
-
|
|
10754
|
-
|
|
10755
|
-
|
|
10756
|
-
|
|
10757
|
-
|
|
10758
|
-
|
|
10759
|
-
|
|
10760
|
-
}
|
|
10761
|
-
this.fieldToIndices.get(field).push(name);
|
|
10476
|
+
evaluateBTreeCandidate(indexName, config, query, queryFields, treeTx, orderByField) {
|
|
10477
|
+
const primaryField = config.fields[0];
|
|
10478
|
+
if (!queryFields.has(primaryField)) return null;
|
|
10479
|
+
const builtCondition = {};
|
|
10480
|
+
let score = 0;
|
|
10481
|
+
let isConsecutive = true;
|
|
10482
|
+
const coveredFields = [];
|
|
10483
|
+
const compositeVerifyFields = [];
|
|
10484
|
+
const startValues = [];
|
|
10485
|
+
const endValues = [];
|
|
10486
|
+
let startOperator = null;
|
|
10487
|
+
let endOperator = null;
|
|
10488
|
+
for (let i = 0, len = config.fields.length; i < len; i++) {
|
|
10489
|
+
const field = config.fields[i];
|
|
10490
|
+
if (!queryFields.has(field)) {
|
|
10491
|
+
isConsecutive = false;
|
|
10492
|
+
continue;
|
|
10762
10493
|
}
|
|
10763
|
-
|
|
10764
|
-
|
|
10765
|
-
|
|
10766
|
-
|
|
10767
|
-
|
|
10768
|
-
|
|
10769
|
-
|
|
10770
|
-
|
|
10494
|
+
coveredFields.push(field);
|
|
10495
|
+
score += 1;
|
|
10496
|
+
if (isConsecutive) {
|
|
10497
|
+
const cond = query[field];
|
|
10498
|
+
if (cond !== void 0) {
|
|
10499
|
+
let isBounded = false;
|
|
10500
|
+
if (typeof cond !== "object" || cond === null) {
|
|
10501
|
+
score += 100;
|
|
10502
|
+
startValues.push(cond);
|
|
10503
|
+
endValues.push(cond);
|
|
10504
|
+
startOperator = "primaryGte";
|
|
10505
|
+
endOperator = "primaryLte";
|
|
10506
|
+
isBounded = true;
|
|
10507
|
+
} else if ("primaryEqual" in cond || "equal" in cond) {
|
|
10508
|
+
const val = cond.primaryEqual?.v ?? cond.equal?.v ?? cond.primaryEqual ?? cond.equal;
|
|
10509
|
+
score += 100;
|
|
10510
|
+
startValues.push(val);
|
|
10511
|
+
endValues.push(val);
|
|
10512
|
+
startOperator = "primaryGte";
|
|
10513
|
+
endOperator = "primaryLte";
|
|
10514
|
+
isBounded = true;
|
|
10515
|
+
} else if ("primaryGte" in cond || "gte" in cond) {
|
|
10516
|
+
const val = cond.primaryGte?.v ?? cond.gte?.v ?? cond.primaryGte ?? cond.gte;
|
|
10517
|
+
score += 50;
|
|
10518
|
+
isConsecutive = false;
|
|
10519
|
+
startValues.push(val);
|
|
10520
|
+
startOperator = "primaryGte";
|
|
10521
|
+
if (endValues.length > 0) endOperator = "primaryLte";
|
|
10522
|
+
isBounded = true;
|
|
10523
|
+
} else if ("primaryGt" in cond || "gt" in cond) {
|
|
10524
|
+
const val = cond.primaryGt?.v ?? cond.gt?.v ?? cond.primaryGt ?? cond.gt;
|
|
10525
|
+
score += 50;
|
|
10526
|
+
isConsecutive = false;
|
|
10527
|
+
startValues.push(val);
|
|
10528
|
+
startOperator = "primaryGt";
|
|
10529
|
+
if (endValues.length > 0) endOperator = "primaryLte";
|
|
10530
|
+
isBounded = true;
|
|
10531
|
+
} else if ("primaryLte" in cond || "lte" in cond) {
|
|
10532
|
+
const val = cond.primaryLte?.v ?? cond.lte?.v ?? cond.primaryLte ?? cond.lte;
|
|
10533
|
+
score += 50;
|
|
10534
|
+
isConsecutive = false;
|
|
10535
|
+
endValues.push(val);
|
|
10536
|
+
endOperator = "primaryLte";
|
|
10537
|
+
if (startValues.length > 0) startOperator = "primaryGte";
|
|
10538
|
+
isBounded = true;
|
|
10539
|
+
} else if ("primaryLt" in cond || "lt" in cond) {
|
|
10540
|
+
const val = cond.primaryLt?.v ?? cond.lt?.v ?? cond.primaryLt ?? cond.lt;
|
|
10541
|
+
score += 50;
|
|
10542
|
+
isConsecutive = false;
|
|
10543
|
+
endValues.push(val);
|
|
10544
|
+
endOperator = "primaryLt";
|
|
10545
|
+
if (startValues.length > 0) startOperator = "primaryGte";
|
|
10546
|
+
isBounded = true;
|
|
10547
|
+
} else if ("primaryOr" in cond || "or" in cond) {
|
|
10548
|
+
score += 20;
|
|
10549
|
+
isConsecutive = false;
|
|
10550
|
+
} else if ("like" in cond) {
|
|
10551
|
+
score += 15;
|
|
10552
|
+
isConsecutive = false;
|
|
10553
|
+
} else {
|
|
10554
|
+
score += 10;
|
|
10555
|
+
isConsecutive = false;
|
|
10556
|
+
}
|
|
10557
|
+
if (!isBounded && field !== primaryField) {
|
|
10558
|
+
compositeVerifyFields.push(field);
|
|
10559
|
+
}
|
|
10560
|
+
}
|
|
10561
|
+
} else {
|
|
10562
|
+
if (field !== primaryField) {
|
|
10563
|
+
compositeVerifyFields.push(field);
|
|
10564
|
+
}
|
|
10565
|
+
}
|
|
10566
|
+
}
|
|
10567
|
+
if (coveredFields.length === 1 && config.fields.length === 1) {
|
|
10568
|
+
Object.assign(builtCondition, query[primaryField]);
|
|
10569
|
+
} else {
|
|
10570
|
+
if (startOperator && startValues.length > 0) {
|
|
10571
|
+
builtCondition[startOperator] = { v: startValues.length === 1 ? startValues[0] : startValues };
|
|
10572
|
+
}
|
|
10573
|
+
if (endOperator && endValues.length > 0) {
|
|
10574
|
+
if (startOperator && startValues.length === endValues.length && startValues.every((val, i) => val === endValues[i])) {
|
|
10575
|
+
delete builtCondition[startOperator];
|
|
10576
|
+
builtCondition["primaryEqual"] = { v: startValues.length === 1 ? startValues[0] : startValues };
|
|
10577
|
+
} else {
|
|
10578
|
+
builtCondition[endOperator] = { v: endValues.length === 1 ? endValues[0] : endValues };
|
|
10579
|
+
}
|
|
10580
|
+
}
|
|
10581
|
+
if (Object.keys(builtCondition).length === 0) {
|
|
10582
|
+
Object.assign(builtCondition, query[primaryField] || {});
|
|
10583
|
+
}
|
|
10584
|
+
}
|
|
10585
|
+
let isIndexOrderSupported = false;
|
|
10586
|
+
if (orderByField) {
|
|
10587
|
+
for (let i = 0, len = config.fields.length; i < len; i++) {
|
|
10588
|
+
const field = config.fields[i];
|
|
10589
|
+
if (field === orderByField) {
|
|
10590
|
+
isIndexOrderSupported = true;
|
|
10591
|
+
break;
|
|
10592
|
+
}
|
|
10593
|
+
const cond = query[field];
|
|
10594
|
+
let isExactMatch = false;
|
|
10595
|
+
if (cond !== void 0) {
|
|
10596
|
+
if (typeof cond !== "object" || cond === null) isExactMatch = true;
|
|
10597
|
+
else if ("primaryEqual" in cond || "equal" in cond) isExactMatch = true;
|
|
10598
|
+
}
|
|
10599
|
+
if (!isExactMatch) break;
|
|
10600
|
+
}
|
|
10601
|
+
if (isIndexOrderSupported) {
|
|
10602
|
+
score += 200;
|
|
10603
|
+
}
|
|
10604
|
+
}
|
|
10605
|
+
return {
|
|
10606
|
+
tree: treeTx,
|
|
10607
|
+
condition: builtCondition,
|
|
10608
|
+
field: primaryField,
|
|
10609
|
+
indexName,
|
|
10610
|
+
isFtsMatch: false,
|
|
10611
|
+
score,
|
|
10612
|
+
compositeVerifyFields,
|
|
10613
|
+
coveredFields,
|
|
10614
|
+
isIndexOrderSupported
|
|
10615
|
+
};
|
|
10616
|
+
}
|
|
10617
|
+
/**
|
|
10618
|
+
* FTS 타입 인덱스의 선택도를 평가합니다.
|
|
10619
|
+
*/
|
|
10620
|
+
evaluateFTSCandidate(indexName, config, query, queryFields, treeTx) {
|
|
10621
|
+
const field = config.fields;
|
|
10622
|
+
if (!queryFields.has(field)) return null;
|
|
10623
|
+
const condition = query[field];
|
|
10624
|
+
if (!condition || typeof condition !== "object" || !("match" in condition)) return null;
|
|
10625
|
+
const ftsConfig = this.api.indexManager.getFtsConfig(config);
|
|
10626
|
+
const matchTokens = ftsConfig ? tokenize(condition.match, ftsConfig) : [];
|
|
10627
|
+
return {
|
|
10628
|
+
tree: treeTx,
|
|
10629
|
+
condition,
|
|
10630
|
+
field,
|
|
10631
|
+
indexName,
|
|
10632
|
+
isFtsMatch: true,
|
|
10633
|
+
matchTokens,
|
|
10634
|
+
score: 90,
|
|
10635
|
+
compositeVerifyFields: [],
|
|
10636
|
+
coveredFields: [field],
|
|
10637
|
+
isIndexOrderSupported: false
|
|
10638
|
+
};
|
|
10639
|
+
}
|
|
10640
|
+
/**
|
|
10641
|
+
* 실행할 최적의 인덱스를 선택합니다. (최적 드라이버 선택)
|
|
10642
|
+
*/
|
|
10643
|
+
async getSelectivityCandidate(query, orderByField) {
|
|
10644
|
+
const queryFields = new Set(Object.keys(query));
|
|
10645
|
+
const candidates = [];
|
|
10646
|
+
for (const [indexName, config] of this.api.indexManager.registeredIndices) {
|
|
10647
|
+
const tree = this.api.trees.get(indexName);
|
|
10648
|
+
if (!tree) continue;
|
|
10649
|
+
if (config.type === "btree") {
|
|
10650
|
+
const treeTx = await tree.createTransaction();
|
|
10651
|
+
const candidate = this.evaluateBTreeCandidate(
|
|
10652
|
+
indexName,
|
|
10653
|
+
config,
|
|
10654
|
+
query,
|
|
10655
|
+
queryFields,
|
|
10656
|
+
treeTx,
|
|
10657
|
+
orderByField
|
|
10658
|
+
);
|
|
10659
|
+
if (candidate) candidates.push(candidate);
|
|
10660
|
+
} else if (config.type === "fts") {
|
|
10661
|
+
const treeTx = await tree.createTransaction();
|
|
10662
|
+
const candidate = this.evaluateFTSCandidate(
|
|
10663
|
+
indexName,
|
|
10664
|
+
config,
|
|
10665
|
+
query,
|
|
10666
|
+
queryFields,
|
|
10667
|
+
treeTx
|
|
10668
|
+
);
|
|
10669
|
+
if (candidate) candidates.push(candidate);
|
|
10670
|
+
}
|
|
10671
|
+
}
|
|
10672
|
+
const rollback = () => {
|
|
10673
|
+
for (const { tree } of candidates) {
|
|
10674
|
+
tree.rollback();
|
|
10675
|
+
}
|
|
10676
|
+
};
|
|
10677
|
+
if (candidates.length === 0) {
|
|
10678
|
+
rollback();
|
|
10679
|
+
return null;
|
|
10680
|
+
}
|
|
10681
|
+
candidates.sort((a, b) => {
|
|
10682
|
+
if (b.score !== a.score) return b.score - a.score;
|
|
10683
|
+
const aConfig = this.api.indexManager.registeredIndices.get(a.indexName);
|
|
10684
|
+
const bConfig = this.api.indexManager.registeredIndices.get(b.indexName);
|
|
10685
|
+
const aFieldCount = aConfig ? Array.isArray(aConfig.fields) ? aConfig.fields.length : 1 : 0;
|
|
10686
|
+
const bFieldCount = bConfig ? Array.isArray(bConfig.fields) ? bConfig.fields.length : 1 : 0;
|
|
10687
|
+
return aFieldCount - bFieldCount;
|
|
10688
|
+
});
|
|
10689
|
+
const driver = candidates[0];
|
|
10690
|
+
const driverCoveredFields = new Set(driver.coveredFields);
|
|
10691
|
+
const others = candidates.slice(1).filter((c) => !driverCoveredFields.has(c.field));
|
|
10692
|
+
const compositeVerifyConditions = [];
|
|
10693
|
+
for (let i = 0, len = driver.compositeVerifyFields.length; i < len; i++) {
|
|
10694
|
+
const field = driver.compositeVerifyFields[i];
|
|
10695
|
+
if (query[field]) {
|
|
10696
|
+
compositeVerifyConditions.push({ field, condition: query[field] });
|
|
10697
|
+
}
|
|
10698
|
+
}
|
|
10699
|
+
return {
|
|
10700
|
+
driver,
|
|
10701
|
+
others,
|
|
10702
|
+
compositeVerifyConditions,
|
|
10703
|
+
rollback
|
|
10704
|
+
};
|
|
10705
|
+
}
|
|
10706
|
+
};
|
|
10707
|
+
|
|
10708
|
+
// src/core/QueryManager.ts
|
|
10709
|
+
var os = __toESM(require("node:os"));
|
|
10710
|
+
|
|
10711
|
+
// src/utils/heap.ts
|
|
10712
|
+
var BinaryHeap = class {
|
|
10713
|
+
constructor(comparator) {
|
|
10714
|
+
this.comparator = comparator;
|
|
10715
|
+
}
|
|
10716
|
+
heap = [];
|
|
10717
|
+
get size() {
|
|
10718
|
+
return this.heap.length;
|
|
10719
|
+
}
|
|
10720
|
+
peek() {
|
|
10721
|
+
return this.heap[0];
|
|
10722
|
+
}
|
|
10723
|
+
push(value) {
|
|
10724
|
+
this.heap.push(value);
|
|
10725
|
+
this.bubbleUp(this.heap.length - 1);
|
|
10726
|
+
}
|
|
10727
|
+
pop() {
|
|
10728
|
+
if (this.size === 0) return void 0;
|
|
10729
|
+
const top = this.heap[0];
|
|
10730
|
+
const bottom = this.heap.pop();
|
|
10731
|
+
if (this.size > 0) {
|
|
10732
|
+
this.heap[0] = bottom;
|
|
10733
|
+
this.sinkDown(0);
|
|
10734
|
+
}
|
|
10735
|
+
return top;
|
|
10736
|
+
}
|
|
10737
|
+
/**
|
|
10738
|
+
* Replace the root element with a new value and re-heapify.
|
|
10739
|
+
* Faster than pop() followed by push().
|
|
10740
|
+
*/
|
|
10741
|
+
replace(value) {
|
|
10742
|
+
const top = this.heap[0];
|
|
10743
|
+
this.heap[0] = value;
|
|
10744
|
+
this.sinkDown(0);
|
|
10745
|
+
return top;
|
|
10746
|
+
}
|
|
10747
|
+
toArray() {
|
|
10748
|
+
return [...this.heap];
|
|
10749
|
+
}
|
|
10750
|
+
bubbleUp(index) {
|
|
10751
|
+
while (index > 0) {
|
|
10752
|
+
const parentIndex = Math.floor((index - 1) / 2);
|
|
10753
|
+
if (this.comparator(this.heap[index], this.heap[parentIndex]) >= 0) break;
|
|
10754
|
+
[this.heap[index], this.heap[parentIndex]] = [this.heap[parentIndex], this.heap[index]];
|
|
10755
|
+
index = parentIndex;
|
|
10756
|
+
}
|
|
10757
|
+
}
|
|
10758
|
+
sinkDown(index) {
|
|
10759
|
+
while (true) {
|
|
10760
|
+
let smallest = index;
|
|
10761
|
+
const left = 2 * index + 1;
|
|
10762
|
+
const right = 2 * index + 2;
|
|
10763
|
+
if (left < this.size && this.comparator(this.heap[left], this.heap[smallest]) < 0) {
|
|
10764
|
+
smallest = left;
|
|
10765
|
+
}
|
|
10766
|
+
if (right < this.size && this.comparator(this.heap[right], this.heap[smallest]) < 0) {
|
|
10767
|
+
smallest = right;
|
|
10768
|
+
}
|
|
10769
|
+
if (smallest === index) break;
|
|
10770
|
+
[this.heap[index], this.heap[smallest]] = [this.heap[smallest], this.heap[index]];
|
|
10771
|
+
index = smallest;
|
|
10772
|
+
}
|
|
10773
|
+
}
|
|
10774
|
+
};
|
|
10775
|
+
|
|
10776
|
+
// src/core/QueryManager.ts
|
|
10777
|
+
var QueryManager = class {
|
|
10778
|
+
constructor(api, optimizer) {
|
|
10779
|
+
this.api = api;
|
|
10780
|
+
this.optimizer = optimizer;
|
|
10781
|
+
}
|
|
10782
|
+
operatorConverters = {
|
|
10783
|
+
equal: "primaryEqual",
|
|
10784
|
+
notEqual: "primaryNotEqual",
|
|
10785
|
+
lt: "primaryLt",
|
|
10786
|
+
lte: "primaryLte",
|
|
10787
|
+
gt: "primaryGt",
|
|
10788
|
+
gte: "primaryGte",
|
|
10789
|
+
or: "primaryOr",
|
|
10790
|
+
like: "like"
|
|
10791
|
+
};
|
|
10792
|
+
/**
|
|
10793
|
+
* Transforms a query object into a verbose query object
|
|
10794
|
+
*/
|
|
10795
|
+
verboseQuery(query) {
|
|
10796
|
+
const result = {};
|
|
10797
|
+
for (const field in query) {
|
|
10798
|
+
const conditions = query[field];
|
|
10799
|
+
let newConditions;
|
|
10800
|
+
if (typeof conditions !== "object" || conditions === null) {
|
|
10801
|
+
newConditions = { primaryEqual: { v: conditions } };
|
|
10802
|
+
} else {
|
|
10803
|
+
newConditions = {};
|
|
10804
|
+
for (const operator in conditions) {
|
|
10805
|
+
const before = operator;
|
|
10806
|
+
const after = this.operatorConverters[before];
|
|
10807
|
+
const v = conditions[before];
|
|
10808
|
+
if (!after) {
|
|
10809
|
+
if (before === "match") {
|
|
10810
|
+
newConditions[before] = v;
|
|
10811
|
+
}
|
|
10812
|
+
continue;
|
|
10813
|
+
}
|
|
10814
|
+
if (before === "or" && Array.isArray(v)) {
|
|
10815
|
+
newConditions[after] = v.map((val) => ({ v: val }));
|
|
10816
|
+
} else if (before === "like") {
|
|
10817
|
+
newConditions[after] = v;
|
|
10818
|
+
} else {
|
|
10819
|
+
newConditions[after] = { v };
|
|
10820
|
+
}
|
|
10821
|
+
}
|
|
10822
|
+
}
|
|
10823
|
+
result[field] = newConditions;
|
|
10824
|
+
}
|
|
10825
|
+
return result;
|
|
10826
|
+
}
|
|
10827
|
+
getFreeMemoryChunkSize() {
|
|
10828
|
+
const freeMem = os.freemem();
|
|
10829
|
+
const safeLimit = freeMem * 0.2;
|
|
10830
|
+
const verySmallChunkSize = safeLimit * 0.05;
|
|
10831
|
+
const smallChunkSize = safeLimit * 0.3;
|
|
10832
|
+
return { verySmallChunkSize, smallChunkSize };
|
|
10833
|
+
}
|
|
10834
|
+
async *applyCandidateByFTSStream(candidate, matchedTokens, filterValues, order) {
|
|
10835
|
+
const keys = /* @__PURE__ */ new Set();
|
|
10836
|
+
for (let i = 0, len = matchedTokens.length; i < len; i++) {
|
|
10837
|
+
const token = matchedTokens[i];
|
|
10838
|
+
for await (const pair of candidate.tree.whereStream(
|
|
10839
|
+
{ primaryEqual: { v: token } },
|
|
10840
|
+
{ order }
|
|
10841
|
+
)) {
|
|
10842
|
+
const pk = pair[1].k;
|
|
10843
|
+
if (filterValues && !filterValues.has(pk)) continue;
|
|
10844
|
+
if (!keys.has(pk)) {
|
|
10845
|
+
keys.add(pk);
|
|
10846
|
+
yield pk;
|
|
10847
|
+
}
|
|
10848
|
+
}
|
|
10849
|
+
}
|
|
10850
|
+
}
|
|
10851
|
+
applyCandidateStream(candidate, filterValues, order) {
|
|
10852
|
+
return candidate.tree.keysStream(
|
|
10853
|
+
candidate.condition,
|
|
10854
|
+
{ filterValues, order }
|
|
10855
|
+
);
|
|
10856
|
+
}
|
|
10857
|
+
async getKeys(query, orderBy, sortOrder = "asc") {
|
|
10858
|
+
const isQueryEmpty = Object.keys(query).length === 0;
|
|
10859
|
+
const normalizedQuery = isQueryEmpty ? { _id: { gte: 0 } } : query;
|
|
10860
|
+
const selectivity = await this.optimizer.getSelectivityCandidate(
|
|
10861
|
+
this.verboseQuery(normalizedQuery),
|
|
10862
|
+
orderBy
|
|
10863
|
+
);
|
|
10864
|
+
if (!selectivity) return new Float64Array(0);
|
|
10865
|
+
const { driver, others, rollback } = selectivity;
|
|
10866
|
+
const useIndexOrder = orderBy === void 0 || driver.isIndexOrderSupported;
|
|
10867
|
+
const candidates = [driver, ...others];
|
|
10868
|
+
let keys = void 0;
|
|
10869
|
+
for (let i = 0, len = candidates.length; i < len; i++) {
|
|
10870
|
+
const candidate = candidates[i];
|
|
10871
|
+
const currentOrder = useIndexOrder ? sortOrder : void 0;
|
|
10872
|
+
if (candidate.isFtsMatch && candidate.matchTokens && candidate.matchTokens.length > 0) {
|
|
10873
|
+
const stream = this.applyCandidateByFTSStream(
|
|
10874
|
+
candidate,
|
|
10875
|
+
candidate.matchTokens,
|
|
10876
|
+
keys,
|
|
10877
|
+
currentOrder
|
|
10878
|
+
);
|
|
10879
|
+
keys = /* @__PURE__ */ new Set();
|
|
10880
|
+
for await (const pk of stream) keys.add(pk);
|
|
10881
|
+
} else {
|
|
10882
|
+
const stream = this.applyCandidateStream(candidate, keys, currentOrder);
|
|
10883
|
+
keys = /* @__PURE__ */ new Set();
|
|
10884
|
+
for await (const pk of stream) keys.add(pk);
|
|
10885
|
+
}
|
|
10886
|
+
}
|
|
10887
|
+
rollback();
|
|
10888
|
+
return new Float64Array(Array.from(keys || []));
|
|
10889
|
+
}
|
|
10890
|
+
async getDriverKeys(query, orderBy, sortOrder = "asc") {
|
|
10891
|
+
const isQueryEmpty = Object.keys(query).length === 0;
|
|
10892
|
+
const normalizedQuery = isQueryEmpty ? { _id: { gte: 0 } } : query;
|
|
10893
|
+
const selectivity = await this.optimizer.getSelectivityCandidate(
|
|
10894
|
+
this.verboseQuery(normalizedQuery),
|
|
10895
|
+
orderBy
|
|
10896
|
+
);
|
|
10897
|
+
if (!selectivity) return null;
|
|
10898
|
+
const { driver, others, compositeVerifyConditions, rollback } = selectivity;
|
|
10899
|
+
const useIndexOrder = orderBy === void 0 || driver.isIndexOrderSupported;
|
|
10900
|
+
const currentOrder = useIndexOrder ? sortOrder : void 0;
|
|
10901
|
+
let keysStream;
|
|
10902
|
+
if (driver.isFtsMatch && driver.matchTokens && driver.matchTokens.length > 0) {
|
|
10903
|
+
keysStream = this.applyCandidateByFTSStream(
|
|
10904
|
+
driver,
|
|
10905
|
+
driver.matchTokens,
|
|
10906
|
+
void 0,
|
|
10907
|
+
currentOrder
|
|
10771
10908
|
);
|
|
10772
|
-
|
|
10773
|
-
this.
|
|
10774
|
-
|
|
10775
|
-
|
|
10776
|
-
|
|
10909
|
+
} else {
|
|
10910
|
+
keysStream = this.applyCandidateStream(driver, void 0, currentOrder);
|
|
10911
|
+
}
|
|
10912
|
+
return {
|
|
10913
|
+
keysStream,
|
|
10914
|
+
others,
|
|
10915
|
+
compositeVerifyConditions,
|
|
10916
|
+
isDriverOrderByField: useIndexOrder,
|
|
10917
|
+
rollback
|
|
10918
|
+
};
|
|
10919
|
+
}
|
|
10920
|
+
verifyFts(doc, ftsConditions) {
|
|
10921
|
+
const flatDoc = this.api.flattenDocument(doc);
|
|
10922
|
+
for (let i = 0, len = ftsConditions.length; i < len; i++) {
|
|
10923
|
+
const { field, matchTokens } = ftsConditions[i];
|
|
10924
|
+
const docValue = flatDoc[field];
|
|
10925
|
+
if (typeof docValue !== "string") return false;
|
|
10926
|
+
for (let j = 0, jLen = matchTokens.length; j < jLen; j++) {
|
|
10927
|
+
const token = matchTokens[j];
|
|
10928
|
+
if (!docValue.includes(token)) return false;
|
|
10777
10929
|
}
|
|
10778
|
-
}
|
|
10930
|
+
}
|
|
10931
|
+
return true;
|
|
10932
|
+
}
|
|
10933
|
+
verifyCompositeConditions(doc, conditions) {
|
|
10934
|
+
if (conditions.length === 0) return true;
|
|
10935
|
+
const flatDoc = this.api.flattenDocument(doc);
|
|
10936
|
+
for (let i = 0, len = conditions.length; i < len; i++) {
|
|
10937
|
+
const { field, condition } = conditions[i];
|
|
10938
|
+
const docValue = flatDoc[field];
|
|
10939
|
+
if (docValue === void 0) return false;
|
|
10940
|
+
if (!this.verifyValue(docValue, condition)) return false;
|
|
10941
|
+
}
|
|
10942
|
+
return true;
|
|
10943
|
+
}
|
|
10944
|
+
verifyValue(value, condition) {
|
|
10945
|
+
if (typeof condition !== "object" || condition === null) {
|
|
10946
|
+
return value === condition;
|
|
10947
|
+
}
|
|
10948
|
+
if ("primaryEqual" in condition) {
|
|
10949
|
+
return value === condition.primaryEqual?.v;
|
|
10950
|
+
}
|
|
10951
|
+
if ("primaryNotEqual" in condition) {
|
|
10952
|
+
return value !== condition.primaryNotEqual?.v;
|
|
10953
|
+
}
|
|
10954
|
+
if ("primaryLt" in condition) {
|
|
10955
|
+
return value !== null && condition.primaryLt?.v !== void 0 && value < condition.primaryLt.v;
|
|
10956
|
+
}
|
|
10957
|
+
if ("primaryLte" in condition) {
|
|
10958
|
+
return value !== null && condition.primaryLte?.v !== void 0 && value <= condition.primaryLte.v;
|
|
10959
|
+
}
|
|
10960
|
+
if ("primaryGt" in condition) {
|
|
10961
|
+
return value !== null && condition.primaryGt?.v !== void 0 && value > condition.primaryGt.v;
|
|
10962
|
+
}
|
|
10963
|
+
if ("primaryGte" in condition) {
|
|
10964
|
+
return value !== null && condition.primaryGte?.v !== void 0 && value >= condition.primaryGte.v;
|
|
10965
|
+
}
|
|
10966
|
+
if ("primaryOr" in condition && Array.isArray(condition.primaryOr)) {
|
|
10967
|
+
return condition.primaryOr.some((c) => value === c?.v);
|
|
10968
|
+
}
|
|
10969
|
+
return true;
|
|
10970
|
+
}
|
|
10971
|
+
adjustChunkSize(currentChunkSize, chunkTotalSize) {
|
|
10972
|
+
if (chunkTotalSize <= 0) return currentChunkSize;
|
|
10973
|
+
const { verySmallChunkSize, smallChunkSize } = this.getFreeMemoryChunkSize();
|
|
10974
|
+
if (chunkTotalSize < verySmallChunkSize) return currentChunkSize * 2;
|
|
10975
|
+
if (chunkTotalSize > smallChunkSize) return Math.max(Math.floor(currentChunkSize / 2), 20);
|
|
10976
|
+
return currentChunkSize;
|
|
10977
|
+
}
|
|
10978
|
+
async *processChunkedKeysWithVerify(keysStream, startIdx, initialChunkSize, limit, ftsConditions, compositeVerifyConditions, others, tx) {
|
|
10979
|
+
const verifyOthers = others.filter((o) => !o.isFtsMatch);
|
|
10980
|
+
const isFts = ftsConditions.length > 0;
|
|
10981
|
+
const isCompositeVerify = compositeVerifyConditions.length > 0;
|
|
10982
|
+
const isVerifyOthers = verifyOthers.length > 0;
|
|
10983
|
+
const isInfinityLimit = !isFinite(limit);
|
|
10984
|
+
const isReadQuotaLimited = !isInfinityLimit || !isCompositeVerify || !isVerifyOthers || !isFts;
|
|
10985
|
+
let currentChunkSize = isReadQuotaLimited ? limit : initialChunkSize;
|
|
10986
|
+
let chunk = [];
|
|
10987
|
+
let chunkSize = 0;
|
|
10988
|
+
let dropped = 0;
|
|
10989
|
+
const processChunk = async (pks) => {
|
|
10990
|
+
const docs = [];
|
|
10991
|
+
const rawResults = await this.api.selectMany(new Float64Array(pks), false, tx);
|
|
10992
|
+
let chunkTotalSize = 0;
|
|
10993
|
+
for (let j = 0, len = rawResults.length; j < len; j++) {
|
|
10994
|
+
const s = rawResults[j];
|
|
10995
|
+
if (!s) continue;
|
|
10996
|
+
const doc = JSON.parse(s);
|
|
10997
|
+
chunkTotalSize += s.length * 2;
|
|
10998
|
+
if (isFts && !this.verifyFts(doc, ftsConditions)) continue;
|
|
10999
|
+
if (isCompositeVerify && this.verifyCompositeConditions(doc, compositeVerifyConditions) === false) continue;
|
|
11000
|
+
if (isVerifyOthers) {
|
|
11001
|
+
const flatDoc = this.api.flattenDocument(doc);
|
|
11002
|
+
let passed = true;
|
|
11003
|
+
for (let k = 0, kLen = verifyOthers.length; k < kLen; k++) {
|
|
11004
|
+
const other = verifyOthers[k];
|
|
11005
|
+
const fieldValue = flatDoc[other.field];
|
|
11006
|
+
if (fieldValue === void 0) {
|
|
11007
|
+
passed = false;
|
|
11008
|
+
break;
|
|
11009
|
+
}
|
|
11010
|
+
const treeValue = { k: doc._id, v: fieldValue };
|
|
11011
|
+
if (!other.tree.verify(treeValue, other.condition)) {
|
|
11012
|
+
passed = false;
|
|
11013
|
+
break;
|
|
11014
|
+
}
|
|
11015
|
+
}
|
|
11016
|
+
if (!passed) continue;
|
|
11017
|
+
}
|
|
11018
|
+
docs.push(doc);
|
|
11019
|
+
}
|
|
11020
|
+
if (!isReadQuotaLimited) {
|
|
11021
|
+
currentChunkSize = this.adjustChunkSize(currentChunkSize, chunkTotalSize);
|
|
11022
|
+
}
|
|
11023
|
+
return docs;
|
|
11024
|
+
};
|
|
11025
|
+
for await (const pk of keysStream) {
|
|
11026
|
+
if (dropped < startIdx) {
|
|
11027
|
+
dropped++;
|
|
11028
|
+
continue;
|
|
11029
|
+
}
|
|
11030
|
+
chunk.push(pk);
|
|
11031
|
+
chunkSize++;
|
|
11032
|
+
if (chunkSize >= currentChunkSize) {
|
|
11033
|
+
const docs = await processChunk(chunk);
|
|
11034
|
+
for (let j = 0, dLen = docs.length; j < dLen; j++) yield docs[j];
|
|
11035
|
+
chunk = [];
|
|
11036
|
+
chunkSize = 0;
|
|
11037
|
+
}
|
|
11038
|
+
}
|
|
11039
|
+
if (chunkSize > 0) {
|
|
11040
|
+
const docs = await processChunk(chunk);
|
|
11041
|
+
for (let j = 0, dLen = docs.length; j < dLen; j++) yield docs[j];
|
|
11042
|
+
}
|
|
10779
11043
|
}
|
|
10780
11044
|
/**
|
|
10781
|
-
*
|
|
10782
|
-
*
|
|
10783
|
-
*
|
|
10784
|
-
* @
|
|
11045
|
+
* Count documents from the database that match the query
|
|
11046
|
+
* @param query The query to use
|
|
11047
|
+
* @param tx The transaction to use
|
|
11048
|
+
* @returns The number of documents that match the query
|
|
10785
11049
|
*/
|
|
10786
|
-
async
|
|
10787
|
-
|
|
10788
|
-
|
|
10789
|
-
|
|
10790
|
-
|
|
10791
|
-
|
|
10792
|
-
|
|
11050
|
+
async countDocuments(query, tx) {
|
|
11051
|
+
return this.api.runWithDefault(async (tx2) => {
|
|
11052
|
+
const pks = await this.getKeys(query);
|
|
11053
|
+
return pks.length;
|
|
11054
|
+
}, tx);
|
|
11055
|
+
}
|
|
11056
|
+
selectDocuments(query, options = {}, tx) {
|
|
11057
|
+
for (const field of Object.keys(query)) {
|
|
11058
|
+
if (!this.api.indexedFields.has(field)) {
|
|
11059
|
+
throw new Error(`Query field "${field}" is not indexed. Available indexed fields: ${Array.from(this.api.indexedFields).join(", ")}`);
|
|
11060
|
+
}
|
|
10793
11061
|
}
|
|
10794
|
-
|
|
10795
|
-
|
|
11062
|
+
const orderBy = options.orderBy;
|
|
11063
|
+
if (orderBy !== void 0 && !this.api.indexedFields.has(orderBy)) {
|
|
11064
|
+
throw new Error(`orderBy field "${orderBy}" is not indexed. Available indexed fields: ${Array.from(this.api.indexedFields).join(", ")}`);
|
|
10796
11065
|
}
|
|
10797
|
-
|
|
10798
|
-
|
|
10799
|
-
|
|
10800
|
-
|
|
10801
|
-
|
|
10802
|
-
|
|
10803
|
-
|
|
10804
|
-
|
|
10805
|
-
|
|
10806
|
-
|
|
10807
|
-
const
|
|
10808
|
-
if (
|
|
10809
|
-
const
|
|
10810
|
-
|
|
10811
|
-
|
|
10812
|
-
if (
|
|
10813
|
-
|
|
11066
|
+
const {
|
|
11067
|
+
limit = Infinity,
|
|
11068
|
+
offset = 0,
|
|
11069
|
+
sortOrder = "asc",
|
|
11070
|
+
orderBy: orderByField
|
|
11071
|
+
} = options;
|
|
11072
|
+
const self = this;
|
|
11073
|
+
const stream = () => this.api.streamWithDefault(async function* (tx2) {
|
|
11074
|
+
const ftsConditions = [];
|
|
11075
|
+
for (const field in query) {
|
|
11076
|
+
const q = query[field];
|
|
11077
|
+
if (q && typeof q === "object" && "match" in q && typeof q.match === "string") {
|
|
11078
|
+
const indexNames = self.api.indexManager.fieldToIndices.get(field) || [];
|
|
11079
|
+
for (const indexName of indexNames) {
|
|
11080
|
+
const config = self.api.indexManager.registeredIndices.get(indexName);
|
|
11081
|
+
if (config && config.type === "fts") {
|
|
11082
|
+
const ftsConfig = self.api.indexManager.getFtsConfig(config);
|
|
11083
|
+
if (ftsConfig) {
|
|
11084
|
+
ftsConditions.push({ field, matchTokens: tokenize(q.match, ftsConfig) });
|
|
11085
|
+
}
|
|
11086
|
+
break;
|
|
11087
|
+
}
|
|
11088
|
+
}
|
|
11089
|
+
}
|
|
11090
|
+
}
|
|
11091
|
+
const driverResult = await self.getDriverKeys(query, orderByField, sortOrder);
|
|
11092
|
+
if (!driverResult) return;
|
|
11093
|
+
const { keysStream, others, compositeVerifyConditions, isDriverOrderByField, rollback } = driverResult;
|
|
11094
|
+
const initialChunkSize = self.api.options.pageSize;
|
|
11095
|
+
try {
|
|
11096
|
+
if (!isDriverOrderByField && orderByField) {
|
|
11097
|
+
const topK = limit === Infinity ? Infinity : offset + limit;
|
|
11098
|
+
let heap = null;
|
|
11099
|
+
if (topK !== Infinity) {
|
|
11100
|
+
heap = new BinaryHeap((a, b) => {
|
|
11101
|
+
const aVal = a[orderByField] ?? a._id;
|
|
11102
|
+
const bVal = b[orderByField] ?? b._id;
|
|
11103
|
+
const cmp = aVal < bVal ? -1 : aVal > bVal ? 1 : 0;
|
|
11104
|
+
return sortOrder === "asc" ? -cmp : cmp;
|
|
11105
|
+
});
|
|
11106
|
+
}
|
|
11107
|
+
const results = [];
|
|
11108
|
+
for await (const doc of self.processChunkedKeysWithVerify(
|
|
11109
|
+
keysStream,
|
|
11110
|
+
0,
|
|
11111
|
+
initialChunkSize,
|
|
11112
|
+
Infinity,
|
|
11113
|
+
ftsConditions,
|
|
11114
|
+
compositeVerifyConditions,
|
|
11115
|
+
others,
|
|
11116
|
+
tx2
|
|
11117
|
+
)) {
|
|
11118
|
+
if (heap) {
|
|
11119
|
+
if (heap.size < topK) heap.push(doc);
|
|
11120
|
+
else {
|
|
11121
|
+
const top = heap.peek();
|
|
11122
|
+
if (top) {
|
|
11123
|
+
const aVal = doc[orderByField] ?? doc._id;
|
|
11124
|
+
const bVal = top[orderByField] ?? top._id;
|
|
11125
|
+
const cmp = aVal < bVal ? -1 : aVal > bVal ? 1 : 0;
|
|
11126
|
+
if (sortOrder === "asc" ? cmp < 0 : cmp > 0) heap.replace(doc);
|
|
11127
|
+
}
|
|
11128
|
+
}
|
|
11129
|
+
} else {
|
|
11130
|
+
results.push(doc);
|
|
11131
|
+
}
|
|
11132
|
+
}
|
|
11133
|
+
const finalDocs = heap ? heap.toArray() : results;
|
|
11134
|
+
finalDocs.sort((a, b) => {
|
|
11135
|
+
const aVal = a[orderByField] ?? a._id;
|
|
11136
|
+
const bVal = b[orderByField] ?? b._id;
|
|
11137
|
+
const cmp = aVal < bVal ? -1 : aVal > bVal ? 1 : 0;
|
|
11138
|
+
return sortOrder === "asc" ? cmp : -cmp;
|
|
11139
|
+
});
|
|
11140
|
+
const end = limit === Infinity ? void 0 : offset + limit;
|
|
11141
|
+
const limitedResults = finalDocs.slice(offset, end);
|
|
11142
|
+
for (let j = 0, len = limitedResults.length; j < len; j++) {
|
|
11143
|
+
yield limitedResults[j];
|
|
11144
|
+
}
|
|
11145
|
+
} else {
|
|
11146
|
+
const hasFilters = ftsConditions.length > 0 || compositeVerifyConditions.length > 0 || others.length > 0;
|
|
11147
|
+
const startIdx = hasFilters ? 0 : offset;
|
|
11148
|
+
let yieldedCount = 0;
|
|
11149
|
+
let skippedCount = hasFilters ? 0 : offset;
|
|
11150
|
+
for await (const doc of self.processChunkedKeysWithVerify(
|
|
11151
|
+
keysStream,
|
|
11152
|
+
startIdx,
|
|
11153
|
+
initialChunkSize,
|
|
11154
|
+
limit,
|
|
11155
|
+
ftsConditions,
|
|
11156
|
+
compositeVerifyConditions,
|
|
11157
|
+
others,
|
|
11158
|
+
tx2
|
|
11159
|
+
)) {
|
|
11160
|
+
if (skippedCount < offset) {
|
|
11161
|
+
skippedCount++;
|
|
11162
|
+
continue;
|
|
10814
11163
|
}
|
|
10815
|
-
|
|
10816
|
-
|
|
11164
|
+
if (yieldedCount >= limit) break;
|
|
11165
|
+
yield doc;
|
|
11166
|
+
yieldedCount++;
|
|
10817
11167
|
}
|
|
10818
11168
|
}
|
|
11169
|
+
} finally {
|
|
11170
|
+
rollback();
|
|
10819
11171
|
}
|
|
10820
|
-
this.trees.delete(name);
|
|
10821
11172
|
}, tx);
|
|
11173
|
+
const drain = async () => {
|
|
11174
|
+
const result = [];
|
|
11175
|
+
for await (const document of stream()) {
|
|
11176
|
+
result.push(document);
|
|
11177
|
+
}
|
|
11178
|
+
return result;
|
|
11179
|
+
};
|
|
11180
|
+
return { stream, drain };
|
|
11181
|
+
}
|
|
11182
|
+
};
|
|
11183
|
+
|
|
11184
|
+
// src/core/IndexManager.ts
|
|
11185
|
+
var import_dataply3 = __toESM(require_cjs());
|
|
11186
|
+
|
|
11187
|
+
// src/core/bptree/documentStrategy.ts
|
|
11188
|
+
var import_dataply2 = __toESM(require_cjs());
|
|
11189
|
+
var DocumentSerializeStrategyAsync = class extends import_dataply2.SerializeStrategyAsync {
|
|
11190
|
+
constructor(order, api, txContext, treeKey) {
|
|
11191
|
+
super(order);
|
|
11192
|
+
this.api = api;
|
|
11193
|
+
this.txContext = txContext;
|
|
11194
|
+
this.treeKey = treeKey;
|
|
10822
11195
|
}
|
|
10823
11196
|
/**
|
|
10824
|
-
*
|
|
11197
|
+
* readHead에서 할당된 headPk를 캐싱하여
|
|
11198
|
+
* writeHead에서 AsyncLocalStorage 컨텍스트 유실 시에도 사용할 수 있도록 함
|
|
10825
11199
|
*/
|
|
10826
|
-
|
|
10827
|
-
|
|
10828
|
-
|
|
11200
|
+
cachedHeadPk = null;
|
|
11201
|
+
async id(isLeaf) {
|
|
11202
|
+
const tx = this.txContext.get();
|
|
11203
|
+
const pk = await this.api.insertAsOverflow("__BPTREE_NODE_PLACEHOLDER__", false, tx);
|
|
11204
|
+
return pk + "";
|
|
11205
|
+
}
|
|
11206
|
+
async read(id) {
|
|
11207
|
+
const tx = this.txContext.get();
|
|
11208
|
+
const row = await this.api.select(Number(id), false, tx);
|
|
11209
|
+
if (row === null || row === "" || row.startsWith("__BPTREE_")) {
|
|
11210
|
+
throw new Error(`Node not found or empty with ID: ${id}`);
|
|
10829
11211
|
}
|
|
10830
|
-
|
|
10831
|
-
|
|
11212
|
+
return JSON.parse(row);
|
|
11213
|
+
}
|
|
11214
|
+
async write(id, node) {
|
|
11215
|
+
const tx = this.txContext.get();
|
|
11216
|
+
const json = JSON.stringify(node);
|
|
11217
|
+
await this.api.update(+id, json, tx);
|
|
11218
|
+
}
|
|
11219
|
+
async delete(id) {
|
|
11220
|
+
const tx = this.txContext.get();
|
|
11221
|
+
await this.api.delete(+id, false, tx);
|
|
11222
|
+
}
|
|
11223
|
+
async readHead() {
|
|
11224
|
+
const tx = this.txContext.get();
|
|
11225
|
+
const metadata = await this.api.getDocumentInnerMetadata(tx);
|
|
11226
|
+
const indexInfo = metadata.indices[this.treeKey];
|
|
11227
|
+
if (!indexInfo) return null;
|
|
11228
|
+
const headPk = indexInfo[0];
|
|
11229
|
+
if (headPk === -1) {
|
|
11230
|
+
const pk = await this.api.insertAsOverflow("__BPTREE_HEAD_PLACEHOLDER__", false, tx);
|
|
11231
|
+
metadata.indices[this.treeKey][0] = pk;
|
|
11232
|
+
await this.api.updateDocumentInnerMetadata(metadata, tx);
|
|
11233
|
+
this.cachedHeadPk = pk;
|
|
11234
|
+
return null;
|
|
10832
11235
|
}
|
|
10833
|
-
|
|
10834
|
-
|
|
10835
|
-
|
|
11236
|
+
this.cachedHeadPk = headPk;
|
|
11237
|
+
const row = await this.api.select(headPk, false, tx);
|
|
11238
|
+
if (row === null || row === "" || row.startsWith("__BPTREE_")) return null;
|
|
11239
|
+
return JSON.parse(row);
|
|
11240
|
+
}
|
|
11241
|
+
async writeHead(head) {
|
|
11242
|
+
const tx = this.txContext.get();
|
|
11243
|
+
let headPk = this.cachedHeadPk;
|
|
11244
|
+
if (headPk === null) {
|
|
11245
|
+
const metadata = await this.api.getDocumentInnerMetadata(tx);
|
|
11246
|
+
const indexInfo = metadata.indices[this.treeKey];
|
|
11247
|
+
if (!indexInfo) {
|
|
11248
|
+
throw new Error(`Index info not found for tree: ${this.treeKey}. Initialization should be handled outside.`);
|
|
10836
11249
|
}
|
|
10837
|
-
|
|
10838
|
-
|
|
10839
|
-
|
|
11250
|
+
headPk = indexInfo[0];
|
|
11251
|
+
}
|
|
11252
|
+
const json = JSON.stringify(head);
|
|
11253
|
+
await this.api.update(headPk, json, tx);
|
|
11254
|
+
}
|
|
11255
|
+
};
|
|
11256
|
+
|
|
11257
|
+
// src/core/IndexManager.ts
|
|
11258
|
+
var IndexManager = class {
|
|
11259
|
+
constructor(api) {
|
|
11260
|
+
this.api = api;
|
|
11261
|
+
this.trees = /* @__PURE__ */ new Map();
|
|
11262
|
+
this.indexedFields = /* @__PURE__ */ new Set(["_id"]);
|
|
11263
|
+
}
|
|
11264
|
+
indices = {};
|
|
11265
|
+
trees = /* @__PURE__ */ new Map();
|
|
11266
|
+
indexedFields;
|
|
11267
|
+
/**
|
|
11268
|
+
* Registered indices via createIndex() (before init)
|
|
11269
|
+
* Key: index name, Value: index configuration
|
|
11270
|
+
*/
|
|
11271
|
+
pendingCreateIndices = /* @__PURE__ */ new Map();
|
|
11272
|
+
/**
|
|
11273
|
+
* Resolved index configurations after init.
|
|
11274
|
+
* Key: index name, Value: index config (from metadata)
|
|
11275
|
+
*/
|
|
11276
|
+
registeredIndices = /* @__PURE__ */ new Map();
|
|
11277
|
+
/**
|
|
11278
|
+
* Maps field name → index names that cover this field.
|
|
11279
|
+
* Used for query resolution.
|
|
11280
|
+
*/
|
|
11281
|
+
fieldToIndices = /* @__PURE__ */ new Map();
|
|
11282
|
+
pendingBackfillFields = [];
|
|
11283
|
+
/**
|
|
11284
|
+
* Validate and apply indices from DB metadata and pending indices.
|
|
11285
|
+
* Called during database initialization.
|
|
11286
|
+
*/
|
|
11287
|
+
async initializeIndices(metadata, isNewlyCreated, tx) {
|
|
11288
|
+
const targetIndices = /* @__PURE__ */ new Map([
|
|
11289
|
+
["_id", { type: "btree", fields: ["_id"] }]
|
|
11290
|
+
]);
|
|
11291
|
+
for (const [name, info] of Object.entries(metadata.indices)) {
|
|
11292
|
+
targetIndices.set(name, info[1]);
|
|
11293
|
+
}
|
|
11294
|
+
for (const [name, option] of this.pendingCreateIndices) {
|
|
11295
|
+
const config = this.toIndexMetaConfig(option);
|
|
11296
|
+
targetIndices.set(name, config);
|
|
11297
|
+
}
|
|
11298
|
+
const backfillTargets = [];
|
|
11299
|
+
let isMetadataChanged = false;
|
|
11300
|
+
for (const [indexName, config] of targetIndices) {
|
|
11301
|
+
const existingIndex = metadata.indices[indexName];
|
|
11302
|
+
if (!existingIndex) {
|
|
11303
|
+
metadata.indices[indexName] = [-1, config];
|
|
11304
|
+
isMetadataChanged = true;
|
|
11305
|
+
if (!isNewlyCreated) {
|
|
11306
|
+
backfillTargets.push(indexName);
|
|
11307
|
+
}
|
|
11308
|
+
} else {
|
|
11309
|
+
const [_pk, existingConfig] = existingIndex;
|
|
11310
|
+
if (JSON.stringify(existingConfig) !== JSON.stringify(config)) {
|
|
11311
|
+
metadata.indices[indexName] = [_pk, config];
|
|
11312
|
+
isMetadataChanged = true;
|
|
11313
|
+
if (!isNewlyCreated) {
|
|
11314
|
+
backfillTargets.push(indexName);
|
|
11315
|
+
}
|
|
10840
11316
|
}
|
|
10841
11317
|
}
|
|
10842
|
-
return {
|
|
10843
|
-
type: "btree",
|
|
10844
|
-
fields: option.fields
|
|
10845
|
-
};
|
|
10846
11318
|
}
|
|
10847
|
-
if (
|
|
10848
|
-
|
|
10849
|
-
|
|
10850
|
-
|
|
10851
|
-
|
|
10852
|
-
|
|
10853
|
-
|
|
11319
|
+
if (isMetadataChanged) {
|
|
11320
|
+
await this.api.updateDocumentInnerMetadata(metadata, tx);
|
|
11321
|
+
}
|
|
11322
|
+
this.indices = metadata.indices;
|
|
11323
|
+
this.registeredIndices = /* @__PURE__ */ new Map();
|
|
11324
|
+
this.fieldToIndices = /* @__PURE__ */ new Map();
|
|
11325
|
+
for (const [indexName, config] of targetIndices) {
|
|
11326
|
+
this.registeredIndices.set(indexName, config);
|
|
11327
|
+
const fields = this.getFieldsFromConfig(config);
|
|
11328
|
+
for (const field of fields) {
|
|
11329
|
+
this.indexedFields.add(field);
|
|
11330
|
+
if (!this.fieldToIndices.has(field)) {
|
|
11331
|
+
this.fieldToIndices.set(field, []);
|
|
10854
11332
|
}
|
|
10855
|
-
|
|
10856
|
-
type: "fts",
|
|
10857
|
-
fields: option.fields,
|
|
10858
|
-
tokenizer: "ngram",
|
|
10859
|
-
gramSize: option.gramSize
|
|
10860
|
-
};
|
|
11333
|
+
this.fieldToIndices.get(field).push(indexName);
|
|
10861
11334
|
}
|
|
10862
|
-
return {
|
|
10863
|
-
type: "fts",
|
|
10864
|
-
fields: option.fields,
|
|
10865
|
-
tokenizer: "whitespace"
|
|
10866
|
-
};
|
|
10867
11335
|
}
|
|
10868
|
-
|
|
11336
|
+
for (const indexName of targetIndices.keys()) {
|
|
11337
|
+
if (metadata.indices[indexName]) {
|
|
11338
|
+
const tree = new import_dataply3.BPTreeAsync(
|
|
11339
|
+
new DocumentSerializeStrategyAsync(
|
|
11340
|
+
this.api.rowTableEngine.order,
|
|
11341
|
+
this.api,
|
|
11342
|
+
this.api.txContext,
|
|
11343
|
+
indexName
|
|
11344
|
+
),
|
|
11345
|
+
this.api.comparator
|
|
11346
|
+
);
|
|
11347
|
+
await tree.init();
|
|
11348
|
+
this.trees.set(indexName, tree);
|
|
11349
|
+
}
|
|
11350
|
+
}
|
|
11351
|
+
this.pendingBackfillFields = backfillTargets;
|
|
11352
|
+
return isMetadataChanged;
|
|
10869
11353
|
}
|
|
10870
11354
|
/**
|
|
10871
|
-
*
|
|
11355
|
+
* Register an index. If called before init(), queues it.
|
|
10872
11356
|
*/
|
|
10873
|
-
|
|
10874
|
-
if (
|
|
10875
|
-
|
|
10876
|
-
|
|
10877
|
-
if (config.type === "fts") {
|
|
10878
|
-
return [config.fields];
|
|
11357
|
+
async registerIndex(name, option, tx) {
|
|
11358
|
+
if (!this.api.isDocInitialized) {
|
|
11359
|
+
this.pendingCreateIndices.set(name, option);
|
|
11360
|
+
return;
|
|
10879
11361
|
}
|
|
10880
|
-
|
|
11362
|
+
await this.registerIndexRuntime(name, option, tx);
|
|
10881
11363
|
}
|
|
10882
11364
|
/**
|
|
10883
|
-
*
|
|
10884
|
-
* For btree: first field in fields array.
|
|
10885
|
-
* For fts: the single field.
|
|
11365
|
+
* Register an index at runtime (after init).
|
|
10886
11366
|
*/
|
|
10887
|
-
|
|
10888
|
-
|
|
10889
|
-
|
|
11367
|
+
async registerIndexRuntime(name, option, tx) {
|
|
11368
|
+
const config = this.toIndexMetaConfig(option);
|
|
11369
|
+
if (this.registeredIndices.has(name)) {
|
|
11370
|
+
throw new Error(`Index "${name}" already exists.`);
|
|
10890
11371
|
}
|
|
10891
|
-
|
|
11372
|
+
await this.api.runWithDefaultWrite(async (tx2) => {
|
|
11373
|
+
const metadata = await this.api.getDocumentInnerMetadata(tx2);
|
|
11374
|
+
metadata.indices[name] = [-1, config];
|
|
11375
|
+
await this.api.updateDocumentInnerMetadata(metadata, tx2);
|
|
11376
|
+
this.indices = metadata.indices;
|
|
11377
|
+
this.registeredIndices.set(name, config);
|
|
11378
|
+
const fields = this.getFieldsFromConfig(config);
|
|
11379
|
+
for (let i = 0; i < fields.length; i++) {
|
|
11380
|
+
const field = fields[i];
|
|
11381
|
+
this.indexedFields.add(field);
|
|
11382
|
+
if (!this.fieldToIndices.has(field)) {
|
|
11383
|
+
this.fieldToIndices.set(field, []);
|
|
11384
|
+
}
|
|
11385
|
+
this.fieldToIndices.get(field).push(name);
|
|
11386
|
+
}
|
|
11387
|
+
const tree = new import_dataply3.BPTreeAsync(
|
|
11388
|
+
new DocumentSerializeStrategyAsync(
|
|
11389
|
+
this.api.rowTableEngine.order,
|
|
11390
|
+
this.api,
|
|
11391
|
+
this.api.txContext,
|
|
11392
|
+
name
|
|
11393
|
+
),
|
|
11394
|
+
this.api.comparator
|
|
11395
|
+
);
|
|
11396
|
+
await tree.init();
|
|
11397
|
+
this.trees.set(name, tree);
|
|
11398
|
+
if (metadata.lastId > 0) {
|
|
11399
|
+
this.pendingBackfillFields = [name];
|
|
11400
|
+
await this.backfillIndices(tx2);
|
|
11401
|
+
}
|
|
11402
|
+
}, tx);
|
|
10892
11403
|
}
|
|
10893
11404
|
/**
|
|
10894
|
-
*
|
|
10895
|
-
* - 단일 필드 btree: Primitive (단일 값)
|
|
10896
|
-
* - 복합 필드 btree: Primitive[] (필드 순서대로 배열)
|
|
10897
|
-
* - fts: 별도 처리 (이 메서드 사용 안 함)
|
|
10898
|
-
* @returns undefined면 해당 문서에 필수 필드가 없으므로 인덱싱 스킵
|
|
11405
|
+
* Drop (remove) a named index.
|
|
10899
11406
|
*/
|
|
10900
|
-
|
|
10901
|
-
if (
|
|
10902
|
-
|
|
10903
|
-
const v = flatDoc[config.fields[0]];
|
|
10904
|
-
return v === void 0 ? void 0 : v;
|
|
11407
|
+
async dropIndex(name, tx) {
|
|
11408
|
+
if (name === "_id") {
|
|
11409
|
+
throw new Error('Cannot drop the "_id" index.');
|
|
10905
11410
|
}
|
|
10906
|
-
|
|
10907
|
-
|
|
10908
|
-
|
|
10909
|
-
if (v === void 0) return void 0;
|
|
10910
|
-
values.push(v);
|
|
11411
|
+
if (!this.api.isDocInitialized) {
|
|
11412
|
+
this.pendingCreateIndices.delete(name);
|
|
11413
|
+
return;
|
|
10911
11414
|
}
|
|
10912
|
-
|
|
10913
|
-
|
|
10914
|
-
/**
|
|
10915
|
-
* Get FTSConfig from IndexMetaConfig (for tokenizer compatibility).
|
|
10916
|
-
*/
|
|
10917
|
-
getFtsConfig(config) {
|
|
10918
|
-
if (config.type !== "fts") return null;
|
|
10919
|
-
if (config.tokenizer === "ngram") {
|
|
10920
|
-
return { type: "fts", tokenizer: "ngram", gramSize: config.gramSize };
|
|
11415
|
+
if (!this.registeredIndices.has(name)) {
|
|
11416
|
+
throw new Error(`Index "${name}" does not exist.`);
|
|
10921
11417
|
}
|
|
10922
|
-
|
|
10923
|
-
|
|
10924
|
-
|
|
10925
|
-
|
|
10926
|
-
|
|
10927
|
-
|
|
10928
|
-
|
|
11418
|
+
await this.api.runWithDefaultWrite(async (tx2) => {
|
|
11419
|
+
const config = this.registeredIndices.get(name);
|
|
11420
|
+
const metadata = await this.api.getDocumentInnerMetadata(tx2);
|
|
11421
|
+
delete metadata.indices[name];
|
|
11422
|
+
await this.api.updateDocumentInnerMetadata(metadata, tx2);
|
|
11423
|
+
this.indices = metadata.indices;
|
|
11424
|
+
this.registeredIndices.delete(name);
|
|
11425
|
+
const fields = this.getFieldsFromConfig(config);
|
|
11426
|
+
for (let i = 0; i < fields.length; i++) {
|
|
11427
|
+
const field = fields[i];
|
|
11428
|
+
const indexNames = this.fieldToIndices.get(field);
|
|
11429
|
+
if (indexNames) {
|
|
11430
|
+
const filtered = indexNames.filter((n) => n !== name);
|
|
11431
|
+
if (filtered.length === 0) {
|
|
11432
|
+
this.fieldToIndices.delete(field);
|
|
11433
|
+
if (field !== "_id") {
|
|
11434
|
+
this.indexedFields.delete(field);
|
|
11435
|
+
}
|
|
11436
|
+
} else {
|
|
11437
|
+
this.fieldToIndices.set(field, filtered);
|
|
11438
|
+
}
|
|
11439
|
+
}
|
|
10929
11440
|
}
|
|
10930
|
-
|
|
11441
|
+
this.trees.delete(name);
|
|
10931
11442
|
}, tx);
|
|
10932
11443
|
}
|
|
10933
11444
|
/**
|
|
10934
11445
|
* Backfill indices for newly created indices after data was inserted.
|
|
10935
|
-
* This method should be called after `init()`.
|
|
10936
|
-
*
|
|
10937
|
-
* @returns Number of documents that were backfilled
|
|
10938
11446
|
*/
|
|
10939
11447
|
async backfillIndices(tx) {
|
|
10940
|
-
return this.runWithDefaultWrite(async (tx2) => {
|
|
11448
|
+
return this.api.runWithDefaultWrite(async (tx2) => {
|
|
10941
11449
|
if (this.pendingBackfillFields.length === 0) {
|
|
10942
11450
|
return 0;
|
|
10943
11451
|
}
|
|
10944
11452
|
const backfillTargets = this.pendingBackfillFields;
|
|
10945
|
-
const metadata = await this.getDocumentInnerMetadata(tx2);
|
|
11453
|
+
const metadata = await this.api.getDocumentInnerMetadata(tx2);
|
|
10946
11454
|
if (metadata.lastId === 0) {
|
|
10947
11455
|
return 0;
|
|
10948
11456
|
}
|
|
@@ -10964,9 +11472,9 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
|
|
|
10964
11472
|
primaryGte: { v: 0 }
|
|
10965
11473
|
});
|
|
10966
11474
|
for await (const [k, complexValue] of stream) {
|
|
10967
|
-
const doc = await this.getDocument(k, tx2);
|
|
11475
|
+
const doc = await this.api.getDocument(k, tx2);
|
|
10968
11476
|
if (!doc) continue;
|
|
10969
|
-
const flatDoc = this.flattenDocument(doc);
|
|
11477
|
+
const flatDoc = this.api.flattenDocument(doc);
|
|
10970
11478
|
for (let i = 0, len = backfillTargets.length; i < len; i++) {
|
|
10971
11479
|
const indexName = backfillTargets[i];
|
|
10972
11480
|
if (!(indexName in indexTxMap)) continue;
|
|
@@ -11000,547 +11508,157 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
|
|
|
11000
11508
|
if (chunkCount >= CHUNK_SIZE) {
|
|
11001
11509
|
try {
|
|
11002
11510
|
for (const btx of Object.values(indexTxMap)) {
|
|
11003
|
-
await btx.commit();
|
|
11004
|
-
}
|
|
11005
|
-
} catch (err) {
|
|
11006
|
-
for (const btx of Object.values(indexTxMap)) {
|
|
11007
|
-
await btx.rollback();
|
|
11008
|
-
}
|
|
11009
|
-
throw err;
|
|
11010
|
-
}
|
|
11011
|
-
for (const indexName of backfillTargets) {
|
|
11012
|
-
const tree = this.trees.get(indexName);
|
|
11013
|
-
if (tree && indexName !== "_id") {
|
|
11014
|
-
indexTxMap[indexName] = await tree.createTransaction();
|
|
11015
|
-
}
|
|
11016
|
-
}
|
|
11017
|
-
chunkCount = 0;
|
|
11018
|
-
}
|
|
11019
|
-
}
|
|
11020
|
-
if (chunkCount > 0) {
|
|
11021
|
-
try {
|
|
11022
|
-
for (const btx of Object.values(indexTxMap)) {
|
|
11023
|
-
await btx.commit();
|
|
11024
|
-
}
|
|
11025
|
-
} catch (err) {
|
|
11026
|
-
for (const btx of Object.values(indexTxMap)) {
|
|
11027
|
-
await btx.rollback();
|
|
11028
|
-
}
|
|
11029
|
-
throw err;
|
|
11030
|
-
}
|
|
11031
|
-
}
|
|
11032
|
-
this.pendingBackfillFields = [];
|
|
11033
|
-
return backfilledCount;
|
|
11034
|
-
}, tx);
|
|
11035
|
-
}
|
|
11036
|
-
createDocumentInnerMetadata(indices) {
|
|
11037
|
-
return {
|
|
11038
|
-
magicString: "document-dataply",
|
|
11039
|
-
version: 1,
|
|
11040
|
-
createdAt: Date.now(),
|
|
11041
|
-
updatedAt: Date.now(),
|
|
11042
|
-
lastId: 0,
|
|
11043
|
-
schemeVersion: 0,
|
|
11044
|
-
indices
|
|
11045
|
-
};
|
|
11046
|
-
}
|
|
11047
|
-
async initializeDocumentFile(tx) {
|
|
11048
|
-
const metadata = await this.select(1, false, tx);
|
|
11049
|
-
if (metadata) {
|
|
11050
|
-
throw new Error("Document metadata already exists");
|
|
11051
|
-
}
|
|
11052
|
-
const metaObj = this.createDocumentInnerMetadata({
|
|
11053
|
-
_id: [-1, { type: "btree", fields: ["_id"] }]
|
|
11054
|
-
});
|
|
11055
|
-
await this.insertAsOverflow(JSON.stringify(metaObj), false, tx);
|
|
11056
|
-
}
|
|
11057
|
-
async verifyDocumentFile(tx) {
|
|
11058
|
-
const row = await this.select(1, false, tx);
|
|
11059
|
-
if (!row) {
|
|
11060
|
-
return false;
|
|
11061
|
-
}
|
|
11062
|
-
const data = JSON.parse(row);
|
|
11063
|
-
return data.magicString === "document-dataply" && data.version === 1;
|
|
11064
|
-
}
|
|
11065
|
-
flatten(obj, parentKey = "", result = {}) {
|
|
11066
|
-
for (const key in obj) {
|
|
11067
|
-
const newKey = parentKey ? `${parentKey}.${key}` : key;
|
|
11068
|
-
if (typeof obj[key] === "object" && obj[key] !== null) {
|
|
11069
|
-
this.flatten(obj[key], newKey, result);
|
|
11070
|
-
} else {
|
|
11071
|
-
result[newKey] = obj[key];
|
|
11072
|
-
}
|
|
11073
|
-
}
|
|
11074
|
-
return result;
|
|
11075
|
-
}
|
|
11076
|
-
/**
|
|
11077
|
-
* returns flattened document
|
|
11078
|
-
* @param document
|
|
11079
|
-
* @returns
|
|
11080
|
-
*/
|
|
11081
|
-
flattenDocument(document) {
|
|
11082
|
-
return this.flatten(document, "", {});
|
|
11083
|
-
}
|
|
11084
|
-
async getDocumentMetadata(tx) {
|
|
11085
|
-
const metadata = await this.getMetadata(tx);
|
|
11086
|
-
const innerMetadata = await this.getDocumentInnerMetadata(tx);
|
|
11087
|
-
const indices = [];
|
|
11088
|
-
for (const name of this.registeredIndices.keys()) {
|
|
11089
|
-
if (name !== "_id") {
|
|
11090
|
-
indices.push(name);
|
|
11091
|
-
}
|
|
11092
|
-
}
|
|
11093
|
-
return {
|
|
11094
|
-
pageSize: metadata.pageSize,
|
|
11095
|
-
pageCount: metadata.pageCount,
|
|
11096
|
-
rowCount: metadata.rowCount,
|
|
11097
|
-
indices,
|
|
11098
|
-
schemeVersion: innerMetadata.schemeVersion ?? 0
|
|
11099
|
-
};
|
|
11100
|
-
}
|
|
11101
|
-
async getDocumentInnerMetadata(tx) {
|
|
11102
|
-
const row = await this.select(1, false, tx);
|
|
11103
|
-
if (!row) {
|
|
11104
|
-
throw new Error("Document metadata not found");
|
|
11105
|
-
}
|
|
11106
|
-
return JSON.parse(row);
|
|
11107
|
-
}
|
|
11108
|
-
async updateDocumentInnerMetadata(metadata, tx) {
|
|
11109
|
-
await this.update(1, JSON.stringify(metadata), tx);
|
|
11110
|
-
}
|
|
11111
|
-
/**
|
|
11112
|
-
* Run a migration if the current schemeVersion is lower than the target version.
|
|
11113
|
-
* After the callback completes, schemeVersion is updated to the target version.
|
|
11114
|
-
* @param version The target scheme version
|
|
11115
|
-
* @param callback The migration callback
|
|
11116
|
-
* @param tx Optional transaction
|
|
11117
|
-
*/
|
|
11118
|
-
async migration(version, callback, tx) {
|
|
11119
|
-
await this.runWithDefaultWrite(async (tx2) => {
|
|
11120
|
-
const innerMetadata = await this.getDocumentInnerMetadata(tx2);
|
|
11121
|
-
const currentVersion = innerMetadata.schemeVersion ?? 0;
|
|
11122
|
-
if (currentVersion < version) {
|
|
11123
|
-
await callback(tx2);
|
|
11124
|
-
const freshMetadata = await this.getDocumentInnerMetadata(tx2);
|
|
11125
|
-
freshMetadata.schemeVersion = version;
|
|
11126
|
-
freshMetadata.updatedAt = Date.now();
|
|
11127
|
-
await this.updateDocumentInnerMetadata(freshMetadata, tx2);
|
|
11128
|
-
}
|
|
11129
|
-
}, tx);
|
|
11130
|
-
}
|
|
11131
|
-
/**
|
|
11132
|
-
* Transforms a query object into a verbose query object
|
|
11133
|
-
* @param query The query object to transform
|
|
11134
|
-
* @returns The verbose query object
|
|
11135
|
-
*/
|
|
11136
|
-
verboseQuery(query) {
|
|
11137
|
-
const result = {};
|
|
11138
|
-
for (const field in query) {
|
|
11139
|
-
const conditions = query[field];
|
|
11140
|
-
let newConditions;
|
|
11141
|
-
if (typeof conditions !== "object" || conditions === null) {
|
|
11142
|
-
newConditions = { primaryEqual: { v: conditions } };
|
|
11143
|
-
} else {
|
|
11144
|
-
newConditions = {};
|
|
11145
|
-
for (const operator in conditions) {
|
|
11146
|
-
const before = operator;
|
|
11147
|
-
const after = this.operatorConverters[before];
|
|
11148
|
-
const v = conditions[before];
|
|
11149
|
-
if (!after) {
|
|
11150
|
-
if (before === "match") {
|
|
11151
|
-
newConditions[before] = v;
|
|
11152
|
-
}
|
|
11153
|
-
continue;
|
|
11154
|
-
}
|
|
11155
|
-
if (before === "or" && Array.isArray(v)) {
|
|
11156
|
-
newConditions[after] = v.map((val) => ({ v: val }));
|
|
11157
|
-
} else if (before === "like") {
|
|
11158
|
-
newConditions[after] = v;
|
|
11159
|
-
} else {
|
|
11160
|
-
newConditions[after] = { v };
|
|
11161
|
-
}
|
|
11162
|
-
}
|
|
11163
|
-
}
|
|
11164
|
-
result[field] = newConditions;
|
|
11165
|
-
}
|
|
11166
|
-
return result;
|
|
11167
|
-
}
|
|
11168
|
-
/**
|
|
11169
|
-
* B-Tree 타입 인덱스의 선택도를 평가하고 트리에 부여할 조건을 산출합니다.
|
|
11170
|
-
* 필드 매칭 여부를 검사하고, 연속된(Prefix) 조건에 대해 점수를 부여하며 Start/End 바운드를 구성합니다.
|
|
11171
|
-
*
|
|
11172
|
-
* @param indexName 평가할 인덱스의 이름 (예: idx_nickname_createdat)
|
|
11173
|
-
* @param config 등록된 인덱스의 설정 객체
|
|
11174
|
-
* @param query 쿼리 객체
|
|
11175
|
-
* @param queryFields 쿼리에 포함된 필드 목록 집합
|
|
11176
|
-
* @param treeTx 조회를 수행할 B-Tree 트랜잭션 객체
|
|
11177
|
-
* @param orderByField 정렬에 사용할 필드명 (옵션)
|
|
11178
|
-
* @returns B-Tree 인덱스 후보 정보 (조건, 점수, 커버된 필드 등), 적합하지 않으면 null
|
|
11179
|
-
*/
|
|
11180
|
-
evaluateBTreeCandidate(indexName, config, query, queryFields, treeTx, orderByField) {
|
|
11181
|
-
const primaryField = config.fields[0];
|
|
11182
|
-
if (!queryFields.has(primaryField)) return null;
|
|
11183
|
-
const builtCondition = {};
|
|
11184
|
-
let score = 0;
|
|
11185
|
-
let isConsecutive = true;
|
|
11186
|
-
const coveredFields = [];
|
|
11187
|
-
const compositeVerifyFields = [];
|
|
11188
|
-
const startValues = [];
|
|
11189
|
-
const endValues = [];
|
|
11190
|
-
let startOperator = null;
|
|
11191
|
-
let endOperator = null;
|
|
11192
|
-
for (let i = 0, len = config.fields.length; i < len; i++) {
|
|
11193
|
-
const field = config.fields[i];
|
|
11194
|
-
if (!queryFields.has(field)) {
|
|
11195
|
-
isConsecutive = false;
|
|
11196
|
-
continue;
|
|
11197
|
-
}
|
|
11198
|
-
coveredFields.push(field);
|
|
11199
|
-
score += 1;
|
|
11200
|
-
if (isConsecutive) {
|
|
11201
|
-
const cond = query[field];
|
|
11202
|
-
if (cond !== void 0) {
|
|
11203
|
-
let isBounded = false;
|
|
11204
|
-
if (typeof cond !== "object" || cond === null) {
|
|
11205
|
-
score += 100;
|
|
11206
|
-
startValues.push(cond);
|
|
11207
|
-
endValues.push(cond);
|
|
11208
|
-
startOperator = "primaryGte";
|
|
11209
|
-
endOperator = "primaryLte";
|
|
11210
|
-
isBounded = true;
|
|
11211
|
-
} else if ("primaryEqual" in cond || "equal" in cond) {
|
|
11212
|
-
const val = cond.primaryEqual?.v ?? cond.equal?.v ?? cond.primaryEqual ?? cond.equal;
|
|
11213
|
-
score += 100;
|
|
11214
|
-
startValues.push(val);
|
|
11215
|
-
endValues.push(val);
|
|
11216
|
-
startOperator = "primaryGte";
|
|
11217
|
-
endOperator = "primaryLte";
|
|
11218
|
-
isBounded = true;
|
|
11219
|
-
} else if ("primaryGte" in cond || "gte" in cond) {
|
|
11220
|
-
const val = cond.primaryGte?.v ?? cond.gte?.v ?? cond.primaryGte ?? cond.gte;
|
|
11221
|
-
score += 50;
|
|
11222
|
-
isConsecutive = false;
|
|
11223
|
-
startValues.push(val);
|
|
11224
|
-
startOperator = "primaryGte";
|
|
11225
|
-
if (endValues.length > 0) endOperator = "primaryLte";
|
|
11226
|
-
isBounded = true;
|
|
11227
|
-
} else if ("primaryGt" in cond || "gt" in cond) {
|
|
11228
|
-
const val = cond.primaryGt?.v ?? cond.gt?.v ?? cond.primaryGt ?? cond.gt;
|
|
11229
|
-
score += 50;
|
|
11230
|
-
isConsecutive = false;
|
|
11231
|
-
startValues.push(val);
|
|
11232
|
-
startOperator = "primaryGt";
|
|
11233
|
-
if (endValues.length > 0) endOperator = "primaryLte";
|
|
11234
|
-
isBounded = true;
|
|
11235
|
-
} else if ("primaryLte" in cond || "lte" in cond) {
|
|
11236
|
-
const val = cond.primaryLte?.v ?? cond.lte?.v ?? cond.primaryLte ?? cond.lte;
|
|
11237
|
-
score += 50;
|
|
11238
|
-
isConsecutive = false;
|
|
11239
|
-
endValues.push(val);
|
|
11240
|
-
endOperator = "primaryLte";
|
|
11241
|
-
if (startValues.length > 0) startOperator = "primaryGte";
|
|
11242
|
-
isBounded = true;
|
|
11243
|
-
} else if ("primaryLt" in cond || "lt" in cond) {
|
|
11244
|
-
const val = cond.primaryLt?.v ?? cond.lt?.v ?? cond.primaryLt ?? cond.lt;
|
|
11245
|
-
score += 50;
|
|
11246
|
-
isConsecutive = false;
|
|
11247
|
-
endValues.push(val);
|
|
11248
|
-
endOperator = "primaryLt";
|
|
11249
|
-
if (startValues.length > 0) startOperator = "primaryGte";
|
|
11250
|
-
isBounded = true;
|
|
11251
|
-
} else if ("primaryOr" in cond || "or" in cond) {
|
|
11252
|
-
score += 20;
|
|
11253
|
-
isConsecutive = false;
|
|
11254
|
-
} else if ("like" in cond) {
|
|
11255
|
-
score += 15;
|
|
11256
|
-
isConsecutive = false;
|
|
11257
|
-
} else {
|
|
11258
|
-
score += 10;
|
|
11259
|
-
isConsecutive = false;
|
|
11511
|
+
await btx.commit();
|
|
11512
|
+
}
|
|
11513
|
+
} catch (err) {
|
|
11514
|
+
for (const btx of Object.values(indexTxMap)) {
|
|
11515
|
+
await btx.rollback();
|
|
11516
|
+
}
|
|
11517
|
+
throw err;
|
|
11260
11518
|
}
|
|
11261
|
-
|
|
11262
|
-
|
|
11519
|
+
for (const indexName of backfillTargets) {
|
|
11520
|
+
const tree = this.trees.get(indexName);
|
|
11521
|
+
if (tree && indexName !== "_id") {
|
|
11522
|
+
indexTxMap[indexName] = await tree.createTransaction();
|
|
11523
|
+
}
|
|
11263
11524
|
}
|
|
11525
|
+
chunkCount = 0;
|
|
11264
11526
|
}
|
|
11265
|
-
} else {
|
|
11266
|
-
if (field !== primaryField) {
|
|
11267
|
-
compositeVerifyFields.push(field);
|
|
11268
|
-
}
|
|
11269
|
-
}
|
|
11270
|
-
}
|
|
11271
|
-
if (coveredFields.length === 1 && config.fields.length === 1) {
|
|
11272
|
-
Object.assign(builtCondition, query[primaryField]);
|
|
11273
|
-
} else {
|
|
11274
|
-
if (startOperator && startValues.length > 0) {
|
|
11275
|
-
builtCondition[startOperator] = { v: startValues.length === 1 ? startValues[0] : startValues };
|
|
11276
|
-
}
|
|
11277
|
-
if (endOperator && endValues.length > 0) {
|
|
11278
|
-
if (startOperator && startValues.length === endValues.length && startValues.every((val, i) => val === endValues[i])) {
|
|
11279
|
-
delete builtCondition[startOperator];
|
|
11280
|
-
builtCondition["primaryEqual"] = { v: startValues.length === 1 ? startValues[0] : startValues };
|
|
11281
|
-
} else {
|
|
11282
|
-
builtCondition[endOperator] = { v: endValues.length === 1 ? endValues[0] : endValues };
|
|
11283
|
-
}
|
|
11284
|
-
}
|
|
11285
|
-
if (Object.keys(builtCondition).length === 0) {
|
|
11286
|
-
Object.assign(builtCondition, query[primaryField] || {});
|
|
11287
11527
|
}
|
|
11288
|
-
|
|
11289
|
-
|
|
11290
|
-
|
|
11291
|
-
|
|
11292
|
-
|
|
11293
|
-
|
|
11294
|
-
|
|
11295
|
-
|
|
11296
|
-
|
|
11297
|
-
|
|
11298
|
-
let isExactMatch = false;
|
|
11299
|
-
if (cond !== void 0) {
|
|
11300
|
-
if (typeof cond !== "object" || cond === null) isExactMatch = true;
|
|
11301
|
-
else if ("primaryEqual" in cond || "equal" in cond) isExactMatch = true;
|
|
11528
|
+
if (chunkCount > 0) {
|
|
11529
|
+
try {
|
|
11530
|
+
for (const btx of Object.values(indexTxMap)) {
|
|
11531
|
+
await btx.commit();
|
|
11532
|
+
}
|
|
11533
|
+
} catch (err) {
|
|
11534
|
+
for (const btx of Object.values(indexTxMap)) {
|
|
11535
|
+
await btx.rollback();
|
|
11536
|
+
}
|
|
11537
|
+
throw err;
|
|
11302
11538
|
}
|
|
11303
|
-
if (!isExactMatch) break;
|
|
11304
|
-
}
|
|
11305
|
-
if (isIndexOrderSupported) {
|
|
11306
|
-
score += 200;
|
|
11307
11539
|
}
|
|
11308
|
-
|
|
11309
|
-
|
|
11310
|
-
|
|
11311
|
-
condition: builtCondition,
|
|
11312
|
-
field: primaryField,
|
|
11313
|
-
indexName,
|
|
11314
|
-
isFtsMatch: false,
|
|
11315
|
-
score,
|
|
11316
|
-
compositeVerifyFields,
|
|
11317
|
-
coveredFields,
|
|
11318
|
-
isIndexOrderSupported
|
|
11319
|
-
};
|
|
11320
|
-
}
|
|
11321
|
-
/**
|
|
11322
|
-
* FTS (Full Text Search) 타입 인덱스의 선택도를 평가합니다.
|
|
11323
|
-
* 'match' 연산자가 쿼리에 존재하는지 확인하고, 검색용 토큰으로 분해(tokenize)하여 점수를 매깁니다.
|
|
11324
|
-
*
|
|
11325
|
-
* @param indexName 평가할 인덱스의 이름
|
|
11326
|
-
* @param config 등록된 인덱스의 설정 객체
|
|
11327
|
-
* @param query 쿼리 객체
|
|
11328
|
-
* @param queryFields 쿼리에 포함된 필드 목록 집합
|
|
11329
|
-
* @param treeTx 조회를 수행할 B-Tree 트랜잭션 객체
|
|
11330
|
-
* @returns FTS 인덱스 후보 정보 (조건, 점수, 분석된 토큰 등), 적합하지 않으면 null
|
|
11331
|
-
*/
|
|
11332
|
-
evaluateFTSCandidate(indexName, config, query, queryFields, treeTx) {
|
|
11333
|
-
const field = config.fields;
|
|
11334
|
-
if (!queryFields.has(field)) return null;
|
|
11335
|
-
const condition = query[field];
|
|
11336
|
-
if (!condition || typeof condition !== "object" || !("match" in condition)) return null;
|
|
11337
|
-
const ftsConfig = this.getFtsConfig(config);
|
|
11338
|
-
const matchTokens = ftsConfig ? tokenize(condition.match, ftsConfig) : [];
|
|
11339
|
-
return {
|
|
11340
|
-
tree: treeTx,
|
|
11341
|
-
condition,
|
|
11342
|
-
field,
|
|
11343
|
-
indexName,
|
|
11344
|
-
isFtsMatch: true,
|
|
11345
|
-
matchTokens,
|
|
11346
|
-
score: 90,
|
|
11347
|
-
// FTS 쿼리는 기본적인 B-Tree 단일 검색(대략 101점)보다는 우선순위를 조금 낮게 가져가도록 90점 부여
|
|
11348
|
-
compositeVerifyFields: [],
|
|
11349
|
-
coveredFields: [field],
|
|
11350
|
-
isIndexOrderSupported: false
|
|
11351
|
-
};
|
|
11540
|
+
this.pendingBackfillFields = [];
|
|
11541
|
+
return backfilledCount;
|
|
11542
|
+
}, tx);
|
|
11352
11543
|
}
|
|
11353
11544
|
/**
|
|
11354
|
-
*
|
|
11355
|
-
* Scores each index based on field coverage and condition type.
|
|
11356
|
-
*
|
|
11357
|
-
* @param query The verbose query conditions
|
|
11358
|
-
* @param orderByField Optional field name for orderBy optimization
|
|
11359
|
-
* @returns Driver and other candidates for query execution
|
|
11545
|
+
* Convert CreateIndexOption to IndexMetaConfig for metadata storage.
|
|
11360
11546
|
*/
|
|
11361
|
-
|
|
11362
|
-
|
|
11363
|
-
|
|
11364
|
-
for (const [indexName, config] of this.registeredIndices) {
|
|
11365
|
-
const tree = this.trees.get(indexName);
|
|
11366
|
-
if (!tree) continue;
|
|
11367
|
-
if (config.type === "btree") {
|
|
11368
|
-
const treeTx = await tree.createTransaction();
|
|
11369
|
-
const candidate = this.evaluateBTreeCandidate(
|
|
11370
|
-
indexName,
|
|
11371
|
-
config,
|
|
11372
|
-
query,
|
|
11373
|
-
queryFields,
|
|
11374
|
-
treeTx,
|
|
11375
|
-
orderByField
|
|
11376
|
-
);
|
|
11377
|
-
if (candidate) candidates.push(candidate);
|
|
11378
|
-
} else if (config.type === "fts") {
|
|
11379
|
-
const treeTx = await tree.createTransaction();
|
|
11380
|
-
const candidate = this.evaluateFTSCandidate(
|
|
11381
|
-
indexName,
|
|
11382
|
-
config,
|
|
11383
|
-
query,
|
|
11384
|
-
queryFields,
|
|
11385
|
-
treeTx
|
|
11386
|
-
);
|
|
11387
|
-
if (candidate) candidates.push(candidate);
|
|
11388
|
-
}
|
|
11547
|
+
toIndexMetaConfig(option) {
|
|
11548
|
+
if (!option || typeof option !== "object") {
|
|
11549
|
+
throw new Error("Index option must be a non-null object");
|
|
11389
11550
|
}
|
|
11390
|
-
|
|
11391
|
-
|
|
11392
|
-
tree.rollback();
|
|
11393
|
-
}
|
|
11394
|
-
};
|
|
11395
|
-
if (candidates.length === 0) {
|
|
11396
|
-
rollback();
|
|
11397
|
-
return null;
|
|
11551
|
+
if (!option.type) {
|
|
11552
|
+
throw new Error('Index option must have a "type" property ("btree" or "fts")');
|
|
11398
11553
|
}
|
|
11399
|
-
|
|
11400
|
-
if (
|
|
11401
|
-
|
|
11402
|
-
|
|
11403
|
-
|
|
11404
|
-
|
|
11405
|
-
|
|
11406
|
-
|
|
11407
|
-
const driver = candidates[0];
|
|
11408
|
-
const driverCoveredFields = new Set(driver.coveredFields);
|
|
11409
|
-
const others = candidates.slice(1).filter((c) => !driverCoveredFields.has(c.field));
|
|
11410
|
-
const compositeVerifyConditions = [];
|
|
11411
|
-
for (let i = 0, len = driver.compositeVerifyFields.length; i < len; i++) {
|
|
11412
|
-
const field = driver.compositeVerifyFields[i];
|
|
11413
|
-
if (query[field]) {
|
|
11414
|
-
compositeVerifyConditions.push({ field, condition: query[field] });
|
|
11554
|
+
if (option.type === "btree") {
|
|
11555
|
+
if (!Array.isArray(option.fields) || option.fields.length === 0) {
|
|
11556
|
+
throw new Error('btree index requires a non-empty "fields" array');
|
|
11557
|
+
}
|
|
11558
|
+
for (let i = 0, len = option.fields.length; i < len; i++) {
|
|
11559
|
+
if (typeof option.fields[i] !== "string" || option.fields[i].length === 0) {
|
|
11560
|
+
throw new Error(`btree index "fields[${i}]" must be a non-empty string, got: ${JSON.stringify(option.fields[i])}`);
|
|
11561
|
+
}
|
|
11415
11562
|
}
|
|
11563
|
+
return {
|
|
11564
|
+
type: "btree",
|
|
11565
|
+
fields: option.fields
|
|
11566
|
+
};
|
|
11416
11567
|
}
|
|
11417
|
-
|
|
11418
|
-
|
|
11419
|
-
|
|
11420
|
-
|
|
11421
|
-
|
|
11422
|
-
|
|
11423
|
-
|
|
11424
|
-
/**
|
|
11425
|
-
* Get Free Memory Chunk Size
|
|
11426
|
-
* @returns { verySmallChunkSize, smallChunkSize }
|
|
11427
|
-
*/
|
|
11428
|
-
getFreeMemoryChunkSize() {
|
|
11429
|
-
const freeMem = os.freemem();
|
|
11430
|
-
const safeLimit = freeMem * 0.2;
|
|
11431
|
-
const verySmallChunkSize = safeLimit * 0.05;
|
|
11432
|
-
const smallChunkSize = safeLimit * 0.3;
|
|
11433
|
-
return { verySmallChunkSize, smallChunkSize };
|
|
11434
|
-
}
|
|
11435
|
-
getTokenKey(pk, token) {
|
|
11436
|
-
return pk + ":" + token;
|
|
11437
|
-
}
|
|
11438
|
-
async *applyCandidateByFTSStream(candidate, matchedTokens, filterValues, order) {
|
|
11439
|
-
const keys = /* @__PURE__ */ new Set();
|
|
11440
|
-
for (let i = 0, len = matchedTokens.length; i < len; i++) {
|
|
11441
|
-
const token = matchedTokens[i];
|
|
11442
|
-
for await (const pair of candidate.tree.whereStream(
|
|
11443
|
-
{ primaryEqual: { v: token } },
|
|
11444
|
-
{ order }
|
|
11445
|
-
)) {
|
|
11446
|
-
const pk = pair[1].k;
|
|
11447
|
-
if (filterValues && !filterValues.has(pk)) continue;
|
|
11448
|
-
if (!keys.has(pk)) {
|
|
11449
|
-
keys.add(pk);
|
|
11450
|
-
yield pk;
|
|
11568
|
+
if (option.type === "fts") {
|
|
11569
|
+
if (typeof option.fields !== "string" || option.fields.length === 0) {
|
|
11570
|
+
throw new Error(`fts index requires a non-empty string "fields", got: ${JSON.stringify(option.fields)}`);
|
|
11571
|
+
}
|
|
11572
|
+
if (option.tokenizer === "ngram") {
|
|
11573
|
+
if (typeof option.gramSize !== "number" || option.gramSize < 1) {
|
|
11574
|
+
throw new Error(`fts ngram index requires a positive "gramSize" number, got: ${JSON.stringify(option.gramSize)}`);
|
|
11451
11575
|
}
|
|
11576
|
+
return {
|
|
11577
|
+
type: "fts",
|
|
11578
|
+
fields: option.fields,
|
|
11579
|
+
tokenizer: "ngram",
|
|
11580
|
+
gramSize: option.gramSize
|
|
11581
|
+
};
|
|
11452
11582
|
}
|
|
11583
|
+
return {
|
|
11584
|
+
type: "fts",
|
|
11585
|
+
fields: option.fields,
|
|
11586
|
+
tokenizer: "whitespace"
|
|
11587
|
+
};
|
|
11453
11588
|
}
|
|
11589
|
+
throw new Error(`Unknown index type: ${option.type}`);
|
|
11454
11590
|
}
|
|
11455
11591
|
/**
|
|
11456
|
-
*
|
|
11457
|
-
*/
|
|
11458
|
-
applyCandidateStream(candidate, filterValues, order) {
|
|
11459
|
-
return candidate.tree.keysStream(
|
|
11460
|
-
candidate.condition,
|
|
11461
|
-
{ filterValues, order }
|
|
11462
|
-
);
|
|
11463
|
-
}
|
|
11464
|
-
/**
|
|
11465
|
-
* 쿼리와 인덱스 선택을 기반으로 기본 키(Primary Keys)를 가져옵니다.
|
|
11466
|
-
* 쿼리 최적화를 통합하기 위한 내부 공통 메서드입니다.
|
|
11592
|
+
* Get all field names from an IndexMetaConfig.
|
|
11467
11593
|
*/
|
|
11468
|
-
|
|
11469
|
-
|
|
11470
|
-
|
|
11471
|
-
const selectivity = await this.getSelectivityCandidate(
|
|
11472
|
-
this.verboseQuery(normalizedQuery),
|
|
11473
|
-
orderBy
|
|
11474
|
-
);
|
|
11475
|
-
if (!selectivity) return new Float64Array(0);
|
|
11476
|
-
const { driver, others, rollback } = selectivity;
|
|
11477
|
-
const useIndexOrder = orderBy === void 0 || driver.isIndexOrderSupported;
|
|
11478
|
-
const candidates = [driver, ...others];
|
|
11479
|
-
let keys = void 0;
|
|
11480
|
-
for (let i = 0, len = candidates.length; i < len; i++) {
|
|
11481
|
-
const candidate = candidates[i];
|
|
11482
|
-
const currentOrder = useIndexOrder ? sortOrder : void 0;
|
|
11483
|
-
if (candidate.isFtsMatch && candidate.matchTokens && candidate.matchTokens.length > 0) {
|
|
11484
|
-
const stream = this.applyCandidateByFTSStream(
|
|
11485
|
-
candidate,
|
|
11486
|
-
candidate.matchTokens,
|
|
11487
|
-
keys,
|
|
11488
|
-
currentOrder
|
|
11489
|
-
);
|
|
11490
|
-
keys = /* @__PURE__ */ new Set();
|
|
11491
|
-
for await (const pk of stream) keys.add(pk);
|
|
11492
|
-
} else {
|
|
11493
|
-
const stream = this.applyCandidateStream(candidate, keys, currentOrder);
|
|
11494
|
-
keys = /* @__PURE__ */ new Set();
|
|
11495
|
-
for await (const pk of stream) keys.add(pk);
|
|
11496
|
-
}
|
|
11594
|
+
getFieldsFromConfig(config) {
|
|
11595
|
+
if (config.type === "btree") {
|
|
11596
|
+
return config.fields;
|
|
11497
11597
|
}
|
|
11498
|
-
|
|
11499
|
-
|
|
11598
|
+
if (config.type === "fts") {
|
|
11599
|
+
return [config.fields];
|
|
11600
|
+
}
|
|
11601
|
+
return [];
|
|
11500
11602
|
}
|
|
11501
11603
|
/**
|
|
11502
|
-
*
|
|
11503
|
-
* selectDocuments에서 사용하며, 나머지 조건(others)은 스트리밍 중 tree.verify()로 검증합니다.
|
|
11504
|
-
* @returns 드라이버 키 스트림, others 후보 목록, rollback 함수. 또는 null.
|
|
11604
|
+
* Get the primary field of an index.
|
|
11505
11605
|
*/
|
|
11506
|
-
|
|
11507
|
-
|
|
11508
|
-
|
|
11509
|
-
const selectivity = await this.getSelectivityCandidate(
|
|
11510
|
-
this.verboseQuery(normalizedQuery),
|
|
11511
|
-
orderBy
|
|
11512
|
-
);
|
|
11513
|
-
if (!selectivity) return null;
|
|
11514
|
-
const { driver, others, compositeVerifyConditions, rollback } = selectivity;
|
|
11515
|
-
const useIndexOrder = orderBy === void 0 || driver.isIndexOrderSupported;
|
|
11516
|
-
const currentOrder = useIndexOrder ? sortOrder : void 0;
|
|
11517
|
-
let keysStream;
|
|
11518
|
-
if (driver.isFtsMatch && driver.matchTokens && driver.matchTokens.length > 0) {
|
|
11519
|
-
keysStream = this.applyCandidateByFTSStream(
|
|
11520
|
-
driver,
|
|
11521
|
-
driver.matchTokens,
|
|
11522
|
-
void 0,
|
|
11523
|
-
currentOrder
|
|
11524
|
-
);
|
|
11525
|
-
} else {
|
|
11526
|
-
keysStream = this.applyCandidateStream(driver, void 0, currentOrder);
|
|
11606
|
+
getPrimaryField(config) {
|
|
11607
|
+
if (config.type === "btree") {
|
|
11608
|
+
return config.fields[0];
|
|
11527
11609
|
}
|
|
11528
|
-
return
|
|
11529
|
-
|
|
11530
|
-
|
|
11531
|
-
|
|
11532
|
-
|
|
11533
|
-
|
|
11534
|
-
|
|
11610
|
+
return config.fields;
|
|
11611
|
+
}
|
|
11612
|
+
/**
|
|
11613
|
+
* Create B+Tree value string for indexing a document
|
|
11614
|
+
*/
|
|
11615
|
+
getIndexValue(config, flatDoc) {
|
|
11616
|
+
if (config.type !== "btree") return void 0;
|
|
11617
|
+
if (config.fields.length === 1) {
|
|
11618
|
+
const v = flatDoc[config.fields[0]];
|
|
11619
|
+
return v === void 0 ? void 0 : v;
|
|
11620
|
+
}
|
|
11621
|
+
const values = [];
|
|
11622
|
+
for (let i = 0, len = config.fields.length; i < len; i++) {
|
|
11623
|
+
const v = flatDoc[config.fields[i]];
|
|
11624
|
+
if (v === void 0) return void 0;
|
|
11625
|
+
values.push(v);
|
|
11626
|
+
}
|
|
11627
|
+
return values;
|
|
11628
|
+
}
|
|
11629
|
+
/**
|
|
11630
|
+
* Get FTSConfig from IndexMetaConfig
|
|
11631
|
+
*/
|
|
11632
|
+
getFtsConfig(config) {
|
|
11633
|
+
if (config.type !== "fts") return null;
|
|
11634
|
+
if (config.tokenizer === "ngram") {
|
|
11635
|
+
return { type: "fts", tokenizer: "ngram", gramSize: config.gramSize };
|
|
11636
|
+
}
|
|
11637
|
+
return { type: "fts", tokenizer: "whitespace" };
|
|
11638
|
+
}
|
|
11639
|
+
getTokenKey(pk, token) {
|
|
11640
|
+
return pk + ":" + token;
|
|
11641
|
+
}
|
|
11642
|
+
};
|
|
11643
|
+
|
|
11644
|
+
// src/utils/catchPromise.ts
|
|
11645
|
+
async function catchPromise(promise) {
|
|
11646
|
+
return promise.then((res) => [void 0, res]).catch((reason) => [reason]);
|
|
11647
|
+
}
|
|
11648
|
+
|
|
11649
|
+
// src/core/MutationManager.ts
|
|
11650
|
+
var MutationManager = class {
|
|
11651
|
+
constructor(api) {
|
|
11652
|
+
this.api = api;
|
|
11535
11653
|
}
|
|
11536
11654
|
async insertDocumentInternal(document, tx) {
|
|
11537
|
-
const metadata = await this.getDocumentInnerMetadata(tx);
|
|
11655
|
+
const metadata = await this.api.getDocumentInnerMetadata(tx);
|
|
11538
11656
|
const id = ++metadata.lastId;
|
|
11539
|
-
await this.updateDocumentInnerMetadata(metadata, tx);
|
|
11657
|
+
await this.api.updateDocumentInnerMetadata(metadata, tx);
|
|
11540
11658
|
const dataplyDocument = Object.assign({
|
|
11541
11659
|
_id: id
|
|
11542
11660
|
}, document);
|
|
11543
|
-
const pk = await
|
|
11661
|
+
const pk = await this.api.insert(JSON.stringify(dataplyDocument), true, tx);
|
|
11544
11662
|
return {
|
|
11545
11663
|
pk,
|
|
11546
11664
|
id,
|
|
@@ -11554,26 +11672,26 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
|
|
|
11554
11672
|
* @returns The primary key of the inserted document
|
|
11555
11673
|
*/
|
|
11556
11674
|
async insertSingleDocument(document, tx) {
|
|
11557
|
-
return this.runWithDefaultWrite(async (tx2) => {
|
|
11675
|
+
return this.api.runWithDefaultWrite(async (tx2) => {
|
|
11558
11676
|
const { pk: dpk, document: dataplyDocument } = await this.insertDocumentInternal(document, tx2);
|
|
11559
|
-
const flattenDocument = this.flattenDocument(dataplyDocument);
|
|
11560
|
-
for (const [indexName, config] of this.registeredIndices) {
|
|
11561
|
-
const tree = this.trees.get(indexName);
|
|
11677
|
+
const flattenDocument = this.api.flattenDocument(dataplyDocument);
|
|
11678
|
+
for (const [indexName, config] of this.api.indexManager.registeredIndices) {
|
|
11679
|
+
const tree = this.api.trees.get(indexName);
|
|
11562
11680
|
if (!tree) continue;
|
|
11563
11681
|
if (config.type === "fts") {
|
|
11564
|
-
const primaryField = this.getPrimaryField(config);
|
|
11682
|
+
const primaryField = this.api.indexManager.getPrimaryField(config);
|
|
11565
11683
|
const v = flattenDocument[primaryField];
|
|
11566
11684
|
if (v === void 0 || typeof v !== "string") continue;
|
|
11567
|
-
const ftsConfig = this.getFtsConfig(config);
|
|
11685
|
+
const ftsConfig = this.api.indexManager.getFtsConfig(config);
|
|
11568
11686
|
const tokens = ftsConfig ? tokenize(v, ftsConfig) : [v];
|
|
11569
11687
|
for (let i = 0, len = tokens.length; i < len; i++) {
|
|
11570
11688
|
const token = tokens[i];
|
|
11571
|
-
const keyToInsert = this.getTokenKey(dpk, token);
|
|
11689
|
+
const keyToInsert = this.api.indexManager.getTokenKey(dpk, token);
|
|
11572
11690
|
const [error] = await catchPromise(tree.insert(keyToInsert, { k: dpk, v: token }));
|
|
11573
11691
|
if (error) throw error;
|
|
11574
11692
|
}
|
|
11575
11693
|
} else {
|
|
11576
|
-
const indexVal = this.getIndexValue(config, flattenDocument);
|
|
11694
|
+
const indexVal = this.api.indexManager.getIndexValue(config, flattenDocument);
|
|
11577
11695
|
if (indexVal === void 0) continue;
|
|
11578
11696
|
const [error] = await catchPromise(tree.insert(dpk, { k: dpk, v: indexVal }));
|
|
11579
11697
|
if (error) throw error;
|
|
@@ -11589,11 +11707,11 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
|
|
|
11589
11707
|
* @returns The primary keys of the inserted documents
|
|
11590
11708
|
*/
|
|
11591
11709
|
async insertBatchDocuments(documents, tx) {
|
|
11592
|
-
return this.runWithDefaultWrite(async (tx2) => {
|
|
11593
|
-
const metadata = await this.getDocumentInnerMetadata(tx2);
|
|
11710
|
+
return this.api.runWithDefaultWrite(async (tx2) => {
|
|
11711
|
+
const metadata = await this.api.getDocumentInnerMetadata(tx2);
|
|
11594
11712
|
const startId = metadata.lastId + 1;
|
|
11595
11713
|
metadata.lastId += documents.length;
|
|
11596
|
-
await this.updateDocumentInnerMetadata(metadata, tx2);
|
|
11714
|
+
await this.api.updateDocumentInnerMetadata(metadata, tx2);
|
|
11597
11715
|
const ids = [];
|
|
11598
11716
|
const dataplyDocuments = [];
|
|
11599
11717
|
const flattenedData = [];
|
|
@@ -11604,22 +11722,22 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
|
|
|
11604
11722
|
}, documents[i]);
|
|
11605
11723
|
const stringified = JSON.stringify(dataplyDocument);
|
|
11606
11724
|
dataplyDocuments.push(stringified);
|
|
11607
|
-
const flattenDocument = this.flattenDocument(dataplyDocument);
|
|
11725
|
+
const flattenDocument = this.api.flattenDocument(dataplyDocument);
|
|
11608
11726
|
flattenedData.push({ pk: -1, data: flattenDocument });
|
|
11609
11727
|
ids.push(id);
|
|
11610
11728
|
}
|
|
11611
|
-
const pks = await
|
|
11729
|
+
const pks = await this.api.insertBatch(dataplyDocuments, true, tx2);
|
|
11612
11730
|
for (let i = 0, len = pks.length; i < len; i++) {
|
|
11613
11731
|
flattenedData[i].pk = pks[i];
|
|
11614
11732
|
}
|
|
11615
|
-
for (const [indexName, config] of this.registeredIndices) {
|
|
11616
|
-
const tree = this.trees.get(indexName);
|
|
11733
|
+
for (const [indexName, config] of this.api.indexManager.registeredIndices) {
|
|
11734
|
+
const tree = this.api.trees.get(indexName);
|
|
11617
11735
|
if (!tree) continue;
|
|
11618
11736
|
const treeTx = await tree.createTransaction();
|
|
11619
11737
|
const batchInsertData = [];
|
|
11620
11738
|
if (config.type === "fts") {
|
|
11621
|
-
const primaryField = this.getPrimaryField(config);
|
|
11622
|
-
const ftsConfig = this.getFtsConfig(config);
|
|
11739
|
+
const primaryField = this.api.indexManager.getPrimaryField(config);
|
|
11740
|
+
const ftsConfig = this.api.indexManager.getFtsConfig(config);
|
|
11623
11741
|
for (let i = 0, len = flattenedData.length; i < len; i++) {
|
|
11624
11742
|
const item = flattenedData[i];
|
|
11625
11743
|
const v = item.data[primaryField];
|
|
@@ -11627,13 +11745,13 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
|
|
|
11627
11745
|
const tokens = ftsConfig ? tokenize(v, ftsConfig) : [v];
|
|
11628
11746
|
for (let j = 0, tLen = tokens.length; j < tLen; j++) {
|
|
11629
11747
|
const token = tokens[j];
|
|
11630
|
-
batchInsertData.push([this.getTokenKey(item.pk, token), { k: item.pk, v: token }]);
|
|
11748
|
+
batchInsertData.push([this.api.indexManager.getTokenKey(item.pk, token), { k: item.pk, v: token }]);
|
|
11631
11749
|
}
|
|
11632
11750
|
}
|
|
11633
11751
|
} else {
|
|
11634
11752
|
for (let i = 0, len = flattenedData.length; i < len; i++) {
|
|
11635
11753
|
const item = flattenedData[i];
|
|
11636
|
-
const indexVal = this.getIndexValue(config, item.data);
|
|
11754
|
+
const indexVal = this.api.indexManager.getIndexValue(config, item.data);
|
|
11637
11755
|
if (indexVal === void 0) continue;
|
|
11638
11756
|
batchInsertData.push([item.pk, { k: item.pk, v: indexVal }]);
|
|
11639
11757
|
}
|
|
@@ -11658,46 +11776,46 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
|
|
|
11658
11776
|
* @returns The number of updated documents
|
|
11659
11777
|
*/
|
|
11660
11778
|
async updateInternal(query, computeUpdatedDoc, tx) {
|
|
11661
|
-
const pks = await this.getKeys(query);
|
|
11779
|
+
const pks = await this.api.queryManager.getKeys(query);
|
|
11662
11780
|
let updatedCount = 0;
|
|
11663
11781
|
const treeTxs = /* @__PURE__ */ new Map();
|
|
11664
|
-
for (const [indexName, tree] of this.trees) {
|
|
11782
|
+
for (const [indexName, tree] of this.api.trees) {
|
|
11665
11783
|
treeTxs.set(indexName, await tree.createTransaction());
|
|
11666
11784
|
}
|
|
11667
11785
|
treeTxs.delete("_id");
|
|
11668
11786
|
for (let i = 0, len = pks.length; i < len; i++) {
|
|
11669
11787
|
const pk = pks[i];
|
|
11670
|
-
const doc = await this.getDocument(pk, tx);
|
|
11788
|
+
const doc = await this.api.getDocument(pk, tx);
|
|
11671
11789
|
if (!doc) continue;
|
|
11672
11790
|
const updatedDoc = computeUpdatedDoc(doc);
|
|
11673
|
-
const oldFlatDoc = this.flattenDocument(doc);
|
|
11674
|
-
const newFlatDoc = this.flattenDocument(updatedDoc);
|
|
11791
|
+
const oldFlatDoc = this.api.flattenDocument(doc);
|
|
11792
|
+
const newFlatDoc = this.api.flattenDocument(updatedDoc);
|
|
11675
11793
|
for (const [indexName, treeTx] of treeTxs) {
|
|
11676
|
-
const config = this.registeredIndices.get(indexName);
|
|
11794
|
+
const config = this.api.indexManager.registeredIndices.get(indexName);
|
|
11677
11795
|
if (!config) continue;
|
|
11678
11796
|
if (config.type === "fts") {
|
|
11679
|
-
const primaryField = this.getPrimaryField(config);
|
|
11797
|
+
const primaryField = this.api.indexManager.getPrimaryField(config);
|
|
11680
11798
|
const oldV = oldFlatDoc[primaryField];
|
|
11681
11799
|
const newV = newFlatDoc[primaryField];
|
|
11682
11800
|
if (oldV === newV) continue;
|
|
11683
|
-
const ftsConfig = this.getFtsConfig(config);
|
|
11801
|
+
const ftsConfig = this.api.indexManager.getFtsConfig(config);
|
|
11684
11802
|
if (typeof oldV === "string") {
|
|
11685
11803
|
const oldTokens = ftsConfig ? tokenize(oldV, ftsConfig) : [oldV];
|
|
11686
11804
|
for (let j = 0, jLen = oldTokens.length; j < jLen; j++) {
|
|
11687
|
-
await treeTx.delete(this.getTokenKey(pk, oldTokens[j]), { k: pk, v: oldTokens[j] });
|
|
11805
|
+
await treeTx.delete(this.api.indexManager.getTokenKey(pk, oldTokens[j]), { k: pk, v: oldTokens[j] });
|
|
11688
11806
|
}
|
|
11689
11807
|
}
|
|
11690
11808
|
if (typeof newV === "string") {
|
|
11691
11809
|
const newTokens = ftsConfig ? tokenize(newV, ftsConfig) : [newV];
|
|
11692
11810
|
const batchInsertData = [];
|
|
11693
11811
|
for (let j = 0, jLen = newTokens.length; j < jLen; j++) {
|
|
11694
|
-
batchInsertData.push([this.getTokenKey(pk, newTokens[j]), { k: pk, v: newTokens[j] }]);
|
|
11812
|
+
batchInsertData.push([this.api.indexManager.getTokenKey(pk, newTokens[j]), { k: pk, v: newTokens[j] }]);
|
|
11695
11813
|
}
|
|
11696
11814
|
await treeTx.batchInsert(batchInsertData);
|
|
11697
11815
|
}
|
|
11698
11816
|
} else {
|
|
11699
|
-
const oldIndexVal = this.getIndexValue(config, oldFlatDoc);
|
|
11700
|
-
const newIndexVal = this.getIndexValue(config, newFlatDoc);
|
|
11817
|
+
const oldIndexVal = this.api.indexManager.getIndexValue(config, oldFlatDoc);
|
|
11818
|
+
const newIndexVal = this.api.indexManager.getIndexValue(config, newFlatDoc);
|
|
11701
11819
|
if (JSON.stringify(oldIndexVal) === JSON.stringify(newIndexVal)) continue;
|
|
11702
11820
|
if (oldIndexVal !== void 0) {
|
|
11703
11821
|
await treeTx.delete(pk, { k: pk, v: oldIndexVal });
|
|
@@ -11707,7 +11825,7 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
|
|
|
11707
11825
|
}
|
|
11708
11826
|
}
|
|
11709
11827
|
}
|
|
11710
|
-
await this.update(pk, JSON.stringify(updatedDoc), tx);
|
|
11828
|
+
await this.api.update(pk, JSON.stringify(updatedDoc), tx);
|
|
11711
11829
|
updatedCount++;
|
|
11712
11830
|
}
|
|
11713
11831
|
for (const [indexName, treeTx] of treeTxs) {
|
|
@@ -11729,7 +11847,7 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
|
|
|
11729
11847
|
* @returns The number of updated documents
|
|
11730
11848
|
*/
|
|
11731
11849
|
async fullUpdate(query, newRecord, tx) {
|
|
11732
|
-
return this.runWithDefaultWrite(async (tx2) => {
|
|
11850
|
+
return this.api.runWithDefaultWrite(async (tx2) => {
|
|
11733
11851
|
return this.updateInternal(query, (doc) => {
|
|
11734
11852
|
const newDoc = typeof newRecord === "function" ? newRecord(doc) : newRecord;
|
|
11735
11853
|
return { _id: doc._id, ...newDoc };
|
|
@@ -11744,7 +11862,7 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
|
|
|
11744
11862
|
* @returns The number of updated documents
|
|
11745
11863
|
*/
|
|
11746
11864
|
async partialUpdate(query, newRecord, tx) {
|
|
11747
|
-
return this.runWithDefaultWrite(async (tx2) => {
|
|
11865
|
+
return this.api.runWithDefaultWrite(async (tx2) => {
|
|
11748
11866
|
return this.updateInternal(query, (doc) => {
|
|
11749
11867
|
const partialUpdateContent = typeof newRecord === "function" ? newRecord(doc) : newRecord;
|
|
11750
11868
|
const finalUpdate = { ...partialUpdateContent };
|
|
@@ -11760,189 +11878,300 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
|
|
|
11760
11878
|
* @returns The number of deleted documents
|
|
11761
11879
|
*/
|
|
11762
11880
|
async deleteDocuments(query, tx) {
|
|
11763
|
-
return this.runWithDefaultWrite(async (tx2) => {
|
|
11764
|
-
const pks = await this.getKeys(query);
|
|
11881
|
+
return this.api.runWithDefaultWrite(async (tx2) => {
|
|
11882
|
+
const pks = await this.api.queryManager.getKeys(query);
|
|
11765
11883
|
let deletedCount = 0;
|
|
11766
11884
|
for (let i = 0, len = pks.length; i < len; i++) {
|
|
11767
11885
|
const pk = pks[i];
|
|
11768
|
-
const doc = await this.getDocument(pk, tx2);
|
|
11886
|
+
const doc = await this.api.getDocument(pk, tx2);
|
|
11769
11887
|
if (!doc) continue;
|
|
11770
|
-
const flatDoc = this.flattenDocument(doc);
|
|
11771
|
-
for (const [indexName, tree] of this.trees) {
|
|
11772
|
-
const config = this.registeredIndices.get(indexName);
|
|
11888
|
+
const flatDoc = this.api.flattenDocument(doc);
|
|
11889
|
+
for (const [indexName, tree] of this.api.trees) {
|
|
11890
|
+
const config = this.api.indexManager.registeredIndices.get(indexName);
|
|
11773
11891
|
if (!config) continue;
|
|
11774
11892
|
if (config.type === "fts") {
|
|
11775
|
-
const primaryField = this.getPrimaryField(config);
|
|
11893
|
+
const primaryField = this.api.indexManager.getPrimaryField(config);
|
|
11776
11894
|
const v = flatDoc[primaryField];
|
|
11777
11895
|
if (v === void 0 || typeof v !== "string") continue;
|
|
11778
|
-
const ftsConfig = this.getFtsConfig(config);
|
|
11896
|
+
const ftsConfig = this.api.indexManager.getFtsConfig(config);
|
|
11779
11897
|
const tokens = ftsConfig ? tokenize(v, ftsConfig) : [v];
|
|
11780
11898
|
for (let j = 0, jLen = tokens.length; j < jLen; j++) {
|
|
11781
|
-
await tree.delete(this.getTokenKey(pk, tokens[j]), { k: pk, v: tokens[j] });
|
|
11899
|
+
await tree.delete(this.api.indexManager.getTokenKey(pk, tokens[j]), { k: pk, v: tokens[j] });
|
|
11782
11900
|
}
|
|
11783
11901
|
} else {
|
|
11784
|
-
const indexVal = this.getIndexValue(config, flatDoc);
|
|
11902
|
+
const indexVal = this.api.indexManager.getIndexValue(config, flatDoc);
|
|
11785
11903
|
if (indexVal === void 0) continue;
|
|
11786
11904
|
await tree.delete(pk, { k: pk, v: indexVal });
|
|
11787
11905
|
}
|
|
11788
11906
|
}
|
|
11789
|
-
await
|
|
11907
|
+
await this.api.delete(pk, true, tx2);
|
|
11790
11908
|
deletedCount++;
|
|
11791
11909
|
}
|
|
11792
11910
|
return deletedCount;
|
|
11793
11911
|
}, tx);
|
|
11794
11912
|
}
|
|
11913
|
+
};
|
|
11914
|
+
|
|
11915
|
+
// src/core/MetadataManager.ts
|
|
11916
|
+
var MetadataManager = class {
|
|
11917
|
+
constructor(api) {
|
|
11918
|
+
this.api = api;
|
|
11919
|
+
}
|
|
11920
|
+
async getDocumentMetadata(tx) {
|
|
11921
|
+
const metadata = await this.api.getMetadata(tx);
|
|
11922
|
+
const innerMetadata = await this.getDocumentInnerMetadata(tx);
|
|
11923
|
+
const indices = [];
|
|
11924
|
+
for (const name of this.api.indexManager.registeredIndices.keys()) {
|
|
11925
|
+
if (name !== "_id") {
|
|
11926
|
+
indices.push(name);
|
|
11927
|
+
}
|
|
11928
|
+
}
|
|
11929
|
+
return {
|
|
11930
|
+
pageSize: metadata.pageSize,
|
|
11931
|
+
pageCount: metadata.pageCount,
|
|
11932
|
+
rowCount: metadata.rowCount,
|
|
11933
|
+
usage: metadata.usage,
|
|
11934
|
+
indices,
|
|
11935
|
+
schemeVersion: innerMetadata.schemeVersion ?? 0
|
|
11936
|
+
};
|
|
11937
|
+
}
|
|
11938
|
+
async getDocumentInnerMetadata(tx) {
|
|
11939
|
+
const row = await this.api.select(1, false, tx);
|
|
11940
|
+
if (!row) {
|
|
11941
|
+
throw new Error("Document metadata not found");
|
|
11942
|
+
}
|
|
11943
|
+
return JSON.parse(row);
|
|
11944
|
+
}
|
|
11945
|
+
async updateDocumentInnerMetadata(metadata, tx) {
|
|
11946
|
+
await this.api.update(1, JSON.stringify(metadata), tx);
|
|
11947
|
+
}
|
|
11795
11948
|
/**
|
|
11796
|
-
*
|
|
11797
|
-
*
|
|
11798
|
-
* @param
|
|
11799
|
-
* @
|
|
11949
|
+
* Run a migration if the current schemeVersion is lower than the target version.
|
|
11950
|
+
* After the callback completes, schemeVersion is updated to the target version.
|
|
11951
|
+
* @param version The target scheme version
|
|
11952
|
+
* @param callback The migration callback
|
|
11953
|
+
* @param tx Optional transaction
|
|
11800
11954
|
*/
|
|
11801
|
-
async
|
|
11955
|
+
async migration(version, callback, tx) {
|
|
11956
|
+
await this.api.runWithDefaultWrite(async (tx2) => {
|
|
11957
|
+
const innerMetadata = await this.getDocumentInnerMetadata(tx2);
|
|
11958
|
+
const currentVersion = innerMetadata.schemeVersion ?? 0;
|
|
11959
|
+
if (currentVersion < version) {
|
|
11960
|
+
await callback(tx2);
|
|
11961
|
+
const freshMetadata = await this.getDocumentInnerMetadata(tx2);
|
|
11962
|
+
freshMetadata.schemeVersion = version;
|
|
11963
|
+
freshMetadata.updatedAt = Date.now();
|
|
11964
|
+
await this.updateDocumentInnerMetadata(freshMetadata, tx2);
|
|
11965
|
+
}
|
|
11966
|
+
}, tx);
|
|
11967
|
+
}
|
|
11968
|
+
};
|
|
11969
|
+
|
|
11970
|
+
// src/core/DocumentFormatter.ts
|
|
11971
|
+
var DocumentFormatter = class {
|
|
11972
|
+
flattenInternal(obj, parentKey = "", result = {}) {
|
|
11973
|
+
for (const key in obj) {
|
|
11974
|
+
const newKey = parentKey ? `${parentKey}.${key}` : key;
|
|
11975
|
+
if (typeof obj[key] === "object" && obj[key] !== null) {
|
|
11976
|
+
this.flattenInternal(obj[key], newKey, result);
|
|
11977
|
+
} else {
|
|
11978
|
+
result[newKey] = obj[key];
|
|
11979
|
+
}
|
|
11980
|
+
}
|
|
11981
|
+
return result;
|
|
11982
|
+
}
|
|
11983
|
+
/**
|
|
11984
|
+
* returns flattened document
|
|
11985
|
+
* @param document
|
|
11986
|
+
* @returns
|
|
11987
|
+
*/
|
|
11988
|
+
flattenDocument(document) {
|
|
11989
|
+
return this.flattenInternal(document, "", {});
|
|
11990
|
+
}
|
|
11991
|
+
};
|
|
11992
|
+
|
|
11993
|
+
// src/core/documentAPI.ts
|
|
11994
|
+
var DocumentDataplyAPI = class extends import_dataply4.DataplyAPI {
|
|
11995
|
+
comparator = new DocumentValueComparator();
|
|
11996
|
+
_initialized = false;
|
|
11997
|
+
optimizer;
|
|
11998
|
+
queryManager;
|
|
11999
|
+
indexManager;
|
|
12000
|
+
mutationManager;
|
|
12001
|
+
metadataManager;
|
|
12002
|
+
documentFormatter;
|
|
12003
|
+
constructor(file, options) {
|
|
12004
|
+
super(file, options);
|
|
12005
|
+
this.optimizer = new Optimizer(this);
|
|
12006
|
+
this.queryManager = new QueryManager(this, this.optimizer);
|
|
12007
|
+
this.indexManager = new IndexManager(this);
|
|
12008
|
+
this.mutationManager = new MutationManager(this);
|
|
12009
|
+
this.metadataManager = new MetadataManager(this);
|
|
12010
|
+
this.documentFormatter = new DocumentFormatter();
|
|
12011
|
+
this.hook.onceAfter("init", async (tx, isNewlyCreated) => {
|
|
12012
|
+
if (isNewlyCreated) {
|
|
12013
|
+
await this.initializeDocumentFile(tx);
|
|
12014
|
+
}
|
|
12015
|
+
if (!await this.verifyDocumentFile(tx)) {
|
|
12016
|
+
throw new Error("Document metadata verification failed");
|
|
12017
|
+
}
|
|
12018
|
+
const metadata = await this.getDocumentInnerMetadata(tx);
|
|
12019
|
+
await this.indexManager.initializeIndices(metadata, isNewlyCreated, tx);
|
|
12020
|
+
this._initialized = true;
|
|
12021
|
+
return tx;
|
|
12022
|
+
});
|
|
12023
|
+
}
|
|
12024
|
+
/**
|
|
12025
|
+
* Whether the document database has been initialized.
|
|
12026
|
+
*/
|
|
12027
|
+
get isDocInitialized() {
|
|
12028
|
+
return this._initialized;
|
|
12029
|
+
}
|
|
12030
|
+
get indices() {
|
|
12031
|
+
return this.indexManager.indices;
|
|
12032
|
+
}
|
|
12033
|
+
get trees() {
|
|
12034
|
+
return this.indexManager.trees;
|
|
12035
|
+
}
|
|
12036
|
+
get indexedFields() {
|
|
12037
|
+
return this.indexManager.indexedFields;
|
|
12038
|
+
}
|
|
12039
|
+
async registerIndex(name, option, tx) {
|
|
12040
|
+
return this.indexManager.registerIndex(name, option, tx);
|
|
12041
|
+
}
|
|
12042
|
+
/**
|
|
12043
|
+
* Drop (remove) a named index.
|
|
12044
|
+
*/
|
|
12045
|
+
async dropIndex(name, tx) {
|
|
12046
|
+
return this.indexManager.dropIndex(name, tx);
|
|
12047
|
+
}
|
|
12048
|
+
async getDocument(pk, tx) {
|
|
11802
12049
|
return this.runWithDefault(async (tx2) => {
|
|
11803
|
-
const
|
|
11804
|
-
|
|
12050
|
+
const row = await this.select(pk, false, tx2);
|
|
12051
|
+
if (!row) {
|
|
12052
|
+
throw new Error(`Document not found with PK: ${pk}`);
|
|
12053
|
+
}
|
|
12054
|
+
return JSON.parse(row);
|
|
11805
12055
|
}, tx);
|
|
11806
12056
|
}
|
|
11807
12057
|
/**
|
|
11808
|
-
*
|
|
12058
|
+
* Backfill indices for newly created indices after data was inserted.
|
|
12059
|
+
* Delegated to IndexManager.
|
|
12060
|
+
*/
|
|
12061
|
+
async backfillIndices(tx) {
|
|
12062
|
+
return this.indexManager.backfillIndices(tx);
|
|
12063
|
+
}
|
|
12064
|
+
createDocumentInnerMetadata(indices) {
|
|
12065
|
+
return {
|
|
12066
|
+
magicString: "document-dataply",
|
|
12067
|
+
version: 1,
|
|
12068
|
+
createdAt: Date.now(),
|
|
12069
|
+
updatedAt: Date.now(),
|
|
12070
|
+
lastId: 0,
|
|
12071
|
+
schemeVersion: 0,
|
|
12072
|
+
indices
|
|
12073
|
+
};
|
|
12074
|
+
}
|
|
12075
|
+
async initializeDocumentFile(tx) {
|
|
12076
|
+
const metadata = await this.select(1, false, tx);
|
|
12077
|
+
if (metadata) {
|
|
12078
|
+
throw new Error("Document metadata already exists");
|
|
12079
|
+
}
|
|
12080
|
+
const metaObj = this.createDocumentInnerMetadata({
|
|
12081
|
+
_id: [-1, { type: "btree", fields: ["_id"] }]
|
|
12082
|
+
});
|
|
12083
|
+
await this.insertAsOverflow(JSON.stringify(metaObj), false, tx);
|
|
12084
|
+
}
|
|
12085
|
+
async verifyDocumentFile(tx) {
|
|
12086
|
+
const row = await this.select(1, false, tx);
|
|
12087
|
+
if (!row) {
|
|
12088
|
+
return false;
|
|
12089
|
+
}
|
|
12090
|
+
const data = JSON.parse(row);
|
|
12091
|
+
return data.magicString === "document-dataply" && data.version === 1;
|
|
12092
|
+
}
|
|
12093
|
+
/**
|
|
12094
|
+
* returns flattened document
|
|
12095
|
+
* @param document
|
|
12096
|
+
* @returns
|
|
12097
|
+
*/
|
|
12098
|
+
flattenDocument(document) {
|
|
12099
|
+
return this.documentFormatter.flattenDocument(document);
|
|
12100
|
+
}
|
|
12101
|
+
async getDocumentMetadata(tx) {
|
|
12102
|
+
return this.metadataManager.getDocumentMetadata(tx);
|
|
12103
|
+
}
|
|
12104
|
+
async getDocumentInnerMetadata(tx) {
|
|
12105
|
+
return this.metadataManager.getDocumentInnerMetadata(tx);
|
|
12106
|
+
}
|
|
12107
|
+
async updateDocumentInnerMetadata(metadata, tx) {
|
|
12108
|
+
return this.metadataManager.updateDocumentInnerMetadata(metadata, tx);
|
|
12109
|
+
}
|
|
12110
|
+
/**
|
|
12111
|
+
* Run a migration if the current schemeVersion is lower than the target version.
|
|
12112
|
+
* After the callback completes, schemeVersion is updated to the target version.
|
|
12113
|
+
* @param version The target scheme version
|
|
12114
|
+
* @param callback The migration callback
|
|
12115
|
+
* @param tx Optional transaction
|
|
12116
|
+
*/
|
|
12117
|
+
async migration(version, callback, tx) {
|
|
12118
|
+
return this.metadataManager.migration(version, callback, tx);
|
|
12119
|
+
}
|
|
12120
|
+
/**
|
|
12121
|
+
* Insert a document into the database
|
|
12122
|
+
* @param document The document to insert
|
|
12123
|
+
* @param tx The transaction to use
|
|
12124
|
+
* @returns The primary key of the inserted document
|
|
12125
|
+
*/
|
|
12126
|
+
async insertSingleDocument(document, tx) {
|
|
12127
|
+
return this.mutationManager.insertSingleDocument(document, tx);
|
|
12128
|
+
}
|
|
12129
|
+
/**
|
|
12130
|
+
* Insert a batch of documents into the database
|
|
12131
|
+
* @param documents The documents to insert
|
|
12132
|
+
* @param tx The transaction to use
|
|
12133
|
+
* @returns The primary keys of the inserted documents
|
|
11809
12134
|
*/
|
|
11810
|
-
|
|
11811
|
-
|
|
11812
|
-
for (let i = 0, len = ftsConditions.length; i < len; i++) {
|
|
11813
|
-
const { field, matchTokens } = ftsConditions[i];
|
|
11814
|
-
const docValue = flatDoc[field];
|
|
11815
|
-
if (typeof docValue !== "string") return false;
|
|
11816
|
-
for (let j = 0, jLen = matchTokens.length; j < jLen; j++) {
|
|
11817
|
-
const token = matchTokens[j];
|
|
11818
|
-
if (!docValue.includes(token)) return false;
|
|
11819
|
-
}
|
|
11820
|
-
}
|
|
11821
|
-
return true;
|
|
12135
|
+
async insertBatchDocuments(documents, tx) {
|
|
12136
|
+
return this.mutationManager.insertBatchDocuments(documents, tx);
|
|
11822
12137
|
}
|
|
11823
12138
|
/**
|
|
11824
|
-
*
|
|
12139
|
+
* Fully update documents from the database that match the query
|
|
12140
|
+
* @param query The query to use
|
|
12141
|
+
* @param newRecord Complete document to replace with, or function that receives current document and returns new document
|
|
12142
|
+
* @param tx The transaction to use
|
|
12143
|
+
* @returns The number of updated documents
|
|
11825
12144
|
*/
|
|
11826
|
-
|
|
11827
|
-
|
|
11828
|
-
const flatDoc = this.flattenDocument(doc);
|
|
11829
|
-
for (let i = 0, len = conditions.length; i < len; i++) {
|
|
11830
|
-
const { field, condition } = conditions[i];
|
|
11831
|
-
const docValue = flatDoc[field];
|
|
11832
|
-
if (docValue === void 0) return false;
|
|
11833
|
-
const treeValue = { k: doc._id, v: docValue };
|
|
11834
|
-
if (!this.verifyValue(docValue, condition)) return false;
|
|
11835
|
-
}
|
|
11836
|
-
return true;
|
|
12145
|
+
async fullUpdate(query, newRecord, tx) {
|
|
12146
|
+
return this.mutationManager.fullUpdate(query, newRecord, tx);
|
|
11837
12147
|
}
|
|
11838
12148
|
/**
|
|
11839
|
-
*
|
|
12149
|
+
* Partially update documents from the database that match the query
|
|
12150
|
+
* @param query The query to use
|
|
12151
|
+
* @param newRecord Partial document to merge, or function that receives current document and returns partial update
|
|
12152
|
+
* @param tx The transaction to use
|
|
12153
|
+
* @returns The number of updated documents
|
|
11840
12154
|
*/
|
|
11841
|
-
|
|
11842
|
-
|
|
11843
|
-
return value === condition;
|
|
11844
|
-
}
|
|
11845
|
-
if ("primaryEqual" in condition) {
|
|
11846
|
-
return value === condition.primaryEqual?.v;
|
|
11847
|
-
}
|
|
11848
|
-
if ("primaryNotEqual" in condition) {
|
|
11849
|
-
return value !== condition.primaryNotEqual?.v;
|
|
11850
|
-
}
|
|
11851
|
-
if ("primaryLt" in condition) {
|
|
11852
|
-
return value !== null && condition.primaryLt?.v !== void 0 && value < condition.primaryLt.v;
|
|
11853
|
-
}
|
|
11854
|
-
if ("primaryLte" in condition) {
|
|
11855
|
-
return value !== null && condition.primaryLte?.v !== void 0 && value <= condition.primaryLte.v;
|
|
11856
|
-
}
|
|
11857
|
-
if ("primaryGt" in condition) {
|
|
11858
|
-
return value !== null && condition.primaryGt?.v !== void 0 && value > condition.primaryGt.v;
|
|
11859
|
-
}
|
|
11860
|
-
if ("primaryGte" in condition) {
|
|
11861
|
-
return value !== null && condition.primaryGte?.v !== void 0 && value >= condition.primaryGte.v;
|
|
11862
|
-
}
|
|
11863
|
-
if ("primaryOr" in condition && Array.isArray(condition.primaryOr)) {
|
|
11864
|
-
return condition.primaryOr.some((c) => value === c?.v);
|
|
11865
|
-
}
|
|
11866
|
-
return true;
|
|
12155
|
+
async partialUpdate(query, newRecord, tx) {
|
|
12156
|
+
return this.mutationManager.partialUpdate(query, newRecord, tx);
|
|
11867
12157
|
}
|
|
11868
12158
|
/**
|
|
11869
|
-
*
|
|
12159
|
+
* Delete documents from the database that match the query
|
|
12160
|
+
* @param query The query to use
|
|
12161
|
+
* @param tx The transaction to use
|
|
12162
|
+
* @returns The number of deleted documents
|
|
11870
12163
|
*/
|
|
11871
|
-
|
|
11872
|
-
|
|
11873
|
-
const { verySmallChunkSize, smallChunkSize } = this.getFreeMemoryChunkSize();
|
|
11874
|
-
if (chunkTotalSize < verySmallChunkSize) return currentChunkSize * 2;
|
|
11875
|
-
if (chunkTotalSize > smallChunkSize) return Math.max(Math.floor(currentChunkSize / 2), 20);
|
|
11876
|
-
return currentChunkSize;
|
|
12164
|
+
async deleteDocuments(query, tx) {
|
|
12165
|
+
return this.mutationManager.deleteDocuments(query, tx);
|
|
11877
12166
|
}
|
|
11878
12167
|
/**
|
|
11879
|
-
*
|
|
11880
|
-
*
|
|
12168
|
+
* Count documents from the database that match the query
|
|
12169
|
+
* @param query The query to use
|
|
12170
|
+
* @param tx The transaction to use
|
|
12171
|
+
* @returns The number of documents that match the query
|
|
11881
12172
|
*/
|
|
11882
|
-
async
|
|
11883
|
-
|
|
11884
|
-
const isFts = ftsConditions.length > 0;
|
|
11885
|
-
const isCompositeVerify = compositeVerifyConditions.length > 0;
|
|
11886
|
-
const isVerifyOthers = verifyOthers.length > 0;
|
|
11887
|
-
const isInfiniteLimit = isFinite(limit);
|
|
11888
|
-
let currentChunkSize = isInfiniteLimit ? initialChunkSize : limit;
|
|
11889
|
-
let chunk = [];
|
|
11890
|
-
let chunkSize = 0;
|
|
11891
|
-
let dropped = 0;
|
|
11892
|
-
const processChunk = async (pks) => {
|
|
11893
|
-
const docs = [];
|
|
11894
|
-
const rawResults = await this.selectMany(new Float64Array(pks), false, tx);
|
|
11895
|
-
let chunkTotalSize = 0;
|
|
11896
|
-
for (let j = 0, len = rawResults.length; j < len; j++) {
|
|
11897
|
-
const s = rawResults[j];
|
|
11898
|
-
if (!s) continue;
|
|
11899
|
-
const doc = JSON.parse(s);
|
|
11900
|
-
chunkTotalSize += s.length * 2;
|
|
11901
|
-
if (isFts && !this.verifyFts(doc, ftsConditions)) continue;
|
|
11902
|
-
if (isCompositeVerify && this.verifyCompositeConditions(doc, compositeVerifyConditions) === false) continue;
|
|
11903
|
-
if (isVerifyOthers) {
|
|
11904
|
-
const flatDoc = this.flattenDocument(doc);
|
|
11905
|
-
let passed = true;
|
|
11906
|
-
for (let k = 0, kLen = verifyOthers.length; k < kLen; k++) {
|
|
11907
|
-
const other = verifyOthers[k];
|
|
11908
|
-
const fieldValue = flatDoc[other.field];
|
|
11909
|
-
if (fieldValue === void 0) {
|
|
11910
|
-
passed = false;
|
|
11911
|
-
break;
|
|
11912
|
-
}
|
|
11913
|
-
const treeValue = { k: doc._id, v: fieldValue };
|
|
11914
|
-
if (!other.tree.verify(treeValue, other.condition)) {
|
|
11915
|
-
passed = false;
|
|
11916
|
-
break;
|
|
11917
|
-
}
|
|
11918
|
-
}
|
|
11919
|
-
if (!passed) continue;
|
|
11920
|
-
}
|
|
11921
|
-
docs.push(doc);
|
|
11922
|
-
}
|
|
11923
|
-
if (isInfiniteLimit) {
|
|
11924
|
-
currentChunkSize = this.adjustChunkSize(currentChunkSize, chunkTotalSize);
|
|
11925
|
-
}
|
|
11926
|
-
return docs;
|
|
11927
|
-
};
|
|
11928
|
-
for await (const pk of keysStream) {
|
|
11929
|
-
if (dropped < startIdx) {
|
|
11930
|
-
dropped++;
|
|
11931
|
-
continue;
|
|
11932
|
-
}
|
|
11933
|
-
chunk.push(pk);
|
|
11934
|
-
chunkSize++;
|
|
11935
|
-
if (chunkSize >= currentChunkSize) {
|
|
11936
|
-
const docs = await processChunk(chunk);
|
|
11937
|
-
for (let j = 0, dLen = docs.length; j < dLen; j++) yield docs[j];
|
|
11938
|
-
chunk = [];
|
|
11939
|
-
chunkSize = 0;
|
|
11940
|
-
}
|
|
11941
|
-
}
|
|
11942
|
-
if (chunkSize > 0) {
|
|
11943
|
-
const docs = await processChunk(chunk);
|
|
11944
|
-
for (let j = 0, dLen = docs.length; j < dLen; j++) yield docs[j];
|
|
11945
|
-
}
|
|
12173
|
+
async countDocuments(query, tx) {
|
|
12174
|
+
return this.queryManager.countDocuments(query, tx);
|
|
11946
12175
|
}
|
|
11947
12176
|
/**
|
|
11948
12177
|
* Select documents from the database
|
|
@@ -11953,130 +12182,7 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
|
|
|
11953
12182
|
* @throws Error if query or orderBy contains non-indexed fields
|
|
11954
12183
|
*/
|
|
11955
12184
|
selectDocuments(query, options = {}, tx) {
|
|
11956
|
-
|
|
11957
|
-
if (!this.indexedFields.has(field)) {
|
|
11958
|
-
throw new Error(`Query field "${field}" is not indexed. Available indexed fields: ${Array.from(this.indexedFields).join(", ")}`);
|
|
11959
|
-
}
|
|
11960
|
-
}
|
|
11961
|
-
const orderBy = options.orderBy;
|
|
11962
|
-
if (orderBy !== void 0 && !this.indexedFields.has(orderBy)) {
|
|
11963
|
-
throw new Error(`orderBy field "${orderBy}" is not indexed. Available indexed fields: ${Array.from(this.indexedFields).join(", ")}`);
|
|
11964
|
-
}
|
|
11965
|
-
const {
|
|
11966
|
-
limit = Infinity,
|
|
11967
|
-
offset = 0,
|
|
11968
|
-
sortOrder = "asc",
|
|
11969
|
-
orderBy: orderByField
|
|
11970
|
-
} = options;
|
|
11971
|
-
const self = this;
|
|
11972
|
-
const stream = () => this.streamWithDefault(async function* (tx2) {
|
|
11973
|
-
const ftsConditions = [];
|
|
11974
|
-
for (const field in query) {
|
|
11975
|
-
const q = query[field];
|
|
11976
|
-
if (q && typeof q === "object" && "match" in q && typeof q.match === "string") {
|
|
11977
|
-
const indexNames = self.fieldToIndices.get(field) || [];
|
|
11978
|
-
for (const indexName of indexNames) {
|
|
11979
|
-
const config = self.registeredIndices.get(indexName);
|
|
11980
|
-
if (config && config.type === "fts") {
|
|
11981
|
-
const ftsConfig = self.getFtsConfig(config);
|
|
11982
|
-
if (ftsConfig) {
|
|
11983
|
-
ftsConditions.push({ field, matchTokens: tokenize(q.match, ftsConfig) });
|
|
11984
|
-
}
|
|
11985
|
-
break;
|
|
11986
|
-
}
|
|
11987
|
-
}
|
|
11988
|
-
}
|
|
11989
|
-
}
|
|
11990
|
-
const driverResult = await self.getDriverKeys(query, orderByField, sortOrder);
|
|
11991
|
-
if (!driverResult) return;
|
|
11992
|
-
const { keysStream, others, compositeVerifyConditions, isDriverOrderByField, rollback } = driverResult;
|
|
11993
|
-
const initialChunkSize = self.options.pageSize;
|
|
11994
|
-
try {
|
|
11995
|
-
if (!isDriverOrderByField && orderByField) {
|
|
11996
|
-
const topK = limit === Infinity ? Infinity : offset + limit;
|
|
11997
|
-
let heap = null;
|
|
11998
|
-
if (topK !== Infinity) {
|
|
11999
|
-
heap = new BinaryHeap((a, b) => {
|
|
12000
|
-
const aVal = a[orderByField] ?? a._id;
|
|
12001
|
-
const bVal = b[orderByField] ?? b._id;
|
|
12002
|
-
const cmp = aVal < bVal ? -1 : aVal > bVal ? 1 : 0;
|
|
12003
|
-
return sortOrder === "asc" ? -cmp : cmp;
|
|
12004
|
-
});
|
|
12005
|
-
}
|
|
12006
|
-
const results = [];
|
|
12007
|
-
for await (const doc of self.processChunkedKeysWithVerify(
|
|
12008
|
-
keysStream,
|
|
12009
|
-
0,
|
|
12010
|
-
initialChunkSize,
|
|
12011
|
-
Infinity,
|
|
12012
|
-
ftsConditions,
|
|
12013
|
-
compositeVerifyConditions,
|
|
12014
|
-
others,
|
|
12015
|
-
tx2
|
|
12016
|
-
)) {
|
|
12017
|
-
if (heap) {
|
|
12018
|
-
if (heap.size < topK) heap.push(doc);
|
|
12019
|
-
else {
|
|
12020
|
-
const top = heap.peek();
|
|
12021
|
-
if (top) {
|
|
12022
|
-
const aVal = doc[orderByField] ?? doc._id;
|
|
12023
|
-
const bVal = top[orderByField] ?? top._id;
|
|
12024
|
-
const cmp = aVal < bVal ? -1 : aVal > bVal ? 1 : 0;
|
|
12025
|
-
if (sortOrder === "asc" ? cmp < 0 : cmp > 0) heap.replace(doc);
|
|
12026
|
-
}
|
|
12027
|
-
}
|
|
12028
|
-
} else {
|
|
12029
|
-
results.push(doc);
|
|
12030
|
-
}
|
|
12031
|
-
}
|
|
12032
|
-
const finalDocs = heap ? heap.toArray() : results;
|
|
12033
|
-
finalDocs.sort((a, b) => {
|
|
12034
|
-
const aVal = a[orderByField] ?? a._id;
|
|
12035
|
-
const bVal = b[orderByField] ?? b._id;
|
|
12036
|
-
const cmp = aVal < bVal ? -1 : aVal > bVal ? 1 : 0;
|
|
12037
|
-
return sortOrder === "asc" ? cmp : -cmp;
|
|
12038
|
-
});
|
|
12039
|
-
const end = limit === Infinity ? void 0 : offset + limit;
|
|
12040
|
-
const limitedResults = finalDocs.slice(offset, end);
|
|
12041
|
-
for (let j = 0, len = limitedResults.length; j < len; j++) {
|
|
12042
|
-
yield limitedResults[j];
|
|
12043
|
-
}
|
|
12044
|
-
} else {
|
|
12045
|
-
const hasFilters = ftsConditions.length > 0 || compositeVerifyConditions.length > 0 || others.length > 0;
|
|
12046
|
-
const startIdx = hasFilters ? 0 : offset;
|
|
12047
|
-
let yieldedCount = 0;
|
|
12048
|
-
let skippedCount = hasFilters ? 0 : offset;
|
|
12049
|
-
for await (const doc of self.processChunkedKeysWithVerify(
|
|
12050
|
-
keysStream,
|
|
12051
|
-
startIdx,
|
|
12052
|
-
initialChunkSize,
|
|
12053
|
-
limit,
|
|
12054
|
-
ftsConditions,
|
|
12055
|
-
compositeVerifyConditions,
|
|
12056
|
-
others,
|
|
12057
|
-
tx2
|
|
12058
|
-
)) {
|
|
12059
|
-
if (skippedCount < offset) {
|
|
12060
|
-
skippedCount++;
|
|
12061
|
-
continue;
|
|
12062
|
-
}
|
|
12063
|
-
if (yieldedCount >= limit) break;
|
|
12064
|
-
yield doc;
|
|
12065
|
-
yieldedCount++;
|
|
12066
|
-
}
|
|
12067
|
-
}
|
|
12068
|
-
} finally {
|
|
12069
|
-
rollback();
|
|
12070
|
-
}
|
|
12071
|
-
}, tx);
|
|
12072
|
-
const drain = async () => {
|
|
12073
|
-
const result = [];
|
|
12074
|
-
for await (const document of stream()) {
|
|
12075
|
-
result.push(document);
|
|
12076
|
-
}
|
|
12077
|
-
return result;
|
|
12078
|
-
};
|
|
12079
|
-
return { stream, drain };
|
|
12185
|
+
return this.queryManager.selectDocuments(query, options, tx);
|
|
12080
12186
|
}
|
|
12081
12187
|
};
|
|
12082
12188
|
|
|
@@ -12248,7 +12354,7 @@ var DocumentDataply = class _DocumentDataply {
|
|
|
12248
12354
|
};
|
|
12249
12355
|
|
|
12250
12356
|
// src/core/index.ts
|
|
12251
|
-
var
|
|
12357
|
+
var import_dataply5 = __toESM(require_cjs());
|
|
12252
12358
|
// Annotate the CommonJS export names for ESM import in node:
|
|
12253
12359
|
0 && (module.exports = {
|
|
12254
12360
|
DocumentDataply,
|