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

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
@@ -9990,6 +9990,11 @@ var DocumentSerializeStrategyAsync = class extends import_dataply.SerializeStrat
9990
9990
  this.txContext = txContext;
9991
9991
  this.treeKey = treeKey;
9992
9992
  }
9993
+ /**
9994
+ * readHead에서 할당된 headPk를 캐싱하여
9995
+ * writeHead에서 AsyncLocalStorage 컨텍스트 유실 시에도 사용할 수 있도록 함
9996
+ */
9997
+ cachedHeadPk = null;
9993
9998
  async id(isLeaf) {
9994
9999
  const tx = this.txContext.get();
9995
10000
  const pk = await this.api.insertAsOverflow("__BPTREE_NODE_PLACEHOLDER__", false, tx);
@@ -10022,20 +10027,25 @@ var DocumentSerializeStrategyAsync = class extends import_dataply.SerializeStrat
10022
10027
  const pk = await this.api.insertAsOverflow("__BPTREE_HEAD_PLACEHOLDER__", false, tx);
10023
10028
  metadata.indices[this.treeKey][0] = pk;
10024
10029
  await this.api.updateDocumentInnerMetadata(metadata, tx);
10030
+ this.cachedHeadPk = pk;
10025
10031
  return null;
10026
10032
  }
10033
+ this.cachedHeadPk = headPk;
10027
10034
  const row = await this.api.select(headPk, false, tx);
10028
10035
  if (row === null || row === "" || row.startsWith("__BPTREE_")) return null;
10029
10036
  return JSON.parse(row);
10030
10037
  }
10031
10038
  async writeHead(head) {
10032
10039
  const tx = this.txContext.get();
10033
- const metadata = await this.api.getDocumentInnerMetadata(tx);
10034
- const indexInfo = metadata.indices[this.treeKey];
10035
- if (!indexInfo) {
10036
- throw new Error(`Index info not found for tree: ${this.treeKey}. Initialization should be handled outside.`);
10040
+ let headPk = this.cachedHeadPk;
10041
+ if (headPk === null) {
10042
+ const metadata = await this.api.getDocumentInnerMetadata(tx);
10043
+ const indexInfo = metadata.indices[this.treeKey];
10044
+ if (!indexInfo) {
10045
+ throw new Error(`Index info not found for tree: ${this.treeKey}. Initialization should be handled outside.`);
10046
+ }
10047
+ headPk = indexInfo[0];
10037
10048
  }
10038
- const headPk = indexInfo[0];
10039
10049
  const json = JSON.stringify(head);
10040
10050
  await this.api.update(headPk, json, tx);
10041
10051
  }
@@ -10043,22 +10053,46 @@ var DocumentSerializeStrategyAsync = class extends import_dataply.SerializeStrat
10043
10053
 
10044
10054
  // src/core/bptree/documentComparator.ts
10045
10055
  var import_dataply2 = __toESM(require_cjs());
10056
+ function comparePrimitive(a, b) {
10057
+ if (a === b) return 0;
10058
+ if (a === null) return -1;
10059
+ if (b === null) return 1;
10060
+ if (typeof a !== typeof b) {
10061
+ const typeOrder = (v) => typeof v === "boolean" ? 0 : typeof v === "number" ? 1 : 2;
10062
+ return typeOrder(a) - typeOrder(b);
10063
+ }
10064
+ if (typeof a === "string" && typeof b === "string") {
10065
+ return a.localeCompare(b);
10066
+ }
10067
+ return +a - +b;
10068
+ }
10069
+ function compareValue(a, b) {
10070
+ const aArr = Array.isArray(a);
10071
+ const bArr = Array.isArray(b);
10072
+ if (!aArr && !bArr) {
10073
+ return comparePrimitive(a, b);
10074
+ }
10075
+ const aList = aArr ? a : [a];
10076
+ const bList = bArr ? b : [b];
10077
+ const len = Math.min(aList.length, bList.length);
10078
+ for (let i = 0; i < len; i++) {
10079
+ const diff = comparePrimitive(aList[i], bList[i]);
10080
+ if (diff !== 0) return diff;
10081
+ }
10082
+ return aList.length - bList.length;
10083
+ }
10046
10084
  var DocumentValueComparator = class extends import_dataply2.ValueComparator {
10047
10085
  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);
10086
+ return compareValue(a.v, b.v);
10052
10087
  }
10053
10088
  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);
10089
+ const diff = compareValue(a.v, b.v);
10059
10090
  return diff === 0 ? a.k - b.k : diff;
10060
10091
  }
10061
10092
  match(value) {
10093
+ if (Array.isArray(value.v)) {
10094
+ return value.v[0] + "";
10095
+ }
10062
10096
  return value.v + "";
10063
10097
  }
10064
10098
  };
@@ -10171,7 +10205,23 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10171
10205
  comparator = new DocumentValueComparator();
10172
10206
  pendingBackfillFields = [];
10173
10207
  lock;
10208
+ _initialized = false;
10174
10209
  indexedFields;
10210
+ /**
10211
+ * Registered indices via createIndex() (before init)
10212
+ * Key: index name, Value: index configuration
10213
+ */
10214
+ pendingCreateIndices = /* @__PURE__ */ new Map();
10215
+ /**
10216
+ * Resolved index configurations after init.
10217
+ * Key: index name, Value: index config (from metadata)
10218
+ */
10219
+ registeredIndices = /* @__PURE__ */ new Map();
10220
+ /**
10221
+ * Maps field name → index names that cover this field.
10222
+ * Used for query resolution.
10223
+ */
10224
+ fieldToIndices = /* @__PURE__ */ new Map();
10175
10225
  operatorConverters = {
10176
10226
  equal: "primaryEqual",
10177
10227
  notEqual: "primaryNotEqual",
@@ -10187,11 +10237,6 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10187
10237
  this.trees = /* @__PURE__ */ new Map();
10188
10238
  this.lock = new import_dataply3.Ryoiki();
10189
10239
  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
10240
  this.hook.onceAfter("init", async (tx, isNewlyCreated) => {
10196
10241
  if (isNewlyCreated) {
10197
10242
  await this.initializeDocumentFile(tx);
@@ -10200,31 +10245,30 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10200
10245
  throw new Error("Document metadata verification failed");
10201
10246
  }
10202
10247
  const metadata = await this.getDocumentInnerMetadata(tx);
10203
- const optionsIndices = options.indices ?? {};
10204
- const targetIndices = {
10205
- ...optionsIndices,
10206
- _id: true
10207
- };
10248
+ const targetIndices = /* @__PURE__ */ new Map();
10249
+ targetIndices.set("_id", { type: "btree", fields: ["_id"] });
10250
+ for (const [name, option] of this.pendingCreateIndices) {
10251
+ const config = this.toIndexMetaConfig(option);
10252
+ targetIndices.set(name, config);
10253
+ }
10208
10254
  const backfillTargets = [];
10209
10255
  let isMetadataChanged = false;
10210
- for (const field in targetIndices) {
10211
- const isBackfillEnabled = targetIndices[field];
10212
- const existingIndex = metadata.indices[field];
10256
+ for (const [indexName, config] of targetIndices) {
10257
+ const existingIndex = metadata.indices[indexName];
10213
10258
  if (!existingIndex) {
10214
- metadata.indices[field] = [-1, isBackfillEnabled];
10259
+ metadata.indices[indexName] = [-1, config];
10215
10260
  isMetadataChanged = true;
10216
- if (isBackfillEnabled && !isNewlyCreated) {
10217
- backfillTargets.push(field);
10261
+ if (!isNewlyCreated) {
10262
+ backfillTargets.push(indexName);
10218
10263
  }
10219
10264
  } 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;
10265
+ const [_pk, existingConfig] = existingIndex;
10266
+ if (JSON.stringify(existingConfig) !== JSON.stringify(config)) {
10267
+ metadata.indices[indexName] = [_pk, config];
10227
10268
  isMetadataChanged = true;
10269
+ if (!isNewlyCreated) {
10270
+ backfillTargets.push(indexName);
10271
+ }
10228
10272
  }
10229
10273
  }
10230
10274
  }
@@ -10232,25 +10276,220 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10232
10276
  await this.updateDocumentInnerMetadata(metadata, tx);
10233
10277
  }
10234
10278
  this.indices = metadata.indices;
10235
- for (const field in this.indices) {
10236
- if (field in targetIndices) {
10279
+ this.registeredIndices = /* @__PURE__ */ new Map();
10280
+ this.fieldToIndices = /* @__PURE__ */ new Map();
10281
+ for (const [indexName, config] of targetIndices) {
10282
+ this.registeredIndices.set(indexName, config);
10283
+ const fields = this.getFieldsFromConfig(config);
10284
+ for (const field of fields) {
10285
+ this.indexedFields.add(field);
10286
+ if (!this.fieldToIndices.has(field)) {
10287
+ this.fieldToIndices.set(field, []);
10288
+ }
10289
+ this.fieldToIndices.get(field).push(indexName);
10290
+ }
10291
+ }
10292
+ for (const indexName of targetIndices.keys()) {
10293
+ if (metadata.indices[indexName]) {
10237
10294
  const tree = new import_dataply3.BPTreeAsync(
10238
10295
  new DocumentSerializeStrategyAsync(
10239
10296
  this.rowTableEngine.order,
10240
10297
  this,
10241
10298
  this.txContext,
10242
- field
10299
+ indexName
10243
10300
  ),
10244
10301
  this.comparator
10245
10302
  );
10246
10303
  await tree.init();
10247
- this.trees.set(field, tree);
10304
+ this.trees.set(indexName, tree);
10248
10305
  }
10249
10306
  }
10250
10307
  this.pendingBackfillFields = backfillTargets;
10308
+ this._initialized = true;
10251
10309
  return tx;
10252
10310
  });
10253
10311
  }
10312
+ /**
10313
+ * Whether the document database has been initialized.
10314
+ */
10315
+ get isDocInitialized() {
10316
+ return this._initialized;
10317
+ }
10318
+ /**
10319
+ * Register an index. If called before init(), queues it for processing during init.
10320
+ * If called after init(), immediately creates the tree, updates metadata, and backfills.
10321
+ */
10322
+ async registerIndex(name, option, tx) {
10323
+ if (!this._initialized) {
10324
+ this.pendingCreateIndices.set(name, option);
10325
+ return;
10326
+ }
10327
+ await this.registerIndexRuntime(name, option, tx);
10328
+ }
10329
+ /**
10330
+ * Register an index at runtime (after init).
10331
+ * Creates the tree, updates metadata, and backfills existing data.
10332
+ */
10333
+ async registerIndexRuntime(name, option, tx) {
10334
+ const config = this.toIndexMetaConfig(option);
10335
+ if (this.registeredIndices.has(name)) {
10336
+ const existing = this.registeredIndices.get(name);
10337
+ if (JSON.stringify(existing) === JSON.stringify(config)) return;
10338
+ }
10339
+ await this.runWithDefault(async (tx2) => {
10340
+ const metadata = await this.getDocumentInnerMetadata(tx2);
10341
+ metadata.indices[name] = [-1, config];
10342
+ await this.updateDocumentInnerMetadata(metadata, tx2);
10343
+ this.indices = metadata.indices;
10344
+ this.registeredIndices.set(name, config);
10345
+ const fields = this.getFieldsFromConfig(config);
10346
+ for (const field of fields) {
10347
+ this.indexedFields.add(field);
10348
+ if (!this.fieldToIndices.has(field)) {
10349
+ this.fieldToIndices.set(field, []);
10350
+ }
10351
+ this.fieldToIndices.get(field).push(name);
10352
+ }
10353
+ const tree = new import_dataply3.BPTreeAsync(
10354
+ new DocumentSerializeStrategyAsync(
10355
+ this.rowTableEngine.order,
10356
+ this,
10357
+ this.txContext,
10358
+ name
10359
+ ),
10360
+ this.comparator
10361
+ );
10362
+ await tree.init();
10363
+ this.trees.set(name, tree);
10364
+ if (metadata.lastId > 0) {
10365
+ this.pendingBackfillFields = [name];
10366
+ await this.backfillIndices(tx2);
10367
+ }
10368
+ }, tx);
10369
+ }
10370
+ /**
10371
+ * Drop (remove) a named index.
10372
+ * Removes the index from metadata, in-memory maps, and trees.
10373
+ * The '_id' index cannot be dropped.
10374
+ * @param name The name of the index to drop
10375
+ */
10376
+ async dropIndex(name, tx) {
10377
+ if (name === "_id") {
10378
+ throw new Error("Cannot drop the _id index");
10379
+ }
10380
+ if (!this._initialized) {
10381
+ this.pendingCreateIndices.delete(name);
10382
+ return;
10383
+ }
10384
+ if (!this.registeredIndices.has(name)) {
10385
+ throw new Error(`Index '${name}' does not exist`);
10386
+ }
10387
+ await this.runWithDefault(async (tx2) => {
10388
+ const config = this.registeredIndices.get(name);
10389
+ const metadata = await this.getDocumentInnerMetadata(tx2);
10390
+ delete metadata.indices[name];
10391
+ await this.updateDocumentInnerMetadata(metadata, tx2);
10392
+ this.indices = metadata.indices;
10393
+ this.registeredIndices.delete(name);
10394
+ const fields = this.getFieldsFromConfig(config);
10395
+ for (const field of fields) {
10396
+ const indexNames = this.fieldToIndices.get(field);
10397
+ if (indexNames) {
10398
+ const filtered = indexNames.filter((n) => n !== name);
10399
+ if (filtered.length === 0) {
10400
+ this.fieldToIndices.delete(field);
10401
+ if (field !== "_id") {
10402
+ this.indexedFields.delete(field);
10403
+ }
10404
+ } else {
10405
+ this.fieldToIndices.set(field, filtered);
10406
+ }
10407
+ }
10408
+ }
10409
+ this.trees.delete(name);
10410
+ }, tx);
10411
+ }
10412
+ /**
10413
+ * Convert CreateIndexOption to IndexMetaConfig for metadata storage.
10414
+ */
10415
+ toIndexMetaConfig(option) {
10416
+ if (option.type === "btree") {
10417
+ return {
10418
+ type: "btree",
10419
+ fields: option.fields
10420
+ };
10421
+ }
10422
+ if (option.type === "fts") {
10423
+ if (option.tokenizer === "ngram") {
10424
+ return {
10425
+ type: "fts",
10426
+ fields: option.fields,
10427
+ tokenizer: "ngram",
10428
+ gramSize: option.ngram
10429
+ };
10430
+ }
10431
+ return {
10432
+ type: "fts",
10433
+ fields: option.fields,
10434
+ tokenizer: "whitespace"
10435
+ };
10436
+ }
10437
+ throw new Error(`Unknown index type: ${option.type}`);
10438
+ }
10439
+ /**
10440
+ * Get all field names from an IndexMetaConfig.
10441
+ */
10442
+ getFieldsFromConfig(config) {
10443
+ if (config.type === "btree") {
10444
+ return config.fields;
10445
+ }
10446
+ if (config.type === "fts") {
10447
+ return [config.fields];
10448
+ }
10449
+ return [];
10450
+ }
10451
+ /**
10452
+ * Get the primary field of an index (the field used as tree key).
10453
+ * For btree: first field in fields array.
10454
+ * For fts: the single field.
10455
+ */
10456
+ getPrimaryField(config) {
10457
+ if (config.type === "btree") {
10458
+ return config.fields[0];
10459
+ }
10460
+ return config.fields;
10461
+ }
10462
+ /**
10463
+ * 인덱스 config에 따라 B+tree에 저장할 v 값을 생성합니다.
10464
+ * - 단일 필드 btree: Primitive (단일 값)
10465
+ * - 복합 필드 btree: Primitive[] (필드 순서대로 배열)
10466
+ * - fts: 별도 처리 (이 메서드 사용 안 함)
10467
+ * @returns undefined면 해당 문서에 필수 필드가 없으므로 인덱싱 스킵
10468
+ */
10469
+ getIndexValue(config, flatDoc) {
10470
+ if (config.type !== "btree") return void 0;
10471
+ if (config.fields.length === 1) {
10472
+ const v = flatDoc[config.fields[0]];
10473
+ return v === void 0 ? void 0 : v;
10474
+ }
10475
+ const values = [];
10476
+ for (let i = 0, len = config.fields.length; i < len; i++) {
10477
+ const v = flatDoc[config.fields[i]];
10478
+ if (v === void 0) return void 0;
10479
+ values.push(v);
10480
+ }
10481
+ return values;
10482
+ }
10483
+ /**
10484
+ * Get FTSConfig from IndexMetaConfig (for tokenizer compatibility).
10485
+ */
10486
+ getFtsConfig(config) {
10487
+ if (config.type !== "fts") return null;
10488
+ if (config.tokenizer === "ngram") {
10489
+ return { type: "fts", tokenizer: "ngram", gramSize: config.gramSize };
10490
+ }
10491
+ return { type: "fts", tokenizer: "whitespace" };
10492
+ }
10254
10493
  async getDocument(pk, tx) {
10255
10494
  return this.runWithDefault(async (tx2) => {
10256
10495
  const row = await this.select(pk, false, tx2);
@@ -10279,10 +10518,9 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10279
10518
  });
10280
10519
  }
10281
10520
  /**
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
- *
10521
+ * Backfill indices for newly created indices after data was inserted.
10522
+ * This method should be called after `init()`.
10523
+ *
10286
10524
  * @returns Number of documents that were backfilled
10287
10525
  */
10288
10526
  async backfillIndices(tx) {
@@ -10295,12 +10533,12 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10295
10533
  if (metadata.lastId === 0) {
10296
10534
  return 0;
10297
10535
  }
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();
10536
+ const indexTxMap = {};
10537
+ const indexEntryMap = /* @__PURE__ */ new Map();
10538
+ for (const indexName of backfillTargets) {
10539
+ const tree = this.trees.get(indexName);
10540
+ if (tree && indexName !== "_id") {
10541
+ indexTxMap[indexName] = await tree.createTransaction();
10304
10542
  }
10305
10543
  }
10306
10544
  let backfilledCount = 0;
@@ -10315,35 +10553,44 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10315
10553
  const doc = await this.getDocument(k, tx2);
10316
10554
  if (!doc) continue;
10317
10555
  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, []);
10556
+ for (const indexName of backfillTargets) {
10557
+ if (!(indexName in indexTxMap)) continue;
10558
+ const config = this.registeredIndices.get(indexName);
10559
+ if (!config) continue;
10560
+ const btx = indexTxMap[indexName];
10561
+ if (config.type === "fts") {
10562
+ const primaryField = this.getPrimaryField(config);
10563
+ const v = flatDoc[primaryField];
10564
+ if (v === void 0 || typeof v !== "string") continue;
10565
+ const ftsConfig = this.getFtsConfig(config);
10566
+ const tokens = ftsConfig ? tokenize(v, ftsConfig) : [v];
10567
+ const batchInsertData = [];
10568
+ for (let i = 0, len = tokens.length; i < len; i++) {
10569
+ const token = tokens[i];
10570
+ const keyToInsert = this.getTokenKey(k, token);
10571
+ const entry = { k, v: token };
10572
+ batchInsertData.push([keyToInsert, entry]);
10573
+ if (!indexEntryMap.has(btx)) {
10574
+ indexEntryMap.set(btx, []);
10575
+ }
10576
+ indexEntryMap.get(btx).push({ k: keyToInsert, v: entry });
10577
+ }
10578
+ await btx.batchInsert(batchInsertData);
10579
+ } else {
10580
+ const indexVal = this.getIndexValue(config, flatDoc);
10581
+ if (indexVal === void 0) continue;
10582
+ const entry = { k, v: indexVal };
10583
+ const batchInsertData = [[k, entry]];
10584
+ if (!indexEntryMap.has(btx)) {
10585
+ indexEntryMap.set(btx, []);
10339
10586
  }
10340
- fieldMap.get(btx).push({ k: keyToInsert, v: entry });
10587
+ indexEntryMap.get(btx).push(entry);
10588
+ await btx.batchInsert(batchInsertData);
10341
10589
  }
10342
- await btx.batchInsert(batchInsertData);
10343
10590
  }
10344
10591
  backfilledCount++;
10345
10592
  }
10346
- const btxs = Object.values(fieldTxMap);
10593
+ const btxs = Object.values(indexTxMap);
10347
10594
  const success = [];
10348
10595
  try {
10349
10596
  for (const btx of btxs) {
@@ -10355,7 +10602,7 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10355
10602
  await btx.rollback();
10356
10603
  }
10357
10604
  for (const btx of success) {
10358
- const entries = fieldMap.get(btx);
10605
+ const entries = indexEntryMap.get(btx);
10359
10606
  if (!entries) continue;
10360
10607
  for (const entry of entries) {
10361
10608
  await btx.delete(entry.k, entry);
@@ -10374,6 +10621,7 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10374
10621
  createdAt: Date.now(),
10375
10622
  updatedAt: Date.now(),
10376
10623
  lastId: 0,
10624
+ schemeVersion: 0,
10377
10625
  indices
10378
10626
  };
10379
10627
  }
@@ -10383,7 +10631,7 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10383
10631
  throw new Error("Document metadata already exists");
10384
10632
  }
10385
10633
  const metaObj = this.createDocumentInnerMetadata({
10386
- _id: [-1, true]
10634
+ _id: [-1, { type: "btree", fields: ["_id"] }]
10387
10635
  });
10388
10636
  await this.insertAsOverflow(JSON.stringify(metaObj), false, tx);
10389
10637
  }
@@ -10408,18 +10656,27 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10408
10656
  }
10409
10657
  /**
10410
10658
  * returns flattened document
10411
- * @param document
10412
- * @returns
10659
+ * @param document
10660
+ * @returns
10413
10661
  */
10414
10662
  flattenDocument(document) {
10415
10663
  return this.flatten(document, "", {});
10416
10664
  }
10417
10665
  async getDocumentMetadata(tx) {
10418
10666
  const metadata = await this.getMetadata(tx);
10667
+ const innerMetadata = await this.getDocumentInnerMetadata(tx);
10668
+ const indices = [];
10669
+ for (const name of this.registeredIndices.keys()) {
10670
+ if (name !== "_id") {
10671
+ indices.push(name);
10672
+ }
10673
+ }
10419
10674
  return {
10420
10675
  pageSize: metadata.pageSize,
10421
10676
  pageCount: metadata.pageCount,
10422
- rowCount: metadata.rowCount
10677
+ rowCount: metadata.rowCount,
10678
+ indices,
10679
+ schemeVersion: innerMetadata.schemeVersion ?? 0
10423
10680
  };
10424
10681
  }
10425
10682
  async getDocumentInnerMetadata(tx) {
@@ -10432,6 +10689,25 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10432
10689
  async updateDocumentInnerMetadata(metadata, tx) {
10433
10690
  await this.update(1, JSON.stringify(metadata), tx);
10434
10691
  }
10692
+ /**
10693
+ * Run a migration if the current schemeVersion is lower than the target version.
10694
+ * After the callback completes, schemeVersion is updated to the target version.
10695
+ * @param version The target scheme version
10696
+ * @param callback The migration callback
10697
+ * @param tx Optional transaction
10698
+ */
10699
+ async migration(version, callback, tx) {
10700
+ await this.runWithDefault(async (tx2) => {
10701
+ const innerMetadata = await this.getDocumentInnerMetadata(tx2);
10702
+ const currentVersion = innerMetadata.schemeVersion ?? 0;
10703
+ if (currentVersion < version) {
10704
+ await callback(tx2);
10705
+ innerMetadata.schemeVersion = version;
10706
+ innerMetadata.updatedAt = Date.now();
10707
+ await this.updateDocumentInnerMetadata(innerMetadata, tx2);
10708
+ }
10709
+ }, tx);
10710
+ }
10435
10711
  /**
10436
10712
  * Transforms a query object into a verbose query object
10437
10713
  * @param query The query object to transform
@@ -10470,33 +10746,74 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10470
10746
  return result;
10471
10747
  }
10472
10748
  /**
10473
- * Get the selectivity candidate for the given query
10474
- * @param query The query conditions
10749
+ * Choose the best index (driver) for the given query.
10750
+ * Scores each index based on field coverage and condition type.
10751
+ *
10752
+ * @param query The verbose query conditions
10475
10753
  * @param orderByField Optional field name for orderBy optimization
10476
10754
  * @returns Driver and other candidates for query execution
10477
10755
  */
10478
10756
  async getSelectivityCandidate(query, orderByField) {
10757
+ const queryFields = new Set(Object.keys(query));
10479
10758
  const candidates = [];
10480
- const metadata = await this.getDocumentInnerMetadata(this.txContext.get());
10481
- for (const field in query) {
10482
- const tree = this.trees.get(field);
10759
+ for (const [indexName, config] of this.registeredIndices) {
10760
+ const tree = this.trees.get(indexName);
10483
10761
  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
- });
10762
+ if (config.type === "btree") {
10763
+ const primaryField = config.fields[0];
10764
+ if (!queryFields.has(primaryField)) continue;
10765
+ const condition = query[primaryField];
10766
+ const treeTx = await tree.createTransaction();
10767
+ let score = 0;
10768
+ const coveredFields = config.fields.filter((f) => queryFields.has(f));
10769
+ score += coveredFields.length;
10770
+ if (condition) {
10771
+ if (typeof condition !== "object" || condition === null) {
10772
+ score += 100;
10773
+ } else if ("primaryEqual" in condition || "equal" in condition) {
10774
+ score += 100;
10775
+ } 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) {
10776
+ score += 50;
10777
+ } else if ("primaryOr" in condition || "or" in condition) {
10778
+ score += 20;
10779
+ } else if ("like" in condition) {
10780
+ score += 15;
10781
+ } else {
10782
+ score += 10;
10783
+ }
10784
+ }
10785
+ if (orderByField && primaryField === orderByField) {
10786
+ score += 200;
10787
+ }
10788
+ const compositeVerifyFields = coveredFields.filter((f) => f !== primaryField);
10789
+ candidates.push({
10790
+ tree: treeTx,
10791
+ condition,
10792
+ field: primaryField,
10793
+ indexName,
10794
+ isFtsMatch: false,
10795
+ score,
10796
+ compositeVerifyFields
10797
+ });
10798
+ } else if (config.type === "fts") {
10799
+ const field = config.fields;
10800
+ if (!queryFields.has(field)) continue;
10801
+ const condition = query[field];
10802
+ if (!condition || typeof condition !== "object" || !("match" in condition)) continue;
10803
+ const treeTx = await tree.createTransaction();
10804
+ const ftsConfig = this.getFtsConfig(config);
10805
+ const matchTokens = ftsConfig ? tokenize(condition.match, ftsConfig) : [];
10806
+ candidates.push({
10807
+ tree: treeTx,
10808
+ condition,
10809
+ field,
10810
+ indexName,
10811
+ isFtsMatch: true,
10812
+ matchTokens,
10813
+ score: 90,
10814
+ compositeVerifyFields: []
10815
+ });
10816
+ }
10500
10817
  }
10501
10818
  const rollback = () => {
10502
10819
  for (const { tree } of candidates) {
@@ -10507,41 +10824,19 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10507
10824
  rollback();
10508
10825
  return null;
10509
10826
  }
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
- };
10827
+ candidates.sort((a, b) => b.score - a.score);
10828
+ const driver = candidates[0];
10829
+ const others = candidates.slice(1);
10830
+ const compositeVerifyConditions = [];
10831
+ for (const field of driver.compositeVerifyFields) {
10832
+ if (query[field]) {
10833
+ compositeVerifyConditions.push({ field, condition: query[field] });
10535
10834
  }
10536
10835
  }
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
10836
  return {
10543
- driver: res,
10544
- others: candidates.filter((c) => c.tree !== res.tree),
10837
+ driver,
10838
+ others,
10839
+ compositeVerifyConditions,
10545
10840
  rollback
10546
10841
  };
10547
10842
  }
@@ -10634,7 +10929,7 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10634
10929
  orderBy
10635
10930
  );
10636
10931
  if (!selectivity) return null;
10637
- const { driver, others, rollback } = selectivity;
10932
+ const { driver, others, compositeVerifyConditions, rollback } = selectivity;
10638
10933
  const useIndexOrder = orderBy === void 0 || driver.field === orderBy;
10639
10934
  const currentOrder = useIndexOrder ? sortOrder : void 0;
10640
10935
  let keys;
@@ -10651,6 +10946,8 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10651
10946
  return {
10652
10947
  keys: new Float64Array(Array.from(keys)),
10653
10948
  others,
10949
+ compositeVerifyConditions,
10950
+ isDriverOrderByField: useIndexOrder,
10654
10951
  rollback
10655
10952
  };
10656
10953
  }
@@ -10677,25 +10974,27 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10677
10974
  async insertSingleDocument(document, tx) {
10678
10975
  return this.writeLock(() => this.runWithDefault(async (tx2) => {
10679
10976
  const { pk: dpk, document: dataplyDocument } = await this.insertDocumentInternal(document, tx2);
10680
- const metadata = await this.getDocumentInnerMetadata(tx2);
10681
10977
  const flattenDocument = this.flattenDocument(dataplyDocument);
10682
- for (const field in flattenDocument) {
10683
- const tree = this.trees.get(field);
10978
+ for (const [indexName, config] of this.registeredIndices) {
10979
+ const tree = this.trees.get(indexName);
10684
10980
  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;
10981
+ if (config.type === "fts") {
10982
+ const primaryField = this.getPrimaryField(config);
10983
+ const v = flattenDocument[primaryField];
10984
+ if (v === void 0 || typeof v !== "string") continue;
10985
+ const ftsConfig = this.getFtsConfig(config);
10986
+ const tokens = ftsConfig ? tokenize(v, ftsConfig) : [v];
10987
+ for (let i = 0, len = tokens.length; i < len; i++) {
10988
+ const token = tokens[i];
10989
+ const keyToInsert = this.getTokenKey(dpk, token);
10990
+ const [error] = await catchPromise(tree.insert(keyToInsert, { k: dpk, v: token }));
10991
+ if (error) throw error;
10698
10992
  }
10993
+ } else {
10994
+ const indexVal = this.getIndexValue(config, flattenDocument);
10995
+ if (indexVal === void 0) continue;
10996
+ const [error] = await catchPromise(tree.insert(dpk, { k: dpk, v: indexVal }));
10997
+ if (error) throw error;
10699
10998
  }
10700
10999
  }
10701
11000
  return dataplyDocument._id;
@@ -10731,23 +11030,30 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10731
11030
  for (let i = 0, len = pks.length; i < len; i++) {
10732
11031
  flattenedData[i].pk = pks[i];
10733
11032
  }
10734
- for (const [field, tree] of this.trees) {
11033
+ for (const [indexName, config] of this.registeredIndices) {
11034
+ const tree = this.trees.get(indexName);
11035
+ if (!tree) continue;
10735
11036
  const treeTx = await tree.createTransaction();
10736
- const indexConfig = metadata.indices[field]?.[1];
10737
11037
  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);
11038
+ if (config.type === "fts") {
11039
+ const primaryField = this.getPrimaryField(config);
11040
+ const ftsConfig = this.getFtsConfig(config);
11041
+ for (let i = 0, len = flattenedData.length; i < len; i++) {
11042
+ const item = flattenedData[i];
11043
+ const v = item.data[primaryField];
11044
+ if (v === void 0 || typeof v !== "string") continue;
11045
+ const tokens = ftsConfig ? tokenize(v, ftsConfig) : [v];
11046
+ for (let j = 0, tLen = tokens.length; j < tLen; j++) {
11047
+ const token = tokens[j];
11048
+ batchInsertData.push([this.getTokenKey(item.pk, token), { k: item.pk, v: token }]);
11049
+ }
10746
11050
  }
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 }]);
11051
+ } else {
11052
+ for (let i = 0, len = flattenedData.length; i < len; i++) {
11053
+ const item = flattenedData[i];
11054
+ const indexVal = this.getIndexValue(config, item.data);
11055
+ if (indexVal === void 0) continue;
11056
+ batchInsertData.push([item.pk, { k: item.pk, v: indexVal }]);
10751
11057
  }
10752
11058
  }
10753
11059
  const [error] = await catchPromise(treeTx.batchInsert(batchInsertData));
@@ -10773,8 +11079,8 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10773
11079
  const pks = await this.getKeys(query);
10774
11080
  let updatedCount = 0;
10775
11081
  const treeTxs = /* @__PURE__ */ new Map();
10776
- for (const [field, tree] of this.trees) {
10777
- treeTxs.set(field, await tree.createTransaction());
11082
+ for (const [indexName, tree] of this.trees) {
11083
+ treeTxs.set(indexName, await tree.createTransaction());
10778
11084
  }
10779
11085
  treeTxs.delete("_id");
10780
11086
  for (let i = 0, len = pks.length; i < len; i++) {
@@ -10784,42 +11090,45 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10784
11090
  const updatedDoc = computeUpdatedDoc(doc);
10785
11091
  const oldFlatDoc = this.flattenDocument(doc);
10786
11092
  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);
11093
+ for (const [indexName, treeTx] of treeTxs) {
11094
+ const config = this.registeredIndices.get(indexName);
11095
+ if (!config) continue;
11096
+ if (config.type === "fts") {
11097
+ const primaryField = this.getPrimaryField(config);
11098
+ const oldV = oldFlatDoc[primaryField];
11099
+ const newV = newFlatDoc[primaryField];
11100
+ if (oldV === newV) continue;
11101
+ const ftsConfig = this.getFtsConfig(config);
11102
+ if (typeof oldV === "string") {
11103
+ const oldTokens = ftsConfig ? tokenize(oldV, ftsConfig) : [oldV];
11104
+ for (let j = 0, jLen = oldTokens.length; j < jLen; j++) {
11105
+ await treeTx.delete(this.getTokenKey(pk, oldTokens[j]), { k: pk, v: oldTokens[j] });
11106
+ }
10798
11107
  }
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 });
11108
+ if (typeof newV === "string") {
11109
+ const newTokens = ftsConfig ? tokenize(newV, ftsConfig) : [newV];
11110
+ const batchInsertData = [];
11111
+ for (let j = 0, jLen = newTokens.length; j < jLen; j++) {
11112
+ batchInsertData.push([this.getTokenKey(pk, newTokens[j]), { k: pk, v: newTokens[j] }]);
11113
+ }
11114
+ await treeTx.batchInsert(batchInsertData);
10803
11115
  }
10804
- }
10805
- if (field in newFlatDoc) {
10806
- let newTokens = [newV];
10807
- if (isFts && typeof newV === "string") {
10808
- newTokens = tokenize(newV, indexConfig);
11116
+ } else {
11117
+ const oldIndexVal = this.getIndexValue(config, oldFlatDoc);
11118
+ const newIndexVal = this.getIndexValue(config, newFlatDoc);
11119
+ if (JSON.stringify(oldIndexVal) === JSON.stringify(newIndexVal)) continue;
11120
+ if (oldIndexVal !== void 0) {
11121
+ await treeTx.delete(pk, { k: pk, v: oldIndexVal });
10809
11122
  }
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 }]);
11123
+ if (newIndexVal !== void 0) {
11124
+ await treeTx.batchInsert([[pk, { k: pk, v: newIndexVal }]]);
10815
11125
  }
10816
- await treeTx.batchInsert(batchInsertData);
10817
11126
  }
10818
11127
  }
10819
11128
  await this.update(pk, JSON.stringify(updatedDoc), tx);
10820
11129
  updatedCount++;
10821
11130
  }
10822
- for (const [field, treeTx] of treeTxs) {
11131
+ for (const [indexName, treeTx] of treeTxs) {
10823
11132
  const result = await treeTx.commit();
10824
11133
  if (!result.success) {
10825
11134
  for (const rollbackTx of treeTxs.values()) {
@@ -10832,7 +11141,7 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10832
11141
  }
10833
11142
  /**
10834
11143
  * Fully update documents from the database that match the query
10835
- * @param query The query to use (only indexed fields + _id allowed)
11144
+ * @param query The query to use
10836
11145
  * @param newRecord Complete document to replace with, or function that receives current document and returns new document
10837
11146
  * @param tx The transaction to use
10838
11147
  * @returns The number of updated documents
@@ -10847,7 +11156,7 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10847
11156
  }
10848
11157
  /**
10849
11158
  * Partially update documents from the database that match the query
10850
- * @param query The query to use (only indexed fields + _id allowed)
11159
+ * @param query The query to use
10851
11160
  * @param newRecord Partial document to merge, or function that receives current document and returns partial update
10852
11161
  * @param tx The transaction to use
10853
11162
  * @returns The number of updated documents
@@ -10864,7 +11173,7 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10864
11173
  }
10865
11174
  /**
10866
11175
  * Delete documents from the database that match the query
10867
- * @param query The query to use (only indexed fields + _id allowed)
11176
+ * @param query The query to use
10868
11177
  * @param tx The transaction to use
10869
11178
  * @returns The number of deleted documents
10870
11179
  */
@@ -10877,20 +11186,22 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10877
11186
  const doc = await this.getDocument(pk, tx2);
10878
11187
  if (!doc) continue;
10879
11188
  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 });
11189
+ for (const [indexName, tree] of this.trees) {
11190
+ const config = this.registeredIndices.get(indexName);
11191
+ if (!config) continue;
11192
+ if (config.type === "fts") {
11193
+ const primaryField = this.getPrimaryField(config);
11194
+ const v = flatDoc[primaryField];
11195
+ if (v === void 0 || typeof v !== "string") continue;
11196
+ const ftsConfig = this.getFtsConfig(config);
11197
+ const tokens = ftsConfig ? tokenize(v, ftsConfig) : [v];
11198
+ for (let j = 0, jLen = tokens.length; j < jLen; j++) {
11199
+ await tree.delete(this.getTokenKey(pk, tokens[j]), { k: pk, v: tokens[j] });
11200
+ }
11201
+ } else {
11202
+ const indexVal = this.getIndexValue(config, flatDoc);
11203
+ if (indexVal === void 0) continue;
11204
+ await tree.delete(pk, { k: pk, v: indexVal });
10894
11205
  }
10895
11206
  }
10896
11207
  await super.delete(pk, true, tx2);
@@ -10901,7 +11212,7 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10901
11212
  }
10902
11213
  /**
10903
11214
  * Count documents from the database that match the query
10904
- * @param query The query to use (only indexed fields + _id allowed)
11215
+ * @param query The query to use
10905
11216
  * @param tx The transaction to use
10906
11217
  * @returns The number of documents that match the query
10907
11218
  */
@@ -10927,6 +11238,51 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10927
11238
  }
10928
11239
  return true;
10929
11240
  }
11241
+ /**
11242
+ * 복합 인덱스의 non-primary 필드에 대해 문서가 유효한지 검증합니다.
11243
+ */
11244
+ verifyCompositeConditions(doc, conditions) {
11245
+ if (conditions.length === 0) return true;
11246
+ const flatDoc = this.flattenDocument(doc);
11247
+ for (let i = 0, len = conditions.length; i < len; i++) {
11248
+ const { field, condition } = conditions[i];
11249
+ const docValue = flatDoc[field];
11250
+ if (docValue === void 0) return false;
11251
+ const treeValue = { k: doc._id, v: docValue };
11252
+ if (!this.verifyValue(docValue, condition)) return false;
11253
+ }
11254
+ return true;
11255
+ }
11256
+ /**
11257
+ * 단일 값에 대해 verbose 조건을 검증합니다.
11258
+ */
11259
+ verifyValue(value, condition) {
11260
+ if (typeof condition !== "object" || condition === null) {
11261
+ return value === condition;
11262
+ }
11263
+ if ("primaryEqual" in condition) {
11264
+ return value === condition.primaryEqual?.v;
11265
+ }
11266
+ if ("primaryNotEqual" in condition) {
11267
+ return value !== condition.primaryNotEqual?.v;
11268
+ }
11269
+ if ("primaryLt" in condition) {
11270
+ return value !== null && condition.primaryLt?.v !== void 0 && value < condition.primaryLt.v;
11271
+ }
11272
+ if ("primaryLte" in condition) {
11273
+ return value !== null && condition.primaryLte?.v !== void 0 && value <= condition.primaryLte.v;
11274
+ }
11275
+ if ("primaryGt" in condition) {
11276
+ return value !== null && condition.primaryGt?.v !== void 0 && value > condition.primaryGt.v;
11277
+ }
11278
+ if ("primaryGte" in condition) {
11279
+ return value !== null && condition.primaryGte?.v !== void 0 && value >= condition.primaryGte.v;
11280
+ }
11281
+ if ("primaryOr" in condition && Array.isArray(condition.primaryOr)) {
11282
+ return condition.primaryOr.some((c) => value === c?.v);
11283
+ }
11284
+ return true;
11285
+ }
10930
11286
  /**
10931
11287
  * 메모리 기반으로 청크 크기를 동적 조절합니다.
10932
11288
  */
@@ -10939,10 +11295,9 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10939
11295
  }
10940
11296
  /**
10941
11297
  * Prefetch 방식으로 키 배열을 청크 단위로 조회하여 문서를 순회합니다.
10942
- * FTS 검증 others 후보에 대한 tree.verify() 검증을 통과한 문서만 yield 합니다.
10943
- * 교집합 대신 스트리밍 중 검증하여 첫 결과 반환 시간을 단축합니다.
11298
+ * FTS 검증, 복합 인덱스 검증, others 후보에 대한 tree.verify() 검증을 통과한 문서만 yield 합니다.
10944
11299
  */
10945
- async *processChunkedKeysWithVerify(keys, startIdx, initialChunkSize, ftsConditions, others, tx) {
11300
+ async *processChunkedKeysWithVerify(keys, startIdx, initialChunkSize, ftsConditions, compositeVerifyConditions, others, tx) {
10946
11301
  const verifyOthers = others.filter((o) => !o.isFtsMatch);
10947
11302
  let i = startIdx;
10948
11303
  const totalKeys = keys.length;
@@ -10968,6 +11323,7 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10968
11323
  const doc = JSON.parse(s);
10969
11324
  chunkTotalSize += s.length * 2;
10970
11325
  if (ftsConditions.length > 0 && !this.verifyFts(doc, ftsConditions)) continue;
11326
+ if (compositeVerifyConditions.length > 0 && this.verifyCompositeConditions(doc, compositeVerifyConditions) === false) continue;
10971
11327
  if (verifyOthers.length > 0) {
10972
11328
  const flatDoc = this.flattenDocument(doc);
10973
11329
  let passed = true;
@@ -10993,7 +11349,7 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
10993
11349
  }
10994
11350
  /**
10995
11351
  * Select documents from the database
10996
- * @param query The query to use (only indexed fields + _id allowed)
11352
+ * @param query The query to use
10997
11353
  * @param options The options to use
10998
11354
  * @param tx The transaction to use
10999
11355
  * @returns The documents that match the query
@@ -11017,32 +11373,30 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
11017
11373
  } = options;
11018
11374
  const self = this;
11019
11375
  const stream = this.streamWithDefault(async function* (tx2) {
11020
- const metadata = await self.getDocumentInnerMetadata(tx2);
11021
11376
  const ftsConditions = [];
11022
11377
  for (const field in query) {
11023
11378
  const q = query[field];
11024
11379
  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) });
11380
+ const indexNames = self.fieldToIndices.get(field) || [];
11381
+ for (const indexName of indexNames) {
11382
+ const config = self.registeredIndices.get(indexName);
11383
+ if (config && config.type === "fts") {
11384
+ const ftsConfig = self.getFtsConfig(config);
11385
+ if (ftsConfig) {
11386
+ ftsConditions.push({ field, matchTokens: tokenize(q.match, ftsConfig) });
11387
+ }
11388
+ break;
11389
+ }
11028
11390
  }
11029
11391
  }
11030
11392
  }
11031
11393
  const driverResult = await self.getDriverKeys(query, orderByField, sortOrder);
11032
11394
  if (!driverResult) return;
11033
- const { keys, others, rollback } = driverResult;
11395
+ const { keys, others, compositeVerifyConditions, isDriverOrderByField, rollback } = driverResult;
11034
11396
  if (keys.length === 0) {
11035
11397
  rollback();
11036
11398
  return;
11037
11399
  }
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
11400
  try {
11047
11401
  if (!isDriverOrderByField && orderByField) {
11048
11402
  const topK = limit === Infinity ? Infinity : offset + limit;
@@ -11061,6 +11415,7 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
11061
11415
  0,
11062
11416
  self.options.pageSize,
11063
11417
  ftsConditions,
11418
+ compositeVerifyConditions,
11064
11419
  others,
11065
11420
  tx2
11066
11421
  )) {
@@ -11098,6 +11453,7 @@ var DocumentDataplyAPI = class extends import_dataply3.DataplyAPI {
11098
11453
  offset,
11099
11454
  self.options.pageSize,
11100
11455
  ftsConditions,
11456
+ compositeVerifyConditions,
11101
11457
  others,
11102
11458
  tx2
11103
11459
  )) {
@@ -11131,8 +11487,7 @@ var DocumentDataply = class _DocumentDataply {
11131
11487
  static Define() {
11132
11488
  return {
11133
11489
  /**
11134
- * Sets the options for the database, such as index configurations and WAL settings.
11135
- * @template IC The configuration of indices.
11490
+ * Sets the options for the database, such as WAL settings.
11136
11491
  * @param options The database initialization options.
11137
11492
  */
11138
11493
  Options: (options) => _DocumentDataply.Options(options)
@@ -11160,6 +11515,30 @@ var DocumentDataply = class _DocumentDataply {
11160
11515
  constructor(file, options) {
11161
11516
  this.api = new DocumentDataplyAPI(file, options ?? {});
11162
11517
  }
11518
+ /**
11519
+ * Create a named index on the database.
11520
+ * Can be called before or after init().
11521
+ * If called after init(), the index is immediately created and backfilled.
11522
+ * @param name The name of the index
11523
+ * @param option The index configuration (btree or fts)
11524
+ * @param tx Optional transaction
11525
+ * @returns Promise<this> for chaining
11526
+ */
11527
+ async createIndex(name, option, tx) {
11528
+ await this.api.registerIndex(name, option, tx);
11529
+ return this;
11530
+ }
11531
+ /**
11532
+ * Drop (remove) a named index from the database.
11533
+ * The '_id' index cannot be dropped.
11534
+ * @param name The name of the index to drop
11535
+ * @param tx Optional transaction
11536
+ * @returns Promise<this> for chaining
11537
+ */
11538
+ async dropIndex(name, tx) {
11539
+ await this.api.dropIndex(name, tx);
11540
+ return this;
11541
+ }
11163
11542
  /**
11164
11543
  * Initialize the document database
11165
11544
  */
@@ -11167,6 +11546,17 @@ var DocumentDataply = class _DocumentDataply {
11167
11546
  await this.api.init();
11168
11547
  await this.api.backfillIndices();
11169
11548
  }
11549
+ /**
11550
+ * Run a migration if the current schemeVersion is lower than the target version.
11551
+ * The callback is only executed when the database's schemeVersion is below the given version.
11552
+ * After the callback completes, schemeVersion is updated to the target version.
11553
+ * @param version The target scheme version
11554
+ * @param callback The migration callback receiving a transaction
11555
+ * @param tx Optional transaction
11556
+ */
11557
+ async migration(version, callback, tx) {
11558
+ await this.api.migration(version, callback, tx);
11559
+ }
11170
11560
  /**
11171
11561
  * Get the metadata of the document database
11172
11562
  */
@@ -11199,7 +11589,7 @@ var DocumentDataply = class _DocumentDataply {
11199
11589
  }
11200
11590
  /**
11201
11591
  * Fully update documents from the database that match the query
11202
- * @param query The query to use (only indexed fields + _id allowed)
11592
+ * @param query The query to use
11203
11593
  * @param newRecord Complete document to replace with, or function that receives current document and returns new document
11204
11594
  * @param tx The transaction to use
11205
11595
  * @returns The number of updated documents
@@ -11209,7 +11599,7 @@ var DocumentDataply = class _DocumentDataply {
11209
11599
  }
11210
11600
  /**
11211
11601
  * Partially update documents from the database that match the query
11212
- * @param query The query to use (only indexed fields + _id allowed)
11602
+ * @param query The query to use
11213
11603
  * @param newRecord Partial document to merge, or function that receives current document and returns partial update
11214
11604
  * @param tx The transaction to use
11215
11605
  * @returns The number of updated documents
@@ -11219,7 +11609,7 @@ var DocumentDataply = class _DocumentDataply {
11219
11609
  }
11220
11610
  /**
11221
11611
  * Delete documents from the database that match the query
11222
- * @param query The query to use (only indexed fields + _id allowed)
11612
+ * @param query The query to use
11223
11613
  * @param tx The transaction to use
11224
11614
  * @returns The number of deleted documents
11225
11615
  */
@@ -11228,7 +11618,7 @@ var DocumentDataply = class _DocumentDataply {
11228
11618
  }
11229
11619
  /**
11230
11620
  * Count documents from the database that match the query
11231
- * @param query The query to use (only indexed fields + _id allowed)
11621
+ * @param query The query to use
11232
11622
  * @param tx The transaction to use
11233
11623
  * @returns The number of documents that match the query
11234
11624
  */
@@ -11237,7 +11627,7 @@ var DocumentDataply = class _DocumentDataply {
11237
11627
  }
11238
11628
  /**
11239
11629
  * Select documents from the database
11240
- * @param query The query to use (only indexed fields + _id allowed)
11630
+ * @param query The query to use
11241
11631
  * @param options The options to use
11242
11632
  * @param tx The transaction to use
11243
11633
  * @returns The documents that match the query