document-dataply 0.0.6 → 0.0.7-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cjs/index.js CHANGED
@@ -1757,6 +1757,39 @@ var require_cjs = __commonJS({
1757
1757
  }
1758
1758
  return true;
1759
1759
  }
1760
+ /**
1761
+ * Inserts a key-value pair into an already-cloned leaf node in-place.
1762
+ * Unlike _insertAtLeaf, this does NOT clone or update the node via MVCC.
1763
+ * Used by batchInsert to batch multiple insertions with a single clone/update.
1764
+ * @returns true if the leaf was modified, false if the key already exists.
1765
+ */
1766
+ _insertValueIntoLeaf(leaf, key, value) {
1767
+ if (leaf.values.length) {
1768
+ for (let i = 0, len = leaf.values.length; i < len; i++) {
1769
+ const nValue = leaf.values[i];
1770
+ if (this.comparator.isSame(value, nValue)) {
1771
+ if (leaf.keys[i].includes(key)) {
1772
+ return false;
1773
+ }
1774
+ leaf.keys[i].push(key);
1775
+ return true;
1776
+ } else if (this.comparator.isLower(value, nValue)) {
1777
+ leaf.values.splice(i, 0, value);
1778
+ leaf.keys.splice(i, 0, [key]);
1779
+ return true;
1780
+ } else if (i + 1 === leaf.values.length) {
1781
+ leaf.values.push(value);
1782
+ leaf.keys.push([key]);
1783
+ return true;
1784
+ }
1785
+ }
1786
+ } else {
1787
+ leaf.values = [value];
1788
+ leaf.keys = [[key]];
1789
+ return true;
1790
+ }
1791
+ return false;
1792
+ }
1760
1793
  _cloneNode(node) {
1761
1794
  return JSON.parse(JSON.stringify(node));
1762
1795
  }
@@ -2213,17 +2246,24 @@ var require_cjs = __commonJS({
2213
2246
  }
2214
2247
  return void 0;
2215
2248
  }
2216
- *keysStream(condition, filterValues, limit, order = "asc") {
2217
- const stream = this.whereStream(condition, limit, order);
2249
+ *keysStream(condition, options) {
2250
+ const { filterValues, limit, order = "asc" } = options ?? {};
2251
+ const stream = this.whereStream(condition, options);
2218
2252
  const intersection = filterValues && filterValues.size > 0 ? filterValues : null;
2253
+ let count = 0;
2219
2254
  for (const [key] of stream) {
2220
2255
  if (intersection && !intersection.has(key)) {
2221
2256
  continue;
2222
2257
  }
2223
2258
  yield key;
2259
+ count++;
2260
+ if (limit !== void 0 && count >= limit) {
2261
+ break;
2262
+ }
2224
2263
  }
2225
2264
  }
2226
- *whereStream(condition, limit, order = "asc") {
2265
+ *whereStream(condition, options) {
2266
+ const { filterValues, limit, order = "asc" } = options ?? {};
2227
2267
  const driverKey = this.getDriverKey(condition);
2228
2268
  if (!driverKey) return;
2229
2269
  const value = condition[driverKey];
@@ -2246,8 +2286,12 @@ var require_cjs = __commonJS({
2246
2286
  earlyTerminate
2247
2287
  );
2248
2288
  let count = 0;
2289
+ const intersection = filterValues && filterValues.size > 0 ? filterValues : null;
2249
2290
  for (const pair of generator) {
2250
2291
  const [k, v] = pair;
2292
+ if (intersection && !intersection.has(k)) {
2293
+ continue;
2294
+ }
2251
2295
  let isMatch = true;
2252
2296
  for (const key in condition) {
2253
2297
  if (key === driverKey) continue;
@@ -2267,16 +2311,16 @@ var require_cjs = __commonJS({
2267
2311
  }
2268
2312
  }
2269
2313
  }
2270
- keys(condition, filterValues, order = "asc") {
2314
+ keys(condition, options) {
2271
2315
  const set = /* @__PURE__ */ new Set();
2272
- for (const key of this.keysStream(condition, filterValues, void 0, order)) {
2316
+ for (const key of this.keysStream(condition, options)) {
2273
2317
  set.add(key);
2274
2318
  }
2275
2319
  return set;
2276
2320
  }
2277
- where(condition, order = "asc") {
2321
+ where(condition, options) {
2278
2322
  const map = /* @__PURE__ */ new Map();
2279
- for (const [key, value] of this.whereStream(condition, void 0, order)) {
2323
+ for (const [key, value] of this.whereStream(condition, options)) {
2280
2324
  map.set(key, value);
2281
2325
  }
2282
2326
  return map;
@@ -2304,6 +2348,50 @@ var require_cjs = __commonJS({
2304
2348
  this._insertInParent(before, after.values[0], after);
2305
2349
  }
2306
2350
  }
2351
+ batchInsert(entries) {
2352
+ if (entries.length === 0) return;
2353
+ const sorted = [...entries].sort((a, b) => this.comparator.asc(a[1], b[1]));
2354
+ let currentLeaf = null;
2355
+ let modified = false;
2356
+ for (const [key, value] of sorted) {
2357
+ const targetLeaf = this.insertableNode(value);
2358
+ if (currentLeaf !== null && currentLeaf.id === targetLeaf.id) {
2359
+ } else {
2360
+ if (currentLeaf !== null && modified) {
2361
+ this._updateNode(currentLeaf);
2362
+ }
2363
+ currentLeaf = this._cloneNode(targetLeaf);
2364
+ modified = false;
2365
+ }
2366
+ const changed = this._insertValueIntoLeaf(currentLeaf, key, value);
2367
+ modified = modified || changed;
2368
+ if (currentLeaf.values.length === this.order) {
2369
+ this._updateNode(currentLeaf);
2370
+ let after = this._createNode(
2371
+ true,
2372
+ [],
2373
+ [],
2374
+ currentLeaf.parent,
2375
+ null,
2376
+ null
2377
+ );
2378
+ const mid = Math.ceil(this.order / 2) - 1;
2379
+ after = this._cloneNode(after);
2380
+ after.values = currentLeaf.values.slice(mid + 1);
2381
+ after.keys = currentLeaf.keys.slice(mid + 1);
2382
+ currentLeaf.values = currentLeaf.values.slice(0, mid + 1);
2383
+ currentLeaf.keys = currentLeaf.keys.slice(0, mid + 1);
2384
+ this._updateNode(currentLeaf);
2385
+ this._updateNode(after);
2386
+ this._insertInParent(currentLeaf, after.values[0], after);
2387
+ currentLeaf = null;
2388
+ modified = false;
2389
+ }
2390
+ }
2391
+ if (currentLeaf !== null && modified) {
2392
+ this._updateNode(currentLeaf);
2393
+ }
2394
+ }
2307
2395
  _deleteEntry(node, key) {
2308
2396
  if (!node.leaf) {
2309
2397
  let keyIndex = -1;
@@ -2655,6 +2743,14 @@ var require_cjs = __commonJS({
2655
2743
  throw new Error(`Transaction failed: ${result.error || "Commit failed due to conflict"}`);
2656
2744
  }
2657
2745
  }
2746
+ batchInsert(entries) {
2747
+ const tx = this.createTransaction();
2748
+ tx.batchInsert(entries);
2749
+ const result = tx.commit();
2750
+ if (!result.success) {
2751
+ throw new Error(`Transaction failed: ${result.error || "Commit failed due to conflict"}`);
2752
+ }
2753
+ }
2658
2754
  };
2659
2755
  var Ryoiki22 = class _Ryoiki2 {
2660
2756
  readings;
@@ -3280,17 +3376,24 @@ var require_cjs = __commonJS({
3280
3376
  }
3281
3377
  return void 0;
3282
3378
  }
3283
- async *keysStream(condition, filterValues, limit, order = "asc") {
3284
- const stream = this.whereStream(condition, limit, order);
3379
+ async *keysStream(condition, options) {
3380
+ const { filterValues, limit, order = "asc" } = options ?? {};
3381
+ const stream = this.whereStream(condition, options);
3285
3382
  const intersection = filterValues && filterValues.size > 0 ? filterValues : null;
3383
+ let count = 0;
3286
3384
  for await (const [key] of stream) {
3287
3385
  if (intersection && !intersection.has(key)) {
3288
3386
  continue;
3289
3387
  }
3290
3388
  yield key;
3389
+ count++;
3390
+ if (limit !== void 0 && count >= limit) {
3391
+ break;
3392
+ }
3291
3393
  }
3292
3394
  }
3293
- async *whereStream(condition, limit, order = "asc") {
3395
+ async *whereStream(condition, options) {
3396
+ const { filterValues, limit, order = "asc" } = options ?? {};
3294
3397
  const driverKey = this.getDriverKey(condition);
3295
3398
  if (!driverKey) return;
3296
3399
  const value = condition[driverKey];
@@ -3313,8 +3416,12 @@ var require_cjs = __commonJS({
3313
3416
  earlyTerminate
3314
3417
  );
3315
3418
  let count = 0;
3419
+ const intersection = filterValues && filterValues.size > 0 ? filterValues : null;
3316
3420
  for await (const pair of generator) {
3317
3421
  const [k, v] = pair;
3422
+ if (intersection && !intersection.has(k)) {
3423
+ continue;
3424
+ }
3318
3425
  let isMatch = true;
3319
3426
  for (const key in condition) {
3320
3427
  if (key === driverKey) continue;
@@ -3334,16 +3441,16 @@ var require_cjs = __commonJS({
3334
3441
  }
3335
3442
  }
3336
3443
  }
3337
- async keys(condition, filterValues, order = "asc") {
3444
+ async keys(condition, options) {
3338
3445
  const set = /* @__PURE__ */ new Set();
3339
- for await (const key of this.keysStream(condition, filterValues, void 0, order)) {
3446
+ for await (const key of this.keysStream(condition, options)) {
3340
3447
  set.add(key);
3341
3448
  }
3342
3449
  return set;
3343
3450
  }
3344
- async where(condition, order = "asc") {
3451
+ async where(condition, options) {
3345
3452
  const map = /* @__PURE__ */ new Map();
3346
- for await (const [key, value] of this.whereStream(condition, void 0, order)) {
3453
+ for await (const [key, value] of this.whereStream(condition, options)) {
3347
3454
  map.set(key, value);
3348
3455
  }
3349
3456
  return map;
@@ -3373,6 +3480,52 @@ var require_cjs = __commonJS({
3373
3480
  }
3374
3481
  });
3375
3482
  }
3483
+ async batchInsert(entries) {
3484
+ if (entries.length === 0) return;
3485
+ return this.writeLock(0, async () => {
3486
+ const sorted = [...entries].sort((a, b) => this.comparator.asc(a[1], b[1]));
3487
+ let currentLeaf = null;
3488
+ let modified = false;
3489
+ for (const [key, value] of sorted) {
3490
+ const targetLeaf = await this.insertableNode(value);
3491
+ if (currentLeaf !== null && currentLeaf.id === targetLeaf.id) {
3492
+ } else {
3493
+ if (currentLeaf !== null && modified) {
3494
+ await this._updateNode(currentLeaf);
3495
+ }
3496
+ currentLeaf = this._cloneNode(targetLeaf);
3497
+ modified = false;
3498
+ }
3499
+ const changed = this._insertValueIntoLeaf(currentLeaf, key, value);
3500
+ modified = modified || changed;
3501
+ if (currentLeaf.values.length === this.order) {
3502
+ await this._updateNode(currentLeaf);
3503
+ let after = await this._createNode(
3504
+ true,
3505
+ [],
3506
+ [],
3507
+ currentLeaf.parent,
3508
+ null,
3509
+ null
3510
+ );
3511
+ const mid = Math.ceil(this.order / 2) - 1;
3512
+ after = this._cloneNode(after);
3513
+ after.values = currentLeaf.values.slice(mid + 1);
3514
+ after.keys = currentLeaf.keys.slice(mid + 1);
3515
+ currentLeaf.values = currentLeaf.values.slice(0, mid + 1);
3516
+ currentLeaf.keys = currentLeaf.keys.slice(0, mid + 1);
3517
+ await this._updateNode(currentLeaf);
3518
+ await this._updateNode(after);
3519
+ await this._insertInParent(currentLeaf, after.values[0], after);
3520
+ currentLeaf = null;
3521
+ modified = false;
3522
+ }
3523
+ }
3524
+ if (currentLeaf !== null && modified) {
3525
+ await this._updateNode(currentLeaf);
3526
+ }
3527
+ });
3528
+ }
3376
3529
  async _deleteEntry(node, key) {
3377
3530
  if (!node.leaf) {
3378
3531
  let keyIndex = -1;
@@ -3730,6 +3883,16 @@ var require_cjs = __commonJS({
3730
3883
  }
3731
3884
  });
3732
3885
  }
3886
+ async batchInsert(entries) {
3887
+ return this.writeLock(1, async () => {
3888
+ const tx = await this.createTransaction();
3889
+ await tx.batchInsert(entries);
3890
+ const result = await tx.commit();
3891
+ if (!result.success) {
3892
+ throw new Error(`Transaction failed: ${result.error || "Commit failed due to conflict"}`);
3893
+ }
3894
+ });
3895
+ }
3733
3896
  };
3734
3897
  var SerializeStrategy = class {
3735
3898
  order;
@@ -8704,6 +8867,7 @@ var require_cjs = __commonJS({
8704
8867
  if (!this.factory.isDataPage(lastInsertDataPage)) {
8705
8868
  throw new Error(`Last insert page is not data page`);
8706
8869
  }
8870
+ const batchInsertData = [];
8707
8871
  for (const data of dataList) {
8708
8872
  const pk = ++lastPk;
8709
8873
  const willRowSize = this.getRequiredRowSize(data);
@@ -8748,9 +8912,10 @@ var require_cjs = __commonJS({
8748
8912
  await this.pfs.setPage(lastInsertDataPageId, lastInsertDataPage, tx);
8749
8913
  }
8750
8914
  }
8751
- await btx.insert(this.getRID(), pk);
8915
+ batchInsertData.push([this.getRID(), pk]);
8752
8916
  pks.push(pk);
8753
8917
  }
8918
+ await btx.batchInsert(batchInsertData);
8754
8919
  tx.__markBPTreeDirty();
8755
8920
  const freshMetadataPage = await this.pfs.getMetadata(tx);
8756
8921
  this.metadataPageManager.setLastInsertPageId(freshMetadataPage, lastInsertDataPageId);
@@ -9953,6 +10118,45 @@ var BinaryHeap = class {
9953
10118
  }
9954
10119
  };
9955
10120
 
10121
+ // src/utils/tokenizer.ts
10122
+ function whitespaceTokenize(text) {
10123
+ if (typeof text !== "string") return [];
10124
+ return Array.from(new Set(text.split(/\s+/).filter(Boolean)));
10125
+ }
10126
+ function ngramTokenize(text, gramSize) {
10127
+ if (typeof text !== "string") return [];
10128
+ const tokens = /* @__PURE__ */ new Set();
10129
+ const words = text.split(/\s+/).filter(Boolean);
10130
+ for (const word of words) {
10131
+ if (word.length < gramSize) {
10132
+ if (word.length > 0) tokens.add(word);
10133
+ continue;
10134
+ }
10135
+ for (let i = 0; i <= word.length - gramSize; i++) {
10136
+ tokens.add(word.slice(i, i + gramSize));
10137
+ }
10138
+ }
10139
+ return Array.from(tokens);
10140
+ }
10141
+ function tokenize(text, options) {
10142
+ if (options.tokenizer === "whitespace") {
10143
+ return whitespaceTokenize(text);
10144
+ }
10145
+ if (options.tokenizer === "ngram") {
10146
+ return ngramTokenize(text, options.gramSize);
10147
+ }
10148
+ return [];
10149
+ }
10150
+
10151
+ // src/utils/hash.ts
10152
+ function fastStringHash(str) {
10153
+ let hash = 0;
10154
+ for (let i = 0; i < str.length; i++) {
10155
+ hash = (hash << 5) - hash + str.charCodeAt(i) | 0;
10156
+ }
10157
+ return hash >>> 0;
10158
+ }
10159
+
9956
10160
  // src/core/documentAPI.ts
9957
10161
  var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
9958
10162
  indices = {};
@@ -10008,7 +10212,7 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10008
10212
  } else {
10009
10213
  const [_pk, isMetaBackfillEnabled] = existingIndex;
10010
10214
  if (isBackfillEnabled && !isMetaBackfillEnabled) {
10011
- metadata.indices[field][1] = true;
10215
+ metadata.indices[field][1] = isBackfillEnabled;
10012
10216
  isMetadataChanged = true;
10013
10217
  backfillTargets.push(field);
10014
10218
  } else if (!isBackfillEnabled && isMetaBackfillEnabled) {
@@ -10111,12 +10315,23 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10111
10315
  }
10112
10316
  const v = flatDoc[field];
10113
10317
  const btx = fieldTxMap[field];
10114
- const entry = { k, v };
10115
- await btx.insert(k, entry);
10116
- if (!fieldMap.has(btx)) {
10117
- fieldMap.set(btx, []);
10318
+ const indexConfig = metadata.indices[field]?.[1];
10319
+ const isFts = typeof indexConfig === "object" && indexConfig?.type === "fts" && typeof v === "string";
10320
+ let tokens = [v];
10321
+ if (isFts) {
10322
+ tokens = tokenize(v, indexConfig);
10323
+ }
10324
+ const batchInsertData = [];
10325
+ for (const token of tokens) {
10326
+ const keyToInsert = isFts ? this.getTokenKey(k, token) : k;
10327
+ const entry = { k, v: token };
10328
+ batchInsertData.push([keyToInsert, entry]);
10329
+ if (!fieldMap.has(btx)) {
10330
+ fieldMap.set(btx, []);
10331
+ }
10332
+ fieldMap.get(btx).push({ k: keyToInsert, v: entry });
10118
10333
  }
10119
- fieldMap.get(btx).push(entry);
10334
+ await btx.batchInsert(batchInsertData);
10120
10335
  }
10121
10336
  backfilledCount++;
10122
10337
  }
@@ -10210,6 +10425,11 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10210
10425
  async updateDocumentInnerMetadata(metadata, tx) {
10211
10426
  await this.update(1, JSON.stringify(metadata), tx);
10212
10427
  }
10428
+ /**
10429
+ * Transforms a query object into a verbose query object
10430
+ * @param query The query object to transform
10431
+ * @returns The verbose query object
10432
+ */
10213
10433
  verboseQuery(query) {
10214
10434
  const result = {};
10215
10435
  for (const field in query) {
@@ -10223,7 +10443,12 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10223
10443
  const before = operator;
10224
10444
  const after = this.operatorConverters[before];
10225
10445
  const v = conditions[before];
10226
- if (!after) continue;
10446
+ if (!after) {
10447
+ if (before === "match") {
10448
+ newConditions[before] = v;
10449
+ }
10450
+ continue;
10451
+ }
10227
10452
  if (before === "or" && Array.isArray(v)) {
10228
10453
  newConditions[after] = v.map((val) => ({ v: val }));
10229
10454
  } else if (before === "like") {
@@ -10245,12 +10470,26 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10245
10470
  */
10246
10471
  async getSelectivityCandidate(query, orderByField) {
10247
10472
  const candidates = [];
10473
+ const metadata = await this.getDocumentInnerMetadata(this.txContext.get());
10248
10474
  for (const field in query) {
10249
10475
  const tree = this.trees.get(field);
10250
10476
  if (!tree) continue;
10251
10477
  const condition = query[field];
10252
10478
  const treeTx = await tree.createTransaction();
10253
- candidates.push({ tree: treeTx, condition, field });
10479
+ const indexConfig = metadata.indices[field]?.[1];
10480
+ let isFtsMatch = false;
10481
+ let matchTokens;
10482
+ if (typeof indexConfig === "object" && indexConfig?.type === "fts" && condition.match) {
10483
+ isFtsMatch = true;
10484
+ matchTokens = tokenize(condition.match, indexConfig);
10485
+ }
10486
+ candidates.push({
10487
+ tree: treeTx,
10488
+ condition,
10489
+ field,
10490
+ isFtsMatch,
10491
+ matchTokens
10492
+ });
10254
10493
  }
10255
10494
  const rollback = () => {
10256
10495
  for (const { tree } of candidates) {
@@ -10293,36 +10532,72 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10293
10532
  const smallChunkSize = safeLimit * 0.3;
10294
10533
  return { verySmallChunkSize, smallChunkSize };
10295
10534
  }
10535
+ getTokenKey(pk, token) {
10536
+ return fastStringHash(pk + ":" + token);
10537
+ }
10538
+ async applyCandidateByFTS(candidate, matchedTokens, filterValues, order) {
10539
+ const keys = /* @__PURE__ */ new Set();
10540
+ for (const token of matchedTokens) {
10541
+ const pairs = await candidate.tree.where(
10542
+ { primaryEqual: { v: token } },
10543
+ {
10544
+ filterValues,
10545
+ order
10546
+ }
10547
+ );
10548
+ for (const c of pairs.values()) {
10549
+ if (!c || typeof c.k !== "number") continue;
10550
+ const dpk = c.k;
10551
+ if (filterValues === void 0 || filterValues.has(dpk)) {
10552
+ keys.add(dpk);
10553
+ }
10554
+ }
10555
+ }
10556
+ return keys;
10557
+ }
10558
+ /**
10559
+ * 특정 인덱스 후보를 조회하여 PK 집합을 필터링합니다.
10560
+ */
10561
+ async applyCandidate(candidate, filterValues, order) {
10562
+ return await candidate.tree.keys(
10563
+ candidate.condition,
10564
+ {
10565
+ filterValues,
10566
+ order
10567
+ }
10568
+ );
10569
+ }
10296
10570
  /**
10297
- * Get Primary Keys based on query and index selection.
10298
- * Internal common method to unify query optimization.
10571
+ * 쿼리와 인덱스 선택을 기반으로 기본 키(Primary Keys)를 가져옵니다.
10572
+ * 쿼리 최적화를 통합하기 위한 내부 공통 메서드입니다.
10299
10573
  */
10300
10574
  async getKeys(query, orderBy, sortOrder = "asc") {
10301
10575
  const isQueryEmpty = Object.keys(query).length === 0;
10302
10576
  const normalizedQuery = isQueryEmpty ? { _id: { gte: 0 } } : query;
10303
- const verbose = this.verboseQuery(normalizedQuery);
10304
10577
  const selectivity = await this.getSelectivityCandidate(
10305
- verbose,
10578
+ this.verboseQuery(normalizedQuery),
10306
10579
  orderBy
10307
10580
  );
10308
10581
  if (!selectivity) return new Float64Array(0);
10309
10582
  const { driver, others, rollback } = selectivity;
10310
- const isDriverOrderByField = orderBy === void 0 || driver.field === orderBy;
10311
- if (isDriverOrderByField) {
10312
- let keysSet = await driver.tree.keys(driver.condition, void 0, sortOrder);
10313
- for (const { tree, condition } of others) {
10314
- keysSet = await tree.keys(condition, keysSet, sortOrder);
10315
- }
10316
- rollback();
10317
- return new Float64Array(keysSet);
10318
- } else {
10319
- let keysSet = await driver.tree.keys(driver.condition, void 0);
10320
- for (const { tree, condition } of others) {
10321
- keysSet = await tree.keys(condition, keysSet);
10583
+ const useIndexOrder = orderBy === void 0 || driver.field === orderBy;
10584
+ const candidates = [driver, ...others];
10585
+ let keys = void 0;
10586
+ for (const candidate of candidates) {
10587
+ const currentOrder = useIndexOrder ? sortOrder : void 0;
10588
+ if (candidate.isFtsMatch && candidate.matchTokens && candidate.matchTokens.length > 0) {
10589
+ keys = await this.applyCandidateByFTS(
10590
+ candidate,
10591
+ candidate.matchTokens,
10592
+ keys,
10593
+ currentOrder
10594
+ );
10595
+ } else {
10596
+ keys = await this.applyCandidate(candidate, keys, currentOrder);
10322
10597
  }
10323
- rollback();
10324
- return new Float64Array(keysSet);
10325
10598
  }
10599
+ rollback();
10600
+ return new Float64Array(Array.from(keys || []));
10326
10601
  }
10327
10602
  async insertDocumentInternal(document, tx) {
10328
10603
  const metadata = await this.getDocumentInnerMetadata(tx);
@@ -10346,15 +10621,25 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10346
10621
  */
10347
10622
  async insertSingleDocument(document, tx) {
10348
10623
  return this.writeLock(() => this.runWithDefault(async (tx2) => {
10349
- const { pk, document: dataplyDocument } = await this.insertDocumentInternal(document, tx2);
10624
+ const { pk: dpk, document: dataplyDocument } = await this.insertDocumentInternal(document, tx2);
10625
+ const metadata = await this.getDocumentInnerMetadata(tx2);
10350
10626
  const flattenDocument = this.flattenDocument(dataplyDocument);
10351
10627
  for (const field in flattenDocument) {
10352
10628
  const tree = this.trees.get(field);
10353
10629
  if (!tree) continue;
10354
10630
  const v = flattenDocument[field];
10355
- const [error] = await catchPromise(tree.insert(pk, { k: pk, v }));
10356
- if (error) {
10357
- console.error(`BPTree indexing failed for field: ${field}`, error);
10631
+ const indexConfig = metadata.indices[field]?.[1];
10632
+ let tokens = [v];
10633
+ const isFts = typeof indexConfig === "object" && indexConfig?.type === "fts" && typeof v === "string";
10634
+ if (isFts) {
10635
+ tokens = tokenize(v, indexConfig);
10636
+ }
10637
+ for (const token of tokens) {
10638
+ const keyToInsert = isFts ? this.getTokenKey(dpk, token) : dpk;
10639
+ const [error] = await catchPromise(tree.insert(keyToInsert, { k: dpk, v: token }));
10640
+ if (error) {
10641
+ throw error;
10642
+ }
10358
10643
  }
10359
10644
  }
10360
10645
  return dataplyDocument._id;
@@ -10392,15 +10677,26 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10392
10677
  }
10393
10678
  for (const [field, tree] of this.trees) {
10394
10679
  const treeTx = await tree.createTransaction();
10680
+ const indexConfig = metadata.indices[field]?.[1];
10681
+ const batchInsertData = [];
10395
10682
  for (let i = 0, len = flattenedData.length; i < len; i++) {
10396
10683
  const item = flattenedData[i];
10397
10684
  const v = item.data[field];
10398
10685
  if (v === void 0) continue;
10399
- const [error] = await catchPromise(treeTx.insert(item.pk, { k: item.pk, v }));
10400
- if (error) {
10401
- console.error(`BPTree indexing failed for field: ${field}`, error);
10686
+ const isFts = typeof indexConfig === "object" && indexConfig?.type === "fts" && typeof v === "string";
10687
+ let tokens = [v];
10688
+ if (isFts) {
10689
+ tokens = tokenize(v, indexConfig);
10690
+ }
10691
+ for (const token of tokens) {
10692
+ const keyToInsert = isFts ? this.getTokenKey(item.pk, token) : item.pk;
10693
+ batchInsertData.push([keyToInsert, { k: item.pk, v: token }]);
10402
10694
  }
10403
10695
  }
10696
+ const [error] = await catchPromise(treeTx.batchInsert(batchInsertData));
10697
+ if (error) {
10698
+ throw error;
10699
+ }
10404
10700
  const res = await treeTx.commit();
10405
10701
  if (!res.success) {
10406
10702
  throw res.error;
@@ -10431,15 +10727,34 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10431
10727
  const updatedDoc = computeUpdatedDoc(doc);
10432
10728
  const oldFlatDoc = this.flattenDocument(doc);
10433
10729
  const newFlatDoc = this.flattenDocument(updatedDoc);
10730
+ const metadata = await this.getDocumentInnerMetadata(tx);
10434
10731
  for (const [field, treeTx] of treeTxs) {
10435
10732
  const oldV = oldFlatDoc[field];
10436
10733
  const newV = newFlatDoc[field];
10437
10734
  if (oldV === newV) continue;
10735
+ const indexConfig = metadata.indices[field]?.[1];
10736
+ const isFts = typeof indexConfig === "object" && indexConfig?.type === "fts";
10438
10737
  if (field in oldFlatDoc) {
10439
- await treeTx.delete(pk, { k: pk, v: oldV });
10738
+ let oldTokens = [oldV];
10739
+ if (isFts && typeof oldV === "string") {
10740
+ oldTokens = tokenize(oldV, indexConfig);
10741
+ }
10742
+ for (const oldToken of oldTokens) {
10743
+ const keyToDelete = isFts ? this.getTokenKey(pk, oldToken) : pk;
10744
+ await treeTx.delete(keyToDelete, { k: pk, v: oldToken });
10745
+ }
10440
10746
  }
10441
10747
  if (field in newFlatDoc) {
10442
- await treeTx.insert(pk, { k: pk, v: newV });
10748
+ let newTokens = [newV];
10749
+ if (isFts && typeof newV === "string") {
10750
+ newTokens = tokenize(newV, indexConfig);
10751
+ }
10752
+ const batchInsertData = [];
10753
+ for (const newToken of newTokens) {
10754
+ const keyToInsert = isFts ? this.getTokenKey(pk, newToken) : pk;
10755
+ batchInsertData.push([keyToInsert, { k: pk, v: newToken }]);
10756
+ }
10757
+ await treeTx.batchInsert(batchInsertData);
10443
10758
  }
10444
10759
  }
10445
10760
  await this.update(pk, JSON.stringify(updatedDoc), tx);
@@ -10503,10 +10818,20 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10503
10818
  const doc = await this.getDocument(pk, tx2);
10504
10819
  if (!doc) continue;
10505
10820
  const flatDoc = this.flattenDocument(doc);
10821
+ const metadata = await this.getDocumentInnerMetadata(tx2);
10506
10822
  for (const [field, tree] of this.trees) {
10507
10823
  const v = flatDoc[field];
10508
10824
  if (v === void 0) continue;
10509
- await tree.delete(pk, { k: pk, v });
10825
+ const indexConfig = metadata.indices[field]?.[1];
10826
+ const isFts = typeof indexConfig === "object" && indexConfig?.type === "fts" && typeof v === "string";
10827
+ let tokens = [v];
10828
+ if (isFts) {
10829
+ tokens = tokenize(v, indexConfig);
10830
+ }
10831
+ for (const token of tokens) {
10832
+ const keyToDelete = isFts ? this.getTokenKey(pk, token) : pk;
10833
+ await tree.delete(keyToDelete, { k: pk, v: token });
10834
+ }
10510
10835
  }
10511
10836
  await super.delete(pk, true, tx2);
10512
10837
  deletedCount++;
@@ -10526,6 +10851,63 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10526
10851
  return pks.length;
10527
10852
  }, tx));
10528
10853
  }
10854
+ /**
10855
+ * FTS 조건에 대해 문서가 유효한지 검증합니다.
10856
+ */
10857
+ verifyFts(doc, ftsConditions) {
10858
+ for (const { field, matchTokens } of ftsConditions) {
10859
+ const docValue = this.flattenDocument(doc)[field];
10860
+ if (typeof docValue !== "string") return false;
10861
+ for (const token of matchTokens) {
10862
+ if (!docValue.includes(token)) return false;
10863
+ }
10864
+ }
10865
+ return true;
10866
+ }
10867
+ /**
10868
+ * 메모리 기반으로 청크 크기를 동적 조절합니다.
10869
+ */
10870
+ adjustChunkSize(currentChunkSize, chunkTotalSize) {
10871
+ if (chunkTotalSize <= 0) return currentChunkSize;
10872
+ const { verySmallChunkSize, smallChunkSize } = this.getFreeMemoryChunkSize();
10873
+ if (chunkTotalSize < verySmallChunkSize) return currentChunkSize * 2;
10874
+ if (chunkTotalSize > smallChunkSize) return Math.max(Math.floor(currentChunkSize / 2), 20);
10875
+ return currentChunkSize;
10876
+ }
10877
+ /**
10878
+ * Prefetch 방식으로 키 배열을 청크 단위로 조회하여 문서를 순회합니다.
10879
+ * FTS 검증을 통과한 문서만 yield 합니다.
10880
+ */
10881
+ async *processChunkedKeys(keys, startIdx, initialChunkSize, ftsConditions, tx) {
10882
+ let i = startIdx;
10883
+ const totalKeys = keys.length;
10884
+ let currentChunkSize = initialChunkSize;
10885
+ let nextChunkPromise = null;
10886
+ if (i < totalKeys) {
10887
+ const endIdx = Math.min(i + currentChunkSize, totalKeys);
10888
+ nextChunkPromise = this.selectMany(keys.subarray(i, endIdx), false, tx);
10889
+ i = endIdx;
10890
+ }
10891
+ while (nextChunkPromise) {
10892
+ const rawResults = await nextChunkPromise;
10893
+ nextChunkPromise = null;
10894
+ if (i < totalKeys) {
10895
+ const endIdx = Math.min(i + currentChunkSize, totalKeys);
10896
+ nextChunkPromise = this.selectMany(keys.subarray(i, endIdx), false, tx);
10897
+ i = endIdx;
10898
+ }
10899
+ let chunkTotalSize = 0;
10900
+ for (let j = 0, len = rawResults.length; j < len; j++) {
10901
+ const s = rawResults[j];
10902
+ if (!s) continue;
10903
+ const doc = JSON.parse(s);
10904
+ chunkTotalSize += s.length * 2;
10905
+ if (!this.verifyFts(doc, ftsConditions)) continue;
10906
+ yield { doc, rawSize: s.length * 2 };
10907
+ }
10908
+ currentChunkSize = this.adjustChunkSize(currentChunkSize, chunkTotalSize);
10909
+ }
10910
+ }
10529
10911
  /**
10530
10912
  * Select documents from the database
10531
10913
  * @param query The query to use (only indexed fields + _id allowed)
@@ -10552,80 +10934,57 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10552
10934
  } = options;
10553
10935
  const self = this;
10554
10936
  const stream = this.streamWithDefault(async function* (tx2) {
10937
+ const metadata = await self.getDocumentInnerMetadata(tx2);
10938
+ const ftsConditions = [];
10939
+ for (const field in query) {
10940
+ const q = query[field];
10941
+ if (q && typeof q === "object" && "match" in q && typeof q.match === "string") {
10942
+ const indexConfig = metadata.indices[field]?.[1];
10943
+ if (typeof indexConfig === "object" && indexConfig?.type === "fts") {
10944
+ ftsConditions.push({ field, matchTokens: tokenize(q.match, indexConfig) });
10945
+ }
10946
+ }
10947
+ }
10555
10948
  const keys = await self.getKeys(query, orderByField, sortOrder);
10556
- const totalKeys = keys.length;
10557
- if (totalKeys === 0) return;
10949
+ if (keys.length === 0) return;
10558
10950
  const selectivity = await self.getSelectivityCandidate(
10559
10951
  self.verboseQuery(query),
10560
10952
  orderByField
10561
10953
  );
10562
10954
  const isDriverOrderByField = orderByField === void 0 || selectivity && selectivity.driver.field === orderByField;
10563
- if (selectivity) {
10564
- selectivity.rollback();
10565
- }
10566
- let currentChunkSize = self.options.pageSize;
10955
+ if (selectivity) selectivity.rollback();
10567
10956
  if (!isDriverOrderByField && orderByField) {
10568
10957
  const topK = limit === Infinity ? Infinity : offset + limit;
10569
10958
  let heap = null;
10570
10959
  if (topK !== Infinity) {
10571
- const heapComparator = (a, b) => {
10960
+ heap = new BinaryHeap((a, b) => {
10572
10961
  const aVal = a[orderByField] ?? a._id;
10573
10962
  const bVal = b[orderByField] ?? b._id;
10574
10963
  const cmp = aVal < bVal ? -1 : aVal > bVal ? 1 : 0;
10575
10964
  return sortOrder === "asc" ? -cmp : cmp;
10576
- };
10577
- heap = new BinaryHeap(heapComparator);
10965
+ });
10578
10966
  }
10579
10967
  const results = [];
10580
- let i = 0;
10581
- let nextChunkPromise = null;
10582
- if (i < totalKeys) {
10583
- const endIdx = Math.min(i + currentChunkSize, totalKeys);
10584
- const chunkKeys = keys.subarray(i, endIdx);
10585
- nextChunkPromise = self.selectMany(chunkKeys, false, tx2);
10586
- i = endIdx;
10587
- }
10588
- while (nextChunkPromise) {
10589
- const rawResults = await nextChunkPromise;
10590
- nextChunkPromise = null;
10591
- const { verySmallChunkSize, smallChunkSize } = self.getFreeMemoryChunkSize();
10592
- if (i < totalKeys) {
10593
- const endIdx = Math.min(i + currentChunkSize, totalKeys);
10594
- const chunkKeys = keys.subarray(i, endIdx);
10595
- nextChunkPromise = self.selectMany(chunkKeys, false, tx2);
10596
- i = endIdx;
10597
- }
10598
- let chunkTotalSize = 0;
10599
- for (let j = 0, len = rawResults.length; j < len; j++) {
10600
- const s = rawResults[j];
10601
- if (s) {
10602
- const doc = JSON.parse(s);
10603
- chunkTotalSize += s.length * 2;
10604
- if (heap) {
10605
- if (heap.size < topK) heap.push(doc);
10606
- else {
10607
- const top = heap.peek();
10608
- if (top) {
10609
- const aVal = doc[orderByField] ?? doc._id;
10610
- const bVal = top[orderByField] ?? top._id;
10611
- const cmp = aVal < bVal ? -1 : aVal > bVal ? 1 : 0;
10612
- const isBetter = sortOrder === "asc" ? cmp < 0 : cmp > 0;
10613
- if (isBetter) {
10614
- heap.replace(doc);
10615
- }
10616
- }
10617
- }
10618
- } else {
10619
- results.push(doc);
10968
+ for await (const { doc } of self.processChunkedKeys(
10969
+ keys,
10970
+ 0,
10971
+ self.options.pageSize,
10972
+ ftsConditions,
10973
+ tx2
10974
+ )) {
10975
+ if (heap) {
10976
+ if (heap.size < topK) heap.push(doc);
10977
+ else {
10978
+ const top = heap.peek();
10979
+ if (top) {
10980
+ const aVal = doc[orderByField] ?? doc._id;
10981
+ const bVal = top[orderByField] ?? top._id;
10982
+ const cmp = aVal < bVal ? -1 : aVal > bVal ? 1 : 0;
10983
+ if (sortOrder === "asc" ? cmp < 0 : cmp > 0) heap.replace(doc);
10620
10984
  }
10621
10985
  }
10622
- }
10623
- if (chunkTotalSize > 0) {
10624
- if (chunkTotalSize < verySmallChunkSize) {
10625
- currentChunkSize = currentChunkSize * 2;
10626
- } else if (chunkTotalSize > smallChunkSize) {
10627
- currentChunkSize = Math.max(Math.floor(currentChunkSize / 2), 20);
10628
- }
10986
+ } else {
10987
+ results.push(doc);
10629
10988
  }
10630
10989
  }
10631
10990
  const finalDocs = heap ? heap.toArray() : results;
@@ -10635,50 +10994,23 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10635
10994
  const cmp = aVal < bVal ? -1 : aVal > bVal ? 1 : 0;
10636
10995
  return sortOrder === "asc" ? cmp : -cmp;
10637
10996
  });
10638
- const start = offset;
10639
- const end = limit === Infinity ? void 0 : start + limit;
10640
- const limitedResults = finalDocs.slice(start, end);
10997
+ const end = limit === Infinity ? void 0 : offset + limit;
10998
+ const limitedResults = finalDocs.slice(offset, end);
10641
10999
  for (let j = 0, len = limitedResults.length; j < len; j++) {
10642
11000
  yield limitedResults[j];
10643
11001
  }
10644
11002
  } else {
10645
11003
  let yieldedCount = 0;
10646
- let i = offset;
10647
- let nextChunkPromise = null;
10648
- if (yieldedCount < limit && i < totalKeys) {
10649
- const endIdx = Math.min(i + currentChunkSize, totalKeys);
10650
- const chunkKeys = keys.subarray(i, endIdx);
10651
- nextChunkPromise = self.selectMany(chunkKeys, false, tx2);
10652
- i = endIdx;
10653
- }
10654
- while (nextChunkPromise) {
10655
- const rawResults = await nextChunkPromise;
10656
- nextChunkPromise = null;
10657
- const { verySmallChunkSize, smallChunkSize } = self.getFreeMemoryChunkSize();
10658
- if (yieldedCount < limit && i < totalKeys) {
10659
- const endIdx = Math.min(i + currentChunkSize, totalKeys);
10660
- const chunkKeys = keys.subarray(i, endIdx);
10661
- nextChunkPromise = self.selectMany(chunkKeys, false, tx2);
10662
- i = endIdx;
10663
- }
10664
- let chunkTotalSize = 0;
10665
- for (let j = 0, len = rawResults.length; j < len; j++) {
10666
- const s = rawResults[j];
10667
- if (s) {
10668
- if (yieldedCount < limit) {
10669
- yield JSON.parse(s);
10670
- yieldedCount++;
10671
- }
10672
- chunkTotalSize += s.length * 2;
10673
- }
10674
- }
10675
- if (chunkTotalSize > 0) {
10676
- if (chunkTotalSize < verySmallChunkSize) {
10677
- currentChunkSize = currentChunkSize * 2;
10678
- } else if (chunkTotalSize > smallChunkSize) {
10679
- currentChunkSize = Math.max(Math.floor(currentChunkSize / 2), 20);
10680
- }
10681
- }
11004
+ for await (const { doc } of self.processChunkedKeys(
11005
+ keys,
11006
+ offset,
11007
+ self.options.pageSize,
11008
+ ftsConditions,
11009
+ tx2
11010
+ )) {
11011
+ if (yieldedCount >= limit) break;
11012
+ yield doc;
11013
+ yieldedCount++;
10682
11014
  }
10683
11015
  }
10684
11016
  }, tx);
@@ -35,6 +35,11 @@ export declare class DocumentDataplyAPI<T extends DocumentJSON, IC extends Index
35
35
  getDocumentMetadata(tx: Transaction): Promise<DocumentDataplyMetadata>;
36
36
  getDocumentInnerMetadata(tx: Transaction): Promise<DocumentDataplyInnerMetadata>;
37
37
  updateDocumentInnerMetadata(metadata: DocumentDataplyInnerMetadata, tx: Transaction): Promise<void>;
38
+ /**
39
+ * Transforms a query object into a verbose query object
40
+ * @param query The query object to transform
41
+ * @returns The verbose query object
42
+ */
38
43
  verboseQuery<U extends Partial<DocumentDataplyIndexedQuery<T, IC>>, V extends DataplyTreeValue<U>>(query: Partial<DocumentDataplyQuery<U>>): Partial<DocumentDataplyQuery<V>>;
39
44
  /**
40
45
  * Get the selectivity candidate for the given query
@@ -47,11 +52,15 @@ export declare class DocumentDataplyAPI<T extends DocumentJSON, IC extends Index
47
52
  tree: BPTreeAsync<number, V>;
48
53
  condition: Partial<DocumentDataplyCondition<U>>;
49
54
  field: string;
55
+ isFtsMatch?: boolean;
56
+ matchTokens?: string[];
50
57
  };
51
58
  others: {
52
59
  tree: BPTreeAsync<number, V>;
53
60
  condition: Partial<DocumentDataplyCondition<U>>;
54
61
  field: string;
62
+ isFtsMatch?: boolean;
63
+ matchTokens?: string[];
55
64
  }[];
56
65
  rollback: () => void;
57
66
  } | null>;
@@ -63,9 +72,15 @@ export declare class DocumentDataplyAPI<T extends DocumentJSON, IC extends Index
63
72
  verySmallChunkSize: number;
64
73
  smallChunkSize: number;
65
74
  };
75
+ private getTokenKey;
76
+ private applyCandidateByFTS;
66
77
  /**
67
- * Get Primary Keys based on query and index selection.
68
- * Internal common method to unify query optimization.
78
+ * 특정 인덱스 후보를 조회하여 PK 집합을 필터링합니다.
79
+ */
80
+ private applyCandidate;
81
+ /**
82
+ * 쿼리와 인덱스 선택을 기반으로 기본 키(Primary Keys)를 가져옵니다.
83
+ * 쿼리 최적화를 통합하기 위한 내부 공통 메서드입니다.
69
84
  */
70
85
  getKeys(query: Partial<DocumentDataplyIndexedQuery<T, IC>>, orderBy?: keyof IC | '_id', sortOrder?: 'asc' | 'desc'): Promise<Float64Array>;
71
86
  private insertDocumentInternal;
@@ -121,6 +136,19 @@ export declare class DocumentDataplyAPI<T extends DocumentJSON, IC extends Index
121
136
  * @returns The number of documents that match the query
122
137
  */
123
138
  countDocuments(query: Partial<DocumentDataplyIndexedQuery<T, IC>>, tx?: Transaction): Promise<number>;
139
+ /**
140
+ * FTS 조건에 대해 문서가 유효한지 검증합니다.
141
+ */
142
+ private verifyFts;
143
+ /**
144
+ * 메모리 기반으로 청크 크기를 동적 조절합니다.
145
+ */
146
+ private adjustChunkSize;
147
+ /**
148
+ * Prefetch 방식으로 키 배열을 청크 단위로 조회하여 문서를 순회합니다.
149
+ * FTS 검증을 통과한 문서만 yield 합니다.
150
+ */
151
+ private processChunkedKeys;
124
152
  /**
125
153
  * Select documents from the database
126
154
  * @param query The query to use (only indexed fields + _id allowed)
@@ -16,7 +16,17 @@ export interface DocumentDataplyInnerMetadata {
16
16
  updatedAt: number;
17
17
  lastId: number;
18
18
  indices: {
19
- [key: string]: [number, boolean];
19
+ [key: string]: [
20
+ number,
21
+ boolean | {
22
+ type: 'fts';
23
+ tokenizer: 'whitespace';
24
+ } | {
25
+ type: 'fts';
26
+ tokenizer: 'ngram';
27
+ gramSize: number;
28
+ }
29
+ ];
20
30
  };
21
31
  }
22
32
  export interface DocumentDataplyMetadata {
@@ -46,6 +56,7 @@ export type DocumentDataplyCondition<V> = {
46
56
  notEqual?: Partial<V>;
47
57
  or?: Partial<V>[];
48
58
  like?: string;
59
+ match?: string;
49
60
  };
50
61
  export type DocumentDataplyQuery<T> = {
51
62
  [key in keyof T]?: T[key] | DocumentDataplyCondition<T[key]>;
@@ -110,10 +121,18 @@ export type DocumentDataplyIndices<T extends DocumentJSON, IC extends IndexConfi
110
121
  [key in keyof IC & keyof FinalFlatten<T>]: GetTypeByPath<T, key>;
111
122
  };
112
123
  /**
113
- * Index configuration type - keys are field names, values are boolean
124
+ * Index configuration type
114
125
  */
126
+ export type FTSConfig = {
127
+ type: 'fts';
128
+ tokenizer: 'whitespace';
129
+ } | {
130
+ type: 'fts';
131
+ tokenizer: 'ngram';
132
+ gramSize: number;
133
+ };
115
134
  export type IndexConfig<T> = Partial<{
116
- [key in keyof FinalFlatten<T>]: boolean;
135
+ [key in keyof FinalFlatten<T>]: boolean | FTSConfig;
117
136
  }>;
118
137
  /**
119
138
  * Extract index keys from IndexConfig
@@ -0,0 +1 @@
1
+ export declare function fastStringHash(str: string): number;
@@ -0,0 +1,4 @@
1
+ import { FTSConfig } from '../types';
2
+ export declare function whitespaceTokenize(text: string): string[];
3
+ export declare function ngramTokenize(text: string, gramSize: number): string[];
4
+ export declare function tokenize(text: string, options: FTSConfig): string[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "document-dataply",
3
- "version": "0.0.6",
3
+ "version": "0.0.7-alpha.0",
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.22"
45
+ "dataply": "^0.0.23-alpha.2"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@types/jest": "^30.0.0",