document-dataply 0.0.8 → 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
@@ -6206,6 +6206,22 @@ var require_cjs = __commonJS({
6206
6206
  }
6207
6207
  return (crc ^ -1) >>> 0;
6208
6208
  }
6209
+ function getMinMaxValue(array) {
6210
+ let i = 0;
6211
+ let min = Infinity;
6212
+ let max = -Infinity;
6213
+ let len = array.length;
6214
+ while (i < len) {
6215
+ if (array[i] < min) {
6216
+ min = array[i];
6217
+ }
6218
+ if (array[i] > max) {
6219
+ max = array[i];
6220
+ }
6221
+ i++;
6222
+ }
6223
+ return [min, max];
6224
+ }
6209
6225
  var Row = class _Row {
6210
6226
  static CONSTANT = {
6211
6227
  FLAG_DELETED: 0,
@@ -9120,8 +9136,7 @@ var require_cjs = __commonJS({
9120
9136
  for (let i = 0, len = pks.length; i < len; i++) {
9121
9137
  pkIndexMap.set(pks[i], i);
9122
9138
  }
9123
- const minPk = Math.min(...pks);
9124
- const maxPk = Math.max(...pks);
9139
+ const [minPk, maxPk] = getMinMaxValue(pks);
9125
9140
  const pkRidPairs = new Array(pks.length).fill(null);
9126
9141
  const btx = await this.getBPTreeTransaction(tx);
9127
9142
  const stream = btx.whereStream({ gte: minPk, lte: maxPk });
@@ -10028,22 +10043,46 @@ var DocumentSerializeStrategyAsync = class extends import_dataply.SerializeStrat
10028
10043
 
10029
10044
  // src/core/bptree/documentComparator.ts
10030
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
+ }
10031
10074
  var DocumentValueComparator = class extends import_dataply2.ValueComparator {
10032
10075
  primaryAsc(a, b) {
10033
- if (typeof a.v !== "string" || typeof b.v !== "string") {
10034
- return +a.v - +b.v;
10035
- }
10036
- return a.v.localeCompare(b.v);
10076
+ return compareValue(a.v, b.v);
10037
10077
  }
10038
10078
  asc(a, b) {
10039
- if (typeof a.v !== "string" || typeof b.v !== "string") {
10040
- const diff2 = +a.v - +b.v;
10041
- return diff2 === 0 ? a.k - b.k : diff2;
10042
- }
10043
- const diff = a.v.localeCompare(b.v);
10079
+ const diff = compareValue(a.v, b.v);
10044
10080
  return diff === 0 ? a.k - b.k : diff;
10045
10081
  }
10046
10082
  match(value) {
10083
+ if (Array.isArray(value.v)) {
10084
+ return value.v[0] + "";
10085
+ }
10047
10086
  return value.v + "";
10048
10087
  }
10049
10088
  };
@@ -10156,7 +10195,23 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10156
10195
  comparator = new DocumentValueComparator();
10157
10196
  pendingBackfillFields = [];
10158
10197
  lock;
10198
+ _initialized = false;
10159
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();
10160
10215
  operatorConverters = {
10161
10216
  equal: "primaryEqual",
10162
10217
  notEqual: "primaryNotEqual",
@@ -10172,11 +10227,6 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10172
10227
  this.trees = /* @__PURE__ */ new Map();
10173
10228
  this.lock = new import_dataply3.Ryoiki();
10174
10229
  this.indexedFields = /* @__PURE__ */ new Set(["_id"]);
10175
- if (options?.indices) {
10176
- for (const field of Object.keys(options.indices)) {
10177
- this.indexedFields.add(field);
10178
- }
10179
- }
10180
10230
  this.hook.onceAfter("init", async (tx, isNewlyCreated) => {
10181
10231
  if (isNewlyCreated) {
10182
10232
  await this.initializeDocumentFile(tx);
@@ -10185,31 +10235,30 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10185
10235
  throw new Error("Document metadata verification failed");
10186
10236
  }
10187
10237
  const metadata = await this.getDocumentInnerMetadata(tx);
10188
- const optionsIndices = options.indices ?? {};
10189
- const targetIndices = {
10190
- ...optionsIndices,
10191
- _id: true
10192
- };
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
+ }
10193
10244
  const backfillTargets = [];
10194
10245
  let isMetadataChanged = false;
10195
- for (const field in targetIndices) {
10196
- const isBackfillEnabled = targetIndices[field];
10197
- const existingIndex = metadata.indices[field];
10246
+ for (const [indexName, config] of targetIndices) {
10247
+ const existingIndex = metadata.indices[indexName];
10198
10248
  if (!existingIndex) {
10199
- metadata.indices[field] = [-1, isBackfillEnabled];
10249
+ metadata.indices[indexName] = [-1, config];
10200
10250
  isMetadataChanged = true;
10201
- if (isBackfillEnabled && !isNewlyCreated) {
10202
- backfillTargets.push(field);
10251
+ if (!isNewlyCreated) {
10252
+ backfillTargets.push(indexName);
10203
10253
  }
10204
10254
  } else {
10205
- const [_pk, isMetaBackfillEnabled] = existingIndex;
10206
- if (isBackfillEnabled && !isMetaBackfillEnabled) {
10207
- metadata.indices[field][1] = isBackfillEnabled;
10208
- isMetadataChanged = true;
10209
- backfillTargets.push(field);
10210
- } else if (!isBackfillEnabled && isMetaBackfillEnabled) {
10211
- metadata.indices[field][1] = false;
10255
+ const [_pk, existingConfig] = existingIndex;
10256
+ if (JSON.stringify(existingConfig) !== JSON.stringify(config)) {
10257
+ metadata.indices[indexName] = [_pk, config];
10212
10258
  isMetadataChanged = true;
10259
+ if (!isNewlyCreated) {
10260
+ backfillTargets.push(indexName);
10261
+ }
10213
10262
  }
10214
10263
  }
10215
10264
  }
@@ -10217,25 +10266,220 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10217
10266
  await this.updateDocumentInnerMetadata(metadata, tx);
10218
10267
  }
10219
10268
  this.indices = metadata.indices;
10220
- for (const field in this.indices) {
10221
- 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]) {
10222
10284
  const tree = new import_dataply3.BPTreeAsync(
10223
10285
  new DocumentSerializeStrategyAsync(
10224
10286
  this.rowTableEngine.order,
10225
10287
  this,
10226
10288
  this.txContext,
10227
- field
10289
+ indexName
10228
10290
  ),
10229
10291
  this.comparator
10230
10292
  );
10231
10293
  await tree.init();
10232
- this.trees.set(field, tree);
10294
+ this.trees.set(indexName, tree);
10233
10295
  }
10234
10296
  }
10235
10297
  this.pendingBackfillFields = backfillTargets;
10298
+ this._initialized = true;
10236
10299
  return tx;
10237
10300
  });
10238
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
+ }
10239
10483
  async getDocument(pk, tx) {
10240
10484
  return this.runWithDefault(async (tx2) => {
10241
10485
  const row = await this.select(pk, false, tx2);
@@ -10264,10 +10508,9 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10264
10508
  });
10265
10509
  }
10266
10510
  /**
10267
- * Backfill indices for fields that were added with `true` option after data was inserted.
10268
- * This method should be called after `init()` if you want to index existing documents
10269
- * for newly added index fields.
10270
- *
10511
+ * Backfill indices for newly created indices after data was inserted.
10512
+ * This method should be called after `init()`.
10513
+ *
10271
10514
  * @returns Number of documents that were backfilled
10272
10515
  */
10273
10516
  async backfillIndices(tx) {
@@ -10280,12 +10523,12 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10280
10523
  if (metadata.lastId === 0) {
10281
10524
  return 0;
10282
10525
  }
10283
- const fieldTxMap = {};
10284
- const fieldMap = /* @__PURE__ */ new Map();
10285
- for (const field of backfillTargets) {
10286
- const tree = this.trees.get(field);
10287
- if (tree && field !== "_id") {
10288
- 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();
10289
10532
  }
10290
10533
  }
10291
10534
  let backfilledCount = 0;
@@ -10300,35 +10543,44 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10300
10543
  const doc = await this.getDocument(k, tx2);
10301
10544
  if (!doc) continue;
10302
10545
  const flatDoc = this.flattenDocument(doc);
10303
- for (const field of backfillTargets) {
10304
- if (!(field in flatDoc) || // 문서에 해당 필드가 없음
10305
- !(field in fieldTxMap)) {
10306
- continue;
10307
- }
10308
- const v = flatDoc[field];
10309
- const btx = fieldTxMap[field];
10310
- const indexConfig = metadata.indices[field]?.[1];
10311
- const isFts = typeof indexConfig === "object" && indexConfig?.type === "fts" && typeof v === "string";
10312
- let tokens = [v];
10313
- if (isFts) {
10314
- tokens = tokenize(v, indexConfig);
10315
- }
10316
- const batchInsertData = [];
10317
- for (let i = 0, len = tokens.length; i < len; i++) {
10318
- const token = tokens[i];
10319
- const keyToInsert = isFts ? this.getTokenKey(k, token) : k;
10320
- const entry = { k, v: token };
10321
- batchInsertData.push([keyToInsert, entry]);
10322
- if (!fieldMap.has(btx)) {
10323
- 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 });
10324
10567
  }
10325
- fieldMap.get(btx).push({ k: keyToInsert, v: entry });
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, []);
10576
+ }
10577
+ indexEntryMap.get(btx).push(entry);
10578
+ await btx.batchInsert(batchInsertData);
10326
10579
  }
10327
- await btx.batchInsert(batchInsertData);
10328
10580
  }
10329
10581
  backfilledCount++;
10330
10582
  }
10331
- const btxs = Object.values(fieldTxMap);
10583
+ const btxs = Object.values(indexTxMap);
10332
10584
  const success = [];
10333
10585
  try {
10334
10586
  for (const btx of btxs) {
@@ -10340,7 +10592,7 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10340
10592
  await btx.rollback();
10341
10593
  }
10342
10594
  for (const btx of success) {
10343
- const entries = fieldMap.get(btx);
10595
+ const entries = indexEntryMap.get(btx);
10344
10596
  if (!entries) continue;
10345
10597
  for (const entry of entries) {
10346
10598
  await btx.delete(entry.k, entry);
@@ -10359,6 +10611,7 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10359
10611
  createdAt: Date.now(),
10360
10612
  updatedAt: Date.now(),
10361
10613
  lastId: 0,
10614
+ schemeVersion: 0,
10362
10615
  indices
10363
10616
  };
10364
10617
  }
@@ -10368,7 +10621,7 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10368
10621
  throw new Error("Document metadata already exists");
10369
10622
  }
10370
10623
  const metaObj = this.createDocumentInnerMetadata({
10371
- _id: [-1, true]
10624
+ _id: [-1, { type: "btree", fields: ["_id"] }]
10372
10625
  });
10373
10626
  await this.insertAsOverflow(JSON.stringify(metaObj), false, tx);
10374
10627
  }
@@ -10393,18 +10646,27 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10393
10646
  }
10394
10647
  /**
10395
10648
  * returns flattened document
10396
- * @param document
10397
- * @returns
10649
+ * @param document
10650
+ * @returns
10398
10651
  */
10399
10652
  flattenDocument(document) {
10400
10653
  return this.flatten(document, "", {});
10401
10654
  }
10402
10655
  async getDocumentMetadata(tx) {
10403
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
+ }
10404
10664
  return {
10405
10665
  pageSize: metadata.pageSize,
10406
10666
  pageCount: metadata.pageCount,
10407
- rowCount: metadata.rowCount
10667
+ rowCount: metadata.rowCount,
10668
+ indices,
10669
+ schemeVersion: innerMetadata.schemeVersion ?? 0
10408
10670
  };
10409
10671
  }
10410
10672
  async getDocumentInnerMetadata(tx) {
@@ -10417,6 +10679,25 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10417
10679
  async updateDocumentInnerMetadata(metadata, tx) {
10418
10680
  await this.update(1, JSON.stringify(metadata), tx);
10419
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
+ }
10420
10701
  /**
10421
10702
  * Transforms a query object into a verbose query object
10422
10703
  * @param query The query object to transform
@@ -10455,33 +10736,74 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10455
10736
  return result;
10456
10737
  }
10457
10738
  /**
10458
- * Get the selectivity candidate for the given query
10459
- * @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
10460
10743
  * @param orderByField Optional field name for orderBy optimization
10461
10744
  * @returns Driver and other candidates for query execution
10462
10745
  */
10463
10746
  async getSelectivityCandidate(query, orderByField) {
10747
+ const queryFields = new Set(Object.keys(query));
10464
10748
  const candidates = [];
10465
- const metadata = await this.getDocumentInnerMetadata(this.txContext.get());
10466
- for (const field in query) {
10467
- const tree = this.trees.get(field);
10749
+ for (const [indexName, config] of this.registeredIndices) {
10750
+ const tree = this.trees.get(indexName);
10468
10751
  if (!tree) continue;
10469
- const condition = query[field];
10470
- const treeTx = await tree.createTransaction();
10471
- const indexConfig = metadata.indices[field]?.[1];
10472
- let isFtsMatch = false;
10473
- let matchTokens;
10474
- if (typeof indexConfig === "object" && indexConfig?.type === "fts" && condition.match) {
10475
- isFtsMatch = true;
10476
- matchTokens = tokenize(condition.match, indexConfig);
10477
- }
10478
- candidates.push({
10479
- tree: treeTx,
10480
- condition,
10481
- field,
10482
- isFtsMatch,
10483
- matchTokens
10484
- });
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
+ }
10485
10807
  }
10486
10808
  const rollback = () => {
10487
10809
  for (const { tree } of candidates) {
@@ -10492,41 +10814,19 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10492
10814
  rollback();
10493
10815
  return null;
10494
10816
  }
10495
- if (orderByField) {
10496
- const orderByCandidate = candidates.find((c) => c.field === orderByField);
10497
- if (orderByCandidate) {
10498
- return {
10499
- driver: orderByCandidate,
10500
- others: candidates.filter((c) => c.field !== orderByField),
10501
- rollback
10502
- };
10503
- }
10504
- }
10505
- const ftsCandidate = candidates.find(
10506
- (c) => c.isFtsMatch && c.matchTokens && c.matchTokens.length > 0
10507
- );
10508
- if (ftsCandidate) {
10509
- const hasHigherPriority = candidates.some((c) => {
10510
- if (c === ftsCandidate) return false;
10511
- const cond = c.condition;
10512
- return "equal" in cond || "primaryEqual" in cond;
10513
- });
10514
- if (!hasHigherPriority) {
10515
- return {
10516
- driver: ftsCandidate,
10517
- others: candidates.filter((c) => c !== ftsCandidate),
10518
- rollback
10519
- };
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] });
10520
10824
  }
10521
10825
  }
10522
- let res = import_dataply3.BPTreeAsync.ChooseDriver(candidates);
10523
- if (!res && candidates.length > 0) {
10524
- res = candidates[0];
10525
- }
10526
- if (!res) return null;
10527
10826
  return {
10528
- driver: res,
10529
- others: candidates.filter((c) => c.tree !== res.tree),
10827
+ driver,
10828
+ others,
10829
+ compositeVerifyConditions,
10530
10830
  rollback
10531
10831
  };
10532
10832
  }
@@ -10619,7 +10919,7 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10619
10919
  orderBy
10620
10920
  );
10621
10921
  if (!selectivity) return null;
10622
- const { driver, others, rollback } = selectivity;
10922
+ const { driver, others, compositeVerifyConditions, rollback } = selectivity;
10623
10923
  const useIndexOrder = orderBy === void 0 || driver.field === orderBy;
10624
10924
  const currentOrder = useIndexOrder ? sortOrder : void 0;
10625
10925
  let keys;
@@ -10636,6 +10936,8 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10636
10936
  return {
10637
10937
  keys: new Float64Array(Array.from(keys)),
10638
10938
  others,
10939
+ compositeVerifyConditions,
10940
+ isDriverOrderByField: useIndexOrder,
10639
10941
  rollback
10640
10942
  };
10641
10943
  }
@@ -10662,25 +10964,27 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10662
10964
  async insertSingleDocument(document, tx) {
10663
10965
  return this.writeLock(() => this.runWithDefault(async (tx2) => {
10664
10966
  const { pk: dpk, document: dataplyDocument } = await this.insertDocumentInternal(document, tx2);
10665
- const metadata = await this.getDocumentInnerMetadata(tx2);
10666
10967
  const flattenDocument = this.flattenDocument(dataplyDocument);
10667
- for (const field in flattenDocument) {
10668
- const tree = this.trees.get(field);
10968
+ for (const [indexName, config] of this.registeredIndices) {
10969
+ const tree = this.trees.get(indexName);
10669
10970
  if (!tree) continue;
10670
- const v = flattenDocument[field];
10671
- const indexConfig = metadata.indices[field]?.[1];
10672
- let tokens = [v];
10673
- const isFts = typeof indexConfig === "object" && indexConfig?.type === "fts" && typeof v === "string";
10674
- if (isFts) {
10675
- tokens = tokenize(v, indexConfig);
10676
- }
10677
- for (let i = 0, len = tokens.length; i < len; i++) {
10678
- const token = tokens[i];
10679
- const keyToInsert = isFts ? this.getTokenKey(dpk, token) : dpk;
10680
- const [error] = await catchPromise(tree.insert(keyToInsert, { k: dpk, v: token }));
10681
- if (error) {
10682
- 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;
10683
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;
10684
10988
  }
10685
10989
  }
10686
10990
  return dataplyDocument._id;
@@ -10716,23 +11020,30 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10716
11020
  for (let i = 0, len = pks.length; i < len; i++) {
10717
11021
  flattenedData[i].pk = pks[i];
10718
11022
  }
10719
- 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;
10720
11026
  const treeTx = await tree.createTransaction();
10721
- const indexConfig = metadata.indices[field]?.[1];
10722
11027
  const batchInsertData = [];
10723
- for (let i = 0, len = flattenedData.length; i < len; i++) {
10724
- const item = flattenedData[i];
10725
- const v = item.data[field];
10726
- if (v === void 0) continue;
10727
- const isFts = typeof indexConfig === "object" && indexConfig?.type === "fts" && typeof v === "string";
10728
- let tokens = [v];
10729
- if (isFts) {
10730
- 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
+ }
10731
11040
  }
10732
- for (let j = 0, len2 = tokens.length; j < len2; j++) {
10733
- const token = tokens[j];
10734
- const keyToInsert = isFts ? this.getTokenKey(item.pk, token) : item.pk;
10735
- 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 }]);
10736
11047
  }
10737
11048
  }
10738
11049
  const [error] = await catchPromise(treeTx.batchInsert(batchInsertData));
@@ -10758,8 +11069,8 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10758
11069
  const pks = await this.getKeys(query);
10759
11070
  let updatedCount = 0;
10760
11071
  const treeTxs = /* @__PURE__ */ new Map();
10761
- for (const [field, tree] of this.trees) {
10762
- treeTxs.set(field, await tree.createTransaction());
11072
+ for (const [indexName, tree] of this.trees) {
11073
+ treeTxs.set(indexName, await tree.createTransaction());
10763
11074
  }
10764
11075
  treeTxs.delete("_id");
10765
11076
  for (let i = 0, len = pks.length; i < len; i++) {
@@ -10769,42 +11080,45 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10769
11080
  const updatedDoc = computeUpdatedDoc(doc);
10770
11081
  const oldFlatDoc = this.flattenDocument(doc);
10771
11082
  const newFlatDoc = this.flattenDocument(updatedDoc);
10772
- const metadata = await this.getDocumentInnerMetadata(tx);
10773
- for (const [field, treeTx] of treeTxs) {
10774
- const oldV = oldFlatDoc[field];
10775
- const newV = newFlatDoc[field];
10776
- if (oldV === newV) continue;
10777
- const indexConfig = metadata.indices[field]?.[1];
10778
- const isFts = typeof indexConfig === "object" && indexConfig?.type === "fts";
10779
- if (field in oldFlatDoc) {
10780
- let oldTokens = [oldV];
10781
- if (isFts && typeof oldV === "string") {
10782
- 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
+ }
10783
11097
  }
10784
- for (let j = 0, len2 = oldTokens.length; j < len2; j++) {
10785
- const oldToken = oldTokens[j];
10786
- const keyToDelete = isFts ? this.getTokenKey(pk, oldToken) : pk;
10787
- 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);
10788
11105
  }
10789
- }
10790
- if (field in newFlatDoc) {
10791
- let newTokens = [newV];
10792
- if (isFts && typeof newV === "string") {
10793
- 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 });
10794
11112
  }
10795
- const batchInsertData = [];
10796
- for (let j = 0, len2 = newTokens.length; j < len2; j++) {
10797
- const newToken = newTokens[j];
10798
- const keyToInsert = isFts ? this.getTokenKey(pk, newToken) : pk;
10799
- batchInsertData.push([keyToInsert, { k: pk, v: newToken }]);
11113
+ if (newIndexVal !== void 0) {
11114
+ await treeTx.batchInsert([[pk, { k: pk, v: newIndexVal }]]);
10800
11115
  }
10801
- await treeTx.batchInsert(batchInsertData);
10802
11116
  }
10803
11117
  }
10804
11118
  await this.update(pk, JSON.stringify(updatedDoc), tx);
10805
11119
  updatedCount++;
10806
11120
  }
10807
- for (const [field, treeTx] of treeTxs) {
11121
+ for (const [indexName, treeTx] of treeTxs) {
10808
11122
  const result = await treeTx.commit();
10809
11123
  if (!result.success) {
10810
11124
  for (const rollbackTx of treeTxs.values()) {
@@ -10817,7 +11131,7 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10817
11131
  }
10818
11132
  /**
10819
11133
  * Fully update documents from the database that match the query
10820
- * @param query The query to use (only indexed fields + _id allowed)
11134
+ * @param query The query to use
10821
11135
  * @param newRecord Complete document to replace with, or function that receives current document and returns new document
10822
11136
  * @param tx The transaction to use
10823
11137
  * @returns The number of updated documents
@@ -10832,7 +11146,7 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10832
11146
  }
10833
11147
  /**
10834
11148
  * Partially update documents from the database that match the query
10835
- * @param query The query to use (only indexed fields + _id allowed)
11149
+ * @param query The query to use
10836
11150
  * @param newRecord Partial document to merge, or function that receives current document and returns partial update
10837
11151
  * @param tx The transaction to use
10838
11152
  * @returns The number of updated documents
@@ -10849,7 +11163,7 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10849
11163
  }
10850
11164
  /**
10851
11165
  * Delete documents from the database that match the query
10852
- * @param query The query to use (only indexed fields + _id allowed)
11166
+ * @param query The query to use
10853
11167
  * @param tx The transaction to use
10854
11168
  * @returns The number of deleted documents
10855
11169
  */
@@ -10862,20 +11176,22 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10862
11176
  const doc = await this.getDocument(pk, tx2);
10863
11177
  if (!doc) continue;
10864
11178
  const flatDoc = this.flattenDocument(doc);
10865
- const metadata = await this.getDocumentInnerMetadata(tx2);
10866
- for (const [field, tree] of this.trees) {
10867
- const v = flatDoc[field];
10868
- if (v === void 0) continue;
10869
- const indexConfig = metadata.indices[field]?.[1];
10870
- const isFts = typeof indexConfig === "object" && indexConfig?.type === "fts" && typeof v === "string";
10871
- let tokens = [v];
10872
- if (isFts) {
10873
- tokens = tokenize(v, indexConfig);
10874
- }
10875
- for (let j = 0, len2 = tokens.length; j < len2; j++) {
10876
- const token = tokens[j];
10877
- const keyToDelete = isFts ? this.getTokenKey(pk, token) : pk;
10878
- 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 });
10879
11195
  }
10880
11196
  }
10881
11197
  await super.delete(pk, true, tx2);
@@ -10886,7 +11202,7 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10886
11202
  }
10887
11203
  /**
10888
11204
  * Count documents from the database that match the query
10889
- * @param query The query to use (only indexed fields + _id allowed)
11205
+ * @param query The query to use
10890
11206
  * @param tx The transaction to use
10891
11207
  * @returns The number of documents that match the query
10892
11208
  */
@@ -10912,6 +11228,51 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10912
11228
  }
10913
11229
  return true;
10914
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
+ }
10915
11276
  /**
10916
11277
  * 메모리 기반으로 청크 크기를 동적 조절합니다.
10917
11278
  */
@@ -10924,10 +11285,9 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10924
11285
  }
10925
11286
  /**
10926
11287
  * Prefetch 방식으로 키 배열을 청크 단위로 조회하여 문서를 순회합니다.
10927
- * FTS 검증 others 후보에 대한 tree.verify() 검증을 통과한 문서만 yield 합니다.
10928
- * 교집합 대신 스트리밍 중 검증하여 첫 결과 반환 시간을 단축합니다.
11288
+ * FTS 검증, 복합 인덱스 검증, others 후보에 대한 tree.verify() 검증을 통과한 문서만 yield 합니다.
10929
11289
  */
10930
- async *processChunkedKeysWithVerify(keys, startIdx, initialChunkSize, ftsConditions, others, tx) {
11290
+ async *processChunkedKeysWithVerify(keys, startIdx, initialChunkSize, ftsConditions, compositeVerifyConditions, others, tx) {
10931
11291
  const verifyOthers = others.filter((o) => !o.isFtsMatch);
10932
11292
  let i = startIdx;
10933
11293
  const totalKeys = keys.length;
@@ -10953,6 +11313,7 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10953
11313
  const doc = JSON.parse(s);
10954
11314
  chunkTotalSize += s.length * 2;
10955
11315
  if (ftsConditions.length > 0 && !this.verifyFts(doc, ftsConditions)) continue;
11316
+ if (compositeVerifyConditions.length > 0 && this.verifyCompositeConditions(doc, compositeVerifyConditions) === false) continue;
10956
11317
  if (verifyOthers.length > 0) {
10957
11318
  const flatDoc = this.flattenDocument(doc);
10958
11319
  let passed = true;
@@ -10978,7 +11339,7 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10978
11339
  }
10979
11340
  /**
10980
11341
  * Select documents from the database
10981
- * @param query The query to use (only indexed fields + _id allowed)
11342
+ * @param query The query to use
10982
11343
  * @param options The options to use
10983
11344
  * @param tx The transaction to use
10984
11345
  * @returns The documents that match the query
@@ -11002,32 +11363,30 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
11002
11363
  } = options;
11003
11364
  const self = this;
11004
11365
  const stream = this.streamWithDefault(async function* (tx2) {
11005
- const metadata = await self.getDocumentInnerMetadata(tx2);
11006
11366
  const ftsConditions = [];
11007
11367
  for (const field in query) {
11008
11368
  const q = query[field];
11009
11369
  if (q && typeof q === "object" && "match" in q && typeof q.match === "string") {
11010
- const indexConfig = metadata.indices[field]?.[1];
11011
- if (typeof indexConfig === "object" && indexConfig?.type === "fts") {
11012
- 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
+ }
11013
11380
  }
11014
11381
  }
11015
11382
  }
11016
11383
  const driverResult = await self.getDriverKeys(query, orderByField, sortOrder);
11017
11384
  if (!driverResult) return;
11018
- const { keys, others, rollback } = driverResult;
11385
+ const { keys, others, compositeVerifyConditions, isDriverOrderByField, rollback } = driverResult;
11019
11386
  if (keys.length === 0) {
11020
11387
  rollback();
11021
11388
  return;
11022
11389
  }
11023
- const isQueryEmpty = Object.keys(query).length === 0;
11024
- const normalizedQuery = isQueryEmpty ? { _id: { gte: 0 } } : query;
11025
- const selectivity = await self.getSelectivityCandidate(
11026
- self.verboseQuery(normalizedQuery),
11027
- orderByField
11028
- );
11029
- const isDriverOrderByField = orderByField === void 0 || selectivity && selectivity.driver.field === orderByField;
11030
- if (selectivity) selectivity.rollback();
11031
11390
  try {
11032
11391
  if (!isDriverOrderByField && orderByField) {
11033
11392
  const topK = limit === Infinity ? Infinity : offset + limit;
@@ -11046,6 +11405,7 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
11046
11405
  0,
11047
11406
  self.options.pageSize,
11048
11407
  ftsConditions,
11408
+ compositeVerifyConditions,
11049
11409
  others,
11050
11410
  tx2
11051
11411
  )) {
@@ -11083,6 +11443,7 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
11083
11443
  offset,
11084
11444
  self.options.pageSize,
11085
11445
  ftsConditions,
11446
+ compositeVerifyConditions,
11086
11447
  others,
11087
11448
  tx2
11088
11449
  )) {
@@ -11116,8 +11477,7 @@ var DocumentDataply = class _DocumentDataply {
11116
11477
  static Define() {
11117
11478
  return {
11118
11479
  /**
11119
- * Sets the options for the database, such as index configurations and WAL settings.
11120
- * @template IC The configuration of indices.
11480
+ * Sets the options for the database, such as WAL settings.
11121
11481
  * @param options The database initialization options.
11122
11482
  */
11123
11483
  Options: (options) => _DocumentDataply.Options(options)
@@ -11145,6 +11505,30 @@ var DocumentDataply = class _DocumentDataply {
11145
11505
  constructor(file, options) {
11146
11506
  this.api = new DocumentDataplyAPI(file, options ?? {});
11147
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
+ }
11148
11532
  /**
11149
11533
  * Initialize the document database
11150
11534
  */
@@ -11152,6 +11536,17 @@ var DocumentDataply = class _DocumentDataply {
11152
11536
  await this.api.init();
11153
11537
  await this.api.backfillIndices();
11154
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
+ }
11155
11550
  /**
11156
11551
  * Get the metadata of the document database
11157
11552
  */
@@ -11184,7 +11579,7 @@ var DocumentDataply = class _DocumentDataply {
11184
11579
  }
11185
11580
  /**
11186
11581
  * Fully update documents from the database that match the query
11187
- * @param query The query to use (only indexed fields + _id allowed)
11582
+ * @param query The query to use
11188
11583
  * @param newRecord Complete document to replace with, or function that receives current document and returns new document
11189
11584
  * @param tx The transaction to use
11190
11585
  * @returns The number of updated documents
@@ -11194,7 +11589,7 @@ var DocumentDataply = class _DocumentDataply {
11194
11589
  }
11195
11590
  /**
11196
11591
  * Partially update documents from the database that match the query
11197
- * @param query The query to use (only indexed fields + _id allowed)
11592
+ * @param query The query to use
11198
11593
  * @param newRecord Partial document to merge, or function that receives current document and returns partial update
11199
11594
  * @param tx The transaction to use
11200
11595
  * @returns The number of updated documents
@@ -11204,7 +11599,7 @@ var DocumentDataply = class _DocumentDataply {
11204
11599
  }
11205
11600
  /**
11206
11601
  * Delete documents from the database that match the query
11207
- * @param query The query to use (only indexed fields + _id allowed)
11602
+ * @param query The query to use
11208
11603
  * @param tx The transaction to use
11209
11604
  * @returns The number of deleted documents
11210
11605
  */
@@ -11213,7 +11608,7 @@ var DocumentDataply = class _DocumentDataply {
11213
11608
  }
11214
11609
  /**
11215
11610
  * Count documents from the database that match the query
11216
- * @param query The query to use (only indexed fields + _id allowed)
11611
+ * @param query The query to use
11217
11612
  * @param tx The transaction to use
11218
11613
  * @returns The number of documents that match the query
11219
11614
  */
@@ -11222,7 +11617,7 @@ var DocumentDataply = class _DocumentDataply {
11222
11617
  }
11223
11618
  /**
11224
11619
  * Select documents from the database
11225
- * @param query The query to use (only indexed fields + _id allowed)
11620
+ * @param query The query to use
11226
11621
  * @param options The options to use
11227
11622
  * @param tx The transaction to use
11228
11623
  * @returns The documents that match the query