document-dataply 0.0.9-alpha.4 → 0.0.9-alpha.6

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
@@ -1580,83 +1580,217 @@ var require_cjs = __commonJS({
1580
1580
  return regexp.test(nodeValue);
1581
1581
  }
1582
1582
  };
1583
- verifierStartNode = {
1584
- gt: (v) => this.insertableNodeByPrimary(v),
1585
- gte: (v) => this.insertableNodeByPrimary(v),
1586
- lt: (v) => this.insertableNodeByPrimary(v),
1587
- lte: (v) => this.insertableRightestNodeByPrimary(v),
1588
- equal: (v) => this.insertableNodeByPrimary(v),
1589
- notEqual: (v) => this.leftestNode(),
1590
- or: (v) => this.insertableNodeByPrimary(this.lowestPrimaryValue(this.ensureValues(v))),
1591
- primaryGt: (v) => this.insertableNodeByPrimary(v),
1592
- primaryGte: (v) => this.insertableNodeByPrimary(v),
1593
- primaryLt: (v) => this.insertableNodeByPrimary(v),
1594
- primaryLte: (v) => this.insertableRightestNodeByPrimary(v),
1595
- primaryEqual: (v) => this.insertableNodeByPrimary(v),
1596
- primaryNotEqual: (v) => this.leftestNode(),
1597
- primaryOr: (v) => this.insertableNodeByPrimary(this.lowestPrimaryValue(this.ensureValues(v))),
1598
- like: (v) => this.leftestNode()
1599
- };
1600
- verifierEndNode = {
1601
- gt: (v) => null,
1602
- gte: (v) => null,
1603
- lt: (v) => null,
1604
- lte: (v) => null,
1605
- equal: (v) => this.insertableEndNode(v, this.verifierDirection.equal),
1606
- notEqual: (v) => null,
1607
- or: (v) => this.insertableEndNode(
1608
- this.highestValue(this.ensureValues(v)),
1609
- this.verifierDirection.or
1610
- ),
1611
- primaryGt: (v) => null,
1612
- primaryGte: (v) => null,
1613
- primaryLt: (v) => null,
1614
- primaryLte: (v) => null,
1615
- primaryEqual: (v) => this.insertableRightestEndNodeByPrimary(v),
1616
- primaryNotEqual: (v) => null,
1617
- primaryOr: (v) => this.insertableRightestEndNodeByPrimary(
1618
- this.highestPrimaryValue(this.ensureValues(v))
1619
- ),
1620
- like: (v) => null
1621
- };
1622
- verifierDirection = {
1623
- gt: 1,
1624
- gte: 1,
1625
- lt: -1,
1626
- lte: -1,
1627
- equal: 1,
1628
- notEqual: 1,
1629
- or: 1,
1630
- primaryGt: 1,
1631
- primaryGte: 1,
1632
- primaryLt: -1,
1633
- primaryLte: -1,
1634
- primaryEqual: 1,
1635
- primaryNotEqual: 1,
1636
- primaryOr: 1,
1637
- like: 1
1638
- };
1639
- /**
1640
- * Determines whether early termination is allowed for each condition.
1641
- * When true, the search will stop once a match is found and then a non-match is encountered.
1642
- * Only applicable for conditions that guarantee contiguous matches in a sorted B+Tree.
1643
- */
1644
- verifierEarlyTerminate = {
1645
- gt: false,
1646
- gte: false,
1647
- lt: false,
1648
- lte: false,
1649
- equal: true,
1650
- notEqual: false,
1651
- or: false,
1652
- primaryGt: false,
1653
- primaryGte: false,
1654
- primaryLt: false,
1655
- primaryLte: false,
1656
- primaryEqual: true,
1657
- primaryNotEqual: false,
1658
- primaryOr: false,
1659
- like: false
1583
+ searchConfigs = {
1584
+ gt: {
1585
+ asc: {
1586
+ start: (tx, v) => tx.insertableNodeByPrimary(v[0]),
1587
+ end: () => null,
1588
+ direction: 1,
1589
+ earlyTerminate: false
1590
+ },
1591
+ desc: {
1592
+ start: (tx) => tx.rightestNode(),
1593
+ end: (tx, v) => tx.insertableEndNode(v[0], -1),
1594
+ direction: -1,
1595
+ earlyTerminate: true
1596
+ }
1597
+ },
1598
+ gte: {
1599
+ asc: {
1600
+ start: (tx, v) => tx.insertableNodeByPrimary(v[0]),
1601
+ end: () => null,
1602
+ direction: 1,
1603
+ earlyTerminate: false
1604
+ },
1605
+ desc: {
1606
+ start: (tx) => tx.rightestNode(),
1607
+ end: (tx, v) => tx.insertableEndNode(v[0], -1),
1608
+ direction: -1,
1609
+ earlyTerminate: true
1610
+ }
1611
+ },
1612
+ lt: {
1613
+ asc: {
1614
+ start: (tx) => tx.leftestNode(),
1615
+ end: (tx, v) => tx.insertableEndNode(v[0], 1),
1616
+ direction: 1,
1617
+ earlyTerminate: true
1618
+ },
1619
+ desc: {
1620
+ start: (tx, v) => tx.insertableNodeByPrimary(v[0]),
1621
+ end: () => null,
1622
+ direction: -1,
1623
+ earlyTerminate: false
1624
+ }
1625
+ },
1626
+ lte: {
1627
+ asc: {
1628
+ start: (tx) => tx.leftestNode(),
1629
+ end: (tx, v) => tx.insertableEndNode(v[0], 1),
1630
+ direction: 1,
1631
+ earlyTerminate: true
1632
+ },
1633
+ desc: {
1634
+ start: (tx, v) => tx.insertableRightestNodeByPrimary(v[0]),
1635
+ end: () => null,
1636
+ direction: -1,
1637
+ earlyTerminate: false
1638
+ }
1639
+ },
1640
+ equal: {
1641
+ asc: {
1642
+ start: (tx, v) => tx.insertableNodeByPrimary(v[0]),
1643
+ end: (tx, v) => tx.insertableEndNode(v[0], 1),
1644
+ direction: 1,
1645
+ earlyTerminate: true
1646
+ },
1647
+ desc: {
1648
+ start: (tx, v) => tx.insertableEndNode(v[0], 1),
1649
+ end: (tx, v) => tx.insertableEndNode(v[0], -1),
1650
+ direction: -1,
1651
+ earlyTerminate: true
1652
+ }
1653
+ },
1654
+ notEqual: {
1655
+ asc: {
1656
+ start: (tx) => tx.leftestNode(),
1657
+ end: () => null,
1658
+ direction: 1,
1659
+ earlyTerminate: false
1660
+ },
1661
+ desc: {
1662
+ start: (tx) => tx.rightestNode(),
1663
+ end: () => null,
1664
+ direction: -1,
1665
+ earlyTerminate: false
1666
+ }
1667
+ },
1668
+ or: {
1669
+ asc: {
1670
+ start: (tx, v) => tx.insertableNodeByPrimary(tx.lowestValue(v)),
1671
+ end: (tx, v) => tx.insertableEndNode(tx.highestValue(v), 1),
1672
+ direction: 1,
1673
+ earlyTerminate: false
1674
+ },
1675
+ desc: {
1676
+ start: (tx, v) => tx.insertableEndNode(tx.highestValue(v), 1),
1677
+ end: (tx, v) => tx.insertableEndNode(tx.lowestValue(v), -1),
1678
+ direction: -1,
1679
+ earlyTerminate: false
1680
+ }
1681
+ },
1682
+ primaryGt: {
1683
+ asc: {
1684
+ start: (tx, v) => tx.insertableNodeByPrimary(v[0]),
1685
+ end: () => null,
1686
+ direction: 1,
1687
+ earlyTerminate: false
1688
+ },
1689
+ desc: {
1690
+ start: (tx) => tx.rightestNode(),
1691
+ end: (tx, v) => tx.insertableEndNode(v[0], -1),
1692
+ direction: -1,
1693
+ earlyTerminate: true
1694
+ }
1695
+ },
1696
+ primaryGte: {
1697
+ asc: {
1698
+ start: (tx, v) => tx.insertableNodeByPrimary(v[0]),
1699
+ end: () => null,
1700
+ direction: 1,
1701
+ earlyTerminate: false
1702
+ },
1703
+ desc: {
1704
+ start: (tx) => tx.rightestNode(),
1705
+ end: (tx, v) => tx.insertableEndNode(v[0], -1),
1706
+ direction: -1,
1707
+ earlyTerminate: true
1708
+ }
1709
+ },
1710
+ primaryLt: {
1711
+ asc: {
1712
+ start: (tx) => tx.leftestNode(),
1713
+ end: (tx, v) => tx.insertableEndNode(v[0], 1),
1714
+ direction: 1,
1715
+ earlyTerminate: true
1716
+ },
1717
+ desc: {
1718
+ start: (tx, v) => tx.insertableNodeByPrimary(v[0]),
1719
+ end: () => null,
1720
+ direction: -1,
1721
+ earlyTerminate: false
1722
+ }
1723
+ },
1724
+ primaryLte: {
1725
+ asc: {
1726
+ start: (tx) => tx.leftestNode(),
1727
+ end: (tx, v) => tx.insertableEndNode(v[0], 1),
1728
+ direction: 1,
1729
+ earlyTerminate: true
1730
+ },
1731
+ desc: {
1732
+ start: (tx, v) => tx.insertableRightestNodeByPrimary(v[0]),
1733
+ end: () => null,
1734
+ direction: -1,
1735
+ earlyTerminate: false
1736
+ }
1737
+ },
1738
+ primaryEqual: {
1739
+ asc: {
1740
+ start: (tx, v) => tx.insertableNodeByPrimary(v[0]),
1741
+ end: (tx, v) => tx.insertableRightestEndNodeByPrimary(v[0]),
1742
+ direction: 1,
1743
+ earlyTerminate: true
1744
+ },
1745
+ desc: {
1746
+ start: (tx, v) => tx.insertableRightestEndNodeByPrimary(v[0]),
1747
+ end: (tx, v) => tx.insertableEndNode(v[0], -1),
1748
+ direction: -1,
1749
+ earlyTerminate: true
1750
+ }
1751
+ },
1752
+ primaryNotEqual: {
1753
+ asc: {
1754
+ start: (tx) => tx.leftestNode(),
1755
+ end: () => null,
1756
+ direction: 1,
1757
+ earlyTerminate: false
1758
+ },
1759
+ desc: {
1760
+ start: (tx) => tx.rightestNode(),
1761
+ end: () => null,
1762
+ direction: -1,
1763
+ earlyTerminate: false
1764
+ }
1765
+ },
1766
+ primaryOr: {
1767
+ asc: {
1768
+ start: (tx, v) => tx.insertableNodeByPrimary(tx.lowestPrimaryValue(v)),
1769
+ end: (tx, v) => tx.insertableRightestEndNodeByPrimary(tx.highestPrimaryValue(v)),
1770
+ direction: 1,
1771
+ earlyTerminate: false
1772
+ },
1773
+ desc: {
1774
+ start: (tx, v) => tx.insertableRightestEndNodeByPrimary(tx.highestPrimaryValue(v)),
1775
+ end: (tx, v) => tx.insertableEndNode(tx.lowestPrimaryValue(v), -1),
1776
+ direction: -1,
1777
+ earlyTerminate: false
1778
+ }
1779
+ },
1780
+ like: {
1781
+ asc: {
1782
+ start: (tx) => tx.leftestNode(),
1783
+ end: () => null,
1784
+ direction: 1,
1785
+ earlyTerminate: false
1786
+ },
1787
+ desc: {
1788
+ start: (tx) => tx.rightestNode(),
1789
+ end: () => null,
1790
+ direction: -1,
1791
+ earlyTerminate: false
1792
+ }
1793
+ }
1660
1794
  };
1661
1795
  /**
1662
1796
  * Priority map for condition types.
@@ -2267,16 +2401,20 @@ var require_cjs = __commonJS({
2267
2401
  const driverKey = this.getDriverKey(condition);
2268
2402
  if (!driverKey) return;
2269
2403
  const value = condition[driverKey];
2270
- let startNode = this.verifierStartNode[driverKey](value);
2271
- let endNode = this.verifierEndNode[driverKey](value);
2272
- let direction = this.verifierDirection[driverKey];
2404
+ const v = this.ensureValues(value);
2405
+ const config = this.searchConfigs[driverKey][order];
2406
+ let startNode = config.start(this, v);
2407
+ let endNode = config.end(this, v);
2408
+ const direction = config.direction;
2409
+ const earlyTerminate = config.earlyTerminate;
2410
+ if (order === "desc" && !startNode) {
2411
+ startNode = this.rightestNode();
2412
+ }
2413
+ if (order === "asc" && !startNode) {
2414
+ startNode = this.leftestNode();
2415
+ }
2416
+ if (!startNode) return;
2273
2417
  const comparator = this.verifierMap[driverKey];
2274
- const earlyTerminate = this.verifierEarlyTerminate[driverKey];
2275
- if (order === "desc") {
2276
- startNode = endNode ?? this.rightestNode();
2277
- endNode = null;
2278
- direction *= -1;
2279
- }
2280
2418
  const generator = this.getPairsGenerator(
2281
2419
  value,
2282
2420
  startNode,
@@ -2288,7 +2426,7 @@ var require_cjs = __commonJS({
2288
2426
  let count = 0;
2289
2427
  const intersection = filterValues && filterValues.size > 0 ? filterValues : null;
2290
2428
  for (const pair of generator) {
2291
- const [k, v] = pair;
2429
+ const [k, v2] = pair;
2292
2430
  if (intersection && !intersection.has(k)) {
2293
2431
  continue;
2294
2432
  }
@@ -2297,7 +2435,7 @@ var require_cjs = __commonJS({
2297
2435
  if (key === driverKey) continue;
2298
2436
  const verify = this.verifierMap[key];
2299
2437
  const condValue = condition[key];
2300
- if (!verify(v, condValue)) {
2438
+ if (!verify(v2, condValue)) {
2301
2439
  isMatch = false;
2302
2440
  break;
2303
2441
  }
@@ -3397,16 +3535,20 @@ var require_cjs = __commonJS({
3397
3535
  const driverKey = this.getDriverKey(condition);
3398
3536
  if (!driverKey) return;
3399
3537
  const value = condition[driverKey];
3400
- let startNode = await this.verifierStartNode[driverKey](value);
3401
- let endNode = await this.verifierEndNode[driverKey](value);
3402
- let direction = this.verifierDirection[driverKey];
3538
+ const v = this.ensureValues(value);
3539
+ const config = this.searchConfigs[driverKey][order];
3540
+ let startNode = await config.start(this, v);
3541
+ let endNode = await config.end(this, v);
3542
+ const direction = config.direction;
3543
+ const earlyTerminate = config.earlyTerminate;
3544
+ if (order === "desc" && !startNode) {
3545
+ startNode = await this.rightestNode();
3546
+ }
3547
+ if (order === "asc" && !startNode) {
3548
+ startNode = await this.leftestNode();
3549
+ }
3550
+ if (!startNode) return;
3403
3551
  const comparator = this.verifierMap[driverKey];
3404
- const earlyTerminate = this.verifierEarlyTerminate[driverKey];
3405
- if (order === "desc") {
3406
- startNode = endNode ?? await this.rightestNode();
3407
- endNode = null;
3408
- direction *= -1;
3409
- }
3410
3552
  const generator = this.getPairsGenerator(
3411
3553
  value,
3412
3554
  startNode,
@@ -3418,7 +3560,7 @@ var require_cjs = __commonJS({
3418
3560
  let count = 0;
3419
3561
  const intersection = filterValues && filterValues.size > 0 ? filterValues : null;
3420
3562
  for await (const pair of generator) {
3421
- const [k, v] = pair;
3563
+ const [k, v2] = pair;
3422
3564
  if (intersection && !intersection.has(k)) {
3423
3565
  continue;
3424
3566
  }
@@ -3427,7 +3569,7 @@ var require_cjs = __commonJS({
3427
3569
  if (key === driverKey) continue;
3428
3570
  const verify = this.verifierMap[key];
3429
3571
  const condValue = condition[key];
3430
- if (!verify(v, condValue)) {
3572
+ if (!verify(v2, condValue)) {
3431
3573
  isMatch = false;
3432
3574
  break;
3433
3575
  }
@@ -8342,7 +8484,7 @@ var require_cjs = __commonJS({
8342
8484
  /**
8343
8485
  * Appends and inserts a new page.
8344
8486
  * If a free page is available in the free list, it reuses it.
8345
- * Otherwise, it appends a new page to the end of the file.
8487
+ * Otherwise, it preallocates `pagePreallocationCount` pages to support sequential reads.
8346
8488
  * @returns Created or reused page ID
8347
8489
  */
8348
8490
  async appendNewPage(pageType = PageManager.CONSTANT.PAGE_TYPE_EMPTY, tx) {
@@ -8363,12 +8505,26 @@ var require_cjs = __commonJS({
8363
8505
  await this.setPage(reusedPageId, newPage2, tx);
8364
8506
  return reusedPageId;
8365
8507
  }
8508
+ const preallocationCount = this.options.pagePreallocationCount;
8366
8509
  const pageCount = metadataManager.getPageCount(metadata);
8367
8510
  const newPageIndex = pageCount;
8368
- const newTotalCount = pageCount + 1;
8511
+ const newTotalCount = pageCount + preallocationCount;
8369
8512
  const manager = this.pageFactory.getManagerFromType(pageType);
8370
8513
  const newPage = manager.create(this.pageSize, newPageIndex);
8371
8514
  await this.setPage(newPageIndex, newPage, tx);
8515
+ const emptyManager = this.pageFactory.getManagerFromType(PageManager.CONSTANT.PAGE_TYPE_EMPTY);
8516
+ const firstFreeIndex = newPageIndex + 1;
8517
+ const lastFreeIndex = newPageIndex + preallocationCount - 1;
8518
+ for (let i = firstFreeIndex; i <= lastFreeIndex; i++) {
8519
+ const emptyPage = emptyManager.create(this.pageSize, i);
8520
+ const nextId = i < lastFreeIndex ? i + 1 : -1;
8521
+ emptyManager.setNextPageId(emptyPage, nextId);
8522
+ await this.setPage(i, emptyPage, tx);
8523
+ await this.updateBitmap(i, true, tx);
8524
+ }
8525
+ if (preallocationCount > 1) {
8526
+ metadataManager.setFreePageId(metadata, firstFreeIndex);
8527
+ }
8372
8528
  metadataManager.setPageCount(metadata, newTotalCount);
8373
8529
  await this.setPage(0, metadata, tx);
8374
8530
  return newPageIndex;
@@ -8452,7 +8608,8 @@ var require_cjs = __commonJS({
8452
8608
  }
8453
8609
  /**
8454
8610
  * Frees the page and marks it as available in the bitmap.
8455
- * It also adds the page to the linked list of free pages in metadata.
8611
+ * Inserts the page into the free list in ascending order by page ID
8612
+ * to support HDD sequential reads.
8456
8613
  * @param pageId Page ID
8457
8614
  * @param tx Transaction
8458
8615
  */
@@ -8462,13 +8619,30 @@ var require_cjs = __commonJS({
8462
8619
  await tx.__acquireWriteLock(pageId);
8463
8620
  const metadata = await this.getMetadata(tx);
8464
8621
  const metadataManager = this.pageFactory.getManager(metadata);
8465
- const currentHeadFreePageId = metadataManager.getFreePageId(metadata);
8466
8622
  const emptyPageManager = this.pageFactory.getManagerFromType(PageManager.CONSTANT.PAGE_TYPE_EMPTY);
8467
8623
  const emptyPage = emptyPageManager.create(this.pageSize, pageId);
8468
- emptyPageManager.setNextPageId(emptyPage, currentHeadFreePageId);
8469
- await this.setPage(pageId, emptyPage, tx);
8624
+ const headFreePageId = metadataManager.getFreePageId(metadata);
8625
+ if (headFreePageId === -1 || pageId < headFreePageId) {
8626
+ emptyPageManager.setNextPageId(emptyPage, headFreePageId);
8627
+ await this.setPage(pageId, emptyPage, tx);
8628
+ metadataManager.setFreePageId(metadata, pageId);
8629
+ } else {
8630
+ let prevPageId = headFreePageId;
8631
+ let prevPage = await this.get(prevPageId, tx);
8632
+ let prevManager = this.pageFactory.getManager(prevPage);
8633
+ let nextPageId = prevManager.getNextPageId(prevPage);
8634
+ while (nextPageId !== -1 && nextPageId < pageId) {
8635
+ prevPageId = nextPageId;
8636
+ prevPage = await this.get(prevPageId, tx);
8637
+ prevManager = this.pageFactory.getManager(prevPage);
8638
+ nextPageId = prevManager.getNextPageId(prevPage);
8639
+ }
8640
+ emptyPageManager.setNextPageId(emptyPage, nextPageId);
8641
+ await this.setPage(pageId, emptyPage, tx);
8642
+ prevManager.setNextPageId(prevPage, pageId);
8643
+ await this.setPage(prevPageId, prevPage, tx);
8644
+ }
8470
8645
  await this.updateBitmap(pageId, true, tx);
8471
- metadataManager.setFreePageId(metadata, pageId);
8472
8646
  await this.setPage(0, metadata, tx);
8473
8647
  }
8474
8648
  /**
@@ -9168,7 +9342,9 @@ var require_cjs = __commonJS({
9168
9342
  }
9169
9343
  pageGroupMap.get(pageId).push({ pk: pair.pk, slotIndex, index: pair.index });
9170
9344
  }
9171
- await Promise.all(Array.from(pageGroupMap).map(async ([pageId, items]) => {
9345
+ const sortedPageIds = Array.from(pageGroupMap.keys()).sort((a, b) => a - b);
9346
+ await Promise.all(sortedPageIds.map(async (pageId) => {
9347
+ const items = pageGroupMap.get(pageId);
9172
9348
  const page = await this.pfs.get(pageId, tx);
9173
9349
  if (!this.factory.isDataPage(page)) {
9174
9350
  throw new Error(`Page ${pageId} is not a data page`);
@@ -9519,6 +9695,7 @@ var require_cjs = __commonJS({
9519
9695
  return Object.assign({
9520
9696
  pageSize: 8192,
9521
9697
  pageCacheCapacity: 1e4,
9698
+ pagePreallocationCount: 1e3,
9522
9699
  wal: null,
9523
9700
  walCheckpointThreshold: 1e3
9524
9701
  }, options);
@@ -10572,8 +10749,7 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10572
10749
  if (metadata.lastId === 0) {
10573
10750
  return 0;
10574
10751
  }
10575
- const indexTxMap = {};
10576
- const indexEntryMap = /* @__PURE__ */ new Map();
10752
+ let indexTxMap = {};
10577
10753
  for (const indexName of backfillTargets) {
10578
10754
  const tree = this.trees.get(indexName);
10579
10755
  if (tree && indexName !== "_id") {
@@ -10581,6 +10757,8 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10581
10757
  }
10582
10758
  }
10583
10759
  let backfilledCount = 0;
10760
+ let chunkCount = 0;
10761
+ const CHUNK_SIZE = 1e3;
10584
10762
  const idTree = this.trees.get("_id");
10585
10763
  if (!idTree) {
10586
10764
  throw new Error("ID tree not found");
@@ -10609,10 +10787,6 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10609
10787
  const keyToInsert = this.getTokenKey(k, token);
10610
10788
  const entry = { k, v: token };
10611
10789
  batchInsertData.push([keyToInsert, entry]);
10612
- if (!indexEntryMap.has(btx)) {
10613
- indexEntryMap.set(btx, []);
10614
- }
10615
- indexEntryMap.get(btx).push({ k: keyToInsert, v: entry });
10616
10790
  }
10617
10791
  await btx.batchInsert(batchInsertData);
10618
10792
  } else {
@@ -10620,34 +10794,42 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10620
10794
  if (indexVal === void 0) continue;
10621
10795
  const entry = { k, v: indexVal };
10622
10796
  const batchInsertData = [[k, entry]];
10623
- if (!indexEntryMap.has(btx)) {
10624
- indexEntryMap.set(btx, []);
10625
- }
10626
- indexEntryMap.get(btx).push(entry);
10627
10797
  await btx.batchInsert(batchInsertData);
10628
10798
  }
10629
10799
  }
10630
10800
  backfilledCount++;
10631
- }
10632
- const btxs = Object.values(indexTxMap);
10633
- const success = [];
10634
- try {
10635
- for (const btx of btxs) {
10636
- await btx.commit();
10637
- success.push(btx);
10638
- }
10639
- } catch (err) {
10640
- for (const btx of btxs) {
10641
- await btx.rollback();
10801
+ chunkCount++;
10802
+ if (chunkCount >= CHUNK_SIZE) {
10803
+ try {
10804
+ for (const btx of Object.values(indexTxMap)) {
10805
+ await btx.commit();
10806
+ }
10807
+ } catch (err) {
10808
+ for (const btx of Object.values(indexTxMap)) {
10809
+ await btx.rollback();
10810
+ }
10811
+ throw err;
10812
+ }
10813
+ for (const indexName of backfillTargets) {
10814
+ const tree = this.trees.get(indexName);
10815
+ if (tree && indexName !== "_id") {
10816
+ indexTxMap[indexName] = await tree.createTransaction();
10817
+ }
10818
+ }
10819
+ chunkCount = 0;
10642
10820
  }
10643
- for (const btx of success) {
10644
- const entries = indexEntryMap.get(btx);
10645
- if (!entries) continue;
10646
- for (const entry of entries) {
10647
- await btx.delete(entry.k, entry);
10821
+ }
10822
+ if (chunkCount > 0) {
10823
+ try {
10824
+ for (const btx of Object.values(indexTxMap)) {
10825
+ await btx.commit();
10648
10826
  }
10827
+ } catch (err) {
10828
+ for (const btx of Object.values(indexTxMap)) {
10829
+ await btx.rollback();
10830
+ }
10831
+ throw err;
10649
10832
  }
10650
- throw err;
10651
10833
  }
10652
10834
  this.pendingBackfillFields = [];
10653
10835
  return backfilledCount;
@@ -10805,27 +10987,61 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10805
10987
  const condition = query[primaryField];
10806
10988
  const treeTx = await tree.createTransaction();
10807
10989
  let score = 0;
10808
- const coveredFields = config.fields.filter((f) => queryFields.has(f));
10809
- score += coveredFields.length;
10810
- if (condition) {
10811
- if (typeof condition !== "object" || condition === null) {
10812
- score += 100;
10813
- } else if ("primaryEqual" in condition || "equal" in condition) {
10814
- score += 100;
10815
- } else if ("primaryGte" in condition || "primaryLte" in condition || "primaryGt" in condition || "primaryLt" in condition || "gte" in condition || "lte" in condition || "gt" in condition || "lt" in condition) {
10816
- score += 50;
10817
- } else if ("primaryOr" in condition || "or" in condition) {
10818
- score += 20;
10819
- } else if ("like" in condition) {
10820
- score += 15;
10821
- } else {
10822
- score += 10;
10990
+ let isConsecutive = true;
10991
+ const coveredFields = [];
10992
+ const compositeVerifyFields = [];
10993
+ for (const field of config.fields) {
10994
+ if (!queryFields.has(field)) {
10995
+ isConsecutive = false;
10996
+ continue;
10997
+ }
10998
+ coveredFields.push(field);
10999
+ if (field !== primaryField) {
11000
+ compositeVerifyFields.push(field);
11001
+ }
11002
+ score += 1;
11003
+ if (isConsecutive) {
11004
+ const cond = query[field];
11005
+ if (cond !== void 0) {
11006
+ if (typeof cond !== "object" || cond === null) {
11007
+ score += 100;
11008
+ } else if ("primaryEqual" in cond || "equal" in cond) {
11009
+ score += 100;
11010
+ } else if ("primaryGte" in cond || "primaryLte" in cond || "primaryGt" in cond || "primaryLt" in cond || "gte" in cond || "lte" in cond || "gt" in cond || "lt" in cond) {
11011
+ score += 50;
11012
+ isConsecutive = false;
11013
+ } else if ("primaryOr" in cond || "or" in cond) {
11014
+ score += 20;
11015
+ isConsecutive = false;
11016
+ } else if ("like" in cond) {
11017
+ score += 15;
11018
+ isConsecutive = false;
11019
+ } else {
11020
+ score += 10;
11021
+ isConsecutive = false;
11022
+ }
11023
+ }
10823
11024
  }
10824
11025
  }
10825
- if (orderByField && primaryField === orderByField) {
10826
- score += 200;
11026
+ let isIndexOrderSupported = false;
11027
+ if (orderByField) {
11028
+ for (const field of config.fields) {
11029
+ if (field === orderByField) {
11030
+ isIndexOrderSupported = true;
11031
+ break;
11032
+ }
11033
+ const cond = query[field];
11034
+ let isExactMatch = false;
11035
+ if (cond !== void 0) {
11036
+ if (typeof cond !== "object" || cond === null) isExactMatch = true;
11037
+ else if ("primaryEqual" in cond || "equal" in cond) isExactMatch = true;
11038
+ }
11039
+ if (!isExactMatch) break;
11040
+ }
11041
+ if (isIndexOrderSupported) {
11042
+ score += 200;
11043
+ }
10827
11044
  }
10828
- const compositeVerifyFields = coveredFields.filter((f) => f !== primaryField);
10829
11045
  candidates.push({
10830
11046
  tree: treeTx,
10831
11047
  condition,
@@ -10833,7 +11049,8 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10833
11049
  indexName,
10834
11050
  isFtsMatch: false,
10835
11051
  score,
10836
- compositeVerifyFields
11052
+ compositeVerifyFields,
11053
+ isIndexOrderSupported
10837
11054
  });
10838
11055
  } else if (config.type === "fts") {
10839
11056
  const field = config.fields;
@@ -10851,7 +11068,8 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10851
11068
  isFtsMatch: true,
10852
11069
  matchTokens,
10853
11070
  score: 90,
10854
- compositeVerifyFields: []
11071
+ compositeVerifyFields: [],
11072
+ isIndexOrderSupported: false
10855
11073
  });
10856
11074
  }
10857
11075
  }
@@ -10894,33 +11112,30 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10894
11112
  getTokenKey(pk, token) {
10895
11113
  return pk + ":" + token;
10896
11114
  }
10897
- async applyCandidateByFTS(candidate, matchedTokens, filterValues, order) {
11115
+ async *applyCandidateByFTSStream(candidate, matchedTokens, filterValues, order) {
10898
11116
  const keys = /* @__PURE__ */ new Set();
10899
11117
  for (let i = 0, len = matchedTokens.length; i < len; i++) {
10900
11118
  const token = matchedTokens[i];
10901
- const pairs = await candidate.tree.where(
11119
+ for await (const pair of candidate.tree.whereStream(
10902
11120
  { primaryEqual: { v: token } },
10903
- {
10904
- order
11121
+ { order }
11122
+ )) {
11123
+ const pk = pair[1].k;
11124
+ if (filterValues && !filterValues.has(pk)) continue;
11125
+ if (!keys.has(pk)) {
11126
+ keys.add(pk);
11127
+ yield pk;
10905
11128
  }
10906
- );
10907
- for (const pair of pairs.values()) {
10908
- if (filterValues && !filterValues.has(pair.k)) continue;
10909
- keys.add(pair.k);
10910
11129
  }
10911
11130
  }
10912
- return keys;
10913
11131
  }
10914
11132
  /**
10915
11133
  * 특정 인덱스 후보를 조회하여 PK 집합을 필터링합니다.
10916
11134
  */
10917
- async applyCandidate(candidate, filterValues, order) {
10918
- return await candidate.tree.keys(
11135
+ applyCandidateStream(candidate, filterValues, order) {
11136
+ return candidate.tree.keysStream(
10919
11137
  candidate.condition,
10920
- {
10921
- filterValues,
10922
- order
10923
- }
11138
+ { filterValues, order }
10924
11139
  );
10925
11140
  }
10926
11141
  /**
@@ -10936,30 +11151,34 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10936
11151
  );
10937
11152
  if (!selectivity) return new Float64Array(0);
10938
11153
  const { driver, others, rollback } = selectivity;
10939
- const useIndexOrder = orderBy === void 0 || driver.field === orderBy;
11154
+ const useIndexOrder = orderBy === void 0 || driver.isIndexOrderSupported;
10940
11155
  const candidates = [driver, ...others];
10941
11156
  let keys = void 0;
10942
11157
  for (let i = 0, len = candidates.length; i < len; i++) {
10943
11158
  const candidate = candidates[i];
10944
11159
  const currentOrder = useIndexOrder ? sortOrder : void 0;
10945
11160
  if (candidate.isFtsMatch && candidate.matchTokens && candidate.matchTokens.length > 0) {
10946
- keys = await this.applyCandidateByFTS(
11161
+ const stream = this.applyCandidateByFTSStream(
10947
11162
  candidate,
10948
11163
  candidate.matchTokens,
10949
11164
  keys,
10950
11165
  currentOrder
10951
11166
  );
11167
+ keys = /* @__PURE__ */ new Set();
11168
+ for await (const pk of stream) keys.add(pk);
10952
11169
  } else {
10953
- keys = await this.applyCandidate(candidate, keys, currentOrder);
11170
+ const stream = this.applyCandidateStream(candidate, keys, currentOrder);
11171
+ keys = /* @__PURE__ */ new Set();
11172
+ for await (const pk of stream) keys.add(pk);
10954
11173
  }
10955
11174
  }
10956
11175
  rollback();
10957
11176
  return new Float64Array(Array.from(keys || []));
10958
11177
  }
10959
11178
  /**
10960
- * 드라이버 인덱스만으로 PK 가져옵니다. (교집합 없이)
11179
+ * 드라이버 인덱스만으로 PK 스트림을 가져옵니다. (교집합 없이)
10961
11180
  * selectDocuments에서 사용하며, 나머지 조건(others)은 스트리밍 중 tree.verify()로 검증합니다.
10962
- * @returns 드라이버 키 배열, others 후보 목록, rollback 함수. 또는 null.
11181
+ * @returns 드라이버 키 스트림, others 후보 목록, rollback 함수. 또는 null.
10963
11182
  */
10964
11183
  async getDriverKeys(query, orderBy, sortOrder = "asc") {
10965
11184
  const isQueryEmpty = Object.keys(query).length === 0;
@@ -10970,21 +11189,21 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10970
11189
  );
10971
11190
  if (!selectivity) return null;
10972
11191
  const { driver, others, compositeVerifyConditions, rollback } = selectivity;
10973
- const useIndexOrder = orderBy === void 0 || driver.field === orderBy;
11192
+ const useIndexOrder = orderBy === void 0 || driver.isIndexOrderSupported;
10974
11193
  const currentOrder = useIndexOrder ? sortOrder : void 0;
10975
- let keys;
11194
+ let keysStream;
10976
11195
  if (driver.isFtsMatch && driver.matchTokens && driver.matchTokens.length > 0) {
10977
- keys = await this.applyCandidateByFTS(
11196
+ keysStream = this.applyCandidateByFTSStream(
10978
11197
  driver,
10979
11198
  driver.matchTokens,
10980
11199
  void 0,
10981
11200
  currentOrder
10982
11201
  );
10983
11202
  } else {
10984
- keys = await this.applyCandidate(driver, void 0, currentOrder);
11203
+ keysStream = this.applyCandidateStream(driver, void 0, currentOrder);
10985
11204
  }
10986
11205
  return {
10987
- keys: new Float64Array(Array.from(keys)),
11206
+ keysStream,
10988
11207
  others,
10989
11208
  compositeVerifyConditions,
10990
11209
  isDriverOrderByField: useIndexOrder,
@@ -11334,28 +11553,17 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
11334
11553
  return currentChunkSize;
11335
11554
  }
11336
11555
  /**
11337
- * Prefetch 방식으로 키 배열을 청크 단위로 조회하여 문서를 순회합니다.
11556
+ * Prefetch 방식으로 키 스트림을 청크 단위로 조회하여 문서를 순회합니다.
11338
11557
  * FTS 검증, 복합 인덱스 검증, others 후보에 대한 tree.verify() 검증을 통과한 문서만 yield 합니다.
11339
11558
  */
11340
- async *processChunkedKeysWithVerify(keys, startIdx, initialChunkSize, ftsConditions, compositeVerifyConditions, others, tx) {
11559
+ async *processChunkedKeysWithVerify(keysStream, startIdx, initialChunkSize, ftsConditions, compositeVerifyConditions, others, tx) {
11341
11560
  const verifyOthers = others.filter((o) => !o.isFtsMatch);
11342
- let i = startIdx;
11343
- const totalKeys = keys.length;
11344
11561
  let currentChunkSize = initialChunkSize;
11345
- let nextChunkPromise = null;
11346
- if (i < totalKeys) {
11347
- const endIdx = Math.min(i + currentChunkSize, totalKeys);
11348
- nextChunkPromise = this.selectMany(keys.subarray(i, endIdx), false, tx);
11349
- i = endIdx;
11350
- }
11351
- while (nextChunkPromise) {
11352
- const rawResults = await nextChunkPromise;
11353
- nextChunkPromise = null;
11354
- if (i < totalKeys) {
11355
- const endIdx = Math.min(i + currentChunkSize, totalKeys);
11356
- nextChunkPromise = this.selectMany(keys.subarray(i, endIdx), false, tx);
11357
- i = endIdx;
11358
- }
11562
+ let chunk = [];
11563
+ let dropped = 0;
11564
+ const processChunk = async (pks) => {
11565
+ const docs = [];
11566
+ const rawResults = await this.selectMany(new Float64Array(pks), false, tx);
11359
11567
  let chunkTotalSize = 0;
11360
11568
  for (let j = 0, len = rawResults.length; j < len; j++) {
11361
11569
  const s = rawResults[j];
@@ -11382,9 +11590,26 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
11382
11590
  }
11383
11591
  if (!passed) continue;
11384
11592
  }
11385
- yield doc;
11593
+ docs.push(doc);
11386
11594
  }
11387
11595
  currentChunkSize = this.adjustChunkSize(currentChunkSize, chunkTotalSize);
11596
+ return docs;
11597
+ };
11598
+ for await (const pk of keysStream) {
11599
+ if (dropped < startIdx) {
11600
+ dropped++;
11601
+ continue;
11602
+ }
11603
+ chunk.push(pk);
11604
+ if (chunk.length >= currentChunkSize) {
11605
+ const docs = await processChunk(chunk);
11606
+ for (let j = 0; j < docs.length; j++) yield docs[j];
11607
+ chunk = [];
11608
+ }
11609
+ }
11610
+ if (chunk.length > 0) {
11611
+ const docs = await processChunk(chunk);
11612
+ for (let j = 0; j < docs.length; j++) yield docs[j];
11388
11613
  }
11389
11614
  }
11390
11615
  /**
@@ -11432,11 +11657,7 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
11432
11657
  }
11433
11658
  const driverResult = await self.getDriverKeys(query, orderByField, sortOrder);
11434
11659
  if (!driverResult) return;
11435
- const { keys, others, compositeVerifyConditions, isDriverOrderByField, rollback } = driverResult;
11436
- if (keys.length === 0) {
11437
- rollback();
11438
- return;
11439
- }
11660
+ const { keysStream, others, compositeVerifyConditions, isDriverOrderByField, rollback } = driverResult;
11440
11661
  try {
11441
11662
  if (!isDriverOrderByField && orderByField) {
11442
11663
  const topK = limit === Infinity ? Infinity : offset + limit;
@@ -11451,7 +11672,7 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
11451
11672
  }
11452
11673
  const results = [];
11453
11674
  for await (const doc of self.processChunkedKeysWithVerify(
11454
- keys,
11675
+ keysStream,
11455
11676
  0,
11456
11677
  self.options.pageSize,
11457
11678
  ftsConditions,
@@ -11487,16 +11708,23 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
11487
11708
  yield limitedResults[j];
11488
11709
  }
11489
11710
  } else {
11711
+ const hasFilters = ftsConditions.length > 0 || compositeVerifyConditions.length > 0 || others.length > 0;
11712
+ const startIdx = hasFilters ? 0 : offset;
11490
11713
  let yieldedCount = 0;
11714
+ let skippedCount = hasFilters ? 0 : offset;
11491
11715
  for await (const doc of self.processChunkedKeysWithVerify(
11492
- keys,
11493
- offset,
11716
+ keysStream,
11717
+ startIdx,
11494
11718
  self.options.pageSize,
11495
11719
  ftsConditions,
11496
11720
  compositeVerifyConditions,
11497
11721
  others,
11498
11722
  tx2
11499
11723
  )) {
11724
+ if (skippedCount < offset) {
11725
+ skippedCount++;
11726
+ continue;
11727
+ }
11500
11728
  if (yieldedCount >= limit) break;
11501
11729
  yield doc;
11502
11730
  yieldedCount++;
@@ -127,6 +127,7 @@ export declare class DocumentDataplyAPI<T extends DocumentJSON> extends DataplyA
127
127
  field: string;
128
128
  indexName: string;
129
129
  isFtsMatch: false;
130
+ isIndexOrderSupported: boolean;
130
131
  } | {
131
132
  tree: BPTreeAsync<string, V>;
132
133
  condition: Partial<DocumentDataplyCondition<U>>;
@@ -134,6 +135,7 @@ export declare class DocumentDataplyAPI<T extends DocumentJSON> extends DataplyA
134
135
  indexName: string;
135
136
  isFtsMatch: true;
136
137
  matchTokens: string[];
138
+ isIndexOrderSupported: boolean;
137
139
  });
138
140
  others: ({
139
141
  tree: BPTreeAsync<number, V>;
@@ -141,6 +143,7 @@ export declare class DocumentDataplyAPI<T extends DocumentJSON> extends DataplyA
141
143
  field: string;
142
144
  indexName: string;
143
145
  isFtsMatch: false;
146
+ isIndexOrderSupported: boolean;
144
147
  } | {
145
148
  tree: BPTreeAsync<string, V>;
146
149
  condition: Partial<DocumentDataplyCondition<U>>;
@@ -148,6 +151,7 @@ export declare class DocumentDataplyAPI<T extends DocumentJSON> extends DataplyA
148
151
  indexName: string;
149
152
  isFtsMatch: true;
150
153
  matchTokens: string[];
154
+ isIndexOrderSupported: boolean;
151
155
  })[];
152
156
  compositeVerifyConditions: {
153
157
  field: string;
@@ -164,20 +168,20 @@ export declare class DocumentDataplyAPI<T extends DocumentJSON> extends DataplyA
164
168
  smallChunkSize: number;
165
169
  };
166
170
  private getTokenKey;
167
- private applyCandidateByFTS;
171
+ private applyCandidateByFTSStream;
168
172
  /**
169
173
  * 특정 인덱스 후보를 조회하여 PK 집합을 필터링합니다.
170
174
  */
171
- private applyCandidate;
175
+ private applyCandidateStream;
172
176
  /**
173
177
  * 쿼리와 인덱스 선택을 기반으로 기본 키(Primary Keys)를 가져옵니다.
174
178
  * 쿼리 최적화를 통합하기 위한 내부 공통 메서드입니다.
175
179
  */
176
180
  getKeys(query: Partial<DocumentDataplyQuery<T>>, orderBy?: string, sortOrder?: 'asc' | 'desc'): Promise<Float64Array>;
177
181
  /**
178
- * 드라이버 인덱스만으로 PK 가져옵니다. (교집합 없이)
182
+ * 드라이버 인덱스만으로 PK 스트림을 가져옵니다. (교집합 없이)
179
183
  * selectDocuments에서 사용하며, 나머지 조건(others)은 스트리밍 중 tree.verify()로 검증합니다.
180
- * @returns 드라이버 키 배열, others 후보 목록, rollback 함수. 또는 null.
184
+ * @returns 드라이버 키 스트림, others 후보 목록, rollback 함수. 또는 null.
181
185
  */
182
186
  private getDriverKeys;
183
187
  private insertDocumentInternal;
@@ -250,7 +254,7 @@ export declare class DocumentDataplyAPI<T extends DocumentJSON> extends DataplyA
250
254
  */
251
255
  private adjustChunkSize;
252
256
  /**
253
- * Prefetch 방식으로 키 배열을 청크 단위로 조회하여 문서를 순회합니다.
257
+ * Prefetch 방식으로 키 스트림을 청크 단위로 조회하여 문서를 순회합니다.
254
258
  * FTS 검증, 복합 인덱스 검증, others 후보에 대한 tree.verify() 검증을 통과한 문서만 yield 합니다.
255
259
  */
256
260
  private processChunkedKeysWithVerify;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "document-dataply",
3
- "version": "0.0.9-alpha.4",
3
+ "version": "0.0.9-alpha.6",
4
4
  "description": "Simple and powerful JSON document database supporting complex queries and flexible indexing policies.",
5
5
  "license": "MIT",
6
6
  "author": "izure <admin@izure.org>",
@@ -42,7 +42,7 @@
42
42
  "dataply"
43
43
  ],
44
44
  "dependencies": {
45
- "dataply": "^0.0.24-alpha.0"
45
+ "dataply": "^0.0.24-alpha.2"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@types/jest": "^30.0.0",
package/readme.md CHANGED
@@ -54,16 +54,21 @@ async function main() {
54
54
  .Options({ wal: 'my-database.wal' })
55
55
  .Open('my-database.db');
56
56
 
57
- // Register indices before init (Recommended)
58
- await db.createIndex('name', { type: 'btree', fields: ['name'] });
59
- await db.createIndex('tags_0', { type: 'btree', fields: ['tags.0'] });
60
-
61
- // Composite Index support
62
- await db.createIndex('idx_name_age', { type: 'btree', fields: ['name', 'age'] });
63
-
64
57
  // Initialize database
65
58
  await db.init();
66
59
 
60
+ // Register indices
61
+ // use transaction to ensure atomicity
62
+ await db.migration(1, async (tx) => {
63
+ await db.createIndex('name', { type: 'btree', fields: ['name'] }, tx);
64
+ await db.createIndex('tags_0', { type: 'btree', fields: ['tags.0'] }, tx);
65
+
66
+ // Composite Index support
67
+ await db.createIndex('idx_name_age', { type: 'btree', fields: ['name', 'age'] }, tx);
68
+
69
+ console.log('Migration completed successfully');
70
+ });
71
+
67
72
  // Insert document
68
73
  const id = await db.insert({
69
74
  name: 'John Doe',