document-dataply 0.0.10-alpha.3 → 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 CHANGED
@@ -95,7 +95,7 @@ var require_cjs = __commonJS({
95
95
  StringComparator: () => StringComparator,
96
96
  SyncMVCCStrategy: () => SyncMVCCStrategy2,
97
97
  SyncMVCCTransaction: () => SyncMVCCTransaction2,
98
- Transaction: () => Transaction3,
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 Transaction3 = class {
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 Transaction3(
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: () => import_dataply4.GlobalTransaction,
10387
- Transaction: () => import_dataply4.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 os = __toESM(require("node:os"));
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 import_dataply2 = __toESM(require_cjs());
10467
- var DocumentValueComparator = class extends import_dataply2.ValueComparator {
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/documentAPI.ts
10610
- var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10611
- indices = {};
10612
- trees = /* @__PURE__ */ new Map();
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
- * Register an index at runtime (after init).
10741
- * Creates the tree, updates metadata, and backfills existing data.
10474
+ * B-Tree 타입 인덱스의 선택도를 평가하고 트리에 부여할 조건을 산출합니다.
10742
10475
  */
10743
- async registerIndexRuntime(name, option, tx) {
10744
- const config = this.toIndexMetaConfig(option);
10745
- if (this.registeredIndices.has(name)) {
10746
- throw new Error(`Index "${name}" already exists.`);
10747
- }
10748
- await this.runWithDefaultWrite(async (tx2) => {
10749
- const metadata = await this.getDocumentInnerMetadata(tx2);
10750
- metadata.indices[name] = [-1, config];
10751
- await this.updateDocumentInnerMetadata(metadata, tx2);
10752
- this.indices = metadata.indices;
10753
- this.registeredIndices.set(name, config);
10754
- const fields = this.getFieldsFromConfig(config);
10755
- for (let i = 0; i < fields.length; i++) {
10756
- const field = fields[i];
10757
- this.indexedFields.add(field);
10758
- if (!this.fieldToIndices.has(field)) {
10759
- this.fieldToIndices.set(field, []);
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
- const tree = new import_dataply3.BPTreeAsync(
10764
- new DocumentSerializeStrategyAsync(
10765
- this.rowTableEngine.order,
10766
- this,
10767
- this.txContext,
10768
- name
10769
- ),
10770
- this.comparator
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
- await tree.init();
10773
- this.trees.set(name, tree);
10774
- if (metadata.lastId > 0) {
10775
- this.pendingBackfillFields = [name];
10776
- await this.backfillIndices(tx2);
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
- }, tx);
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
- * Drop (remove) a named index.
10782
- * Removes the index from metadata, in-memory maps, and trees.
10783
- * The '_id' index cannot be dropped.
10784
- * @param name The name of the index to drop
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 dropIndex(name, tx) {
10787
- if (name === "_id") {
10788
- throw new Error('Cannot drop the "_id" index.');
10789
- }
10790
- if (!this._initialized) {
10791
- this.pendingCreateIndices.delete(name);
10792
- return;
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
- if (!this.registeredIndices.has(name)) {
10795
- throw new Error(`Index "${name}" does not exist.`);
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
- await this.runWithDefaultWrite(async (tx2) => {
10798
- const config = this.registeredIndices.get(name);
10799
- const metadata = await this.getDocumentInnerMetadata(tx2);
10800
- delete metadata.indices[name];
10801
- await this.updateDocumentInnerMetadata(metadata, tx2);
10802
- this.indices = metadata.indices;
10803
- this.registeredIndices.delete(name);
10804
- const fields = this.getFieldsFromConfig(config);
10805
- for (let i = 0; i < fields.length; i++) {
10806
- const field = fields[i];
10807
- const indexNames = this.fieldToIndices.get(field);
10808
- if (indexNames) {
10809
- const filtered = indexNames.filter((n) => n !== name);
10810
- if (filtered.length === 0) {
10811
- this.fieldToIndices.delete(field);
10812
- if (field !== "_id") {
10813
- this.indexedFields.delete(field);
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
- } else {
10816
- this.fieldToIndices.set(field, filtered);
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
- * Convert CreateIndexOption to IndexMetaConfig for metadata storage.
11197
+ * readHead에서 할당된 headPk를 캐싱하여
11198
+ * writeHead에서 AsyncLocalStorage 컨텍스트 유실 시에도 사용할 수 있도록 함
10825
11199
  */
10826
- toIndexMetaConfig(option) {
10827
- if (!option || typeof option !== "object") {
10828
- throw new Error("Index option must be a non-null object");
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
- if (!option.type) {
10831
- throw new Error('Index option must have a "type" property ("btree" or "fts")');
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
- if (option.type === "btree") {
10834
- if (!Array.isArray(option.fields) || option.fields.length === 0) {
10835
- throw new Error('btree index requires a non-empty "fields" array');
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
- for (let i = 0, len = option.fields.length; i < len; i++) {
10838
- if (typeof option.fields[i] !== "string" || option.fields[i].length === 0) {
10839
- throw new Error(`btree index "fields[${i}]" must be a non-empty string, got: ${JSON.stringify(option.fields[i])}`);
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 (option.type === "fts") {
10848
- if (typeof option.fields !== "string" || option.fields.length === 0) {
10849
- throw new Error(`fts index requires a non-empty string "fields", got: ${JSON.stringify(option.fields)}`);
10850
- }
10851
- if (option.tokenizer === "ngram") {
10852
- if (typeof option.gramSize !== "number" || option.gramSize < 1) {
10853
- throw new Error(`fts ngram index requires a positive "gramSize" number, got: ${JSON.stringify(option.gramSize)}`);
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
- return {
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
- throw new Error(`Unknown index type: ${option.type}`);
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
- * Get all field names from an IndexMetaConfig.
11355
+ * Register an index. If called before init(), queues it.
10872
11356
  */
10873
- getFieldsFromConfig(config) {
10874
- if (config.type === "btree") {
10875
- return config.fields;
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
- return [];
11362
+ await this.registerIndexRuntime(name, option, tx);
10881
11363
  }
10882
11364
  /**
10883
- * Get the primary field of an index (the field used as tree key).
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
- getPrimaryField(config) {
10888
- if (config.type === "btree") {
10889
- return config.fields[0];
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
- return config.fields;
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
- * 인덱스 config에 따라 B+tree에 저장할 v 값을 생성합니다.
10895
- * - 단일 필드 btree: Primitive (단일 값)
10896
- * - 복합 필드 btree: Primitive[] (필드 순서대로 배열)
10897
- * - fts: 별도 처리 (이 메서드 사용 안 함)
10898
- * @returns undefined면 해당 문서에 필수 필드가 없으므로 인덱싱 스킵
11405
+ * Drop (remove) a named index.
10899
11406
  */
10900
- getIndexValue(config, flatDoc) {
10901
- if (config.type !== "btree") return void 0;
10902
- if (config.fields.length === 1) {
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
- const values = [];
10907
- for (let i = 0, len = config.fields.length; i < len; i++) {
10908
- const v = flatDoc[config.fields[i]];
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
- return values;
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
- return { type: "fts", tokenizer: "whitespace" };
10923
- }
10924
- async getDocument(pk, tx) {
10925
- return this.runWithDefault(async (tx2) => {
10926
- const row = await this.select(pk, false, tx2);
10927
- if (!row) {
10928
- throw new Error(`Document not found with PK: ${pk}`);
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
- return JSON.parse(row);
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;
@@ -11001,547 +11509,156 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
11001
11509
  try {
11002
11510
  for (const btx of Object.values(indexTxMap)) {
11003
11511
  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
- usage: metadata.usage,
11098
- indices,
11099
- schemeVersion: innerMetadata.schemeVersion ?? 0
11100
- };
11101
- }
11102
- async getDocumentInnerMetadata(tx) {
11103
- const row = await this.select(1, false, tx);
11104
- if (!row) {
11105
- throw new Error("Document metadata not found");
11106
- }
11107
- return JSON.parse(row);
11108
- }
11109
- async updateDocumentInnerMetadata(metadata, tx) {
11110
- await this.update(1, JSON.stringify(metadata), tx);
11111
- }
11112
- /**
11113
- * Run a migration if the current schemeVersion is lower than the target version.
11114
- * After the callback completes, schemeVersion is updated to the target version.
11115
- * @param version The target scheme version
11116
- * @param callback The migration callback
11117
- * @param tx Optional transaction
11118
- */
11119
- async migration(version, callback, tx) {
11120
- await this.runWithDefaultWrite(async (tx2) => {
11121
- const innerMetadata = await this.getDocumentInnerMetadata(tx2);
11122
- const currentVersion = innerMetadata.schemeVersion ?? 0;
11123
- if (currentVersion < version) {
11124
- await callback(tx2);
11125
- const freshMetadata = await this.getDocumentInnerMetadata(tx2);
11126
- freshMetadata.schemeVersion = version;
11127
- freshMetadata.updatedAt = Date.now();
11128
- await this.updateDocumentInnerMetadata(freshMetadata, tx2);
11129
- }
11130
- }, tx);
11131
- }
11132
- /**
11133
- * Transforms a query object into a verbose query object
11134
- * @param query The query object to transform
11135
- * @returns The verbose query object
11136
- */
11137
- verboseQuery(query) {
11138
- const result = {};
11139
- for (const field in query) {
11140
- const conditions = query[field];
11141
- let newConditions;
11142
- if (typeof conditions !== "object" || conditions === null) {
11143
- newConditions = { primaryEqual: { v: conditions } };
11144
- } else {
11145
- newConditions = {};
11146
- for (const operator in conditions) {
11147
- const before = operator;
11148
- const after = this.operatorConverters[before];
11149
- const v = conditions[before];
11150
- if (!after) {
11151
- if (before === "match") {
11152
- newConditions[before] = v;
11153
- }
11154
- continue;
11155
- }
11156
- if (before === "or" && Array.isArray(v)) {
11157
- newConditions[after] = v.map((val) => ({ v: val }));
11158
- } else if (before === "like") {
11159
- newConditions[after] = v;
11160
- } else {
11161
- newConditions[after] = { v };
11162
- }
11163
- }
11164
- }
11165
- result[field] = newConditions;
11166
- }
11167
- return result;
11168
- }
11169
- /**
11170
- * B-Tree 타입 인덱스의 선택도를 평가하고 트리에 부여할 조건을 산출합니다.
11171
- * 필드 매칭 여부를 검사하고, 연속된(Prefix) 조건에 대해 점수를 부여하며 Start/End 바운드를 구성합니다.
11172
- *
11173
- * @param indexName 평가할 인덱스의 이름 (예: idx_nickname_createdat)
11174
- * @param config 등록된 인덱스의 설정 객체
11175
- * @param query 쿼리 객체
11176
- * @param queryFields 쿼리에 포함된 필드 목록 집합
11177
- * @param treeTx 조회를 수행할 B-Tree 트랜잭션 객체
11178
- * @param orderByField 정렬에 사용할 필드명 (옵션)
11179
- * @returns B-Tree 인덱스 후보 정보 (조건, 점수, 커버된 필드 등), 적합하지 않으면 null
11180
- */
11181
- evaluateBTreeCandidate(indexName, config, query, queryFields, treeTx, orderByField) {
11182
- const primaryField = config.fields[0];
11183
- if (!queryFields.has(primaryField)) return null;
11184
- const builtCondition = {};
11185
- let score = 0;
11186
- let isConsecutive = true;
11187
- const coveredFields = [];
11188
- const compositeVerifyFields = [];
11189
- const startValues = [];
11190
- const endValues = [];
11191
- let startOperator = null;
11192
- let endOperator = null;
11193
- for (let i = 0, len = config.fields.length; i < len; i++) {
11194
- const field = config.fields[i];
11195
- if (!queryFields.has(field)) {
11196
- isConsecutive = false;
11197
- continue;
11198
- }
11199
- coveredFields.push(field);
11200
- score += 1;
11201
- if (isConsecutive) {
11202
- const cond = query[field];
11203
- if (cond !== void 0) {
11204
- let isBounded = false;
11205
- if (typeof cond !== "object" || cond === null) {
11206
- score += 100;
11207
- startValues.push(cond);
11208
- endValues.push(cond);
11209
- startOperator = "primaryGte";
11210
- endOperator = "primaryLte";
11211
- isBounded = true;
11212
- } else if ("primaryEqual" in cond || "equal" in cond) {
11213
- const val = cond.primaryEqual?.v ?? cond.equal?.v ?? cond.primaryEqual ?? cond.equal;
11214
- score += 100;
11215
- startValues.push(val);
11216
- endValues.push(val);
11217
- startOperator = "primaryGte";
11218
- endOperator = "primaryLte";
11219
- isBounded = true;
11220
- } else if ("primaryGte" in cond || "gte" in cond) {
11221
- const val = cond.primaryGte?.v ?? cond.gte?.v ?? cond.primaryGte ?? cond.gte;
11222
- score += 50;
11223
- isConsecutive = false;
11224
- startValues.push(val);
11225
- startOperator = "primaryGte";
11226
- if (endValues.length > 0) endOperator = "primaryLte";
11227
- isBounded = true;
11228
- } else if ("primaryGt" in cond || "gt" in cond) {
11229
- const val = cond.primaryGt?.v ?? cond.gt?.v ?? cond.primaryGt ?? cond.gt;
11230
- score += 50;
11231
- isConsecutive = false;
11232
- startValues.push(val);
11233
- startOperator = "primaryGt";
11234
- if (endValues.length > 0) endOperator = "primaryLte";
11235
- isBounded = true;
11236
- } else if ("primaryLte" in cond || "lte" in cond) {
11237
- const val = cond.primaryLte?.v ?? cond.lte?.v ?? cond.primaryLte ?? cond.lte;
11238
- score += 50;
11239
- isConsecutive = false;
11240
- endValues.push(val);
11241
- endOperator = "primaryLte";
11242
- if (startValues.length > 0) startOperator = "primaryGte";
11243
- isBounded = true;
11244
- } else if ("primaryLt" in cond || "lt" in cond) {
11245
- const val = cond.primaryLt?.v ?? cond.lt?.v ?? cond.primaryLt ?? cond.lt;
11246
- score += 50;
11247
- isConsecutive = false;
11248
- endValues.push(val);
11249
- endOperator = "primaryLt";
11250
- if (startValues.length > 0) startOperator = "primaryGte";
11251
- isBounded = true;
11252
- } else if ("primaryOr" in cond || "or" in cond) {
11253
- score += 20;
11254
- isConsecutive = false;
11255
- } else if ("like" in cond) {
11256
- score += 15;
11257
- isConsecutive = false;
11258
- } else {
11259
- score += 10;
11260
- isConsecutive = false;
11512
+ }
11513
+ } catch (err) {
11514
+ for (const btx of Object.values(indexTxMap)) {
11515
+ await btx.rollback();
11516
+ }
11517
+ throw err;
11261
11518
  }
11262
- if (!isBounded && field !== primaryField) {
11263
- compositeVerifyFields.push(field);
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
+ }
11264
11524
  }
11525
+ chunkCount = 0;
11265
11526
  }
11266
- } else {
11267
- if (field !== primaryField) {
11268
- compositeVerifyFields.push(field);
11269
- }
11270
- }
11271
- }
11272
- if (coveredFields.length === 1 && config.fields.length === 1) {
11273
- Object.assign(builtCondition, query[primaryField]);
11274
- } else {
11275
- if (startOperator && startValues.length > 0) {
11276
- builtCondition[startOperator] = { v: startValues.length === 1 ? startValues[0] : startValues };
11277
- }
11278
- if (endOperator && endValues.length > 0) {
11279
- if (startOperator && startValues.length === endValues.length && startValues.every((val, i) => val === endValues[i])) {
11280
- delete builtCondition[startOperator];
11281
- builtCondition["primaryEqual"] = { v: startValues.length === 1 ? startValues[0] : startValues };
11282
- } else {
11283
- builtCondition[endOperator] = { v: endValues.length === 1 ? endValues[0] : endValues };
11284
- }
11285
- }
11286
- if (Object.keys(builtCondition).length === 0) {
11287
- Object.assign(builtCondition, query[primaryField] || {});
11288
11527
  }
11289
- }
11290
- let isIndexOrderSupported = false;
11291
- if (orderByField) {
11292
- for (let i = 0, len = config.fields.length; i < len; i++) {
11293
- const field = config.fields[i];
11294
- if (field === orderByField) {
11295
- isIndexOrderSupported = true;
11296
- break;
11297
- }
11298
- const cond = query[field];
11299
- let isExactMatch = false;
11300
- if (cond !== void 0) {
11301
- if (typeof cond !== "object" || cond === null) isExactMatch = true;
11302
- 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;
11303
11538
  }
11304
- if (!isExactMatch) break;
11305
- }
11306
- if (isIndexOrderSupported) {
11307
- score += 200;
11308
11539
  }
11309
- }
11310
- return {
11311
- tree: treeTx,
11312
- condition: builtCondition,
11313
- field: primaryField,
11314
- indexName,
11315
- isFtsMatch: false,
11316
- score,
11317
- compositeVerifyFields,
11318
- coveredFields,
11319
- isIndexOrderSupported
11320
- };
11321
- }
11322
- /**
11323
- * FTS (Full Text Search) 타입 인덱스의 선택도를 평가합니다.
11324
- * 'match' 연산자가 쿼리에 존재하는지 확인하고, 검색용 토큰으로 분해(tokenize)하여 점수를 매깁니다.
11325
- *
11326
- * @param indexName 평가할 인덱스의 이름
11327
- * @param config 등록된 인덱스의 설정 객체
11328
- * @param query 쿼리 객체
11329
- * @param queryFields 쿼리에 포함된 필드 목록 집합
11330
- * @param treeTx 조회를 수행할 B-Tree 트랜잭션 객체
11331
- * @returns FTS 인덱스 후보 정보 (조건, 점수, 분석된 토큰 등), 적합하지 않으면 null
11332
- */
11333
- evaluateFTSCandidate(indexName, config, query, queryFields, treeTx) {
11334
- const field = config.fields;
11335
- if (!queryFields.has(field)) return null;
11336
- const condition = query[field];
11337
- if (!condition || typeof condition !== "object" || !("match" in condition)) return null;
11338
- const ftsConfig = this.getFtsConfig(config);
11339
- const matchTokens = ftsConfig ? tokenize(condition.match, ftsConfig) : [];
11340
- return {
11341
- tree: treeTx,
11342
- condition,
11343
- field,
11344
- indexName,
11345
- isFtsMatch: true,
11346
- matchTokens,
11347
- score: 90,
11348
- // FTS 쿼리는 기본적인 B-Tree 단일 검색(대략 101점)보다는 우선순위를 조금 낮게 가져가도록 90점 부여
11349
- compositeVerifyFields: [],
11350
- coveredFields: [field],
11351
- isIndexOrderSupported: false
11352
- };
11540
+ this.pendingBackfillFields = [];
11541
+ return backfilledCount;
11542
+ }, tx);
11353
11543
  }
11354
11544
  /**
11355
- * Choose the best index (driver) for the given query.
11356
- * Scores each index based on field coverage and condition type.
11357
- *
11358
- * @param query The verbose query conditions
11359
- * @param orderByField Optional field name for orderBy optimization
11360
- * @returns Driver and other candidates for query execution
11545
+ * Convert CreateIndexOption to IndexMetaConfig for metadata storage.
11361
11546
  */
11362
- async getSelectivityCandidate(query, orderByField) {
11363
- const queryFields = new Set(Object.keys(query));
11364
- const candidates = [];
11365
- for (const [indexName, config] of this.registeredIndices) {
11366
- const tree = this.trees.get(indexName);
11367
- if (!tree) continue;
11368
- if (config.type === "btree") {
11369
- const treeTx = await tree.createTransaction();
11370
- const candidate = this.evaluateBTreeCandidate(
11371
- indexName,
11372
- config,
11373
- query,
11374
- queryFields,
11375
- treeTx,
11376
- orderByField
11377
- );
11378
- if (candidate) candidates.push(candidate);
11379
- } else if (config.type === "fts") {
11380
- const treeTx = await tree.createTransaction();
11381
- const candidate = this.evaluateFTSCandidate(
11382
- indexName,
11383
- config,
11384
- query,
11385
- queryFields,
11386
- treeTx
11387
- );
11388
- if (candidate) candidates.push(candidate);
11389
- }
11547
+ toIndexMetaConfig(option) {
11548
+ if (!option || typeof option !== "object") {
11549
+ throw new Error("Index option must be a non-null object");
11390
11550
  }
11391
- const rollback = () => {
11392
- for (const { tree } of candidates) {
11393
- tree.rollback();
11394
- }
11395
- };
11396
- if (candidates.length === 0) {
11397
- rollback();
11398
- return null;
11551
+ if (!option.type) {
11552
+ throw new Error('Index option must have a "type" property ("btree" or "fts")');
11399
11553
  }
11400
- candidates.sort((a, b) => {
11401
- if (b.score !== a.score) return b.score - a.score;
11402
- const aConfig = this.registeredIndices.get(a.indexName);
11403
- const bConfig = this.registeredIndices.get(b.indexName);
11404
- const aFieldCount = aConfig ? Array.isArray(aConfig.fields) ? aConfig.fields.length : 1 : 0;
11405
- const bFieldCount = bConfig ? Array.isArray(bConfig.fields) ? bConfig.fields.length : 1 : 0;
11406
- return aFieldCount - bFieldCount;
11407
- });
11408
- const driver = candidates[0];
11409
- const driverCoveredFields = new Set(driver.coveredFields);
11410
- const others = candidates.slice(1).filter((c) => !driverCoveredFields.has(c.field));
11411
- const compositeVerifyConditions = [];
11412
- for (let i = 0, len = driver.compositeVerifyFields.length; i < len; i++) {
11413
- const field = driver.compositeVerifyFields[i];
11414
- if (query[field]) {
11415
- 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
+ }
11416
11562
  }
11563
+ return {
11564
+ type: "btree",
11565
+ fields: option.fields
11566
+ };
11417
11567
  }
11418
- return {
11419
- driver,
11420
- others,
11421
- compositeVerifyConditions,
11422
- rollback
11423
- };
11424
- }
11425
- /**
11426
- * Get Free Memory Chunk Size
11427
- * @returns { verySmallChunkSize, smallChunkSize }
11428
- */
11429
- getFreeMemoryChunkSize() {
11430
- const freeMem = os.freemem();
11431
- const safeLimit = freeMem * 0.2;
11432
- const verySmallChunkSize = safeLimit * 0.05;
11433
- const smallChunkSize = safeLimit * 0.3;
11434
- return { verySmallChunkSize, smallChunkSize };
11435
- }
11436
- getTokenKey(pk, token) {
11437
- return pk + ":" + token;
11438
- }
11439
- async *applyCandidateByFTSStream(candidate, matchedTokens, filterValues, order) {
11440
- const keys = /* @__PURE__ */ new Set();
11441
- for (let i = 0, len = matchedTokens.length; i < len; i++) {
11442
- const token = matchedTokens[i];
11443
- for await (const pair of candidate.tree.whereStream(
11444
- { primaryEqual: { v: token } },
11445
- { order }
11446
- )) {
11447
- const pk = pair[1].k;
11448
- if (filterValues && !filterValues.has(pk)) continue;
11449
- if (!keys.has(pk)) {
11450
- keys.add(pk);
11451
- 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)}`);
11452
11575
  }
11576
+ return {
11577
+ type: "fts",
11578
+ fields: option.fields,
11579
+ tokenizer: "ngram",
11580
+ gramSize: option.gramSize
11581
+ };
11453
11582
  }
11583
+ return {
11584
+ type: "fts",
11585
+ fields: option.fields,
11586
+ tokenizer: "whitespace"
11587
+ };
11454
11588
  }
11589
+ throw new Error(`Unknown index type: ${option.type}`);
11455
11590
  }
11456
11591
  /**
11457
- * 특정 인덱스 후보를 조회하여 PK 집합을 필터링합니다.
11458
- */
11459
- applyCandidateStream(candidate, filterValues, order) {
11460
- return candidate.tree.keysStream(
11461
- candidate.condition,
11462
- { filterValues, order }
11463
- );
11464
- }
11465
- /**
11466
- * 쿼리와 인덱스 선택을 기반으로 기본 키(Primary Keys)를 가져옵니다.
11467
- * 쿼리 최적화를 통합하기 위한 내부 공통 메서드입니다.
11592
+ * Get all field names from an IndexMetaConfig.
11468
11593
  */
11469
- async getKeys(query, orderBy, sortOrder = "asc") {
11470
- const isQueryEmpty = Object.keys(query).length === 0;
11471
- const normalizedQuery = isQueryEmpty ? { _id: { gte: 0 } } : query;
11472
- const selectivity = await this.getSelectivityCandidate(
11473
- this.verboseQuery(normalizedQuery),
11474
- orderBy
11475
- );
11476
- if (!selectivity) return new Float64Array(0);
11477
- const { driver, others, rollback } = selectivity;
11478
- const useIndexOrder = orderBy === void 0 || driver.isIndexOrderSupported;
11479
- const candidates = [driver, ...others];
11480
- let keys = void 0;
11481
- for (let i = 0, len = candidates.length; i < len; i++) {
11482
- const candidate = candidates[i];
11483
- const currentOrder = useIndexOrder ? sortOrder : void 0;
11484
- if (candidate.isFtsMatch && candidate.matchTokens && candidate.matchTokens.length > 0) {
11485
- const stream = this.applyCandidateByFTSStream(
11486
- candidate,
11487
- candidate.matchTokens,
11488
- keys,
11489
- currentOrder
11490
- );
11491
- keys = /* @__PURE__ */ new Set();
11492
- for await (const pk of stream) keys.add(pk);
11493
- } else {
11494
- const stream = this.applyCandidateStream(candidate, keys, currentOrder);
11495
- keys = /* @__PURE__ */ new Set();
11496
- for await (const pk of stream) keys.add(pk);
11497
- }
11594
+ getFieldsFromConfig(config) {
11595
+ if (config.type === "btree") {
11596
+ return config.fields;
11498
11597
  }
11499
- rollback();
11500
- return new Float64Array(Array.from(keys || []));
11598
+ if (config.type === "fts") {
11599
+ return [config.fields];
11600
+ }
11601
+ return [];
11501
11602
  }
11502
11603
  /**
11503
- * 드라이버 인덱스만으로 PK 스트림을 가져옵니다. (교집합 없이)
11504
- * selectDocuments에서 사용하며, 나머지 조건(others)은 스트리밍 중 tree.verify()로 검증합니다.
11505
- * @returns 드라이버 키 스트림, others 후보 목록, rollback 함수. 또는 null.
11604
+ * Get the primary field of an index.
11506
11605
  */
11507
- async getDriverKeys(query, orderBy, sortOrder = "asc") {
11508
- const isQueryEmpty = Object.keys(query).length === 0;
11509
- const normalizedQuery = isQueryEmpty ? { _id: { gte: 0 } } : query;
11510
- const selectivity = await this.getSelectivityCandidate(
11511
- this.verboseQuery(normalizedQuery),
11512
- orderBy
11513
- );
11514
- if (!selectivity) return null;
11515
- const { driver, others, compositeVerifyConditions, rollback } = selectivity;
11516
- const useIndexOrder = orderBy === void 0 || driver.isIndexOrderSupported;
11517
- const currentOrder = useIndexOrder ? sortOrder : void 0;
11518
- let keysStream;
11519
- if (driver.isFtsMatch && driver.matchTokens && driver.matchTokens.length > 0) {
11520
- keysStream = this.applyCandidateByFTSStream(
11521
- driver,
11522
- driver.matchTokens,
11523
- void 0,
11524
- currentOrder
11525
- );
11526
- } else {
11527
- keysStream = this.applyCandidateStream(driver, void 0, currentOrder);
11606
+ getPrimaryField(config) {
11607
+ if (config.type === "btree") {
11608
+ return config.fields[0];
11528
11609
  }
11529
- return {
11530
- keysStream,
11531
- others,
11532
- compositeVerifyConditions,
11533
- isDriverOrderByField: useIndexOrder,
11534
- rollback
11535
- };
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;
11536
11653
  }
11537
11654
  async insertDocumentInternal(document, tx) {
11538
- const metadata = await this.getDocumentInnerMetadata(tx);
11655
+ const metadata = await this.api.getDocumentInnerMetadata(tx);
11539
11656
  const id = ++metadata.lastId;
11540
- await this.updateDocumentInnerMetadata(metadata, tx);
11657
+ await this.api.updateDocumentInnerMetadata(metadata, tx);
11541
11658
  const dataplyDocument = Object.assign({
11542
11659
  _id: id
11543
11660
  }, document);
11544
- const pk = await super.insert(JSON.stringify(dataplyDocument), true, tx);
11661
+ const pk = await this.api.insert(JSON.stringify(dataplyDocument), true, tx);
11545
11662
  return {
11546
11663
  pk,
11547
11664
  id,
@@ -11555,26 +11672,26 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
11555
11672
  * @returns The primary key of the inserted document
11556
11673
  */
11557
11674
  async insertSingleDocument(document, tx) {
11558
- return this.runWithDefaultWrite(async (tx2) => {
11675
+ return this.api.runWithDefaultWrite(async (tx2) => {
11559
11676
  const { pk: dpk, document: dataplyDocument } = await this.insertDocumentInternal(document, tx2);
11560
- const flattenDocument = this.flattenDocument(dataplyDocument);
11561
- for (const [indexName, config] of this.registeredIndices) {
11562
- 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);
11563
11680
  if (!tree) continue;
11564
11681
  if (config.type === "fts") {
11565
- const primaryField = this.getPrimaryField(config);
11682
+ const primaryField = this.api.indexManager.getPrimaryField(config);
11566
11683
  const v = flattenDocument[primaryField];
11567
11684
  if (v === void 0 || typeof v !== "string") continue;
11568
- const ftsConfig = this.getFtsConfig(config);
11685
+ const ftsConfig = this.api.indexManager.getFtsConfig(config);
11569
11686
  const tokens = ftsConfig ? tokenize(v, ftsConfig) : [v];
11570
11687
  for (let i = 0, len = tokens.length; i < len; i++) {
11571
11688
  const token = tokens[i];
11572
- const keyToInsert = this.getTokenKey(dpk, token);
11689
+ const keyToInsert = this.api.indexManager.getTokenKey(dpk, token);
11573
11690
  const [error] = await catchPromise(tree.insert(keyToInsert, { k: dpk, v: token }));
11574
11691
  if (error) throw error;
11575
11692
  }
11576
11693
  } else {
11577
- const indexVal = this.getIndexValue(config, flattenDocument);
11694
+ const indexVal = this.api.indexManager.getIndexValue(config, flattenDocument);
11578
11695
  if (indexVal === void 0) continue;
11579
11696
  const [error] = await catchPromise(tree.insert(dpk, { k: dpk, v: indexVal }));
11580
11697
  if (error) throw error;
@@ -11590,11 +11707,11 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
11590
11707
  * @returns The primary keys of the inserted documents
11591
11708
  */
11592
11709
  async insertBatchDocuments(documents, tx) {
11593
- return this.runWithDefaultWrite(async (tx2) => {
11594
- const metadata = await this.getDocumentInnerMetadata(tx2);
11710
+ return this.api.runWithDefaultWrite(async (tx2) => {
11711
+ const metadata = await this.api.getDocumentInnerMetadata(tx2);
11595
11712
  const startId = metadata.lastId + 1;
11596
11713
  metadata.lastId += documents.length;
11597
- await this.updateDocumentInnerMetadata(metadata, tx2);
11714
+ await this.api.updateDocumentInnerMetadata(metadata, tx2);
11598
11715
  const ids = [];
11599
11716
  const dataplyDocuments = [];
11600
11717
  const flattenedData = [];
@@ -11605,22 +11722,22 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
11605
11722
  }, documents[i]);
11606
11723
  const stringified = JSON.stringify(dataplyDocument);
11607
11724
  dataplyDocuments.push(stringified);
11608
- const flattenDocument = this.flattenDocument(dataplyDocument);
11725
+ const flattenDocument = this.api.flattenDocument(dataplyDocument);
11609
11726
  flattenedData.push({ pk: -1, data: flattenDocument });
11610
11727
  ids.push(id);
11611
11728
  }
11612
- const pks = await super.insertBatch(dataplyDocuments, true, tx2);
11729
+ const pks = await this.api.insertBatch(dataplyDocuments, true, tx2);
11613
11730
  for (let i = 0, len = pks.length; i < len; i++) {
11614
11731
  flattenedData[i].pk = pks[i];
11615
11732
  }
11616
- for (const [indexName, config] of this.registeredIndices) {
11617
- const tree = this.trees.get(indexName);
11733
+ for (const [indexName, config] of this.api.indexManager.registeredIndices) {
11734
+ const tree = this.api.trees.get(indexName);
11618
11735
  if (!tree) continue;
11619
11736
  const treeTx = await tree.createTransaction();
11620
11737
  const batchInsertData = [];
11621
11738
  if (config.type === "fts") {
11622
- const primaryField = this.getPrimaryField(config);
11623
- const ftsConfig = this.getFtsConfig(config);
11739
+ const primaryField = this.api.indexManager.getPrimaryField(config);
11740
+ const ftsConfig = this.api.indexManager.getFtsConfig(config);
11624
11741
  for (let i = 0, len = flattenedData.length; i < len; i++) {
11625
11742
  const item = flattenedData[i];
11626
11743
  const v = item.data[primaryField];
@@ -11628,13 +11745,13 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
11628
11745
  const tokens = ftsConfig ? tokenize(v, ftsConfig) : [v];
11629
11746
  for (let j = 0, tLen = tokens.length; j < tLen; j++) {
11630
11747
  const token = tokens[j];
11631
- 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 }]);
11632
11749
  }
11633
11750
  }
11634
11751
  } else {
11635
11752
  for (let i = 0, len = flattenedData.length; i < len; i++) {
11636
11753
  const item = flattenedData[i];
11637
- const indexVal = this.getIndexValue(config, item.data);
11754
+ const indexVal = this.api.indexManager.getIndexValue(config, item.data);
11638
11755
  if (indexVal === void 0) continue;
11639
11756
  batchInsertData.push([item.pk, { k: item.pk, v: indexVal }]);
11640
11757
  }
@@ -11659,46 +11776,46 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
11659
11776
  * @returns The number of updated documents
11660
11777
  */
11661
11778
  async updateInternal(query, computeUpdatedDoc, tx) {
11662
- const pks = await this.getKeys(query);
11779
+ const pks = await this.api.queryManager.getKeys(query);
11663
11780
  let updatedCount = 0;
11664
11781
  const treeTxs = /* @__PURE__ */ new Map();
11665
- for (const [indexName, tree] of this.trees) {
11782
+ for (const [indexName, tree] of this.api.trees) {
11666
11783
  treeTxs.set(indexName, await tree.createTransaction());
11667
11784
  }
11668
11785
  treeTxs.delete("_id");
11669
11786
  for (let i = 0, len = pks.length; i < len; i++) {
11670
11787
  const pk = pks[i];
11671
- const doc = await this.getDocument(pk, tx);
11788
+ const doc = await this.api.getDocument(pk, tx);
11672
11789
  if (!doc) continue;
11673
11790
  const updatedDoc = computeUpdatedDoc(doc);
11674
- const oldFlatDoc = this.flattenDocument(doc);
11675
- const newFlatDoc = this.flattenDocument(updatedDoc);
11791
+ const oldFlatDoc = this.api.flattenDocument(doc);
11792
+ const newFlatDoc = this.api.flattenDocument(updatedDoc);
11676
11793
  for (const [indexName, treeTx] of treeTxs) {
11677
- const config = this.registeredIndices.get(indexName);
11794
+ const config = this.api.indexManager.registeredIndices.get(indexName);
11678
11795
  if (!config) continue;
11679
11796
  if (config.type === "fts") {
11680
- const primaryField = this.getPrimaryField(config);
11797
+ const primaryField = this.api.indexManager.getPrimaryField(config);
11681
11798
  const oldV = oldFlatDoc[primaryField];
11682
11799
  const newV = newFlatDoc[primaryField];
11683
11800
  if (oldV === newV) continue;
11684
- const ftsConfig = this.getFtsConfig(config);
11801
+ const ftsConfig = this.api.indexManager.getFtsConfig(config);
11685
11802
  if (typeof oldV === "string") {
11686
11803
  const oldTokens = ftsConfig ? tokenize(oldV, ftsConfig) : [oldV];
11687
11804
  for (let j = 0, jLen = oldTokens.length; j < jLen; j++) {
11688
- 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] });
11689
11806
  }
11690
11807
  }
11691
11808
  if (typeof newV === "string") {
11692
11809
  const newTokens = ftsConfig ? tokenize(newV, ftsConfig) : [newV];
11693
11810
  const batchInsertData = [];
11694
11811
  for (let j = 0, jLen = newTokens.length; j < jLen; j++) {
11695
- 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] }]);
11696
11813
  }
11697
11814
  await treeTx.batchInsert(batchInsertData);
11698
11815
  }
11699
11816
  } else {
11700
- const oldIndexVal = this.getIndexValue(config, oldFlatDoc);
11701
- const newIndexVal = this.getIndexValue(config, newFlatDoc);
11817
+ const oldIndexVal = this.api.indexManager.getIndexValue(config, oldFlatDoc);
11818
+ const newIndexVal = this.api.indexManager.getIndexValue(config, newFlatDoc);
11702
11819
  if (JSON.stringify(oldIndexVal) === JSON.stringify(newIndexVal)) continue;
11703
11820
  if (oldIndexVal !== void 0) {
11704
11821
  await treeTx.delete(pk, { k: pk, v: oldIndexVal });
@@ -11708,7 +11825,7 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
11708
11825
  }
11709
11826
  }
11710
11827
  }
11711
- await this.update(pk, JSON.stringify(updatedDoc), tx);
11828
+ await this.api.update(pk, JSON.stringify(updatedDoc), tx);
11712
11829
  updatedCount++;
11713
11830
  }
11714
11831
  for (const [indexName, treeTx] of treeTxs) {
@@ -11730,7 +11847,7 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
11730
11847
  * @returns The number of updated documents
11731
11848
  */
11732
11849
  async fullUpdate(query, newRecord, tx) {
11733
- return this.runWithDefaultWrite(async (tx2) => {
11850
+ return this.api.runWithDefaultWrite(async (tx2) => {
11734
11851
  return this.updateInternal(query, (doc) => {
11735
11852
  const newDoc = typeof newRecord === "function" ? newRecord(doc) : newRecord;
11736
11853
  return { _id: doc._id, ...newDoc };
@@ -11745,7 +11862,7 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
11745
11862
  * @returns The number of updated documents
11746
11863
  */
11747
11864
  async partialUpdate(query, newRecord, tx) {
11748
- return this.runWithDefaultWrite(async (tx2) => {
11865
+ return this.api.runWithDefaultWrite(async (tx2) => {
11749
11866
  return this.updateInternal(query, (doc) => {
11750
11867
  const partialUpdateContent = typeof newRecord === "function" ? newRecord(doc) : newRecord;
11751
11868
  const finalUpdate = { ...partialUpdateContent };
@@ -11761,189 +11878,300 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
11761
11878
  * @returns The number of deleted documents
11762
11879
  */
11763
11880
  async deleteDocuments(query, tx) {
11764
- return this.runWithDefaultWrite(async (tx2) => {
11765
- const pks = await this.getKeys(query);
11881
+ return this.api.runWithDefaultWrite(async (tx2) => {
11882
+ const pks = await this.api.queryManager.getKeys(query);
11766
11883
  let deletedCount = 0;
11767
11884
  for (let i = 0, len = pks.length; i < len; i++) {
11768
11885
  const pk = pks[i];
11769
- const doc = await this.getDocument(pk, tx2);
11886
+ const doc = await this.api.getDocument(pk, tx2);
11770
11887
  if (!doc) continue;
11771
- const flatDoc = this.flattenDocument(doc);
11772
- for (const [indexName, tree] of this.trees) {
11773
- 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);
11774
11891
  if (!config) continue;
11775
11892
  if (config.type === "fts") {
11776
- const primaryField = this.getPrimaryField(config);
11893
+ const primaryField = this.api.indexManager.getPrimaryField(config);
11777
11894
  const v = flatDoc[primaryField];
11778
11895
  if (v === void 0 || typeof v !== "string") continue;
11779
- const ftsConfig = this.getFtsConfig(config);
11896
+ const ftsConfig = this.api.indexManager.getFtsConfig(config);
11780
11897
  const tokens = ftsConfig ? tokenize(v, ftsConfig) : [v];
11781
11898
  for (let j = 0, jLen = tokens.length; j < jLen; j++) {
11782
- 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] });
11783
11900
  }
11784
11901
  } else {
11785
- const indexVal = this.getIndexValue(config, flatDoc);
11902
+ const indexVal = this.api.indexManager.getIndexValue(config, flatDoc);
11786
11903
  if (indexVal === void 0) continue;
11787
11904
  await tree.delete(pk, { k: pk, v: indexVal });
11788
11905
  }
11789
11906
  }
11790
- await super.delete(pk, true, tx2);
11907
+ await this.api.delete(pk, true, tx2);
11791
11908
  deletedCount++;
11792
11909
  }
11793
11910
  return deletedCount;
11794
11911
  }, tx);
11795
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
+ }
11796
11948
  /**
11797
- * Count documents from the database that match the query
11798
- * @param query The query to use
11799
- * @param tx The transaction to use
11800
- * @returns The number of documents that match the query
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
11801
11954
  */
11802
- async countDocuments(query, tx) {
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) {
11803
12049
  return this.runWithDefault(async (tx2) => {
11804
- const pks = await this.getKeys(query);
11805
- return pks.length;
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);
11806
12055
  }, tx);
11807
12056
  }
11808
12057
  /**
11809
- * FTS 조건에 대해 문서가 유효한지 검증합니다.
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
11810
12134
  */
11811
- verifyFts(doc, ftsConditions) {
11812
- const flatDoc = this.flattenDocument(doc);
11813
- for (let i = 0, len = ftsConditions.length; i < len; i++) {
11814
- const { field, matchTokens } = ftsConditions[i];
11815
- const docValue = flatDoc[field];
11816
- if (typeof docValue !== "string") return false;
11817
- for (let j = 0, jLen = matchTokens.length; j < jLen; j++) {
11818
- const token = matchTokens[j];
11819
- if (!docValue.includes(token)) return false;
11820
- }
11821
- }
11822
- return true;
12135
+ async insertBatchDocuments(documents, tx) {
12136
+ return this.mutationManager.insertBatchDocuments(documents, tx);
11823
12137
  }
11824
12138
  /**
11825
- * 복합 인덱스의 non-primary 필드에 대해 문서가 유효한지 검증합니다.
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
11826
12144
  */
11827
- verifyCompositeConditions(doc, conditions) {
11828
- if (conditions.length === 0) return true;
11829
- const flatDoc = this.flattenDocument(doc);
11830
- for (let i = 0, len = conditions.length; i < len; i++) {
11831
- const { field, condition } = conditions[i];
11832
- const docValue = flatDoc[field];
11833
- if (docValue === void 0) return false;
11834
- const treeValue = { k: doc._id, v: docValue };
11835
- if (!this.verifyValue(docValue, condition)) return false;
11836
- }
11837
- return true;
12145
+ async fullUpdate(query, newRecord, tx) {
12146
+ return this.mutationManager.fullUpdate(query, newRecord, tx);
11838
12147
  }
11839
12148
  /**
11840
- * 단일 값에 대해 verbose 조건을 검증합니다.
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
11841
12154
  */
11842
- verifyValue(value, condition) {
11843
- if (typeof condition !== "object" || condition === null) {
11844
- return value === condition;
11845
- }
11846
- if ("primaryEqual" in condition) {
11847
- return value === condition.primaryEqual?.v;
11848
- }
11849
- if ("primaryNotEqual" in condition) {
11850
- return value !== condition.primaryNotEqual?.v;
11851
- }
11852
- if ("primaryLt" in condition) {
11853
- return value !== null && condition.primaryLt?.v !== void 0 && value < condition.primaryLt.v;
11854
- }
11855
- if ("primaryLte" in condition) {
11856
- return value !== null && condition.primaryLte?.v !== void 0 && value <= condition.primaryLte.v;
11857
- }
11858
- if ("primaryGt" in condition) {
11859
- return value !== null && condition.primaryGt?.v !== void 0 && value > condition.primaryGt.v;
11860
- }
11861
- if ("primaryGte" in condition) {
11862
- return value !== null && condition.primaryGte?.v !== void 0 && value >= condition.primaryGte.v;
11863
- }
11864
- if ("primaryOr" in condition && Array.isArray(condition.primaryOr)) {
11865
- return condition.primaryOr.some((c) => value === c?.v);
11866
- }
11867
- return true;
12155
+ async partialUpdate(query, newRecord, tx) {
12156
+ return this.mutationManager.partialUpdate(query, newRecord, tx);
11868
12157
  }
11869
12158
  /**
11870
- * 메모리 기반으로 청크 크기를 동적 조절합니다.
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
11871
12163
  */
11872
- adjustChunkSize(currentChunkSize, chunkTotalSize) {
11873
- if (chunkTotalSize <= 0) return currentChunkSize;
11874
- const { verySmallChunkSize, smallChunkSize } = this.getFreeMemoryChunkSize();
11875
- if (chunkTotalSize < verySmallChunkSize) return currentChunkSize * 2;
11876
- if (chunkTotalSize > smallChunkSize) return Math.max(Math.floor(currentChunkSize / 2), 20);
11877
- return currentChunkSize;
12164
+ async deleteDocuments(query, tx) {
12165
+ return this.mutationManager.deleteDocuments(query, tx);
11878
12166
  }
11879
12167
  /**
11880
- * Prefetch 방식으로 스트림을 청크 단위로 조회하여 문서를 순회합니다.
11881
- * FTS 검증, 복합 인덱스 검증, others 후보에 대한 tree.verify() 검증을 통과한 문서만 yield 합니다.
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
11882
12172
  */
11883
- async *processChunkedKeysWithVerify(keysStream, startIdx, initialChunkSize, limit, ftsConditions, compositeVerifyConditions, others, tx) {
11884
- const verifyOthers = others.filter((o) => !o.isFtsMatch);
11885
- const isFts = ftsConditions.length > 0;
11886
- const isCompositeVerify = compositeVerifyConditions.length > 0;
11887
- const isVerifyOthers = verifyOthers.length > 0;
11888
- const isFiniteLimit = isFinite(limit);
11889
- let currentChunkSize = isFiniteLimit ? limit : initialChunkSize;
11890
- let chunk = [];
11891
- let chunkSize = 0;
11892
- let dropped = 0;
11893
- const processChunk = async (pks) => {
11894
- const docs = [];
11895
- const rawResults = await this.selectMany(new Float64Array(pks), false, tx);
11896
- let chunkTotalSize = 0;
11897
- for (let j = 0, len = rawResults.length; j < len; j++) {
11898
- const s = rawResults[j];
11899
- if (!s) continue;
11900
- const doc = JSON.parse(s);
11901
- chunkTotalSize += s.length * 2;
11902
- if (isFts && !this.verifyFts(doc, ftsConditions)) continue;
11903
- if (isCompositeVerify && this.verifyCompositeConditions(doc, compositeVerifyConditions) === false) continue;
11904
- if (isVerifyOthers) {
11905
- const flatDoc = this.flattenDocument(doc);
11906
- let passed = true;
11907
- for (let k = 0, kLen = verifyOthers.length; k < kLen; k++) {
11908
- const other = verifyOthers[k];
11909
- const fieldValue = flatDoc[other.field];
11910
- if (fieldValue === void 0) {
11911
- passed = false;
11912
- break;
11913
- }
11914
- const treeValue = { k: doc._id, v: fieldValue };
11915
- if (!other.tree.verify(treeValue, other.condition)) {
11916
- passed = false;
11917
- break;
11918
- }
11919
- }
11920
- if (!passed) continue;
11921
- }
11922
- docs.push(doc);
11923
- }
11924
- if (!isFiniteLimit) {
11925
- currentChunkSize = this.adjustChunkSize(currentChunkSize, chunkTotalSize);
11926
- }
11927
- return docs;
11928
- };
11929
- for await (const pk of keysStream) {
11930
- if (dropped < startIdx) {
11931
- dropped++;
11932
- continue;
11933
- }
11934
- chunk.push(pk);
11935
- chunkSize++;
11936
- if (chunkSize >= currentChunkSize) {
11937
- const docs = await processChunk(chunk);
11938
- for (let j = 0, dLen = docs.length; j < dLen; j++) yield docs[j];
11939
- chunk = [];
11940
- chunkSize = 0;
11941
- }
11942
- }
11943
- if (chunkSize > 0) {
11944
- const docs = await processChunk(chunk);
11945
- for (let j = 0, dLen = docs.length; j < dLen; j++) yield docs[j];
11946
- }
12173
+ async countDocuments(query, tx) {
12174
+ return this.queryManager.countDocuments(query, tx);
11947
12175
  }
11948
12176
  /**
11949
12177
  * Select documents from the database
@@ -11954,130 +12182,7 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
11954
12182
  * @throws Error if query or orderBy contains non-indexed fields
11955
12183
  */
11956
12184
  selectDocuments(query, options = {}, tx) {
11957
- for (const field of Object.keys(query)) {
11958
- if (!this.indexedFields.has(field)) {
11959
- throw new Error(`Query field "${field}" is not indexed. Available indexed fields: ${Array.from(this.indexedFields).join(", ")}`);
11960
- }
11961
- }
11962
- const orderBy = options.orderBy;
11963
- if (orderBy !== void 0 && !this.indexedFields.has(orderBy)) {
11964
- throw new Error(`orderBy field "${orderBy}" is not indexed. Available indexed fields: ${Array.from(this.indexedFields).join(", ")}`);
11965
- }
11966
- const {
11967
- limit = Infinity,
11968
- offset = 0,
11969
- sortOrder = "asc",
11970
- orderBy: orderByField
11971
- } = options;
11972
- const self = this;
11973
- const stream = () => this.streamWithDefault(async function* (tx2) {
11974
- const ftsConditions = [];
11975
- for (const field in query) {
11976
- const q = query[field];
11977
- if (q && typeof q === "object" && "match" in q && typeof q.match === "string") {
11978
- const indexNames = self.fieldToIndices.get(field) || [];
11979
- for (const indexName of indexNames) {
11980
- const config = self.registeredIndices.get(indexName);
11981
- if (config && config.type === "fts") {
11982
- const ftsConfig = self.getFtsConfig(config);
11983
- if (ftsConfig) {
11984
- ftsConditions.push({ field, matchTokens: tokenize(q.match, ftsConfig) });
11985
- }
11986
- break;
11987
- }
11988
- }
11989
- }
11990
- }
11991
- const driverResult = await self.getDriverKeys(query, orderByField, sortOrder);
11992
- if (!driverResult) return;
11993
- const { keysStream, others, compositeVerifyConditions, isDriverOrderByField, rollback } = driverResult;
11994
- const initialChunkSize = self.options.pageSize;
11995
- try {
11996
- if (!isDriverOrderByField && orderByField) {
11997
- const topK = limit === Infinity ? Infinity : offset + limit;
11998
- let heap = null;
11999
- if (topK !== Infinity) {
12000
- heap = new BinaryHeap((a, b) => {
12001
- const aVal = a[orderByField] ?? a._id;
12002
- const bVal = b[orderByField] ?? b._id;
12003
- const cmp = aVal < bVal ? -1 : aVal > bVal ? 1 : 0;
12004
- return sortOrder === "asc" ? -cmp : cmp;
12005
- });
12006
- }
12007
- const results = [];
12008
- for await (const doc of self.processChunkedKeysWithVerify(
12009
- keysStream,
12010
- 0,
12011
- initialChunkSize,
12012
- Infinity,
12013
- ftsConditions,
12014
- compositeVerifyConditions,
12015
- others,
12016
- tx2
12017
- )) {
12018
- if (heap) {
12019
- if (heap.size < topK) heap.push(doc);
12020
- else {
12021
- const top = heap.peek();
12022
- if (top) {
12023
- const aVal = doc[orderByField] ?? doc._id;
12024
- const bVal = top[orderByField] ?? top._id;
12025
- const cmp = aVal < bVal ? -1 : aVal > bVal ? 1 : 0;
12026
- if (sortOrder === "asc" ? cmp < 0 : cmp > 0) heap.replace(doc);
12027
- }
12028
- }
12029
- } else {
12030
- results.push(doc);
12031
- }
12032
- }
12033
- const finalDocs = heap ? heap.toArray() : results;
12034
- finalDocs.sort((a, b) => {
12035
- const aVal = a[orderByField] ?? a._id;
12036
- const bVal = b[orderByField] ?? b._id;
12037
- const cmp = aVal < bVal ? -1 : aVal > bVal ? 1 : 0;
12038
- return sortOrder === "asc" ? cmp : -cmp;
12039
- });
12040
- const end = limit === Infinity ? void 0 : offset + limit;
12041
- const limitedResults = finalDocs.slice(offset, end);
12042
- for (let j = 0, len = limitedResults.length; j < len; j++) {
12043
- yield limitedResults[j];
12044
- }
12045
- } else {
12046
- const hasFilters = ftsConditions.length > 0 || compositeVerifyConditions.length > 0 || others.length > 0;
12047
- const startIdx = hasFilters ? 0 : offset;
12048
- let yieldedCount = 0;
12049
- let skippedCount = hasFilters ? 0 : offset;
12050
- for await (const doc of self.processChunkedKeysWithVerify(
12051
- keysStream,
12052
- startIdx,
12053
- initialChunkSize,
12054
- limit,
12055
- ftsConditions,
12056
- compositeVerifyConditions,
12057
- others,
12058
- tx2
12059
- )) {
12060
- if (skippedCount < offset) {
12061
- skippedCount++;
12062
- continue;
12063
- }
12064
- if (yieldedCount >= limit) break;
12065
- yield doc;
12066
- yieldedCount++;
12067
- }
12068
- }
12069
- } finally {
12070
- rollback();
12071
- }
12072
- }, tx);
12073
- const drain = async () => {
12074
- const result = [];
12075
- for await (const document of stream()) {
12076
- result.push(document);
12077
- }
12078
- return result;
12079
- };
12080
- return { stream, drain };
12185
+ return this.queryManager.selectDocuments(query, options, tx);
12081
12186
  }
12082
12187
  };
12083
12188
 
@@ -12249,7 +12354,7 @@ var DocumentDataply = class _DocumentDataply {
12249
12354
  };
12250
12355
 
12251
12356
  // src/core/index.ts
12252
- var import_dataply4 = __toESM(require_cjs());
12357
+ var import_dataply5 = __toESM(require_cjs());
12253
12358
  // Annotate the CommonJS export names for ESM import in node:
12254
12359
  0 && (module.exports = {
12255
12360
  DocumentDataply,