document-dataply 0.0.9-alpha.1 → 0.0.9-alpha.11

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
@@ -873,7 +873,7 @@ var require_cjs = __commonJS({
873
873
  };
874
874
  var AsyncMVCCStrategy = class extends MVCCStrategy {
875
875
  };
876
- var Ryoiki2 = class _Ryoiki {
876
+ var Ryoiki = class _Ryoiki {
877
877
  readings;
878
878
  writings;
879
879
  readQueue;
@@ -1130,7 +1130,7 @@ var require_cjs = __commonJS({
1130
1130
  }
1131
1131
  };
1132
1132
  var AsyncMVCCTransaction = class _AsyncMVCCTransaction extends MVCCTransaction {
1133
- lock = new Ryoiki2();
1133
+ lock = new Ryoiki();
1134
1134
  async writeLock(fn) {
1135
1135
  let lockId;
1136
1136
  return this.lock.writeLock(async (_lockId) => {
@@ -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.insertableRightestEndNodeByPrimary(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.
@@ -2086,7 +2220,7 @@ var require_cjs = __commonJS({
2086
2220
  return this.getNode(node.next);
2087
2221
  }
2088
2222
  insertableEndNode(value, direction) {
2089
- const insertableNode = this.insertableNode(value);
2223
+ const insertableNode = direction === -1 ? this.insertableNodeByPrimary(value) : this.insertableRightestNodeByPrimary(value);
2090
2224
  let key;
2091
2225
  switch (direction) {
2092
2226
  case -1:
@@ -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
  }
@@ -2752,7 +2890,7 @@ var require_cjs = __commonJS({
2752
2890
  }
2753
2891
  }
2754
2892
  };
2755
- var Ryoiki22 = class _Ryoiki2 {
2893
+ var Ryoiki2 = class _Ryoiki2 {
2756
2894
  readings;
2757
2895
  writings;
2758
2896
  readQueue;
@@ -3019,7 +3157,7 @@ var require_cjs = __commonJS({
3019
3157
  comparator,
3020
3158
  option
3021
3159
  );
3022
- this.lock = new Ryoiki22();
3160
+ this.lock = new Ryoiki2();
3023
3161
  }
3024
3162
  async writeLock(id, fn) {
3025
3163
  let lockId;
@@ -3210,7 +3348,7 @@ var require_cjs = __commonJS({
3210
3348
  return await this.getNode(node.next);
3211
3349
  }
3212
3350
  async insertableEndNode(value, direction) {
3213
- const insertableNode = await this.insertableNode(value);
3351
+ const insertableNode = direction === -1 ? await this.insertableNodeByPrimary(value) : await this.insertableRightestNodeByPrimary(value);
3214
3352
  let key;
3215
3353
  switch (direction) {
3216
3354
  case -1:
@@ -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
  }
@@ -3957,7 +4099,7 @@ var require_cjs = __commonJS({
3957
4099
  }
3958
4100
  };
3959
4101
  var SerializeStrategyAsync2 = class extends SerializeStrategy {
3960
- lock = new Ryoiki22();
4102
+ lock = new Ryoiki2();
3961
4103
  async acquireLock(action) {
3962
4104
  let lockId;
3963
4105
  return this.lock.writeLock((_lockId) => {
@@ -8038,9 +8180,6 @@ var require_cjs = __commonJS({
8038
8180
  */
8039
8181
  async write(pageId, data) {
8040
8182
  const pageStartPos = pageId * this.pageSize;
8041
- if (pageStartPos + this.pageSize > 512 * 1024 * 1024) {
8042
- throw new Error(`[Safety Limit] File write exceeds 512MB limit at position ${pageStartPos}`);
8043
- }
8044
8183
  const dataCopy = new Uint8Array(this.pageSize);
8045
8184
  dataCopy.set(data);
8046
8185
  this.dirtyPages.set(pageId, dataCopy);
@@ -8067,6 +8206,22 @@ var require_cjs = __commonJS({
8067
8206
  this.dirtyPages.delete(pageId);
8068
8207
  }
8069
8208
  }
8209
+ /**
8210
+ * 지정된 페이지들만 디스크에 기록합니다.
8211
+ * WAL 없이 트랜잭션 커밋 시, 해당 트랜잭션의 dirty pages만 선택적으로 flush합니다.
8212
+ * @param pages 기록할 페이지 맵 (PageID -> PageData)
8213
+ */
8214
+ async flushPages(pages) {
8215
+ if (pages.size === 0) {
8216
+ return;
8217
+ }
8218
+ const sortedPageIds = Array.from(pages.keys()).sort((a, b) => a - b);
8219
+ for (const pageId of sortedPageIds) {
8220
+ const data = pages.get(pageId);
8221
+ const position = pageId * this.pageSize;
8222
+ await this._writeToDisk(data, position);
8223
+ }
8224
+ }
8070
8225
  /**
8071
8226
  * 메인 DB 파일의 물리적 동기화를 수행합니다 (fsync).
8072
8227
  */
@@ -8342,7 +8497,7 @@ var require_cjs = __commonJS({
8342
8497
  /**
8343
8498
  * Appends and inserts a new page.
8344
8499
  * 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.
8500
+ * Otherwise, it preallocates `pagePreallocationCount` pages to support sequential reads.
8346
8501
  * @returns Created or reused page ID
8347
8502
  */
8348
8503
  async appendNewPage(pageType = PageManager.CONSTANT.PAGE_TYPE_EMPTY, tx) {
@@ -8357,20 +8512,32 @@ var require_cjs = __commonJS({
8357
8512
  const nextFreePageId = reusedPageManager.getNextPageId(reusedPage);
8358
8513
  metadataManager.setFreePageId(metadata, nextFreePageId);
8359
8514
  await this.setPage(0, metadata, tx);
8360
- await this.updateBitmap(reusedPageId, false, tx);
8361
8515
  const manager2 = this.pageFactory.getManagerFromType(pageType);
8362
8516
  const newPage2 = manager2.create(this.pageSize, reusedPageId);
8363
8517
  await this.setPage(reusedPageId, newPage2, tx);
8364
8518
  return reusedPageId;
8365
8519
  }
8520
+ const preallocationCount = this.options.pagePreallocationCount;
8366
8521
  const pageCount = metadataManager.getPageCount(metadata);
8367
8522
  const newPageIndex = pageCount;
8368
- const newTotalCount = pageCount + 1;
8523
+ const newTotalCount = pageCount + preallocationCount;
8369
8524
  const manager = this.pageFactory.getManagerFromType(pageType);
8370
8525
  const newPage = manager.create(this.pageSize, newPageIndex);
8371
8526
  await this.setPage(newPageIndex, newPage, tx);
8527
+ const emptyManager = this.pageFactory.getManagerFromType(PageManager.CONSTANT.PAGE_TYPE_EMPTY);
8528
+ const firstFreeIndex = newPageIndex + 1;
8529
+ const lastFreeIndex = newPageIndex + preallocationCount - 1;
8372
8530
  metadataManager.setPageCount(metadata, newTotalCount);
8531
+ if (preallocationCount > 1) {
8532
+ metadataManager.setFreePageId(metadata, firstFreeIndex);
8533
+ }
8373
8534
  await this.setPage(0, metadata, tx);
8535
+ for (let i = firstFreeIndex; i <= lastFreeIndex; i++) {
8536
+ const emptyPage = emptyManager.create(this.pageSize, i);
8537
+ const nextId = i < lastFreeIndex ? i + 1 : -1;
8538
+ emptyManager.setNextPageId(emptyPage, nextId);
8539
+ await this.setPage(i, emptyPage, tx);
8540
+ }
8374
8541
  return newPageIndex;
8375
8542
  }
8376
8543
  /**
@@ -8467,7 +8634,6 @@ var require_cjs = __commonJS({
8467
8634
  const emptyPage = emptyPageManager.create(this.pageSize, pageId);
8468
8635
  emptyPageManager.setNextPageId(emptyPage, currentHeadFreePageId);
8469
8636
  await this.setPage(pageId, emptyPage, tx);
8470
- await this.updateBitmap(pageId, true, tx);
8471
8637
  metadataManager.setFreePageId(metadata, pageId);
8472
8638
  await this.setPage(0, metadata, tx);
8473
8639
  }
@@ -9168,7 +9334,9 @@ var require_cjs = __commonJS({
9168
9334
  }
9169
9335
  pageGroupMap.get(pageId).push({ pk: pair.pk, slotIndex, index: pair.index });
9170
9336
  }
9171
- await Promise.all(Array.from(pageGroupMap).map(async ([pageId, items]) => {
9337
+ const sortedPageIds = Array.from(pageGroupMap.keys()).sort((a, b) => a - b);
9338
+ await Promise.all(sortedPageIds.map(async (pageId) => {
9339
+ const items = pageGroupMap.get(pageId);
9172
9340
  const page = await this.pfs.get(pageId, tx);
9173
9341
  if (!this.factory.isDataPage(page)) {
9174
9342
  throw new Error(`Page ${pageId} is not a data page`);
@@ -9302,6 +9470,8 @@ var require_cjs = __commonJS({
9302
9470
  commitHooks = [];
9303
9471
  /** Page MVCC Strategy for disk access */
9304
9472
  pageStrategy;
9473
+ /** Release function for global write lock, set by DataplyAPI */
9474
+ _writeLockRelease = null;
9305
9475
  /**
9306
9476
  * Sets the BPTree transaction.
9307
9477
  * @param tx BPTree transaction
@@ -9336,6 +9506,19 @@ var require_cjs = __commonJS({
9336
9506
  onCommit(hook) {
9337
9507
  this.commitHooks.push(hook);
9338
9508
  }
9509
+ /**
9510
+ * Sets the global write lock release function.
9511
+ * Called by DataplyAPI.runWithDefaultWrite when acquiring the lock.
9512
+ */
9513
+ __setWriteLockRelease(release) {
9514
+ this._writeLockRelease = release;
9515
+ }
9516
+ /**
9517
+ * Returns whether this transaction already has a write lock.
9518
+ */
9519
+ __hasWriteLockRelease() {
9520
+ return this._writeLockRelease !== null;
9521
+ }
9339
9522
  /**
9340
9523
  * Reads a page. Uses dirty buffer if available, otherwise disk.
9341
9524
  * @param pageId Page ID
@@ -9381,44 +9564,60 @@ var require_cjs = __commonJS({
9381
9564
  * Commits the transaction.
9382
9565
  */
9383
9566
  async commit() {
9384
- await this.context.run(this, async () => {
9385
- for (const hook of this.commitHooks) {
9386
- await hook();
9387
- }
9388
- });
9389
- let shouldTriggerCheckpoint = false;
9390
- await this.pfs.runGlobalLock(async () => {
9391
- if (this.pfs.wal && this.dirtyPages.size > 0) {
9392
- await this.pfs.wal.prepareCommit(this.dirtyPages);
9393
- await this.pfs.wal.writeCommitMarker();
9394
- }
9395
- for (const [pageId, data] of this.dirtyPages) {
9396
- await this.pageStrategy.write(pageId, data);
9397
- }
9398
- if (this.pfs.wal) {
9399
- this.pfs.wal.incrementWrittenPages(this.dirtyPages.size);
9400
- if (this.pfs.wal.shouldCheckpoint(this.pfs.options.walCheckpointThreshold)) {
9401
- shouldTriggerCheckpoint = true;
9567
+ try {
9568
+ await this.context.run(this, async () => {
9569
+ for (const hook of this.commitHooks) {
9570
+ await hook();
9571
+ }
9572
+ });
9573
+ let shouldTriggerCheckpoint = false;
9574
+ await this.pfs.runGlobalLock(async () => {
9575
+ if (this.pfs.wal && this.dirtyPages.size > 0) {
9576
+ await this.pfs.wal.prepareCommit(this.dirtyPages);
9577
+ await this.pfs.wal.writeCommitMarker();
9578
+ }
9579
+ for (const [pageId, data] of this.dirtyPages) {
9580
+ await this.pageStrategy.write(pageId, data);
9581
+ }
9582
+ if (!this.pfs.wal) {
9583
+ await this.pfs.strategy.flushPages(this.dirtyPages);
9584
+ } else {
9585
+ this.pfs.wal.incrementWrittenPages(this.dirtyPages.size);
9586
+ if (this.pfs.wal.shouldCheckpoint(this.pfs.options.walCheckpointThreshold)) {
9587
+ shouldTriggerCheckpoint = true;
9588
+ }
9402
9589
  }
9590
+ });
9591
+ if (shouldTriggerCheckpoint) {
9592
+ await this.pfs.checkpoint();
9593
+ }
9594
+ this.dirtyPages.clear();
9595
+ this.undoPages.clear();
9596
+ this.releaseAllLocks();
9597
+ } finally {
9598
+ if (this._writeLockRelease) {
9599
+ this._writeLockRelease();
9600
+ this._writeLockRelease = null;
9403
9601
  }
9404
- });
9405
- if (shouldTriggerCheckpoint) {
9406
- await this.pfs.checkpoint();
9407
9602
  }
9408
- this.dirtyPages.clear();
9409
- this.undoPages.clear();
9410
- this.releaseAllLocks();
9411
9603
  }
9412
9604
  /**
9413
9605
  * Rolls back the transaction.
9414
9606
  */
9415
9607
  async rollback() {
9416
- if (this.bptreeTx) {
9417
- this.bptreeTx.rollback();
9608
+ try {
9609
+ if (this.bptreeTx) {
9610
+ this.bptreeTx.rollback();
9611
+ }
9612
+ this.dirtyPages.clear();
9613
+ this.undoPages.clear();
9614
+ this.releaseAllLocks();
9615
+ } finally {
9616
+ if (this._writeLockRelease) {
9617
+ this._writeLockRelease();
9618
+ this._writeLockRelease = null;
9619
+ }
9418
9620
  }
9419
- this.dirtyPages.clear();
9420
- this.undoPages.clear();
9421
- this.releaseAllLocks();
9422
9621
  }
9423
9622
  /**
9424
9623
  * Returns the dirty pages map.
@@ -9495,6 +9694,8 @@ var require_cjs = __commonJS({
9495
9694
  /** Whether the database was created this time. */
9496
9695
  isNewlyCreated;
9497
9696
  txIdCounter;
9697
+ /** Promise-chain mutex for serializing write operations */
9698
+ writeQueue = Promise.resolve();
9498
9699
  /**
9499
9700
  * Verifies if the page file is a valid Dataply file.
9500
9701
  * The metadata page must be located at the beginning of the Dataply file.
@@ -9519,6 +9720,7 @@ var require_cjs = __commonJS({
9519
9720
  return Object.assign({
9520
9721
  pageSize: 8192,
9521
9722
  pageCacheCapacity: 1e4,
9723
+ pagePreallocationCount: 1e3,
9522
9724
  wal: null,
9523
9725
  walCheckpointThreshold: 1e3
9524
9726
  }, options);
@@ -9651,6 +9853,52 @@ var require_cjs = __commonJS({
9651
9853
  * @param tx The transaction to use. If not provided, a new transaction is created.
9652
9854
  * @returns The result of the callback function.
9653
9855
  */
9856
+ /**
9857
+ * Acquires the global write lock.
9858
+ * Returns a release function that MUST be called to unlock.
9859
+ * Used internally by runWithDefaultWrite.
9860
+ * @returns A release function
9861
+ */
9862
+ acquireWriteLock() {
9863
+ const previous = this.writeQueue;
9864
+ let release;
9865
+ this.writeQueue = new Promise((resolve) => {
9866
+ release = resolve;
9867
+ });
9868
+ return previous.then(() => release);
9869
+ }
9870
+ /**
9871
+ * Runs a write callback within a transaction context with global write serialization.
9872
+ * If no transaction is provided, a new transaction is created, committed on success, rolled back on error.
9873
+ * If a transaction is provided (external), the write lock is acquired on first call and held until commit/rollback.
9874
+ * Subclasses MUST use this method for all write operations instead of runWithDefault.
9875
+ * @param callback The callback function to run.
9876
+ * @param tx Optional external transaction.
9877
+ * @returns The result of the callback.
9878
+ */
9879
+ async runWithDefaultWrite(callback, tx) {
9880
+ if (!tx) {
9881
+ const release = await this.acquireWriteLock();
9882
+ const internalTx = this.createTransaction();
9883
+ internalTx.__setWriteLockRelease(release);
9884
+ const [error2, result2] = await catchPromise2(this.txContext.run(internalTx, () => callback(internalTx)));
9885
+ if (error2) {
9886
+ await internalTx.rollback();
9887
+ throw error2;
9888
+ }
9889
+ await internalTx.commit();
9890
+ return result2;
9891
+ }
9892
+ if (!tx.__hasWriteLockRelease()) {
9893
+ const release = await this.acquireWriteLock();
9894
+ tx.__setWriteLockRelease(release);
9895
+ }
9896
+ const [error, result] = await catchPromise2(this.txContext.run(tx, () => callback(tx)));
9897
+ if (error) {
9898
+ throw error;
9899
+ }
9900
+ return result;
9901
+ }
9654
9902
  async runWithDefault(callback, tx) {
9655
9903
  const isInternalTx = !tx;
9656
9904
  if (!tx) {
@@ -9722,7 +9970,7 @@ var require_cjs = __commonJS({
9722
9970
  if (!this.initialized) {
9723
9971
  throw new Error("Dataply instance is not initialized");
9724
9972
  }
9725
- return this.runWithDefault(async (tx2) => {
9973
+ return this.runWithDefaultWrite(async (tx2) => {
9726
9974
  incrementRowCount = incrementRowCount ?? true;
9727
9975
  if (typeof data === "string") {
9728
9976
  data = this.textCodec.encode(data);
@@ -9742,7 +9990,7 @@ var require_cjs = __commonJS({
9742
9990
  if (!this.initialized) {
9743
9991
  throw new Error("Dataply instance is not initialized");
9744
9992
  }
9745
- return this.runWithDefault(async (tx2) => {
9993
+ return this.runWithDefaultWrite(async (tx2) => {
9746
9994
  incrementRowCount = incrementRowCount ?? true;
9747
9995
  if (typeof data === "string") {
9748
9996
  data = this.textCodec.encode(data);
@@ -9763,7 +10011,7 @@ var require_cjs = __commonJS({
9763
10011
  if (!this.initialized) {
9764
10012
  throw new Error("Dataply instance is not initialized");
9765
10013
  }
9766
- return this.runWithDefault(async (tx2) => {
10014
+ return this.runWithDefaultWrite(async (tx2) => {
9767
10015
  incrementRowCount = incrementRowCount ?? true;
9768
10016
  const encodedList = dataList.map(
9769
10017
  (data) => typeof data === "string" ? this.textCodec.encode(data) : data
@@ -9781,7 +10029,7 @@ var require_cjs = __commonJS({
9781
10029
  if (!this.initialized) {
9782
10030
  throw new Error("Dataply instance is not initialized");
9783
10031
  }
9784
- return this.runWithDefault(async (tx2) => {
10032
+ return this.runWithDefaultWrite(async (tx2) => {
9785
10033
  if (typeof data === "string") {
9786
10034
  data = this.textCodec.encode(data);
9787
10035
  }
@@ -9798,7 +10046,7 @@ var require_cjs = __commonJS({
9798
10046
  if (!this.initialized) {
9799
10047
  throw new Error("Dataply instance is not initialized");
9800
10048
  }
9801
- return this.runWithDefault(async (tx2) => {
10049
+ return this.runWithDefaultWrite(async (tx2) => {
9802
10050
  decrementRowCount = decrementRowCount ?? true;
9803
10051
  await this.rowTableEngine.delete(pk, decrementRowCount, tx2);
9804
10052
  }, tx);
@@ -9990,6 +10238,11 @@ var DocumentSerializeStrategyAsync = class extends import_dataply.SerializeStrat
9990
10238
  this.txContext = txContext;
9991
10239
  this.treeKey = treeKey;
9992
10240
  }
10241
+ /**
10242
+ * readHead에서 할당된 headPk를 캐싱하여
10243
+ * writeHead에서 AsyncLocalStorage 컨텍스트 유실 시에도 사용할 수 있도록 함
10244
+ */
10245
+ cachedHeadPk = null;
9993
10246
  async id(isLeaf) {
9994
10247
  const tx = this.txContext.get();
9995
10248
  const pk = await this.api.insertAsOverflow("__BPTREE_NODE_PLACEHOLDER__", false, tx);
@@ -10022,20 +10275,25 @@ var DocumentSerializeStrategyAsync = class extends import_dataply.SerializeStrat
10022
10275
  const pk = await this.api.insertAsOverflow("__BPTREE_HEAD_PLACEHOLDER__", false, tx);
10023
10276
  metadata.indices[this.treeKey][0] = pk;
10024
10277
  await this.api.updateDocumentInnerMetadata(metadata, tx);
10278
+ this.cachedHeadPk = pk;
10025
10279
  return null;
10026
10280
  }
10281
+ this.cachedHeadPk = headPk;
10027
10282
  const row = await this.api.select(headPk, false, tx);
10028
10283
  if (row === null || row === "" || row.startsWith("__BPTREE_")) return null;
10029
10284
  return JSON.parse(row);
10030
10285
  }
10031
10286
  async writeHead(head) {
10032
10287
  const tx = this.txContext.get();
10033
- const metadata = await this.api.getDocumentInnerMetadata(tx);
10034
- const indexInfo = metadata.indices[this.treeKey];
10035
- if (!indexInfo) {
10036
- throw new Error(`Index info not found for tree: ${this.treeKey}. Initialization should be handled outside.`);
10288
+ let headPk = this.cachedHeadPk;
10289
+ if (headPk === null) {
10290
+ const metadata = await this.api.getDocumentInnerMetadata(tx);
10291
+ const indexInfo = metadata.indices[this.treeKey];
10292
+ if (!indexInfo) {
10293
+ throw new Error(`Index info not found for tree: ${this.treeKey}. Initialization should be handled outside.`);
10294
+ }
10295
+ headPk = indexInfo[0];
10037
10296
  }
10038
- const headPk = indexInfo[0];
10039
10297
  const json = JSON.stringify(head);
10040
10298
  await this.api.update(headPk, json, tx);
10041
10299
  }
@@ -10071,9 +10329,24 @@ function compareValue(a, b) {
10071
10329
  }
10072
10330
  return aList.length - bList.length;
10073
10331
  }
10332
+ function comparePrimaryValue(a, b) {
10333
+ const aArr = Array.isArray(a);
10334
+ const bArr = Array.isArray(b);
10335
+ if (!aArr && !bArr) {
10336
+ return comparePrimitive(a, b);
10337
+ }
10338
+ const aList = aArr ? a : [a];
10339
+ const bList = bArr ? b : [b];
10340
+ const len = Math.min(aList.length, bList.length);
10341
+ for (let i = 0; i < len; i++) {
10342
+ const diff = comparePrimitive(aList[i], bList[i]);
10343
+ if (diff !== 0) return diff;
10344
+ }
10345
+ return 0;
10346
+ }
10074
10347
  var DocumentValueComparator = class extends import_dataply2.ValueComparator {
10075
10348
  primaryAsc(a, b) {
10076
- return compareValue(a.v, b.v);
10349
+ return comparePrimaryValue(a.v, b.v);
10077
10350
  }
10078
10351
  asc(a, b) {
10079
10352
  const diff = compareValue(a.v, b.v);
@@ -10194,7 +10467,6 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10194
10467
  trees = /* @__PURE__ */ new Map();
10195
10468
  comparator = new DocumentValueComparator();
10196
10469
  pendingBackfillFields = [];
10197
- lock;
10198
10470
  _initialized = false;
10199
10471
  indexedFields;
10200
10472
  /**
@@ -10225,7 +10497,6 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10225
10497
  constructor(file, options) {
10226
10498
  super(file, options);
10227
10499
  this.trees = /* @__PURE__ */ new Map();
10228
- this.lock = new import_dataply3.Ryoiki();
10229
10500
  this.indexedFields = /* @__PURE__ */ new Set(["_id"]);
10230
10501
  this.hook.onceAfter("init", async (tx, isNewlyCreated) => {
10231
10502
  if (isNewlyCreated) {
@@ -10235,8 +10506,12 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10235
10506
  throw new Error("Document metadata verification failed");
10236
10507
  }
10237
10508
  const metadata = await this.getDocumentInnerMetadata(tx);
10238
- const targetIndices = /* @__PURE__ */ new Map();
10239
- targetIndices.set("_id", { type: "btree", fields: ["_id"] });
10509
+ const targetIndices = /* @__PURE__ */ new Map([
10510
+ ["_id", { type: "btree", fields: ["_id"] }]
10511
+ ]);
10512
+ for (const [name, info] of Object.entries(metadata.indices)) {
10513
+ targetIndices.set(name, info[1]);
10514
+ }
10240
10515
  for (const [name, option] of this.pendingCreateIndices) {
10241
10516
  const config = this.toIndexMetaConfig(option);
10242
10517
  targetIndices.set(name, config);
@@ -10326,7 +10601,7 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10326
10601
  const existing = this.registeredIndices.get(name);
10327
10602
  if (JSON.stringify(existing) === JSON.stringify(config)) return;
10328
10603
  }
10329
- await this.runWithDefault(async (tx2) => {
10604
+ await this.runWithDefaultWrite(async (tx2) => {
10330
10605
  const metadata = await this.getDocumentInnerMetadata(tx2);
10331
10606
  metadata.indices[name] = [-1, config];
10332
10607
  await this.updateDocumentInnerMetadata(metadata, tx2);
@@ -10374,7 +10649,7 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10374
10649
  if (!this.registeredIndices.has(name)) {
10375
10650
  throw new Error(`Index '${name}' does not exist`);
10376
10651
  }
10377
- await this.runWithDefault(async (tx2) => {
10652
+ await this.runWithDefaultWrite(async (tx2) => {
10378
10653
  const config = this.registeredIndices.get(name);
10379
10654
  const metadata = await this.getDocumentInnerMetadata(tx2);
10380
10655
  delete metadata.indices[name];
@@ -10403,19 +10678,39 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10403
10678
  * Convert CreateIndexOption to IndexMetaConfig for metadata storage.
10404
10679
  */
10405
10680
  toIndexMetaConfig(option) {
10681
+ if (!option || typeof option !== "object") {
10682
+ throw new Error("Index option must be a non-null object");
10683
+ }
10684
+ if (!option.type) {
10685
+ throw new Error('Index option must have a "type" property ("btree" or "fts")');
10686
+ }
10406
10687
  if (option.type === "btree") {
10688
+ if (!Array.isArray(option.fields) || option.fields.length === 0) {
10689
+ throw new Error('btree index requires a non-empty "fields" array');
10690
+ }
10691
+ for (let i = 0; i < option.fields.length; i++) {
10692
+ if (typeof option.fields[i] !== "string" || option.fields[i].length === 0) {
10693
+ throw new Error(`btree index "fields[${i}]" must be a non-empty string, got: ${JSON.stringify(option.fields[i])}`);
10694
+ }
10695
+ }
10407
10696
  return {
10408
10697
  type: "btree",
10409
10698
  fields: option.fields
10410
10699
  };
10411
10700
  }
10412
10701
  if (option.type === "fts") {
10702
+ if (typeof option.fields !== "string" || option.fields.length === 0) {
10703
+ throw new Error(`fts index requires a non-empty string "fields", got: ${JSON.stringify(option.fields)}`);
10704
+ }
10413
10705
  if (option.tokenizer === "ngram") {
10706
+ if (typeof option.gramSize !== "number" || option.gramSize < 1) {
10707
+ throw new Error(`fts ngram index requires a positive "gramSize" number, got: ${JSON.stringify(option.gramSize)}`);
10708
+ }
10414
10709
  return {
10415
10710
  type: "fts",
10416
10711
  fields: option.fields,
10417
10712
  tokenizer: "ngram",
10418
- gramSize: option.ngram
10713
+ gramSize: option.gramSize
10419
10714
  };
10420
10715
  }
10421
10716
  return {
@@ -10489,24 +10784,6 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10489
10784
  return JSON.parse(row);
10490
10785
  }, tx);
10491
10786
  }
10492
- async readLock(fn) {
10493
- let lockId;
10494
- return this.lock.readLock(async (_lockId) => {
10495
- lockId = _lockId;
10496
- return await fn();
10497
- }).finally(() => {
10498
- this.lock.readUnlock(lockId);
10499
- });
10500
- }
10501
- async writeLock(fn) {
10502
- let lockId;
10503
- return this.lock.writeLock(async (_lockId) => {
10504
- lockId = _lockId;
10505
- return await fn();
10506
- }).finally(() => {
10507
- this.lock.writeUnlock(lockId);
10508
- });
10509
- }
10510
10787
  /**
10511
10788
  * Backfill indices for newly created indices after data was inserted.
10512
10789
  * This method should be called after `init()`.
@@ -10514,7 +10791,7 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10514
10791
  * @returns Number of documents that were backfilled
10515
10792
  */
10516
10793
  async backfillIndices(tx) {
10517
- return this.runWithDefault(async (tx2) => {
10794
+ return this.runWithDefaultWrite(async (tx2) => {
10518
10795
  if (this.pendingBackfillFields.length === 0) {
10519
10796
  return 0;
10520
10797
  }
@@ -10523,8 +10800,7 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10523
10800
  if (metadata.lastId === 0) {
10524
10801
  return 0;
10525
10802
  }
10526
- const indexTxMap = {};
10527
- const indexEntryMap = /* @__PURE__ */ new Map();
10803
+ let indexTxMap = {};
10528
10804
  for (const indexName of backfillTargets) {
10529
10805
  const tree = this.trees.get(indexName);
10530
10806
  if (tree && indexName !== "_id") {
@@ -10532,6 +10808,8 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10532
10808
  }
10533
10809
  }
10534
10810
  let backfilledCount = 0;
10811
+ let chunkCount = 0;
10812
+ const CHUNK_SIZE = 1e3;
10535
10813
  const idTree = this.trees.get("_id");
10536
10814
  if (!idTree) {
10537
10815
  throw new Error("ID tree not found");
@@ -10560,10 +10838,6 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10560
10838
  const keyToInsert = this.getTokenKey(k, token);
10561
10839
  const entry = { k, v: token };
10562
10840
  batchInsertData.push([keyToInsert, entry]);
10563
- if (!indexEntryMap.has(btx)) {
10564
- indexEntryMap.set(btx, []);
10565
- }
10566
- indexEntryMap.get(btx).push({ k: keyToInsert, v: entry });
10567
10841
  }
10568
10842
  await btx.batchInsert(batchInsertData);
10569
10843
  } else {
@@ -10571,34 +10845,42 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10571
10845
  if (indexVal === void 0) continue;
10572
10846
  const entry = { k, v: indexVal };
10573
10847
  const batchInsertData = [[k, entry]];
10574
- if (!indexEntryMap.has(btx)) {
10575
- indexEntryMap.set(btx, []);
10576
- }
10577
- indexEntryMap.get(btx).push(entry);
10578
10848
  await btx.batchInsert(batchInsertData);
10579
10849
  }
10580
10850
  }
10581
10851
  backfilledCount++;
10582
- }
10583
- const btxs = Object.values(indexTxMap);
10584
- const success = [];
10585
- try {
10586
- for (const btx of btxs) {
10587
- await btx.commit();
10588
- success.push(btx);
10589
- }
10590
- } catch (err) {
10591
- for (const btx of btxs) {
10592
- await btx.rollback();
10852
+ chunkCount++;
10853
+ if (chunkCount >= CHUNK_SIZE) {
10854
+ try {
10855
+ for (const btx of Object.values(indexTxMap)) {
10856
+ await btx.commit();
10857
+ }
10858
+ } catch (err) {
10859
+ for (const btx of Object.values(indexTxMap)) {
10860
+ await btx.rollback();
10861
+ }
10862
+ throw err;
10863
+ }
10864
+ for (const indexName of backfillTargets) {
10865
+ const tree = this.trees.get(indexName);
10866
+ if (tree && indexName !== "_id") {
10867
+ indexTxMap[indexName] = await tree.createTransaction();
10868
+ }
10869
+ }
10870
+ chunkCount = 0;
10593
10871
  }
10594
- for (const btx of success) {
10595
- const entries = indexEntryMap.get(btx);
10596
- if (!entries) continue;
10597
- for (const entry of entries) {
10598
- await btx.delete(entry.k, entry);
10872
+ }
10873
+ if (chunkCount > 0) {
10874
+ try {
10875
+ for (const btx of Object.values(indexTxMap)) {
10876
+ await btx.commit();
10877
+ }
10878
+ } catch (err) {
10879
+ for (const btx of Object.values(indexTxMap)) {
10880
+ await btx.rollback();
10599
10881
  }
10882
+ throw err;
10600
10883
  }
10601
- throw err;
10602
10884
  }
10603
10885
  this.pendingBackfillFields = [];
10604
10886
  return backfilledCount;
@@ -10687,14 +10969,15 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10687
10969
  * @param tx Optional transaction
10688
10970
  */
10689
10971
  async migration(version, callback, tx) {
10690
- await this.runWithDefault(async (tx2) => {
10972
+ await this.runWithDefaultWrite(async (tx2) => {
10691
10973
  const innerMetadata = await this.getDocumentInnerMetadata(tx2);
10692
10974
  const currentVersion = innerMetadata.schemeVersion ?? 0;
10693
10975
  if (currentVersion < version) {
10694
10976
  await callback(tx2);
10695
- innerMetadata.schemeVersion = version;
10696
- innerMetadata.updatedAt = Date.now();
10697
- await this.updateDocumentInnerMetadata(innerMetadata, tx2);
10977
+ const freshMetadata = await this.getDocumentInnerMetadata(tx2);
10978
+ freshMetadata.schemeVersion = version;
10979
+ freshMetadata.updatedAt = Date.now();
10980
+ await this.updateDocumentInnerMetadata(freshMetadata, tx2);
10698
10981
  }
10699
10982
  }, tx);
10700
10983
  }
@@ -10755,27 +11038,61 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10755
11038
  const condition = query[primaryField];
10756
11039
  const treeTx = await tree.createTransaction();
10757
11040
  let score = 0;
10758
- const coveredFields = config.fields.filter((f) => queryFields.has(f));
10759
- score += coveredFields.length;
10760
- if (condition) {
10761
- if (typeof condition !== "object" || condition === null) {
10762
- score += 100;
10763
- } else if ("primaryEqual" in condition || "equal" in condition) {
10764
- score += 100;
10765
- } 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) {
10766
- score += 50;
10767
- } else if ("primaryOr" in condition || "or" in condition) {
10768
- score += 20;
10769
- } else if ("like" in condition) {
10770
- score += 15;
10771
- } else {
10772
- score += 10;
11041
+ let isConsecutive = true;
11042
+ const coveredFields = [];
11043
+ const compositeVerifyFields = [];
11044
+ for (const field of config.fields) {
11045
+ if (!queryFields.has(field)) {
11046
+ isConsecutive = false;
11047
+ continue;
11048
+ }
11049
+ coveredFields.push(field);
11050
+ if (field !== primaryField) {
11051
+ compositeVerifyFields.push(field);
11052
+ }
11053
+ score += 1;
11054
+ if (isConsecutive) {
11055
+ const cond = query[field];
11056
+ if (cond !== void 0) {
11057
+ if (typeof cond !== "object" || cond === null) {
11058
+ score += 100;
11059
+ } else if ("primaryEqual" in cond || "equal" in cond) {
11060
+ score += 100;
11061
+ } 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) {
11062
+ score += 50;
11063
+ isConsecutive = false;
11064
+ } else if ("primaryOr" in cond || "or" in cond) {
11065
+ score += 20;
11066
+ isConsecutive = false;
11067
+ } else if ("like" in cond) {
11068
+ score += 15;
11069
+ isConsecutive = false;
11070
+ } else {
11071
+ score += 10;
11072
+ isConsecutive = false;
11073
+ }
11074
+ }
10773
11075
  }
10774
11076
  }
10775
- if (orderByField && primaryField === orderByField) {
10776
- score += 200;
11077
+ let isIndexOrderSupported = false;
11078
+ if (orderByField) {
11079
+ for (const field of config.fields) {
11080
+ if (field === orderByField) {
11081
+ isIndexOrderSupported = true;
11082
+ break;
11083
+ }
11084
+ const cond = query[field];
11085
+ let isExactMatch = false;
11086
+ if (cond !== void 0) {
11087
+ if (typeof cond !== "object" || cond === null) isExactMatch = true;
11088
+ else if ("primaryEqual" in cond || "equal" in cond) isExactMatch = true;
11089
+ }
11090
+ if (!isExactMatch) break;
11091
+ }
11092
+ if (isIndexOrderSupported) {
11093
+ score += 200;
11094
+ }
10777
11095
  }
10778
- const compositeVerifyFields = coveredFields.filter((f) => f !== primaryField);
10779
11096
  candidates.push({
10780
11097
  tree: treeTx,
10781
11098
  condition,
@@ -10783,7 +11100,8 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10783
11100
  indexName,
10784
11101
  isFtsMatch: false,
10785
11102
  score,
10786
- compositeVerifyFields
11103
+ compositeVerifyFields,
11104
+ isIndexOrderSupported
10787
11105
  });
10788
11106
  } else if (config.type === "fts") {
10789
11107
  const field = config.fields;
@@ -10801,7 +11119,8 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10801
11119
  isFtsMatch: true,
10802
11120
  matchTokens,
10803
11121
  score: 90,
10804
- compositeVerifyFields: []
11122
+ compositeVerifyFields: [],
11123
+ isIndexOrderSupported: false
10805
11124
  });
10806
11125
  }
10807
11126
  }
@@ -10844,33 +11163,30 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10844
11163
  getTokenKey(pk, token) {
10845
11164
  return pk + ":" + token;
10846
11165
  }
10847
- async applyCandidateByFTS(candidate, matchedTokens, filterValues, order) {
11166
+ async *applyCandidateByFTSStream(candidate, matchedTokens, filterValues, order) {
10848
11167
  const keys = /* @__PURE__ */ new Set();
10849
11168
  for (let i = 0, len = matchedTokens.length; i < len; i++) {
10850
11169
  const token = matchedTokens[i];
10851
- const pairs = await candidate.tree.where(
11170
+ for await (const pair of candidate.tree.whereStream(
10852
11171
  { primaryEqual: { v: token } },
10853
- {
10854
- order
11172
+ { order }
11173
+ )) {
11174
+ const pk = pair[1].k;
11175
+ if (filterValues && !filterValues.has(pk)) continue;
11176
+ if (!keys.has(pk)) {
11177
+ keys.add(pk);
11178
+ yield pk;
10855
11179
  }
10856
- );
10857
- for (const pair of pairs.values()) {
10858
- if (filterValues && !filterValues.has(pair.k)) continue;
10859
- keys.add(pair.k);
10860
11180
  }
10861
11181
  }
10862
- return keys;
10863
11182
  }
10864
11183
  /**
10865
11184
  * 특정 인덱스 후보를 조회하여 PK 집합을 필터링합니다.
10866
11185
  */
10867
- async applyCandidate(candidate, filterValues, order) {
10868
- return await candidate.tree.keys(
11186
+ applyCandidateStream(candidate, filterValues, order) {
11187
+ return candidate.tree.keysStream(
10869
11188
  candidate.condition,
10870
- {
10871
- filterValues,
10872
- order
10873
- }
11189
+ { filterValues, order }
10874
11190
  );
10875
11191
  }
10876
11192
  /**
@@ -10886,30 +11202,34 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10886
11202
  );
10887
11203
  if (!selectivity) return new Float64Array(0);
10888
11204
  const { driver, others, rollback } = selectivity;
10889
- const useIndexOrder = orderBy === void 0 || driver.field === orderBy;
11205
+ const useIndexOrder = orderBy === void 0 || driver.isIndexOrderSupported;
10890
11206
  const candidates = [driver, ...others];
10891
11207
  let keys = void 0;
10892
11208
  for (let i = 0, len = candidates.length; i < len; i++) {
10893
11209
  const candidate = candidates[i];
10894
11210
  const currentOrder = useIndexOrder ? sortOrder : void 0;
10895
11211
  if (candidate.isFtsMatch && candidate.matchTokens && candidate.matchTokens.length > 0) {
10896
- keys = await this.applyCandidateByFTS(
11212
+ const stream = this.applyCandidateByFTSStream(
10897
11213
  candidate,
10898
11214
  candidate.matchTokens,
10899
11215
  keys,
10900
11216
  currentOrder
10901
11217
  );
11218
+ keys = /* @__PURE__ */ new Set();
11219
+ for await (const pk of stream) keys.add(pk);
10902
11220
  } else {
10903
- keys = await this.applyCandidate(candidate, keys, currentOrder);
11221
+ const stream = this.applyCandidateStream(candidate, keys, currentOrder);
11222
+ keys = /* @__PURE__ */ new Set();
11223
+ for await (const pk of stream) keys.add(pk);
10904
11224
  }
10905
11225
  }
10906
11226
  rollback();
10907
11227
  return new Float64Array(Array.from(keys || []));
10908
11228
  }
10909
11229
  /**
10910
- * 드라이버 인덱스만으로 PK 가져옵니다. (교집합 없이)
11230
+ * 드라이버 인덱스만으로 PK 스트림을 가져옵니다. (교집합 없이)
10911
11231
  * selectDocuments에서 사용하며, 나머지 조건(others)은 스트리밍 중 tree.verify()로 검증합니다.
10912
- * @returns 드라이버 키 배열, others 후보 목록, rollback 함수. 또는 null.
11232
+ * @returns 드라이버 키 스트림, others 후보 목록, rollback 함수. 또는 null.
10913
11233
  */
10914
11234
  async getDriverKeys(query, orderBy, sortOrder = "asc") {
10915
11235
  const isQueryEmpty = Object.keys(query).length === 0;
@@ -10920,21 +11240,21 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10920
11240
  );
10921
11241
  if (!selectivity) return null;
10922
11242
  const { driver, others, compositeVerifyConditions, rollback } = selectivity;
10923
- const useIndexOrder = orderBy === void 0 || driver.field === orderBy;
11243
+ const useIndexOrder = orderBy === void 0 || driver.isIndexOrderSupported;
10924
11244
  const currentOrder = useIndexOrder ? sortOrder : void 0;
10925
- let keys;
11245
+ let keysStream;
10926
11246
  if (driver.isFtsMatch && driver.matchTokens && driver.matchTokens.length > 0) {
10927
- keys = await this.applyCandidateByFTS(
11247
+ keysStream = this.applyCandidateByFTSStream(
10928
11248
  driver,
10929
11249
  driver.matchTokens,
10930
11250
  void 0,
10931
11251
  currentOrder
10932
11252
  );
10933
11253
  } else {
10934
- keys = await this.applyCandidate(driver, void 0, currentOrder);
11254
+ keysStream = this.applyCandidateStream(driver, void 0, currentOrder);
10935
11255
  }
10936
11256
  return {
10937
- keys: new Float64Array(Array.from(keys)),
11257
+ keysStream,
10938
11258
  others,
10939
11259
  compositeVerifyConditions,
10940
11260
  isDriverOrderByField: useIndexOrder,
@@ -10962,7 +11282,7 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10962
11282
  * @returns The primary key of the inserted document
10963
11283
  */
10964
11284
  async insertSingleDocument(document, tx) {
10965
- return this.writeLock(() => this.runWithDefault(async (tx2) => {
11285
+ return this.runWithDefaultWrite(async (tx2) => {
10966
11286
  const { pk: dpk, document: dataplyDocument } = await this.insertDocumentInternal(document, tx2);
10967
11287
  const flattenDocument = this.flattenDocument(dataplyDocument);
10968
11288
  for (const [indexName, config] of this.registeredIndices) {
@@ -10988,7 +11308,7 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10988
11308
  }
10989
11309
  }
10990
11310
  return dataplyDocument._id;
10991
- }, tx));
11311
+ }, tx);
10992
11312
  }
10993
11313
  /**
10994
11314
  * Insert a batch of documents into the database
@@ -10997,7 +11317,7 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10997
11317
  * @returns The primary keys of the inserted documents
10998
11318
  */
10999
11319
  async insertBatchDocuments(documents, tx) {
11000
- return this.writeLock(() => this.runWithDefault(async (tx2) => {
11320
+ return this.runWithDefaultWrite(async (tx2) => {
11001
11321
  const metadata = await this.getDocumentInnerMetadata(tx2);
11002
11322
  const startId = metadata.lastId + 1;
11003
11323
  metadata.lastId += documents.length;
@@ -11056,7 +11376,7 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
11056
11376
  }
11057
11377
  }
11058
11378
  return ids;
11059
- }, tx));
11379
+ }, tx);
11060
11380
  }
11061
11381
  /**
11062
11382
  * Internal update method used by both fullUpdate and partialUpdate
@@ -11137,12 +11457,12 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
11137
11457
  * @returns The number of updated documents
11138
11458
  */
11139
11459
  async fullUpdate(query, newRecord, tx) {
11140
- return await this.writeLock(() => this.runWithDefault(async (tx2) => {
11460
+ return this.runWithDefaultWrite(async (tx2) => {
11141
11461
  return this.updateInternal(query, (doc) => {
11142
11462
  const newDoc = typeof newRecord === "function" ? newRecord(doc) : newRecord;
11143
11463
  return { _id: doc._id, ...newDoc };
11144
11464
  }, tx2);
11145
- }, tx));
11465
+ }, tx);
11146
11466
  }
11147
11467
  /**
11148
11468
  * Partially update documents from the database that match the query
@@ -11152,14 +11472,14 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
11152
11472
  * @returns The number of updated documents
11153
11473
  */
11154
11474
  async partialUpdate(query, newRecord, tx) {
11155
- return this.writeLock(() => this.runWithDefault(async (tx2) => {
11475
+ return this.runWithDefaultWrite(async (tx2) => {
11156
11476
  return this.updateInternal(query, (doc) => {
11157
11477
  const partialUpdateContent = typeof newRecord === "function" ? newRecord(doc) : newRecord;
11158
11478
  const finalUpdate = { ...partialUpdateContent };
11159
11479
  delete finalUpdate._id;
11160
11480
  return { ...doc, ...finalUpdate };
11161
11481
  }, tx2);
11162
- }, tx));
11482
+ }, tx);
11163
11483
  }
11164
11484
  /**
11165
11485
  * Delete documents from the database that match the query
@@ -11168,7 +11488,7 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
11168
11488
  * @returns The number of deleted documents
11169
11489
  */
11170
11490
  async deleteDocuments(query, tx) {
11171
- return this.writeLock(() => this.runWithDefault(async (tx2) => {
11491
+ return this.runWithDefaultWrite(async (tx2) => {
11172
11492
  const pks = await this.getKeys(query);
11173
11493
  let deletedCount = 0;
11174
11494
  for (let i = 0, len = pks.length; i < len; i++) {
@@ -11198,7 +11518,7 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
11198
11518
  deletedCount++;
11199
11519
  }
11200
11520
  return deletedCount;
11201
- }, tx));
11521
+ }, tx);
11202
11522
  }
11203
11523
  /**
11204
11524
  * Count documents from the database that match the query
@@ -11207,10 +11527,10 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
11207
11527
  * @returns The number of documents that match the query
11208
11528
  */
11209
11529
  async countDocuments(query, tx) {
11210
- return this.readLock(() => this.runWithDefault(async (tx2) => {
11530
+ return this.runWithDefault(async (tx2) => {
11211
11531
  const pks = await this.getKeys(query);
11212
11532
  return pks.length;
11213
- }, tx));
11533
+ }, tx);
11214
11534
  }
11215
11535
  /**
11216
11536
  * FTS 조건에 대해 문서가 유효한지 검증합니다.
@@ -11284,28 +11604,17 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
11284
11604
  return currentChunkSize;
11285
11605
  }
11286
11606
  /**
11287
- * Prefetch 방식으로 키 배열을 청크 단위로 조회하여 문서를 순회합니다.
11607
+ * Prefetch 방식으로 키 스트림을 청크 단위로 조회하여 문서를 순회합니다.
11288
11608
  * FTS 검증, 복합 인덱스 검증, others 후보에 대한 tree.verify() 검증을 통과한 문서만 yield 합니다.
11289
11609
  */
11290
- async *processChunkedKeysWithVerify(keys, startIdx, initialChunkSize, ftsConditions, compositeVerifyConditions, others, tx) {
11610
+ async *processChunkedKeysWithVerify(keysStream, startIdx, initialChunkSize, ftsConditions, compositeVerifyConditions, others, tx) {
11291
11611
  const verifyOthers = others.filter((o) => !o.isFtsMatch);
11292
- let i = startIdx;
11293
- const totalKeys = keys.length;
11294
11612
  let currentChunkSize = initialChunkSize;
11295
- let nextChunkPromise = null;
11296
- if (i < totalKeys) {
11297
- const endIdx = Math.min(i + currentChunkSize, totalKeys);
11298
- nextChunkPromise = this.selectMany(keys.subarray(i, endIdx), false, tx);
11299
- i = endIdx;
11300
- }
11301
- while (nextChunkPromise) {
11302
- const rawResults = await nextChunkPromise;
11303
- nextChunkPromise = null;
11304
- if (i < totalKeys) {
11305
- const endIdx = Math.min(i + currentChunkSize, totalKeys);
11306
- nextChunkPromise = this.selectMany(keys.subarray(i, endIdx), false, tx);
11307
- i = endIdx;
11308
- }
11613
+ let chunk = [];
11614
+ let dropped = 0;
11615
+ const processChunk = async (pks) => {
11616
+ const docs = [];
11617
+ const rawResults = await this.selectMany(new Float64Array(pks), false, tx);
11309
11618
  let chunkTotalSize = 0;
11310
11619
  for (let j = 0, len = rawResults.length; j < len; j++) {
11311
11620
  const s = rawResults[j];
@@ -11332,9 +11641,26 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
11332
11641
  }
11333
11642
  if (!passed) continue;
11334
11643
  }
11335
- yield doc;
11644
+ docs.push(doc);
11336
11645
  }
11337
11646
  currentChunkSize = this.adjustChunkSize(currentChunkSize, chunkTotalSize);
11647
+ return docs;
11648
+ };
11649
+ for await (const pk of keysStream) {
11650
+ if (dropped < startIdx) {
11651
+ dropped++;
11652
+ continue;
11653
+ }
11654
+ chunk.push(pk);
11655
+ if (chunk.length >= currentChunkSize) {
11656
+ const docs = await processChunk(chunk);
11657
+ for (let j = 0; j < docs.length; j++) yield docs[j];
11658
+ chunk = [];
11659
+ }
11660
+ }
11661
+ if (chunk.length > 0) {
11662
+ const docs = await processChunk(chunk);
11663
+ for (let j = 0; j < docs.length; j++) yield docs[j];
11338
11664
  }
11339
11665
  }
11340
11666
  /**
@@ -11382,11 +11708,7 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
11382
11708
  }
11383
11709
  const driverResult = await self.getDriverKeys(query, orderByField, sortOrder);
11384
11710
  if (!driverResult) return;
11385
- const { keys, others, compositeVerifyConditions, isDriverOrderByField, rollback } = driverResult;
11386
- if (keys.length === 0) {
11387
- rollback();
11388
- return;
11389
- }
11711
+ const { keysStream, others, compositeVerifyConditions, isDriverOrderByField, rollback } = driverResult;
11390
11712
  try {
11391
11713
  if (!isDriverOrderByField && orderByField) {
11392
11714
  const topK = limit === Infinity ? Infinity : offset + limit;
@@ -11401,7 +11723,7 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
11401
11723
  }
11402
11724
  const results = [];
11403
11725
  for await (const doc of self.processChunkedKeysWithVerify(
11404
- keys,
11726
+ keysStream,
11405
11727
  0,
11406
11728
  self.options.pageSize,
11407
11729
  ftsConditions,
@@ -11437,16 +11759,23 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
11437
11759
  yield limitedResults[j];
11438
11760
  }
11439
11761
  } else {
11762
+ const hasFilters = ftsConditions.length > 0 || compositeVerifyConditions.length > 0 || others.length > 0;
11763
+ const startIdx = hasFilters ? 0 : offset;
11440
11764
  let yieldedCount = 0;
11765
+ let skippedCount = hasFilters ? 0 : offset;
11441
11766
  for await (const doc of self.processChunkedKeysWithVerify(
11442
- keys,
11443
- offset,
11767
+ keysStream,
11768
+ startIdx,
11444
11769
  self.options.pageSize,
11445
11770
  ftsConditions,
11446
11771
  compositeVerifyConditions,
11447
11772
  others,
11448
11773
  tx2
11449
11774
  )) {
11775
+ if (skippedCount < offset) {
11776
+ skippedCount++;
11777
+ continue;
11778
+ }
11450
11779
  if (yieldedCount >= limit) break;
11451
11780
  yield doc;
11452
11781
  yieldedCount++;