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

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
@@ -10043,22 +10043,46 @@ var DocumentSerializeStrategyAsync = class extends import_dataply.SerializeStrat
10043
10043
 
10044
10044
  // src/core/bptree/documentComparator.ts
10045
10045
  var import_dataply2 = __toESM(require_cjs());
10046
+ function comparePrimitive(a, b) {
10047
+ if (a === b) return 0;
10048
+ if (a === null) return -1;
10049
+ if (b === null) return 1;
10050
+ if (typeof a !== typeof b) {
10051
+ const typeOrder = (v) => typeof v === "boolean" ? 0 : typeof v === "number" ? 1 : 2;
10052
+ return typeOrder(a) - typeOrder(b);
10053
+ }
10054
+ if (typeof a === "string" && typeof b === "string") {
10055
+ return a.localeCompare(b);
10056
+ }
10057
+ return +a - +b;
10058
+ }
10059
+ function compareValue(a, b) {
10060
+ const aArr = Array.isArray(a);
10061
+ const bArr = Array.isArray(b);
10062
+ if (!aArr && !bArr) {
10063
+ return comparePrimitive(a, b);
10064
+ }
10065
+ const aList = aArr ? a : [a];
10066
+ const bList = bArr ? b : [b];
10067
+ const len = Math.min(aList.length, bList.length);
10068
+ for (let i = 0; i < len; i++) {
10069
+ const diff = comparePrimitive(aList[i], bList[i]);
10070
+ if (diff !== 0) return diff;
10071
+ }
10072
+ return aList.length - bList.length;
10073
+ }
10046
10074
  var DocumentValueComparator = class extends import_dataply2.ValueComparator {
10047
10075
  primaryAsc(a, b) {
10048
- if (typeof a.v !== "string" || typeof b.v !== "string") {
10049
- return +a.v - +b.v;
10050
- }
10051
- return a.v.localeCompare(b.v);
10076
+ return compareValue(a.v, b.v);
10052
10077
  }
10053
10078
  asc(a, b) {
10054
- if (typeof a.v !== "string" || typeof b.v !== "string") {
10055
- const diff2 = +a.v - +b.v;
10056
- return diff2 === 0 ? a.k - b.k : diff2;
10057
- }
10058
- const diff = a.v.localeCompare(b.v);
10079
+ const diff = compareValue(a.v, b.v);
10059
10080
  return diff === 0 ? a.k - b.k : diff;
10060
10081
  }
10061
10082
  match(value) {
10083
+ if (Array.isArray(value.v)) {
10084
+ return value.v[0] + "";
10085
+ }
10062
10086
  return value.v + "";
10063
10087
  }
10064
10088
  };
@@ -10171,7 +10195,23 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10171
10195
  comparator = new DocumentValueComparator();
10172
10196
  pendingBackfillFields = [];
10173
10197
  lock;
10198
+ _initialized = false;
10174
10199
  indexedFields;
10200
+ /**
10201
+ * Registered indices via createIndex() (before init)
10202
+ * Key: index name, Value: index configuration
10203
+ */
10204
+ pendingCreateIndices = /* @__PURE__ */ new Map();
10205
+ /**
10206
+ * Resolved index configurations after init.
10207
+ * Key: index name, Value: index config (from metadata)
10208
+ */
10209
+ registeredIndices = /* @__PURE__ */ new Map();
10210
+ /**
10211
+ * Maps field name → index names that cover this field.
10212
+ * Used for query resolution.
10213
+ */
10214
+ fieldToIndices = /* @__PURE__ */ new Map();
10175
10215
  operatorConverters = {
10176
10216
  equal: "primaryEqual",
10177
10217
  notEqual: "primaryNotEqual",
@@ -10187,11 +10227,6 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10187
10227
  this.trees = /* @__PURE__ */ new Map();
10188
10228
  this.lock = new import_dataply3.Ryoiki();
10189
10229
  this.indexedFields = /* @__PURE__ */ new Set(["_id"]);
10190
- if (options?.indices) {
10191
- for (const field of Object.keys(options.indices)) {
10192
- this.indexedFields.add(field);
10193
- }
10194
- }
10195
10230
  this.hook.onceAfter("init", async (tx, isNewlyCreated) => {
10196
10231
  if (isNewlyCreated) {
10197
10232
  await this.initializeDocumentFile(tx);
@@ -10200,31 +10235,30 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10200
10235
  throw new Error("Document metadata verification failed");
10201
10236
  }
10202
10237
  const metadata = await this.getDocumentInnerMetadata(tx);
10203
- const optionsIndices = options.indices ?? {};
10204
- const targetIndices = {
10205
- ...optionsIndices,
10206
- _id: true
10207
- };
10238
+ const targetIndices = /* @__PURE__ */ new Map();
10239
+ targetIndices.set("_id", { type: "btree", fields: ["_id"] });
10240
+ for (const [name, option] of this.pendingCreateIndices) {
10241
+ const config = this.toIndexMetaConfig(option);
10242
+ targetIndices.set(name, config);
10243
+ }
10208
10244
  const backfillTargets = [];
10209
10245
  let isMetadataChanged = false;
10210
- for (const field in targetIndices) {
10211
- const isBackfillEnabled = targetIndices[field];
10212
- const existingIndex = metadata.indices[field];
10246
+ for (const [indexName, config] of targetIndices) {
10247
+ const existingIndex = metadata.indices[indexName];
10213
10248
  if (!existingIndex) {
10214
- metadata.indices[field] = [-1, isBackfillEnabled];
10249
+ metadata.indices[indexName] = [-1, config];
10215
10250
  isMetadataChanged = true;
10216
- if (isBackfillEnabled && !isNewlyCreated) {
10217
- backfillTargets.push(field);
10251
+ if (!isNewlyCreated) {
10252
+ backfillTargets.push(indexName);
10218
10253
  }
10219
10254
  } else {
10220
- const [_pk, isMetaBackfillEnabled] = existingIndex;
10221
- if (isBackfillEnabled && !isMetaBackfillEnabled) {
10222
- metadata.indices[field][1] = isBackfillEnabled;
10223
- isMetadataChanged = true;
10224
- backfillTargets.push(field);
10225
- } else if (!isBackfillEnabled && isMetaBackfillEnabled) {
10226
- metadata.indices[field][1] = false;
10255
+ const [_pk, existingConfig] = existingIndex;
10256
+ if (JSON.stringify(existingConfig) !== JSON.stringify(config)) {
10257
+ metadata.indices[indexName] = [_pk, config];
10227
10258
  isMetadataChanged = true;
10259
+ if (!isNewlyCreated) {
10260
+ backfillTargets.push(indexName);
10261
+ }
10228
10262
  }
10229
10263
  }
10230
10264
  }
@@ -10232,25 +10266,220 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10232
10266
  await this.updateDocumentInnerMetadata(metadata, tx);
10233
10267
  }
10234
10268
  this.indices = metadata.indices;
10235
- for (const field in this.indices) {
10236
- if (field in targetIndices) {
10269
+ this.registeredIndices = /* @__PURE__ */ new Map();
10270
+ this.fieldToIndices = /* @__PURE__ */ new Map();
10271
+ for (const [indexName, config] of targetIndices) {
10272
+ this.registeredIndices.set(indexName, config);
10273
+ const fields = this.getFieldsFromConfig(config);
10274
+ for (const field of fields) {
10275
+ this.indexedFields.add(field);
10276
+ if (!this.fieldToIndices.has(field)) {
10277
+ this.fieldToIndices.set(field, []);
10278
+ }
10279
+ this.fieldToIndices.get(field).push(indexName);
10280
+ }
10281
+ }
10282
+ for (const indexName of targetIndices.keys()) {
10283
+ if (metadata.indices[indexName]) {
10237
10284
  const tree = new import_dataply3.BPTreeAsync(
10238
10285
  new DocumentSerializeStrategyAsync(
10239
10286
  this.rowTableEngine.order,
10240
10287
  this,
10241
10288
  this.txContext,
10242
- field
10289
+ indexName
10243
10290
  ),
10244
10291
  this.comparator
10245
10292
  );
10246
10293
  await tree.init();
10247
- this.trees.set(field, tree);
10294
+ this.trees.set(indexName, tree);
10248
10295
  }
10249
10296
  }
10250
10297
  this.pendingBackfillFields = backfillTargets;
10298
+ this._initialized = true;
10251
10299
  return tx;
10252
10300
  });
10253
10301
  }
10302
+ /**
10303
+ * Whether the document database has been initialized.
10304
+ */
10305
+ get isDocInitialized() {
10306
+ return this._initialized;
10307
+ }
10308
+ /**
10309
+ * Register an index. If called before init(), queues it for processing during init.
10310
+ * If called after init(), immediately creates the tree, updates metadata, and backfills.
10311
+ */
10312
+ async registerIndex(name, option, tx) {
10313
+ if (!this._initialized) {
10314
+ this.pendingCreateIndices.set(name, option);
10315
+ return;
10316
+ }
10317
+ await this.registerIndexRuntime(name, option, tx);
10318
+ }
10319
+ /**
10320
+ * Register an index at runtime (after init).
10321
+ * Creates the tree, updates metadata, and backfills existing data.
10322
+ */
10323
+ async registerIndexRuntime(name, option, tx) {
10324
+ const config = this.toIndexMetaConfig(option);
10325
+ if (this.registeredIndices.has(name)) {
10326
+ const existing = this.registeredIndices.get(name);
10327
+ if (JSON.stringify(existing) === JSON.stringify(config)) return;
10328
+ }
10329
+ await this.runWithDefault(async (tx2) => {
10330
+ const metadata = await this.getDocumentInnerMetadata(tx2);
10331
+ metadata.indices[name] = [-1, config];
10332
+ await this.updateDocumentInnerMetadata(metadata, tx2);
10333
+ this.indices = metadata.indices;
10334
+ this.registeredIndices.set(name, config);
10335
+ const fields = this.getFieldsFromConfig(config);
10336
+ for (const field of fields) {
10337
+ this.indexedFields.add(field);
10338
+ if (!this.fieldToIndices.has(field)) {
10339
+ this.fieldToIndices.set(field, []);
10340
+ }
10341
+ this.fieldToIndices.get(field).push(name);
10342
+ }
10343
+ const tree = new import_dataply3.BPTreeAsync(
10344
+ new DocumentSerializeStrategyAsync(
10345
+ this.rowTableEngine.order,
10346
+ this,
10347
+ this.txContext,
10348
+ name
10349
+ ),
10350
+ this.comparator
10351
+ );
10352
+ await tree.init();
10353
+ this.trees.set(name, tree);
10354
+ if (metadata.lastId > 0) {
10355
+ this.pendingBackfillFields = [name];
10356
+ await this.backfillIndices(tx2);
10357
+ }
10358
+ }, tx);
10359
+ }
10360
+ /**
10361
+ * Drop (remove) a named index.
10362
+ * Removes the index from metadata, in-memory maps, and trees.
10363
+ * The '_id' index cannot be dropped.
10364
+ * @param name The name of the index to drop
10365
+ */
10366
+ async dropIndex(name, tx) {
10367
+ if (name === "_id") {
10368
+ throw new Error("Cannot drop the _id index");
10369
+ }
10370
+ if (!this._initialized) {
10371
+ this.pendingCreateIndices.delete(name);
10372
+ return;
10373
+ }
10374
+ if (!this.registeredIndices.has(name)) {
10375
+ throw new Error(`Index '${name}' does not exist`);
10376
+ }
10377
+ await this.runWithDefault(async (tx2) => {
10378
+ const config = this.registeredIndices.get(name);
10379
+ const metadata = await this.getDocumentInnerMetadata(tx2);
10380
+ delete metadata.indices[name];
10381
+ await this.updateDocumentInnerMetadata(metadata, tx2);
10382
+ this.indices = metadata.indices;
10383
+ this.registeredIndices.delete(name);
10384
+ const fields = this.getFieldsFromConfig(config);
10385
+ for (const field of fields) {
10386
+ const indexNames = this.fieldToIndices.get(field);
10387
+ if (indexNames) {
10388
+ const filtered = indexNames.filter((n) => n !== name);
10389
+ if (filtered.length === 0) {
10390
+ this.fieldToIndices.delete(field);
10391
+ if (field !== "_id") {
10392
+ this.indexedFields.delete(field);
10393
+ }
10394
+ } else {
10395
+ this.fieldToIndices.set(field, filtered);
10396
+ }
10397
+ }
10398
+ }
10399
+ this.trees.delete(name);
10400
+ }, tx);
10401
+ }
10402
+ /**
10403
+ * Convert CreateIndexOption to IndexMetaConfig for metadata storage.
10404
+ */
10405
+ toIndexMetaConfig(option) {
10406
+ if (option.type === "btree") {
10407
+ return {
10408
+ type: "btree",
10409
+ fields: option.fields
10410
+ };
10411
+ }
10412
+ if (option.type === "fts") {
10413
+ if (option.tokenizer === "ngram") {
10414
+ return {
10415
+ type: "fts",
10416
+ fields: option.fields,
10417
+ tokenizer: "ngram",
10418
+ gramSize: option.ngram
10419
+ };
10420
+ }
10421
+ return {
10422
+ type: "fts",
10423
+ fields: option.fields,
10424
+ tokenizer: "whitespace"
10425
+ };
10426
+ }
10427
+ throw new Error(`Unknown index type: ${option.type}`);
10428
+ }
10429
+ /**
10430
+ * Get all field names from an IndexMetaConfig.
10431
+ */
10432
+ getFieldsFromConfig(config) {
10433
+ if (config.type === "btree") {
10434
+ return config.fields;
10435
+ }
10436
+ if (config.type === "fts") {
10437
+ return [config.fields];
10438
+ }
10439
+ return [];
10440
+ }
10441
+ /**
10442
+ * Get the primary field of an index (the field used as tree key).
10443
+ * For btree: first field in fields array.
10444
+ * For fts: the single field.
10445
+ */
10446
+ getPrimaryField(config) {
10447
+ if (config.type === "btree") {
10448
+ return config.fields[0];
10449
+ }
10450
+ return config.fields;
10451
+ }
10452
+ /**
10453
+ * 인덱스 config에 따라 B+tree에 저장할 v 값을 생성합니다.
10454
+ * - 단일 필드 btree: Primitive (단일 값)
10455
+ * - 복합 필드 btree: Primitive[] (필드 순서대로 배열)
10456
+ * - fts: 별도 처리 (이 메서드 사용 안 함)
10457
+ * @returns undefined면 해당 문서에 필수 필드가 없으므로 인덱싱 스킵
10458
+ */
10459
+ getIndexValue(config, flatDoc) {
10460
+ if (config.type !== "btree") return void 0;
10461
+ if (config.fields.length === 1) {
10462
+ const v = flatDoc[config.fields[0]];
10463
+ return v === void 0 ? void 0 : v;
10464
+ }
10465
+ const values = [];
10466
+ for (let i = 0, len = config.fields.length; i < len; i++) {
10467
+ const v = flatDoc[config.fields[i]];
10468
+ if (v === void 0) return void 0;
10469
+ values.push(v);
10470
+ }
10471
+ return values;
10472
+ }
10473
+ /**
10474
+ * Get FTSConfig from IndexMetaConfig (for tokenizer compatibility).
10475
+ */
10476
+ getFtsConfig(config) {
10477
+ if (config.type !== "fts") return null;
10478
+ if (config.tokenizer === "ngram") {
10479
+ return { type: "fts", tokenizer: "ngram", gramSize: config.gramSize };
10480
+ }
10481
+ return { type: "fts", tokenizer: "whitespace" };
10482
+ }
10254
10483
  async getDocument(pk, tx) {
10255
10484
  return this.runWithDefault(async (tx2) => {
10256
10485
  const row = await this.select(pk, false, tx2);
@@ -10279,10 +10508,9 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10279
10508
  });
10280
10509
  }
10281
10510
  /**
10282
- * Backfill indices for fields that were added with `true` option after data was inserted.
10283
- * This method should be called after `init()` if you want to index existing documents
10284
- * for newly added index fields.
10285
- *
10511
+ * Backfill indices for newly created indices after data was inserted.
10512
+ * This method should be called after `init()`.
10513
+ *
10286
10514
  * @returns Number of documents that were backfilled
10287
10515
  */
10288
10516
  async backfillIndices(tx) {
@@ -10295,12 +10523,12 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10295
10523
  if (metadata.lastId === 0) {
10296
10524
  return 0;
10297
10525
  }
10298
- const fieldTxMap = {};
10299
- const fieldMap = /* @__PURE__ */ new Map();
10300
- for (const field of backfillTargets) {
10301
- const tree = this.trees.get(field);
10302
- if (tree && field !== "_id") {
10303
- fieldTxMap[field] = await tree.createTransaction();
10526
+ const indexTxMap = {};
10527
+ const indexEntryMap = /* @__PURE__ */ new Map();
10528
+ for (const indexName of backfillTargets) {
10529
+ const tree = this.trees.get(indexName);
10530
+ if (tree && indexName !== "_id") {
10531
+ indexTxMap[indexName] = await tree.createTransaction();
10304
10532
  }
10305
10533
  }
10306
10534
  let backfilledCount = 0;
@@ -10315,35 +10543,44 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10315
10543
  const doc = await this.getDocument(k, tx2);
10316
10544
  if (!doc) continue;
10317
10545
  const flatDoc = this.flattenDocument(doc);
10318
- for (const field of backfillTargets) {
10319
- if (!(field in flatDoc) || // 문서에 해당 필드가 없음
10320
- !(field in fieldTxMap)) {
10321
- continue;
10322
- }
10323
- const v = flatDoc[field];
10324
- const btx = fieldTxMap[field];
10325
- const indexConfig = metadata.indices[field]?.[1];
10326
- const isFts = typeof indexConfig === "object" && indexConfig?.type === "fts" && typeof v === "string";
10327
- let tokens = [v];
10328
- if (isFts) {
10329
- tokens = tokenize(v, indexConfig);
10330
- }
10331
- const batchInsertData = [];
10332
- for (let i = 0, len = tokens.length; i < len; i++) {
10333
- const token = tokens[i];
10334
- const keyToInsert = isFts ? this.getTokenKey(k, token) : k;
10335
- const entry = { k, v: token };
10336
- batchInsertData.push([keyToInsert, entry]);
10337
- if (!fieldMap.has(btx)) {
10338
- fieldMap.set(btx, []);
10546
+ for (const indexName of backfillTargets) {
10547
+ if (!(indexName in indexTxMap)) continue;
10548
+ const config = this.registeredIndices.get(indexName);
10549
+ if (!config) continue;
10550
+ const btx = indexTxMap[indexName];
10551
+ if (config.type === "fts") {
10552
+ const primaryField = this.getPrimaryField(config);
10553
+ const v = flatDoc[primaryField];
10554
+ if (v === void 0 || typeof v !== "string") continue;
10555
+ const ftsConfig = this.getFtsConfig(config);
10556
+ const tokens = ftsConfig ? tokenize(v, ftsConfig) : [v];
10557
+ const batchInsertData = [];
10558
+ for (let i = 0, len = tokens.length; i < len; i++) {
10559
+ const token = tokens[i];
10560
+ const keyToInsert = this.getTokenKey(k, token);
10561
+ const entry = { k, v: token };
10562
+ batchInsertData.push([keyToInsert, entry]);
10563
+ if (!indexEntryMap.has(btx)) {
10564
+ indexEntryMap.set(btx, []);
10565
+ }
10566
+ indexEntryMap.get(btx).push({ k: keyToInsert, v: entry });
10567
+ }
10568
+ await btx.batchInsert(batchInsertData);
10569
+ } else {
10570
+ const indexVal = this.getIndexValue(config, flatDoc);
10571
+ if (indexVal === void 0) continue;
10572
+ const entry = { k, v: indexVal };
10573
+ const batchInsertData = [[k, entry]];
10574
+ if (!indexEntryMap.has(btx)) {
10575
+ indexEntryMap.set(btx, []);
10339
10576
  }
10340
- fieldMap.get(btx).push({ k: keyToInsert, v: entry });
10577
+ indexEntryMap.get(btx).push(entry);
10578
+ await btx.batchInsert(batchInsertData);
10341
10579
  }
10342
- await btx.batchInsert(batchInsertData);
10343
10580
  }
10344
10581
  backfilledCount++;
10345
10582
  }
10346
- const btxs = Object.values(fieldTxMap);
10583
+ const btxs = Object.values(indexTxMap);
10347
10584
  const success = [];
10348
10585
  try {
10349
10586
  for (const btx of btxs) {
@@ -10355,7 +10592,7 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10355
10592
  await btx.rollback();
10356
10593
  }
10357
10594
  for (const btx of success) {
10358
- const entries = fieldMap.get(btx);
10595
+ const entries = indexEntryMap.get(btx);
10359
10596
  if (!entries) continue;
10360
10597
  for (const entry of entries) {
10361
10598
  await btx.delete(entry.k, entry);
@@ -10374,6 +10611,7 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10374
10611
  createdAt: Date.now(),
10375
10612
  updatedAt: Date.now(),
10376
10613
  lastId: 0,
10614
+ schemeVersion: 0,
10377
10615
  indices
10378
10616
  };
10379
10617
  }
@@ -10383,7 +10621,7 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10383
10621
  throw new Error("Document metadata already exists");
10384
10622
  }
10385
10623
  const metaObj = this.createDocumentInnerMetadata({
10386
- _id: [-1, true]
10624
+ _id: [-1, { type: "btree", fields: ["_id"] }]
10387
10625
  });
10388
10626
  await this.insertAsOverflow(JSON.stringify(metaObj), false, tx);
10389
10627
  }
@@ -10408,18 +10646,27 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10408
10646
  }
10409
10647
  /**
10410
10648
  * returns flattened document
10411
- * @param document
10412
- * @returns
10649
+ * @param document
10650
+ * @returns
10413
10651
  */
10414
10652
  flattenDocument(document) {
10415
10653
  return this.flatten(document, "", {});
10416
10654
  }
10417
10655
  async getDocumentMetadata(tx) {
10418
10656
  const metadata = await this.getMetadata(tx);
10657
+ const innerMetadata = await this.getDocumentInnerMetadata(tx);
10658
+ const indices = [];
10659
+ for (const name of this.registeredIndices.keys()) {
10660
+ if (name !== "_id") {
10661
+ indices.push(name);
10662
+ }
10663
+ }
10419
10664
  return {
10420
10665
  pageSize: metadata.pageSize,
10421
10666
  pageCount: metadata.pageCount,
10422
- rowCount: metadata.rowCount
10667
+ rowCount: metadata.rowCount,
10668
+ indices,
10669
+ schemeVersion: innerMetadata.schemeVersion ?? 0
10423
10670
  };
10424
10671
  }
10425
10672
  async getDocumentInnerMetadata(tx) {
@@ -10432,6 +10679,25 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10432
10679
  async updateDocumentInnerMetadata(metadata, tx) {
10433
10680
  await this.update(1, JSON.stringify(metadata), tx);
10434
10681
  }
10682
+ /**
10683
+ * Run a migration if the current schemeVersion is lower than the target version.
10684
+ * After the callback completes, schemeVersion is updated to the target version.
10685
+ * @param version The target scheme version
10686
+ * @param callback The migration callback
10687
+ * @param tx Optional transaction
10688
+ */
10689
+ async migration(version, callback, tx) {
10690
+ await this.runWithDefault(async (tx2) => {
10691
+ const innerMetadata = await this.getDocumentInnerMetadata(tx2);
10692
+ const currentVersion = innerMetadata.schemeVersion ?? 0;
10693
+ if (currentVersion < version) {
10694
+ await callback(tx2);
10695
+ innerMetadata.schemeVersion = version;
10696
+ innerMetadata.updatedAt = Date.now();
10697
+ await this.updateDocumentInnerMetadata(innerMetadata, tx2);
10698
+ }
10699
+ }, tx);
10700
+ }
10435
10701
  /**
10436
10702
  * Transforms a query object into a verbose query object
10437
10703
  * @param query The query object to transform
@@ -10470,33 +10736,74 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10470
10736
  return result;
10471
10737
  }
10472
10738
  /**
10473
- * Get the selectivity candidate for the given query
10474
- * @param query The query conditions
10739
+ * Choose the best index (driver) for the given query.
10740
+ * Scores each index based on field coverage and condition type.
10741
+ *
10742
+ * @param query The verbose query conditions
10475
10743
  * @param orderByField Optional field name for orderBy optimization
10476
10744
  * @returns Driver and other candidates for query execution
10477
10745
  */
10478
10746
  async getSelectivityCandidate(query, orderByField) {
10747
+ const queryFields = new Set(Object.keys(query));
10479
10748
  const candidates = [];
10480
- const metadata = await this.getDocumentInnerMetadata(this.txContext.get());
10481
- for (const field in query) {
10482
- const tree = this.trees.get(field);
10749
+ for (const [indexName, config] of this.registeredIndices) {
10750
+ const tree = this.trees.get(indexName);
10483
10751
  if (!tree) continue;
10484
- const condition = query[field];
10485
- const treeTx = await tree.createTransaction();
10486
- const indexConfig = metadata.indices[field]?.[1];
10487
- let isFtsMatch = false;
10488
- let matchTokens;
10489
- if (typeof indexConfig === "object" && indexConfig?.type === "fts" && condition.match) {
10490
- isFtsMatch = true;
10491
- matchTokens = tokenize(condition.match, indexConfig);
10492
- }
10493
- candidates.push({
10494
- tree: treeTx,
10495
- condition,
10496
- field,
10497
- isFtsMatch,
10498
- matchTokens
10499
- });
10752
+ if (config.type === "btree") {
10753
+ const primaryField = config.fields[0];
10754
+ if (!queryFields.has(primaryField)) continue;
10755
+ const condition = query[primaryField];
10756
+ const treeTx = await tree.createTransaction();
10757
+ 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;
10773
+ }
10774
+ }
10775
+ if (orderByField && primaryField === orderByField) {
10776
+ score += 200;
10777
+ }
10778
+ const compositeVerifyFields = coveredFields.filter((f) => f !== primaryField);
10779
+ candidates.push({
10780
+ tree: treeTx,
10781
+ condition,
10782
+ field: primaryField,
10783
+ indexName,
10784
+ isFtsMatch: false,
10785
+ score,
10786
+ compositeVerifyFields
10787
+ });
10788
+ } else if (config.type === "fts") {
10789
+ const field = config.fields;
10790
+ if (!queryFields.has(field)) continue;
10791
+ const condition = query[field];
10792
+ if (!condition || typeof condition !== "object" || !("match" in condition)) continue;
10793
+ const treeTx = await tree.createTransaction();
10794
+ const ftsConfig = this.getFtsConfig(config);
10795
+ const matchTokens = ftsConfig ? tokenize(condition.match, ftsConfig) : [];
10796
+ candidates.push({
10797
+ tree: treeTx,
10798
+ condition,
10799
+ field,
10800
+ indexName,
10801
+ isFtsMatch: true,
10802
+ matchTokens,
10803
+ score: 90,
10804
+ compositeVerifyFields: []
10805
+ });
10806
+ }
10500
10807
  }
10501
10808
  const rollback = () => {
10502
10809
  for (const { tree } of candidates) {
@@ -10507,41 +10814,19 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10507
10814
  rollback();
10508
10815
  return null;
10509
10816
  }
10510
- if (orderByField) {
10511
- const orderByCandidate = candidates.find((c) => c.field === orderByField);
10512
- if (orderByCandidate) {
10513
- return {
10514
- driver: orderByCandidate,
10515
- others: candidates.filter((c) => c.field !== orderByField),
10516
- rollback
10517
- };
10518
- }
10519
- }
10520
- const ftsCandidate = candidates.find(
10521
- (c) => c.isFtsMatch && c.matchTokens && c.matchTokens.length > 0
10522
- );
10523
- if (ftsCandidate) {
10524
- const hasHigherPriority = candidates.some((c) => {
10525
- if (c === ftsCandidate) return false;
10526
- const cond = c.condition;
10527
- return "equal" in cond || "primaryEqual" in cond;
10528
- });
10529
- if (!hasHigherPriority) {
10530
- return {
10531
- driver: ftsCandidate,
10532
- others: candidates.filter((c) => c !== ftsCandidate),
10533
- rollback
10534
- };
10817
+ candidates.sort((a, b) => b.score - a.score);
10818
+ const driver = candidates[0];
10819
+ const others = candidates.slice(1);
10820
+ const compositeVerifyConditions = [];
10821
+ for (const field of driver.compositeVerifyFields) {
10822
+ if (query[field]) {
10823
+ compositeVerifyConditions.push({ field, condition: query[field] });
10535
10824
  }
10536
10825
  }
10537
- let res = import_dataply3.BPTreeAsync.ChooseDriver(candidates);
10538
- if (!res && candidates.length > 0) {
10539
- res = candidates[0];
10540
- }
10541
- if (!res) return null;
10542
10826
  return {
10543
- driver: res,
10544
- others: candidates.filter((c) => c.tree !== res.tree),
10827
+ driver,
10828
+ others,
10829
+ compositeVerifyConditions,
10545
10830
  rollback
10546
10831
  };
10547
10832
  }
@@ -10634,7 +10919,7 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10634
10919
  orderBy
10635
10920
  );
10636
10921
  if (!selectivity) return null;
10637
- const { driver, others, rollback } = selectivity;
10922
+ const { driver, others, compositeVerifyConditions, rollback } = selectivity;
10638
10923
  const useIndexOrder = orderBy === void 0 || driver.field === orderBy;
10639
10924
  const currentOrder = useIndexOrder ? sortOrder : void 0;
10640
10925
  let keys;
@@ -10651,6 +10936,8 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10651
10936
  return {
10652
10937
  keys: new Float64Array(Array.from(keys)),
10653
10938
  others,
10939
+ compositeVerifyConditions,
10940
+ isDriverOrderByField: useIndexOrder,
10654
10941
  rollback
10655
10942
  };
10656
10943
  }
@@ -10677,25 +10964,27 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10677
10964
  async insertSingleDocument(document, tx) {
10678
10965
  return this.writeLock(() => this.runWithDefault(async (tx2) => {
10679
10966
  const { pk: dpk, document: dataplyDocument } = await this.insertDocumentInternal(document, tx2);
10680
- const metadata = await this.getDocumentInnerMetadata(tx2);
10681
10967
  const flattenDocument = this.flattenDocument(dataplyDocument);
10682
- for (const field in flattenDocument) {
10683
- const tree = this.trees.get(field);
10968
+ for (const [indexName, config] of this.registeredIndices) {
10969
+ const tree = this.trees.get(indexName);
10684
10970
  if (!tree) continue;
10685
- const v = flattenDocument[field];
10686
- const indexConfig = metadata.indices[field]?.[1];
10687
- let tokens = [v];
10688
- const isFts = typeof indexConfig === "object" && indexConfig?.type === "fts" && typeof v === "string";
10689
- if (isFts) {
10690
- tokens = tokenize(v, indexConfig);
10691
- }
10692
- for (let i = 0, len = tokens.length; i < len; i++) {
10693
- const token = tokens[i];
10694
- const keyToInsert = isFts ? this.getTokenKey(dpk, token) : dpk;
10695
- const [error] = await catchPromise(tree.insert(keyToInsert, { k: dpk, v: token }));
10696
- if (error) {
10697
- throw error;
10971
+ if (config.type === "fts") {
10972
+ const primaryField = this.getPrimaryField(config);
10973
+ const v = flattenDocument[primaryField];
10974
+ if (v === void 0 || typeof v !== "string") continue;
10975
+ const ftsConfig = this.getFtsConfig(config);
10976
+ const tokens = ftsConfig ? tokenize(v, ftsConfig) : [v];
10977
+ for (let i = 0, len = tokens.length; i < len; i++) {
10978
+ const token = tokens[i];
10979
+ const keyToInsert = this.getTokenKey(dpk, token);
10980
+ const [error] = await catchPromise(tree.insert(keyToInsert, { k: dpk, v: token }));
10981
+ if (error) throw error;
10698
10982
  }
10983
+ } else {
10984
+ const indexVal = this.getIndexValue(config, flattenDocument);
10985
+ if (indexVal === void 0) continue;
10986
+ const [error] = await catchPromise(tree.insert(dpk, { k: dpk, v: indexVal }));
10987
+ if (error) throw error;
10699
10988
  }
10700
10989
  }
10701
10990
  return dataplyDocument._id;
@@ -10731,23 +11020,30 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10731
11020
  for (let i = 0, len = pks.length; i < len; i++) {
10732
11021
  flattenedData[i].pk = pks[i];
10733
11022
  }
10734
- for (const [field, tree] of this.trees) {
11023
+ for (const [indexName, config] of this.registeredIndices) {
11024
+ const tree = this.trees.get(indexName);
11025
+ if (!tree) continue;
10735
11026
  const treeTx = await tree.createTransaction();
10736
- const indexConfig = metadata.indices[field]?.[1];
10737
11027
  const batchInsertData = [];
10738
- for (let i = 0, len = flattenedData.length; i < len; i++) {
10739
- const item = flattenedData[i];
10740
- const v = item.data[field];
10741
- if (v === void 0) continue;
10742
- const isFts = typeof indexConfig === "object" && indexConfig?.type === "fts" && typeof v === "string";
10743
- let tokens = [v];
10744
- if (isFts) {
10745
- tokens = tokenize(v, indexConfig);
11028
+ if (config.type === "fts") {
11029
+ const primaryField = this.getPrimaryField(config);
11030
+ const ftsConfig = this.getFtsConfig(config);
11031
+ for (let i = 0, len = flattenedData.length; i < len; i++) {
11032
+ const item = flattenedData[i];
11033
+ const v = item.data[primaryField];
11034
+ if (v === void 0 || typeof v !== "string") continue;
11035
+ const tokens = ftsConfig ? tokenize(v, ftsConfig) : [v];
11036
+ for (let j = 0, tLen = tokens.length; j < tLen; j++) {
11037
+ const token = tokens[j];
11038
+ batchInsertData.push([this.getTokenKey(item.pk, token), { k: item.pk, v: token }]);
11039
+ }
10746
11040
  }
10747
- for (let j = 0, len2 = tokens.length; j < len2; j++) {
10748
- const token = tokens[j];
10749
- const keyToInsert = isFts ? this.getTokenKey(item.pk, token) : item.pk;
10750
- batchInsertData.push([keyToInsert, { k: item.pk, v: token }]);
11041
+ } else {
11042
+ for (let i = 0, len = flattenedData.length; i < len; i++) {
11043
+ const item = flattenedData[i];
11044
+ const indexVal = this.getIndexValue(config, item.data);
11045
+ if (indexVal === void 0) continue;
11046
+ batchInsertData.push([item.pk, { k: item.pk, v: indexVal }]);
10751
11047
  }
10752
11048
  }
10753
11049
  const [error] = await catchPromise(treeTx.batchInsert(batchInsertData));
@@ -10773,8 +11069,8 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10773
11069
  const pks = await this.getKeys(query);
10774
11070
  let updatedCount = 0;
10775
11071
  const treeTxs = /* @__PURE__ */ new Map();
10776
- for (const [field, tree] of this.trees) {
10777
- treeTxs.set(field, await tree.createTransaction());
11072
+ for (const [indexName, tree] of this.trees) {
11073
+ treeTxs.set(indexName, await tree.createTransaction());
10778
11074
  }
10779
11075
  treeTxs.delete("_id");
10780
11076
  for (let i = 0, len = pks.length; i < len; i++) {
@@ -10784,42 +11080,45 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10784
11080
  const updatedDoc = computeUpdatedDoc(doc);
10785
11081
  const oldFlatDoc = this.flattenDocument(doc);
10786
11082
  const newFlatDoc = this.flattenDocument(updatedDoc);
10787
- const metadata = await this.getDocumentInnerMetadata(tx);
10788
- for (const [field, treeTx] of treeTxs) {
10789
- const oldV = oldFlatDoc[field];
10790
- const newV = newFlatDoc[field];
10791
- if (oldV === newV) continue;
10792
- const indexConfig = metadata.indices[field]?.[1];
10793
- const isFts = typeof indexConfig === "object" && indexConfig?.type === "fts";
10794
- if (field in oldFlatDoc) {
10795
- let oldTokens = [oldV];
10796
- if (isFts && typeof oldV === "string") {
10797
- oldTokens = tokenize(oldV, indexConfig);
11083
+ for (const [indexName, treeTx] of treeTxs) {
11084
+ const config = this.registeredIndices.get(indexName);
11085
+ if (!config) continue;
11086
+ if (config.type === "fts") {
11087
+ const primaryField = this.getPrimaryField(config);
11088
+ const oldV = oldFlatDoc[primaryField];
11089
+ const newV = newFlatDoc[primaryField];
11090
+ if (oldV === newV) continue;
11091
+ const ftsConfig = this.getFtsConfig(config);
11092
+ if (typeof oldV === "string") {
11093
+ const oldTokens = ftsConfig ? tokenize(oldV, ftsConfig) : [oldV];
11094
+ for (let j = 0, jLen = oldTokens.length; j < jLen; j++) {
11095
+ await treeTx.delete(this.getTokenKey(pk, oldTokens[j]), { k: pk, v: oldTokens[j] });
11096
+ }
10798
11097
  }
10799
- for (let j = 0, len2 = oldTokens.length; j < len2; j++) {
10800
- const oldToken = oldTokens[j];
10801
- const keyToDelete = isFts ? this.getTokenKey(pk, oldToken) : pk;
10802
- await treeTx.delete(keyToDelete, { k: pk, v: oldToken });
11098
+ if (typeof newV === "string") {
11099
+ const newTokens = ftsConfig ? tokenize(newV, ftsConfig) : [newV];
11100
+ const batchInsertData = [];
11101
+ for (let j = 0, jLen = newTokens.length; j < jLen; j++) {
11102
+ batchInsertData.push([this.getTokenKey(pk, newTokens[j]), { k: pk, v: newTokens[j] }]);
11103
+ }
11104
+ await treeTx.batchInsert(batchInsertData);
10803
11105
  }
10804
- }
10805
- if (field in newFlatDoc) {
10806
- let newTokens = [newV];
10807
- if (isFts && typeof newV === "string") {
10808
- newTokens = tokenize(newV, indexConfig);
11106
+ } else {
11107
+ const oldIndexVal = this.getIndexValue(config, oldFlatDoc);
11108
+ const newIndexVal = this.getIndexValue(config, newFlatDoc);
11109
+ if (JSON.stringify(oldIndexVal) === JSON.stringify(newIndexVal)) continue;
11110
+ if (oldIndexVal !== void 0) {
11111
+ await treeTx.delete(pk, { k: pk, v: oldIndexVal });
10809
11112
  }
10810
- const batchInsertData = [];
10811
- for (let j = 0, len2 = newTokens.length; j < len2; j++) {
10812
- const newToken = newTokens[j];
10813
- const keyToInsert = isFts ? this.getTokenKey(pk, newToken) : pk;
10814
- batchInsertData.push([keyToInsert, { k: pk, v: newToken }]);
11113
+ if (newIndexVal !== void 0) {
11114
+ await treeTx.batchInsert([[pk, { k: pk, v: newIndexVal }]]);
10815
11115
  }
10816
- await treeTx.batchInsert(batchInsertData);
10817
11116
  }
10818
11117
  }
10819
11118
  await this.update(pk, JSON.stringify(updatedDoc), tx);
10820
11119
  updatedCount++;
10821
11120
  }
10822
- for (const [field, treeTx] of treeTxs) {
11121
+ for (const [indexName, treeTx] of treeTxs) {
10823
11122
  const result = await treeTx.commit();
10824
11123
  if (!result.success) {
10825
11124
  for (const rollbackTx of treeTxs.values()) {
@@ -10832,7 +11131,7 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10832
11131
  }
10833
11132
  /**
10834
11133
  * Fully update documents from the database that match the query
10835
- * @param query The query to use (only indexed fields + _id allowed)
11134
+ * @param query The query to use
10836
11135
  * @param newRecord Complete document to replace with, or function that receives current document and returns new document
10837
11136
  * @param tx The transaction to use
10838
11137
  * @returns The number of updated documents
@@ -10847,7 +11146,7 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10847
11146
  }
10848
11147
  /**
10849
11148
  * Partially update documents from the database that match the query
10850
- * @param query The query to use (only indexed fields + _id allowed)
11149
+ * @param query The query to use
10851
11150
  * @param newRecord Partial document to merge, or function that receives current document and returns partial update
10852
11151
  * @param tx The transaction to use
10853
11152
  * @returns The number of updated documents
@@ -10864,7 +11163,7 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10864
11163
  }
10865
11164
  /**
10866
11165
  * Delete documents from the database that match the query
10867
- * @param query The query to use (only indexed fields + _id allowed)
11166
+ * @param query The query to use
10868
11167
  * @param tx The transaction to use
10869
11168
  * @returns The number of deleted documents
10870
11169
  */
@@ -10877,20 +11176,22 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10877
11176
  const doc = await this.getDocument(pk, tx2);
10878
11177
  if (!doc) continue;
10879
11178
  const flatDoc = this.flattenDocument(doc);
10880
- const metadata = await this.getDocumentInnerMetadata(tx2);
10881
- for (const [field, tree] of this.trees) {
10882
- const v = flatDoc[field];
10883
- if (v === void 0) continue;
10884
- const indexConfig = metadata.indices[field]?.[1];
10885
- const isFts = typeof indexConfig === "object" && indexConfig?.type === "fts" && typeof v === "string";
10886
- let tokens = [v];
10887
- if (isFts) {
10888
- tokens = tokenize(v, indexConfig);
10889
- }
10890
- for (let j = 0, len2 = tokens.length; j < len2; j++) {
10891
- const token = tokens[j];
10892
- const keyToDelete = isFts ? this.getTokenKey(pk, token) : pk;
10893
- await tree.delete(keyToDelete, { k: pk, v: token });
11179
+ for (const [indexName, tree] of this.trees) {
11180
+ const config = this.registeredIndices.get(indexName);
11181
+ if (!config) continue;
11182
+ if (config.type === "fts") {
11183
+ const primaryField = this.getPrimaryField(config);
11184
+ const v = flatDoc[primaryField];
11185
+ if (v === void 0 || typeof v !== "string") continue;
11186
+ const ftsConfig = this.getFtsConfig(config);
11187
+ const tokens = ftsConfig ? tokenize(v, ftsConfig) : [v];
11188
+ for (let j = 0, jLen = tokens.length; j < jLen; j++) {
11189
+ await tree.delete(this.getTokenKey(pk, tokens[j]), { k: pk, v: tokens[j] });
11190
+ }
11191
+ } else {
11192
+ const indexVal = this.getIndexValue(config, flatDoc);
11193
+ if (indexVal === void 0) continue;
11194
+ await tree.delete(pk, { k: pk, v: indexVal });
10894
11195
  }
10895
11196
  }
10896
11197
  await super.delete(pk, true, tx2);
@@ -10901,7 +11202,7 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10901
11202
  }
10902
11203
  /**
10903
11204
  * Count documents from the database that match the query
10904
- * @param query The query to use (only indexed fields + _id allowed)
11205
+ * @param query The query to use
10905
11206
  * @param tx The transaction to use
10906
11207
  * @returns The number of documents that match the query
10907
11208
  */
@@ -10927,6 +11228,51 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10927
11228
  }
10928
11229
  return true;
10929
11230
  }
11231
+ /**
11232
+ * 복합 인덱스의 non-primary 필드에 대해 문서가 유효한지 검증합니다.
11233
+ */
11234
+ verifyCompositeConditions(doc, conditions) {
11235
+ if (conditions.length === 0) return true;
11236
+ const flatDoc = this.flattenDocument(doc);
11237
+ for (let i = 0, len = conditions.length; i < len; i++) {
11238
+ const { field, condition } = conditions[i];
11239
+ const docValue = flatDoc[field];
11240
+ if (docValue === void 0) return false;
11241
+ const treeValue = { k: doc._id, v: docValue };
11242
+ if (!this.verifyValue(docValue, condition)) return false;
11243
+ }
11244
+ return true;
11245
+ }
11246
+ /**
11247
+ * 단일 값에 대해 verbose 조건을 검증합니다.
11248
+ */
11249
+ verifyValue(value, condition) {
11250
+ if (typeof condition !== "object" || condition === null) {
11251
+ return value === condition;
11252
+ }
11253
+ if ("primaryEqual" in condition) {
11254
+ return value === condition.primaryEqual?.v;
11255
+ }
11256
+ if ("primaryNotEqual" in condition) {
11257
+ return value !== condition.primaryNotEqual?.v;
11258
+ }
11259
+ if ("primaryLt" in condition) {
11260
+ return value !== null && condition.primaryLt?.v !== void 0 && value < condition.primaryLt.v;
11261
+ }
11262
+ if ("primaryLte" in condition) {
11263
+ return value !== null && condition.primaryLte?.v !== void 0 && value <= condition.primaryLte.v;
11264
+ }
11265
+ if ("primaryGt" in condition) {
11266
+ return value !== null && condition.primaryGt?.v !== void 0 && value > condition.primaryGt.v;
11267
+ }
11268
+ if ("primaryGte" in condition) {
11269
+ return value !== null && condition.primaryGte?.v !== void 0 && value >= condition.primaryGte.v;
11270
+ }
11271
+ if ("primaryOr" in condition && Array.isArray(condition.primaryOr)) {
11272
+ return condition.primaryOr.some((c) => value === c?.v);
11273
+ }
11274
+ return true;
11275
+ }
10930
11276
  /**
10931
11277
  * 메모리 기반으로 청크 크기를 동적 조절합니다.
10932
11278
  */
@@ -10939,10 +11285,9 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10939
11285
  }
10940
11286
  /**
10941
11287
  * Prefetch 방식으로 키 배열을 청크 단위로 조회하여 문서를 순회합니다.
10942
- * FTS 검증 others 후보에 대한 tree.verify() 검증을 통과한 문서만 yield 합니다.
10943
- * 교집합 대신 스트리밍 중 검증하여 첫 결과 반환 시간을 단축합니다.
11288
+ * FTS 검증, 복합 인덱스 검증, others 후보에 대한 tree.verify() 검증을 통과한 문서만 yield 합니다.
10944
11289
  */
10945
- async *processChunkedKeysWithVerify(keys, startIdx, initialChunkSize, ftsConditions, others, tx) {
11290
+ async *processChunkedKeysWithVerify(keys, startIdx, initialChunkSize, ftsConditions, compositeVerifyConditions, others, tx) {
10946
11291
  const verifyOthers = others.filter((o) => !o.isFtsMatch);
10947
11292
  let i = startIdx;
10948
11293
  const totalKeys = keys.length;
@@ -10968,6 +11313,7 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10968
11313
  const doc = JSON.parse(s);
10969
11314
  chunkTotalSize += s.length * 2;
10970
11315
  if (ftsConditions.length > 0 && !this.verifyFts(doc, ftsConditions)) continue;
11316
+ if (compositeVerifyConditions.length > 0 && this.verifyCompositeConditions(doc, compositeVerifyConditions) === false) continue;
10971
11317
  if (verifyOthers.length > 0) {
10972
11318
  const flatDoc = this.flattenDocument(doc);
10973
11319
  let passed = true;
@@ -10993,7 +11339,7 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10993
11339
  }
10994
11340
  /**
10995
11341
  * Select documents from the database
10996
- * @param query The query to use (only indexed fields + _id allowed)
11342
+ * @param query The query to use
10997
11343
  * @param options The options to use
10998
11344
  * @param tx The transaction to use
10999
11345
  * @returns The documents that match the query
@@ -11017,32 +11363,30 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
11017
11363
  } = options;
11018
11364
  const self = this;
11019
11365
  const stream = this.streamWithDefault(async function* (tx2) {
11020
- const metadata = await self.getDocumentInnerMetadata(tx2);
11021
11366
  const ftsConditions = [];
11022
11367
  for (const field in query) {
11023
11368
  const q = query[field];
11024
11369
  if (q && typeof q === "object" && "match" in q && typeof q.match === "string") {
11025
- const indexConfig = metadata.indices[field]?.[1];
11026
- if (typeof indexConfig === "object" && indexConfig?.type === "fts") {
11027
- ftsConditions.push({ field, matchTokens: tokenize(q.match, indexConfig) });
11370
+ const indexNames = self.fieldToIndices.get(field) || [];
11371
+ for (const indexName of indexNames) {
11372
+ const config = self.registeredIndices.get(indexName);
11373
+ if (config && config.type === "fts") {
11374
+ const ftsConfig = self.getFtsConfig(config);
11375
+ if (ftsConfig) {
11376
+ ftsConditions.push({ field, matchTokens: tokenize(q.match, ftsConfig) });
11377
+ }
11378
+ break;
11379
+ }
11028
11380
  }
11029
11381
  }
11030
11382
  }
11031
11383
  const driverResult = await self.getDriverKeys(query, orderByField, sortOrder);
11032
11384
  if (!driverResult) return;
11033
- const { keys, others, rollback } = driverResult;
11385
+ const { keys, others, compositeVerifyConditions, isDriverOrderByField, rollback } = driverResult;
11034
11386
  if (keys.length === 0) {
11035
11387
  rollback();
11036
11388
  return;
11037
11389
  }
11038
- const isQueryEmpty = Object.keys(query).length === 0;
11039
- const normalizedQuery = isQueryEmpty ? { _id: { gte: 0 } } : query;
11040
- const selectivity = await self.getSelectivityCandidate(
11041
- self.verboseQuery(normalizedQuery),
11042
- orderByField
11043
- );
11044
- const isDriverOrderByField = orderByField === void 0 || selectivity && selectivity.driver.field === orderByField;
11045
- if (selectivity) selectivity.rollback();
11046
11390
  try {
11047
11391
  if (!isDriverOrderByField && orderByField) {
11048
11392
  const topK = limit === Infinity ? Infinity : offset + limit;
@@ -11061,6 +11405,7 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
11061
11405
  0,
11062
11406
  self.options.pageSize,
11063
11407
  ftsConditions,
11408
+ compositeVerifyConditions,
11064
11409
  others,
11065
11410
  tx2
11066
11411
  )) {
@@ -11098,6 +11443,7 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
11098
11443
  offset,
11099
11444
  self.options.pageSize,
11100
11445
  ftsConditions,
11446
+ compositeVerifyConditions,
11101
11447
  others,
11102
11448
  tx2
11103
11449
  )) {
@@ -11131,8 +11477,7 @@ var DocumentDataply = class _DocumentDataply {
11131
11477
  static Define() {
11132
11478
  return {
11133
11479
  /**
11134
- * Sets the options for the database, such as index configurations and WAL settings.
11135
- * @template IC The configuration of indices.
11480
+ * Sets the options for the database, such as WAL settings.
11136
11481
  * @param options The database initialization options.
11137
11482
  */
11138
11483
  Options: (options) => _DocumentDataply.Options(options)
@@ -11160,6 +11505,30 @@ var DocumentDataply = class _DocumentDataply {
11160
11505
  constructor(file, options) {
11161
11506
  this.api = new DocumentDataplyAPI(file, options ?? {});
11162
11507
  }
11508
+ /**
11509
+ * Create a named index on the database.
11510
+ * Can be called before or after init().
11511
+ * If called after init(), the index is immediately created and backfilled.
11512
+ * @param name The name of the index
11513
+ * @param option The index configuration (btree or fts)
11514
+ * @param tx Optional transaction
11515
+ * @returns Promise<this> for chaining
11516
+ */
11517
+ async createIndex(name, option, tx) {
11518
+ await this.api.registerIndex(name, option, tx);
11519
+ return this;
11520
+ }
11521
+ /**
11522
+ * Drop (remove) a named index from the database.
11523
+ * The '_id' index cannot be dropped.
11524
+ * @param name The name of the index to drop
11525
+ * @param tx Optional transaction
11526
+ * @returns Promise<this> for chaining
11527
+ */
11528
+ async dropIndex(name, tx) {
11529
+ await this.api.dropIndex(name, tx);
11530
+ return this;
11531
+ }
11163
11532
  /**
11164
11533
  * Initialize the document database
11165
11534
  */
@@ -11167,6 +11536,17 @@ var DocumentDataply = class _DocumentDataply {
11167
11536
  await this.api.init();
11168
11537
  await this.api.backfillIndices();
11169
11538
  }
11539
+ /**
11540
+ * Run a migration if the current schemeVersion is lower than the target version.
11541
+ * The callback is only executed when the database's schemeVersion is below the given version.
11542
+ * After the callback completes, schemeVersion is updated to the target version.
11543
+ * @param version The target scheme version
11544
+ * @param callback The migration callback receiving a transaction
11545
+ * @param tx Optional transaction
11546
+ */
11547
+ async migration(version, callback, tx) {
11548
+ await this.api.migration(version, callback, tx);
11549
+ }
11170
11550
  /**
11171
11551
  * Get the metadata of the document database
11172
11552
  */
@@ -11199,7 +11579,7 @@ var DocumentDataply = class _DocumentDataply {
11199
11579
  }
11200
11580
  /**
11201
11581
  * Fully update documents from the database that match the query
11202
- * @param query The query to use (only indexed fields + _id allowed)
11582
+ * @param query The query to use
11203
11583
  * @param newRecord Complete document to replace with, or function that receives current document and returns new document
11204
11584
  * @param tx The transaction to use
11205
11585
  * @returns The number of updated documents
@@ -11209,7 +11589,7 @@ var DocumentDataply = class _DocumentDataply {
11209
11589
  }
11210
11590
  /**
11211
11591
  * Partially update documents from the database that match the query
11212
- * @param query The query to use (only indexed fields + _id allowed)
11592
+ * @param query The query to use
11213
11593
  * @param newRecord Partial document to merge, or function that receives current document and returns partial update
11214
11594
  * @param tx The transaction to use
11215
11595
  * @returns The number of updated documents
@@ -11219,7 +11599,7 @@ var DocumentDataply = class _DocumentDataply {
11219
11599
  }
11220
11600
  /**
11221
11601
  * Delete documents from the database that match the query
11222
- * @param query The query to use (only indexed fields + _id allowed)
11602
+ * @param query The query to use
11223
11603
  * @param tx The transaction to use
11224
11604
  * @returns The number of deleted documents
11225
11605
  */
@@ -11228,7 +11608,7 @@ var DocumentDataply = class _DocumentDataply {
11228
11608
  }
11229
11609
  /**
11230
11610
  * Count documents from the database that match the query
11231
- * @param query The query to use (only indexed fields + _id allowed)
11611
+ * @param query The query to use
11232
11612
  * @param tx The transaction to use
11233
11613
  * @returns The number of documents that match the query
11234
11614
  */
@@ -11237,7 +11617,7 @@ var DocumentDataply = class _DocumentDataply {
11237
11617
  }
11238
11618
  /**
11239
11619
  * Select documents from the database
11240
- * @param query The query to use (only indexed fields + _id allowed)
11620
+ * @param query The query to use
11241
11621
  * @param options The options to use
11242
11622
  * @param tx The transaction to use
11243
11623
  * @returns The documents that match the query