@topgunbuild/core 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -4510,6 +4510,593 @@ _InvertedIndex.RETRIEVAL_COST = 50;
4510
4510
  _InvertedIndex.SUPPORTED_QUERIES = ["contains", "containsAll", "containsAny", "has"];
4511
4511
  var InvertedIndex = _InvertedIndex;
4512
4512
 
4513
+ // src/query/indexes/CompoundIndex.ts
4514
+ var _CompoundIndex = class _CompoundIndex {
4515
+ /**
4516
+ * Create a CompoundIndex.
4517
+ *
4518
+ * @param attributes - Array of attributes to index (order matters!)
4519
+ * @param options - Optional configuration
4520
+ *
4521
+ * @example
4522
+ * ```typescript
4523
+ * const statusAttr = simpleAttribute<Product, string>('status', p => p.status);
4524
+ * const categoryAttr = simpleAttribute<Product, string>('category', p => p.category);
4525
+ *
4526
+ * const compoundIndex = new CompoundIndex<string, Product>([statusAttr, categoryAttr]);
4527
+ * ```
4528
+ */
4529
+ constructor(attributes, options = {}) {
4530
+ this.type = "compound";
4531
+ /** Map from composite key to set of record keys */
4532
+ this.data = /* @__PURE__ */ new Map();
4533
+ /** Set of all indexed keys */
4534
+ this.allKeys = /* @__PURE__ */ new Set();
4535
+ if (attributes.length < 2) {
4536
+ throw new Error("CompoundIndex requires at least 2 attributes");
4537
+ }
4538
+ this._attributes = attributes;
4539
+ this.separator = options.separator ?? "|";
4540
+ }
4541
+ /**
4542
+ * Get the first attribute (used for Index interface compatibility).
4543
+ * Note: CompoundIndex spans multiple attributes.
4544
+ */
4545
+ get attribute() {
4546
+ return this._attributes[0];
4547
+ }
4548
+ /**
4549
+ * Get all attributes in this compound index.
4550
+ */
4551
+ get attributes() {
4552
+ return [...this._attributes];
4553
+ }
4554
+ /**
4555
+ * Get attribute names as a combined identifier.
4556
+ */
4557
+ get compoundName() {
4558
+ return this._attributes.map((a) => a.name).join("+");
4559
+ }
4560
+ getRetrievalCost() {
4561
+ return _CompoundIndex.RETRIEVAL_COST;
4562
+ }
4563
+ supportsQuery(queryType) {
4564
+ return queryType === "compound";
4565
+ }
4566
+ /**
4567
+ * Retrieve records matching compound query.
4568
+ *
4569
+ * @param query - Compound query with values matching each attribute
4570
+ * @returns ResultSet of matching keys
4571
+ *
4572
+ * @example
4573
+ * ```typescript
4574
+ * // Find products where status='active' AND category='electronics'
4575
+ * index.retrieve({
4576
+ * type: 'compound',
4577
+ * values: ['active', 'electronics']
4578
+ * });
4579
+ * ```
4580
+ */
4581
+ retrieve(query) {
4582
+ if (query.type !== "compound") {
4583
+ throw new Error(`CompoundIndex only supports 'compound' query type, got: ${query.type}`);
4584
+ }
4585
+ const compoundQuery = query;
4586
+ const values = compoundQuery.values;
4587
+ if (values.length !== this._attributes.length) {
4588
+ throw new Error(
4589
+ `CompoundIndex requires ${this._attributes.length} values, got ${values.length}`
4590
+ );
4591
+ }
4592
+ const compositeKey = this.buildCompositeKey(values);
4593
+ const keys = this.data.get(compositeKey);
4594
+ return new SetResultSet(
4595
+ keys ? new Set(keys) : /* @__PURE__ */ new Set(),
4596
+ _CompoundIndex.RETRIEVAL_COST
4597
+ );
4598
+ }
4599
+ /**
4600
+ * Retrieve with explicit values (convenience method).
4601
+ *
4602
+ * @param values - Values in order of index attributes
4603
+ * @returns ResultSet of matching keys
4604
+ */
4605
+ retrieveByValues(...values) {
4606
+ return this.retrieve({ type: "compound", values });
4607
+ }
4608
+ add(key, record) {
4609
+ const compositeKey = this.buildCompositeKeyFromRecord(record);
4610
+ if (compositeKey === null) return;
4611
+ let keys = this.data.get(compositeKey);
4612
+ if (!keys) {
4613
+ keys = /* @__PURE__ */ new Set();
4614
+ this.data.set(compositeKey, keys);
4615
+ }
4616
+ keys.add(key);
4617
+ this.allKeys.add(key);
4618
+ }
4619
+ remove(key, record) {
4620
+ const compositeKey = this.buildCompositeKeyFromRecord(record);
4621
+ if (compositeKey === null) return;
4622
+ const keys = this.data.get(compositeKey);
4623
+ if (keys) {
4624
+ keys.delete(key);
4625
+ if (keys.size === 0) {
4626
+ this.data.delete(compositeKey);
4627
+ }
4628
+ }
4629
+ this.allKeys.delete(key);
4630
+ }
4631
+ update(key, oldRecord, newRecord) {
4632
+ const oldKey = this.buildCompositeKeyFromRecord(oldRecord);
4633
+ const newKey = this.buildCompositeKeyFromRecord(newRecord);
4634
+ if (oldKey === newKey) {
4635
+ return;
4636
+ }
4637
+ this.remove(key, oldRecord);
4638
+ this.add(key, newRecord);
4639
+ }
4640
+ clear() {
4641
+ this.data.clear();
4642
+ this.allKeys.clear();
4643
+ }
4644
+ getStats() {
4645
+ let totalEntries = 0;
4646
+ for (const keys of this.data.values()) {
4647
+ totalEntries += keys.size;
4648
+ }
4649
+ return {
4650
+ distinctValues: this.data.size,
4651
+ totalEntries,
4652
+ avgEntriesPerValue: this.data.size > 0 ? totalEntries / this.data.size : 0
4653
+ };
4654
+ }
4655
+ /**
4656
+ * Get extended statistics for compound index.
4657
+ */
4658
+ getExtendedStats() {
4659
+ const stats = this.getStats();
4660
+ return {
4661
+ ...stats,
4662
+ attributeCount: this._attributes.length,
4663
+ attributeNames: this._attributes.map((a) => a.name),
4664
+ compositeKeyCount: this.data.size
4665
+ };
4666
+ }
4667
+ /**
4668
+ * Check if this compound index can answer a query on the given attributes.
4669
+ * Compound indexes can be used if query attributes match in prefix order.
4670
+ *
4671
+ * @param attributeNames - Attribute names being queried
4672
+ * @returns true if this index can answer the query
4673
+ */
4674
+ canAnswerQuery(attributeNames) {
4675
+ if (attributeNames.length !== this._attributes.length) {
4676
+ return false;
4677
+ }
4678
+ for (let i = 0; i < attributeNames.length; i++) {
4679
+ if (attributeNames[i] !== this._attributes[i].name) {
4680
+ return false;
4681
+ }
4682
+ }
4683
+ return true;
4684
+ }
4685
+ /**
4686
+ * Build composite key from array of values.
4687
+ */
4688
+ buildCompositeKey(values) {
4689
+ return values.map((v) => this.encodeValue(v)).join(this.separator);
4690
+ }
4691
+ /**
4692
+ * Build composite key from record by extracting attribute values.
4693
+ * Returns null if any attribute value is undefined.
4694
+ */
4695
+ buildCompositeKeyFromRecord(record) {
4696
+ const values = [];
4697
+ for (const attr of this._attributes) {
4698
+ const value = attr.getValue(record);
4699
+ if (value === void 0) {
4700
+ return null;
4701
+ }
4702
+ values.push(value);
4703
+ }
4704
+ return this.buildCompositeKey(values);
4705
+ }
4706
+ /**
4707
+ * Encode value for composite key.
4708
+ * Handles common types and escapes separator.
4709
+ */
4710
+ encodeValue(value) {
4711
+ if (value === null) return "__null__";
4712
+ if (value === void 0) return "__undefined__";
4713
+ const str = String(value);
4714
+ return str.replace(
4715
+ new RegExp(this.escapeRegex(this.separator), "g"),
4716
+ `\\${this.separator}`
4717
+ );
4718
+ }
4719
+ /**
4720
+ * Escape regex special characters.
4721
+ */
4722
+ escapeRegex(str) {
4723
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4724
+ }
4725
+ };
4726
+ /** Retrieval cost (lower than individual indexes combined) */
4727
+ _CompoundIndex.RETRIEVAL_COST = 20;
4728
+ var CompoundIndex = _CompoundIndex;
4729
+ function isCompoundIndex(index) {
4730
+ return index.type === "compound";
4731
+ }
4732
+
4733
+ // src/query/indexes/lazy/LazyHashIndex.ts
4734
+ var LazyHashIndex = class {
4735
+ constructor(attribute, options = {}) {
4736
+ this.attribute = attribute;
4737
+ this.type = "hash";
4738
+ this.isLazy = true;
4739
+ /** Underlying hash index (created on first query) */
4740
+ this.innerIndex = null;
4741
+ /** Pending records before materialization */
4742
+ this.pendingRecords = /* @__PURE__ */ new Map();
4743
+ /** Track if index has been built */
4744
+ this.built = false;
4745
+ this.onProgress = options.onProgress;
4746
+ this.progressBatchSize = options.progressBatchSize ?? 1e3;
4747
+ }
4748
+ get isBuilt() {
4749
+ return this.built;
4750
+ }
4751
+ get pendingCount() {
4752
+ return this.pendingRecords.size;
4753
+ }
4754
+ getRetrievalCost() {
4755
+ return 30;
4756
+ }
4757
+ supportsQuery(queryType) {
4758
+ return ["equal", "in", "has"].includes(queryType);
4759
+ }
4760
+ retrieve(query) {
4761
+ if (!this.built) {
4762
+ this.materialize();
4763
+ }
4764
+ return this.innerIndex.retrieve(query);
4765
+ }
4766
+ add(key, record) {
4767
+ if (this.built) {
4768
+ this.innerIndex.add(key, record);
4769
+ } else {
4770
+ this.pendingRecords.set(key, record);
4771
+ }
4772
+ }
4773
+ remove(key, record) {
4774
+ if (this.built) {
4775
+ this.innerIndex.remove(key, record);
4776
+ } else {
4777
+ this.pendingRecords.delete(key);
4778
+ }
4779
+ }
4780
+ update(key, oldRecord, newRecord) {
4781
+ if (this.built) {
4782
+ this.innerIndex.update(key, oldRecord, newRecord);
4783
+ } else {
4784
+ this.pendingRecords.set(key, newRecord);
4785
+ }
4786
+ }
4787
+ clear() {
4788
+ if (this.built) {
4789
+ this.innerIndex.clear();
4790
+ }
4791
+ this.pendingRecords.clear();
4792
+ }
4793
+ getStats() {
4794
+ if (this.built) {
4795
+ return this.innerIndex.getStats();
4796
+ }
4797
+ return {
4798
+ distinctValues: 0,
4799
+ totalEntries: this.pendingRecords.size,
4800
+ avgEntriesPerValue: 0
4801
+ };
4802
+ }
4803
+ /**
4804
+ * Force materialization of the index.
4805
+ * Called automatically on first query.
4806
+ */
4807
+ materialize(progressCallback) {
4808
+ if (this.built) return;
4809
+ const callback = progressCallback ?? this.onProgress;
4810
+ const total = this.pendingRecords.size;
4811
+ this.innerIndex = new HashIndex(this.attribute);
4812
+ let processed = 0;
4813
+ for (const [key, record] of this.pendingRecords) {
4814
+ this.innerIndex.add(key, record);
4815
+ processed++;
4816
+ if (callback && processed % this.progressBatchSize === 0) {
4817
+ const progress = Math.round(processed / total * 100);
4818
+ callback(this.attribute.name, progress, processed, total);
4819
+ }
4820
+ }
4821
+ if (callback && total > 0) {
4822
+ callback(this.attribute.name, 100, total, total);
4823
+ }
4824
+ this.pendingRecords.clear();
4825
+ this.built = true;
4826
+ }
4827
+ /**
4828
+ * Get the underlying HashIndex (for testing/debugging).
4829
+ * Returns null if not yet materialized.
4830
+ */
4831
+ getInnerIndex() {
4832
+ return this.innerIndex;
4833
+ }
4834
+ };
4835
+
4836
+ // src/query/indexes/lazy/LazyNavigableIndex.ts
4837
+ var LazyNavigableIndex = class {
4838
+ constructor(attribute, comparator, options = {}) {
4839
+ this.attribute = attribute;
4840
+ this.type = "navigable";
4841
+ this.isLazy = true;
4842
+ /** Underlying navigable index (created on first query) */
4843
+ this.innerIndex = null;
4844
+ /** Pending records before materialization */
4845
+ this.pendingRecords = /* @__PURE__ */ new Map();
4846
+ /** Track if index has been built */
4847
+ this.built = false;
4848
+ this.comparator = comparator;
4849
+ this.onProgress = options.onProgress;
4850
+ this.progressBatchSize = options.progressBatchSize ?? 1e3;
4851
+ }
4852
+ get isBuilt() {
4853
+ return this.built;
4854
+ }
4855
+ get pendingCount() {
4856
+ return this.pendingRecords.size;
4857
+ }
4858
+ getRetrievalCost() {
4859
+ return 40;
4860
+ }
4861
+ supportsQuery(queryType) {
4862
+ return ["equal", "in", "has", "gt", "gte", "lt", "lte", "between"].includes(queryType);
4863
+ }
4864
+ retrieve(query) {
4865
+ if (!this.built) {
4866
+ this.materialize();
4867
+ }
4868
+ return this.innerIndex.retrieve(query);
4869
+ }
4870
+ add(key, record) {
4871
+ if (this.built) {
4872
+ this.innerIndex.add(key, record);
4873
+ } else {
4874
+ this.pendingRecords.set(key, record);
4875
+ }
4876
+ }
4877
+ remove(key, record) {
4878
+ if (this.built) {
4879
+ this.innerIndex.remove(key, record);
4880
+ } else {
4881
+ this.pendingRecords.delete(key);
4882
+ }
4883
+ }
4884
+ update(key, oldRecord, newRecord) {
4885
+ if (this.built) {
4886
+ this.innerIndex.update(key, oldRecord, newRecord);
4887
+ } else {
4888
+ this.pendingRecords.set(key, newRecord);
4889
+ }
4890
+ }
4891
+ clear() {
4892
+ if (this.built) {
4893
+ this.innerIndex.clear();
4894
+ }
4895
+ this.pendingRecords.clear();
4896
+ }
4897
+ getStats() {
4898
+ if (this.built) {
4899
+ return this.innerIndex.getStats();
4900
+ }
4901
+ return {
4902
+ distinctValues: 0,
4903
+ totalEntries: this.pendingRecords.size,
4904
+ avgEntriesPerValue: 0
4905
+ };
4906
+ }
4907
+ /**
4908
+ * Force materialization of the index.
4909
+ * Called automatically on first query.
4910
+ */
4911
+ materialize(progressCallback) {
4912
+ if (this.built) return;
4913
+ const callback = progressCallback ?? this.onProgress;
4914
+ const total = this.pendingRecords.size;
4915
+ this.innerIndex = new NavigableIndex(this.attribute, this.comparator);
4916
+ let processed = 0;
4917
+ for (const [key, record] of this.pendingRecords) {
4918
+ this.innerIndex.add(key, record);
4919
+ processed++;
4920
+ if (callback && processed % this.progressBatchSize === 0) {
4921
+ const progress = Math.round(processed / total * 100);
4922
+ callback(this.attribute.name, progress, processed, total);
4923
+ }
4924
+ }
4925
+ if (callback && total > 0) {
4926
+ callback(this.attribute.name, 100, total, total);
4927
+ }
4928
+ this.pendingRecords.clear();
4929
+ this.built = true;
4930
+ }
4931
+ /**
4932
+ * Get the underlying NavigableIndex (for testing/debugging).
4933
+ * Returns null if not yet materialized.
4934
+ */
4935
+ getInnerIndex() {
4936
+ return this.innerIndex;
4937
+ }
4938
+ /**
4939
+ * Get the minimum indexed value.
4940
+ * Forces materialization if not built.
4941
+ */
4942
+ getMinValue() {
4943
+ if (!this.built) {
4944
+ this.materialize();
4945
+ }
4946
+ return this.innerIndex.getMinValue();
4947
+ }
4948
+ /**
4949
+ * Get the maximum indexed value.
4950
+ * Forces materialization if not built.
4951
+ */
4952
+ getMaxValue() {
4953
+ if (!this.built) {
4954
+ this.materialize();
4955
+ }
4956
+ return this.innerIndex.getMaxValue();
4957
+ }
4958
+ };
4959
+
4960
+ // src/query/indexes/lazy/LazyInvertedIndex.ts
4961
+ var LazyInvertedIndex = class {
4962
+ constructor(attribute, pipeline, options = {}) {
4963
+ this.attribute = attribute;
4964
+ this.type = "inverted";
4965
+ this.isLazy = true;
4966
+ /** Underlying inverted index (created on first query) */
4967
+ this.innerIndex = null;
4968
+ /** Pending records before materialization */
4969
+ this.pendingRecords = /* @__PURE__ */ new Map();
4970
+ /** Track if index has been built */
4971
+ this.built = false;
4972
+ this.pipeline = pipeline ?? TokenizationPipeline.simple();
4973
+ this.onProgress = options.onProgress;
4974
+ this.progressBatchSize = options.progressBatchSize ?? 1e3;
4975
+ }
4976
+ get isBuilt() {
4977
+ return this.built;
4978
+ }
4979
+ get pendingCount() {
4980
+ return this.pendingRecords.size;
4981
+ }
4982
+ getRetrievalCost() {
4983
+ return 50;
4984
+ }
4985
+ supportsQuery(queryType) {
4986
+ return ["contains", "containsAll", "containsAny", "has"].includes(queryType);
4987
+ }
4988
+ retrieve(query) {
4989
+ if (!this.built) {
4990
+ this.materialize();
4991
+ }
4992
+ return this.innerIndex.retrieve(query);
4993
+ }
4994
+ add(key, record) {
4995
+ if (this.built) {
4996
+ this.innerIndex.add(key, record);
4997
+ } else {
4998
+ this.pendingRecords.set(key, record);
4999
+ }
5000
+ }
5001
+ remove(key, record) {
5002
+ if (this.built) {
5003
+ this.innerIndex.remove(key, record);
5004
+ } else {
5005
+ this.pendingRecords.delete(key);
5006
+ }
5007
+ }
5008
+ update(key, oldRecord, newRecord) {
5009
+ if (this.built) {
5010
+ this.innerIndex.update(key, oldRecord, newRecord);
5011
+ } else {
5012
+ this.pendingRecords.set(key, newRecord);
5013
+ }
5014
+ }
5015
+ clear() {
5016
+ if (this.built) {
5017
+ this.innerIndex.clear();
5018
+ }
5019
+ this.pendingRecords.clear();
5020
+ }
5021
+ getStats() {
5022
+ if (this.built) {
5023
+ return this.innerIndex.getStats();
5024
+ }
5025
+ return {
5026
+ distinctValues: 0,
5027
+ totalEntries: this.pendingRecords.size,
5028
+ avgEntriesPerValue: 0
5029
+ };
5030
+ }
5031
+ /**
5032
+ * Get extended statistics for full-text index.
5033
+ * Forces materialization if not built.
5034
+ */
5035
+ getExtendedStats() {
5036
+ if (!this.built) {
5037
+ this.materialize();
5038
+ }
5039
+ return this.innerIndex.getExtendedStats();
5040
+ }
5041
+ /**
5042
+ * Force materialization of the index.
5043
+ * Called automatically on first query.
5044
+ */
5045
+ materialize(progressCallback) {
5046
+ if (this.built) return;
5047
+ const callback = progressCallback ?? this.onProgress;
5048
+ const total = this.pendingRecords.size;
5049
+ this.innerIndex = new InvertedIndex(this.attribute, this.pipeline);
5050
+ let processed = 0;
5051
+ for (const [key, record] of this.pendingRecords) {
5052
+ this.innerIndex.add(key, record);
5053
+ processed++;
5054
+ if (callback && processed % this.progressBatchSize === 0) {
5055
+ const progress = Math.round(processed / total * 100);
5056
+ callback(this.attribute.name, progress, processed, total);
5057
+ }
5058
+ }
5059
+ if (callback && total > 0) {
5060
+ callback(this.attribute.name, 100, total, total);
5061
+ }
5062
+ this.pendingRecords.clear();
5063
+ this.built = true;
5064
+ }
5065
+ /**
5066
+ * Get the underlying InvertedIndex (for testing/debugging).
5067
+ * Returns null if not yet materialized.
5068
+ */
5069
+ getInnerIndex() {
5070
+ return this.innerIndex;
5071
+ }
5072
+ /**
5073
+ * Get the tokenization pipeline.
5074
+ */
5075
+ getPipeline() {
5076
+ return this.pipeline;
5077
+ }
5078
+ /**
5079
+ * Check if a specific token exists in the index.
5080
+ * Forces materialization if not built.
5081
+ */
5082
+ hasToken(token) {
5083
+ if (!this.built) {
5084
+ this.materialize();
5085
+ }
5086
+ return this.innerIndex.hasToken(token);
5087
+ }
5088
+ /**
5089
+ * Get the number of documents for a specific token.
5090
+ * Forces materialization if not built.
5091
+ */
5092
+ getTokenDocumentCount(token) {
5093
+ if (!this.built) {
5094
+ this.materialize();
5095
+ }
5096
+ return this.innerIndex.getTokenDocumentCount(token);
5097
+ }
5098
+ };
5099
+
4513
5100
  // src/query/resultset/IntersectionResultSet.ts
4514
5101
  var IntersectionResultSet = class {
4515
5102
  /**
@@ -5063,6 +5650,8 @@ var IndexRegistry = class {
5063
5650
  constructor() {
5064
5651
  /** Indexes grouped by attribute name */
5065
5652
  this.attributeIndexes = /* @__PURE__ */ new Map();
5653
+ /** Compound indexes (Phase 9.03) - keyed by sorted attribute names */
5654
+ this.compoundIndexes = /* @__PURE__ */ new Map();
5066
5655
  /** Fallback index for full scan (optional) */
5067
5656
  this.fallbackIndex = null;
5068
5657
  }
@@ -5073,6 +5662,10 @@ var IndexRegistry = class {
5073
5662
  * @param index - Index to register
5074
5663
  */
5075
5664
  addIndex(index) {
5665
+ if (isCompoundIndex(index)) {
5666
+ this.addCompoundIndex(index);
5667
+ return;
5668
+ }
5076
5669
  const attrName = index.attribute.name;
5077
5670
  let indexes = this.attributeIndexes.get(attrName);
5078
5671
  if (!indexes) {
@@ -5083,6 +5676,15 @@ var IndexRegistry = class {
5083
5676
  indexes.push(index);
5084
5677
  }
5085
5678
  }
5679
+ /**
5680
+ * Register a compound index (Phase 9.03).
5681
+ *
5682
+ * @param index - Compound index to register
5683
+ */
5684
+ addCompoundIndex(index) {
5685
+ const key = this.makeCompoundKey(index.attributes.map((a) => a.name));
5686
+ this.compoundIndexes.set(key, index);
5687
+ }
5086
5688
  /**
5087
5689
  * Remove an index from the registry.
5088
5690
  *
@@ -5090,6 +5692,9 @@ var IndexRegistry = class {
5090
5692
  * @returns true if index was found and removed
5091
5693
  */
5092
5694
  removeIndex(index) {
5695
+ if (isCompoundIndex(index)) {
5696
+ return this.removeCompoundIndex(index);
5697
+ }
5093
5698
  const attrName = index.attribute.name;
5094
5699
  const indexes = this.attributeIndexes.get(attrName);
5095
5700
  if (!indexes) {
@@ -5105,6 +5710,16 @@ var IndexRegistry = class {
5105
5710
  }
5106
5711
  return true;
5107
5712
  }
5713
+ /**
5714
+ * Remove a compound index (Phase 9.03).
5715
+ *
5716
+ * @param index - Compound index to remove
5717
+ * @returns true if index was found and removed
5718
+ */
5719
+ removeCompoundIndex(index) {
5720
+ const key = this.makeCompoundKey(index.attributes.map((a) => a.name));
5721
+ return this.compoundIndexes.delete(key);
5722
+ }
5108
5723
  /**
5109
5724
  * Get all indexes for an attribute.
5110
5725
  *
@@ -5175,6 +5790,50 @@ var IndexRegistry = class {
5175
5790
  const indexes = this.getIndexes(attributeName);
5176
5791
  return indexes.filter((index) => index.supportsQuery(queryType)).sort((a, b) => a.getRetrievalCost() - b.getRetrievalCost());
5177
5792
  }
5793
+ // ========================================
5794
+ // Phase 9.03: Compound Index Methods
5795
+ // ========================================
5796
+ /**
5797
+ * Find a compound index that covers the given attribute names (Phase 9.03).
5798
+ * The compound index must cover ALL the attributes (exact match or superset).
5799
+ *
5800
+ * @param attributeNames - Array of attribute names to search for
5801
+ * @returns Matching compound index or null
5802
+ */
5803
+ findCompoundIndex(attributeNames) {
5804
+ if (attributeNames.length < 2) {
5805
+ return null;
5806
+ }
5807
+ const key = this.makeCompoundKey(attributeNames);
5808
+ const exactMatch = this.compoundIndexes.get(key);
5809
+ if (exactMatch) {
5810
+ return exactMatch;
5811
+ }
5812
+ return null;
5813
+ }
5814
+ /**
5815
+ * Check if a compound index exists for the given attributes (Phase 9.03).
5816
+ *
5817
+ * @param attributeNames - Array of attribute names
5818
+ * @returns true if a compound index exists
5819
+ */
5820
+ hasCompoundIndex(attributeNames) {
5821
+ return this.findCompoundIndex(attributeNames) !== null;
5822
+ }
5823
+ /**
5824
+ * Get all compound indexes (Phase 9.03).
5825
+ *
5826
+ * @returns Array of all compound indexes
5827
+ */
5828
+ getCompoundIndexes() {
5829
+ return Array.from(this.compoundIndexes.values());
5830
+ }
5831
+ /**
5832
+ * Create a compound key from attribute names (sorted for consistency).
5833
+ */
5834
+ makeCompoundKey(attributeNames) {
5835
+ return [...attributeNames].sort().join("+");
5836
+ }
5178
5837
  /**
5179
5838
  * Set a fallback index for queries without a suitable index.
5180
5839
  * Typically a FallbackIndex that performs full scan.
@@ -5205,6 +5864,9 @@ var IndexRegistry = class {
5205
5864
  index.add(key, record);
5206
5865
  }
5207
5866
  }
5867
+ for (const compoundIndex of this.compoundIndexes.values()) {
5868
+ compoundIndex.add(key, record);
5869
+ }
5208
5870
  }
5209
5871
  /**
5210
5872
  * Notify all indexes of a record update.
@@ -5220,6 +5882,9 @@ var IndexRegistry = class {
5220
5882
  index.update(key, oldRecord, newRecord);
5221
5883
  }
5222
5884
  }
5885
+ for (const compoundIndex of this.compoundIndexes.values()) {
5886
+ compoundIndex.update(key, oldRecord, newRecord);
5887
+ }
5223
5888
  }
5224
5889
  /**
5225
5890
  * Notify all indexes of a record removal.
@@ -5234,6 +5899,9 @@ var IndexRegistry = class {
5234
5899
  index.remove(key, record);
5235
5900
  }
5236
5901
  }
5902
+ for (const compoundIndex of this.compoundIndexes.values()) {
5903
+ compoundIndex.remove(key, record);
5904
+ }
5237
5905
  }
5238
5906
  /**
5239
5907
  * Clear all indexes.
@@ -5245,6 +5913,9 @@ var IndexRegistry = class {
5245
5913
  index.clear();
5246
5914
  }
5247
5915
  }
5916
+ for (const compoundIndex of this.compoundIndexes.values()) {
5917
+ compoundIndex.clear();
5918
+ }
5248
5919
  }
5249
5920
  /**
5250
5921
  * Get total number of registered indexes.
@@ -5254,6 +5925,7 @@ var IndexRegistry = class {
5254
5925
  for (const indexes of this.attributeIndexes.values()) {
5255
5926
  count += indexes.length;
5256
5927
  }
5928
+ count += this.compoundIndexes.size;
5257
5929
  return count;
5258
5930
  }
5259
5931
  /**
@@ -5266,9 +5938,17 @@ var IndexRegistry = class {
5266
5938
  type: index.type,
5267
5939
  stats: index.getStats()
5268
5940
  }));
5941
+ for (const compoundIndex of this.compoundIndexes.values()) {
5942
+ indexStats.push({
5943
+ attribute: compoundIndex.compoundName,
5944
+ type: "compound",
5945
+ stats: compoundIndex.getStats()
5946
+ });
5947
+ }
5269
5948
  return {
5270
- totalIndexes: indexes.length,
5949
+ totalIndexes: indexes.length + this.compoundIndexes.size,
5271
5950
  indexedAttributes: this.getIndexedAttributes().length,
5951
+ compoundIndexes: this.compoundIndexes.size,
5272
5952
  indexes: indexStats
5273
5953
  };
5274
5954
  }
@@ -5402,9 +6082,10 @@ var QueryOptimizer = class {
5402
6082
  * Strategy: Find child with lowest cost, use as base, filter with rest.
5403
6083
  *
5404
6084
  * CQEngine "smallest first" strategy:
5405
- * 1. Sort children by merge cost
5406
- * 2. Use intersection if multiple indexes available
5407
- * 3. Apply remaining predicates as filters
6085
+ * 1. Check for CompoundIndex covering all eq children (Phase 9.03)
6086
+ * 2. Sort children by merge cost
6087
+ * 3. Use intersection if multiple indexes available
6088
+ * 4. Apply remaining predicates as filters
5408
6089
  */
5409
6090
  optimizeAnd(query) {
5410
6091
  if (!query.children || query.children.length === 0) {
@@ -5413,6 +6094,10 @@ var QueryOptimizer = class {
5413
6094
  if (query.children.length === 1) {
5414
6095
  return this.optimizeNode(query.children[0]);
5415
6096
  }
6097
+ const compoundStep = this.tryCompoundIndex(query.children);
6098
+ if (compoundStep) {
6099
+ return compoundStep;
6100
+ }
5416
6101
  const childSteps = query.children.map((child) => this.optimizeNode(child));
5417
6102
  const sortedWithIndex = childSteps.map((step, index) => ({ step, originalIndex: index })).sort((a, b) => this.estimateCost(a.step) - this.estimateCost(b.step));
5418
6103
  const sortedSteps = sortedWithIndex.map((s) => s.step);
@@ -5437,6 +6122,71 @@ var QueryOptimizer = class {
5437
6122
  }
5438
6123
  return { type: "intersection", steps: indexedSteps };
5439
6124
  }
6125
+ /**
6126
+ * Try to use a CompoundIndex for an AND query (Phase 9.03).
6127
+ *
6128
+ * Returns a compound index scan step if:
6129
+ * 1. All children are simple 'eq' queries
6130
+ * 2. A CompoundIndex exists covering all queried attributes
6131
+ *
6132
+ * @param children - Children of the AND query
6133
+ * @returns IndexScanStep using CompoundIndex, or null if not applicable
6134
+ */
6135
+ tryCompoundIndex(children) {
6136
+ const eqQueries = [];
6137
+ const otherQueries = [];
6138
+ for (const child of children) {
6139
+ if (isSimpleQuery(child) && child.type === "eq") {
6140
+ eqQueries.push(child);
6141
+ } else {
6142
+ otherQueries.push(child);
6143
+ }
6144
+ }
6145
+ if (eqQueries.length < 2) {
6146
+ return null;
6147
+ }
6148
+ const attributeNames = eqQueries.map((q) => q.attribute);
6149
+ const compoundIndex = this.indexRegistry.findCompoundIndex(attributeNames);
6150
+ if (!compoundIndex) {
6151
+ return null;
6152
+ }
6153
+ const values = this.buildCompoundValues(compoundIndex, eqQueries);
6154
+ if (!values) {
6155
+ return null;
6156
+ }
6157
+ const compoundStep = {
6158
+ type: "index-scan",
6159
+ index: compoundIndex,
6160
+ query: { type: "compound", values }
6161
+ };
6162
+ if (otherQueries.length > 0) {
6163
+ const filterPredicate = otherQueries.length === 1 ? otherQueries[0] : { type: "and", children: otherQueries };
6164
+ return { type: "filter", source: compoundStep, predicate: filterPredicate };
6165
+ }
6166
+ return compoundStep;
6167
+ }
6168
+ /**
6169
+ * Build values array for compound index query in correct attribute order.
6170
+ *
6171
+ * @param compoundIndex - The compound index to use
6172
+ * @param eqQueries - Array of 'eq' queries
6173
+ * @returns Values array in compound index order, or null if mismatch
6174
+ */
6175
+ buildCompoundValues(compoundIndex, eqQueries) {
6176
+ const attributeNames = compoundIndex.attributes.map((a) => a.name);
6177
+ const values = [];
6178
+ const queryMap = /* @__PURE__ */ new Map();
6179
+ for (const q of eqQueries) {
6180
+ queryMap.set(q.attribute, q.value);
6181
+ }
6182
+ for (const attrName of attributeNames) {
6183
+ if (!queryMap.has(attrName)) {
6184
+ return null;
6185
+ }
6186
+ values.push(queryMap.get(attrName));
6187
+ }
6188
+ return values;
6189
+ }
5440
6190
  /**
5441
6191
  * Optimize OR query.
5442
6192
  * Strategy: Union of all child results with deduplication.
@@ -5993,7 +6743,9 @@ var MEMORY_OVERHEAD_ESTIMATES = {
5993
6743
  /** Navigable index overhead per record */
5994
6744
  navigable: 32,
5995
6745
  /** Inverted index overhead per record (depends on token count) */
5996
- inverted: 48
6746
+ inverted: 48,
6747
+ /** Compound index overhead per record (includes composite key) */
6748
+ compound: 40
5997
6749
  };
5998
6750
 
5999
6751
  // src/query/adaptive/QueryPatternTracker.ts
@@ -6010,6 +6762,7 @@ function parseStatsKey(key) {
6010
6762
  var QueryPatternTracker = class {
6011
6763
  constructor(options = {}) {
6012
6764
  this.stats = /* @__PURE__ */ new Map();
6765
+ this.compoundStats = /* @__PURE__ */ new Map();
6013
6766
  this.queryCounter = 0;
6014
6767
  this.samplingRate = options.samplingRate ?? TRACKING_SAMPLE_RATE;
6015
6768
  this.maxTrackedPatterns = options.maxTrackedPatterns ?? 1e3;
@@ -6056,6 +6809,94 @@ var QueryPatternTracker = class {
6056
6809
  });
6057
6810
  }
6058
6811
  }
6812
+ /**
6813
+ * Record a compound (AND) query execution for pattern tracking (Phase 9.03).
6814
+ *
6815
+ * @param attributes - Array of attribute names being queried together
6816
+ * @param executionTime - Query execution time in milliseconds
6817
+ * @param resultSize - Number of results returned
6818
+ * @param hasCompoundIndex - Whether a compound index was used
6819
+ */
6820
+ recordCompoundQuery(attributes, executionTime, resultSize, hasCompoundIndex) {
6821
+ if (attributes.length < 2) return;
6822
+ this.queryCounter++;
6823
+ if (this.samplingRate > 1 && this.queryCounter % this.samplingRate !== 0) {
6824
+ return;
6825
+ }
6826
+ const sortedAttrs = [...attributes].sort();
6827
+ const compoundKey = sortedAttrs.join("+");
6828
+ const existing = this.compoundStats.get(compoundKey);
6829
+ const now = Date.now();
6830
+ if (existing) {
6831
+ existing.queryCount++;
6832
+ existing.totalCost += executionTime;
6833
+ existing.averageCost = existing.totalCost / existing.queryCount;
6834
+ existing.lastQueried = now;
6835
+ existing.hasCompoundIndex = hasCompoundIndex;
6836
+ } else {
6837
+ if (this.compoundStats.size >= this.maxTrackedPatterns) {
6838
+ this.evictOldestCompound();
6839
+ }
6840
+ this.compoundStats.set(compoundKey, {
6841
+ attributes: sortedAttrs,
6842
+ compoundKey,
6843
+ queryCount: this.samplingRate,
6844
+ // Adjust for sampling
6845
+ totalCost: executionTime * this.samplingRate,
6846
+ averageCost: executionTime,
6847
+ lastQueried: now,
6848
+ hasCompoundIndex
6849
+ });
6850
+ }
6851
+ }
6852
+ /**
6853
+ * Get all compound query statistics (Phase 9.03).
6854
+ *
6855
+ * @returns Array of compound query statistics, sorted by query count descending
6856
+ */
6857
+ getCompoundStatistics() {
6858
+ this.pruneStaleCompound();
6859
+ return Array.from(this.compoundStats.values()).sort((a, b) => b.queryCount - a.queryCount);
6860
+ }
6861
+ /**
6862
+ * Get compound statistics for a specific attribute combination.
6863
+ *
6864
+ * @param attributes - Array of attribute names
6865
+ * @returns Compound query statistics or undefined
6866
+ */
6867
+ getCompoundStats(attributes) {
6868
+ const sortedAttrs = [...attributes].sort();
6869
+ const compoundKey = sortedAttrs.join("+");
6870
+ return this.compoundStats.get(compoundKey);
6871
+ }
6872
+ /**
6873
+ * Check if attributes appear in any tracked compound queries.
6874
+ *
6875
+ * @param attribute - The attribute name to check
6876
+ * @returns True if attribute is part of any compound query pattern
6877
+ */
6878
+ isInCompoundPattern(attribute) {
6879
+ for (const stat of this.compoundStats.values()) {
6880
+ if (stat.attributes.includes(attribute)) {
6881
+ return true;
6882
+ }
6883
+ }
6884
+ return false;
6885
+ }
6886
+ /**
6887
+ * Update compound index status.
6888
+ *
6889
+ * @param attributes - Array of attribute names
6890
+ * @param hasCompoundIndex - Whether a compound index exists
6891
+ */
6892
+ updateCompoundIndexStatus(attributes, hasCompoundIndex) {
6893
+ const sortedAttrs = [...attributes].sort();
6894
+ const compoundKey = sortedAttrs.join("+");
6895
+ const stat = this.compoundStats.get(compoundKey);
6896
+ if (stat) {
6897
+ stat.hasCompoundIndex = hasCompoundIndex;
6898
+ }
6899
+ }
6059
6900
  /**
6060
6901
  * Get all query statistics.
6061
6902
  *
@@ -6153,6 +6994,7 @@ var QueryPatternTracker = class {
6153
6994
  */
6154
6995
  clear() {
6155
6996
  this.stats.clear();
6997
+ this.compoundStats.clear();
6156
6998
  this.queryCounter = 0;
6157
6999
  }
6158
7000
  /**
@@ -6161,9 +7003,10 @@ var QueryPatternTracker = class {
6161
7003
  * @returns Tracking overhead info
6162
7004
  */
6163
7005
  getTrackingInfo() {
6164
- const memoryEstimate = this.stats.size * 200;
7006
+ const memoryEstimate = this.stats.size * 200 + this.compoundStats.size * 300;
6165
7007
  return {
6166
7008
  patternsTracked: this.stats.size,
7009
+ compoundPatternsTracked: this.compoundStats.size,
6167
7010
  totalQueries: this.queryCounter,
6168
7011
  samplingRate: this.samplingRate,
6169
7012
  memoryEstimate
@@ -6196,6 +7039,33 @@ var QueryPatternTracker = class {
6196
7039
  }
6197
7040
  }
6198
7041
  }
7042
+ /**
7043
+ * Evict the oldest compound query entry (Phase 9.03).
7044
+ */
7045
+ evictOldestCompound() {
7046
+ let oldestKey = null;
7047
+ let oldestTime = Infinity;
7048
+ for (const [key, stat] of this.compoundStats.entries()) {
7049
+ if (stat.lastQueried < oldestTime) {
7050
+ oldestTime = stat.lastQueried;
7051
+ oldestKey = key;
7052
+ }
7053
+ }
7054
+ if (oldestKey) {
7055
+ this.compoundStats.delete(oldestKey);
7056
+ }
7057
+ }
7058
+ /**
7059
+ * Prune stale compound statistics (Phase 9.03).
7060
+ */
7061
+ pruneStaleCompound() {
7062
+ const cutoff = Date.now() - this.statsTtl;
7063
+ for (const [key, stat] of this.compoundStats.entries()) {
7064
+ if (stat.lastQueried < cutoff) {
7065
+ this.compoundStats.delete(key);
7066
+ }
7067
+ }
7068
+ }
6199
7069
  };
6200
7070
 
6201
7071
  // src/query/adaptive/IndexAdvisor.ts
@@ -6229,6 +7099,12 @@ var IndexAdvisor = class {
6229
7099
  suggestions.push(suggestion);
6230
7100
  }
6231
7101
  }
7102
+ const compoundSuggestions = this.getCompoundSuggestions({
7103
+ minQueryCount,
7104
+ minAverageCost,
7105
+ excludeExistingIndexes
7106
+ });
7107
+ suggestions.push(...compoundSuggestions);
6232
7108
  suggestions.sort((a, b) => {
6233
7109
  const priorityOrder = { high: 3, medium: 2, low: 1 };
6234
7110
  const priorityDiff = priorityOrder[b.priority] - priorityOrder[a.priority];
@@ -6428,6 +7304,136 @@ var IndexAdvisor = class {
6428
7304
  }
6429
7305
  return reason;
6430
7306
  }
7307
+ // ========================================
7308
+ // Phase 9.03: Compound Index Suggestions
7309
+ // ========================================
7310
+ /**
7311
+ * Get compound index suggestions based on AND query patterns.
7312
+ *
7313
+ * @param options - Suggestion options
7314
+ * @returns Array of compound index suggestions
7315
+ */
7316
+ getCompoundSuggestions(options = {}) {
7317
+ const {
7318
+ minQueryCount = ADAPTIVE_INDEXING_DEFAULTS.advisor.minQueryCount,
7319
+ minAverageCost = ADAPTIVE_INDEXING_DEFAULTS.advisor.minAverageCost,
7320
+ excludeExistingIndexes = true
7321
+ } = options;
7322
+ const compoundStats = this.tracker.getCompoundStatistics();
7323
+ const suggestions = [];
7324
+ for (const stat of compoundStats) {
7325
+ if (excludeExistingIndexes && stat.hasCompoundIndex) continue;
7326
+ if (stat.queryCount < minQueryCount) continue;
7327
+ if (stat.averageCost < minAverageCost) continue;
7328
+ const suggestion = this.generateCompoundSuggestion(stat);
7329
+ if (suggestion) {
7330
+ suggestions.push(suggestion);
7331
+ }
7332
+ }
7333
+ return suggestions;
7334
+ }
7335
+ /**
7336
+ * Get a suggestion for a specific compound attribute combination.
7337
+ *
7338
+ * @param attributes - Array of attribute names
7339
+ * @returns Compound index suggestion or null if not recommended
7340
+ */
7341
+ getCompoundSuggestionFor(attributes) {
7342
+ const stat = this.tracker.getCompoundStats(attributes);
7343
+ if (!stat) return null;
7344
+ return this.generateCompoundSuggestion(stat);
7345
+ }
7346
+ /**
7347
+ * Check if a compound index should be created for the given attributes.
7348
+ *
7349
+ * @param attributes - Array of attribute names
7350
+ * @param threshold - Minimum query count threshold
7351
+ * @returns True if compound index should be created
7352
+ */
7353
+ shouldCreateCompoundIndex(attributes, threshold = ADAPTIVE_INDEXING_DEFAULTS.autoIndex.threshold) {
7354
+ const stat = this.tracker.getCompoundStats(attributes);
7355
+ if (!stat) return false;
7356
+ return !stat.hasCompoundIndex && stat.queryCount >= threshold;
7357
+ }
7358
+ /**
7359
+ * Generate a suggestion for a compound query pattern.
7360
+ */
7361
+ generateCompoundSuggestion(stat) {
7362
+ const estimatedBenefit = this.estimateCompoundBenefit(stat);
7363
+ const estimatedCost = this.estimateCompoundMemoryCost(stat);
7364
+ const priority = this.calculateCompoundPriority(stat, estimatedBenefit);
7365
+ return {
7366
+ attribute: stat.compoundKey,
7367
+ indexType: "compound",
7368
+ reason: this.generateCompoundReason(stat, estimatedBenefit),
7369
+ estimatedBenefit,
7370
+ estimatedCost,
7371
+ priority,
7372
+ queryCount: stat.queryCount,
7373
+ averageCost: stat.averageCost,
7374
+ compoundAttributes: stat.attributes
7375
+ };
7376
+ }
7377
+ /**
7378
+ * Estimate performance benefit of adding a compound index.
7379
+ *
7380
+ * Compound indexes provide significant speedup for AND queries:
7381
+ * - Eliminates intersection operations (100-1000× for each attribute)
7382
+ * - Single O(1) lookup instead of multiple index scans
7383
+ */
7384
+ estimateCompoundBenefit(stat) {
7385
+ const attributeMultiplier = Math.pow(2, stat.attributes.length - 1);
7386
+ let baseBenefit;
7387
+ if (stat.averageCost > 20) {
7388
+ baseBenefit = 1e3;
7389
+ } else if (stat.averageCost > 5) {
7390
+ baseBenefit = 500;
7391
+ } else if (stat.averageCost > 1) {
7392
+ baseBenefit = 100;
7393
+ } else {
7394
+ baseBenefit = 50;
7395
+ }
7396
+ const frequencyMultiplier = Math.min(stat.queryCount / 10, 100);
7397
+ return Math.floor(baseBenefit * attributeMultiplier * frequencyMultiplier);
7398
+ }
7399
+ /**
7400
+ * Estimate memory cost of adding a compound index.
7401
+ */
7402
+ estimateCompoundMemoryCost(stat) {
7403
+ const bytesPerRecord = MEMORY_OVERHEAD_ESTIMATES.compound;
7404
+ const attributeOverhead = stat.attributes.length * 8;
7405
+ const estimatedRecords = 1e3;
7406
+ return Math.floor(estimatedRecords * (bytesPerRecord + attributeOverhead) * 1.5);
7407
+ }
7408
+ /**
7409
+ * Calculate priority for compound index suggestion.
7410
+ */
7411
+ calculateCompoundPriority(stat, estimatedBenefit) {
7412
+ if (stat.queryCount > 100 && stat.averageCost > 10) {
7413
+ return "high";
7414
+ }
7415
+ if (stat.queryCount > 500) {
7416
+ return "high";
7417
+ }
7418
+ if (stat.queryCount > 50 || stat.averageCost > 5) {
7419
+ return "medium";
7420
+ }
7421
+ if (estimatedBenefit > 2e3) {
7422
+ return "medium";
7423
+ }
7424
+ return "low";
7425
+ }
7426
+ /**
7427
+ * Generate human-readable reason for compound index suggestion.
7428
+ */
7429
+ generateCompoundReason(stat, benefit) {
7430
+ const costStr = stat.averageCost.toFixed(2);
7431
+ const attrList = stat.attributes.join(", ");
7432
+ let reason = `Compound AND query on [${attrList}] executed ${stat.queryCount}\xD7 with average cost ${costStr}ms. `;
7433
+ reason += `Expected ~${benefit}\xD7 cumulative speedup with compound index. `;
7434
+ reason += `Eliminates ${stat.attributes.length - 1} ResultSet intersection(s).`;
7435
+ return reason;
7436
+ }
6431
7437
  };
6432
7438
 
6433
7439
  // src/query/adaptive/AutoIndexManager.ts
@@ -6913,11 +7919,20 @@ var IndexedLWWMap = class extends LWWMap {
6913
7919
  // ==================== Index Management ====================
6914
7920
  /**
6915
7921
  * Add a hash index on an attribute.
7922
+ * If lazyIndexBuilding is enabled, creates a LazyHashIndex instead.
6916
7923
  *
6917
7924
  * @param attribute - Attribute to index
6918
- * @returns Created HashIndex
7925
+ * @returns Created HashIndex (or LazyHashIndex)
6919
7926
  */
6920
7927
  addHashIndex(attribute) {
7928
+ if (this.options.lazyIndexBuilding) {
7929
+ const index2 = new LazyHashIndex(attribute, {
7930
+ onProgress: this.options.onIndexBuilding
7931
+ });
7932
+ this.indexRegistry.addIndex(index2);
7933
+ this.buildIndex(index2);
7934
+ return index2;
7935
+ }
6921
7936
  const index = new HashIndex(attribute);
6922
7937
  this.indexRegistry.addIndex(index);
6923
7938
  this.buildIndex(index);
@@ -6926,12 +7941,21 @@ var IndexedLWWMap = class extends LWWMap {
6926
7941
  /**
6927
7942
  * Add a navigable index on an attribute.
6928
7943
  * Navigable indexes support range queries (gt, gte, lt, lte, between).
7944
+ * If lazyIndexBuilding is enabled, creates a LazyNavigableIndex instead.
6929
7945
  *
6930
7946
  * @param attribute - Attribute to index
6931
7947
  * @param comparator - Optional custom comparator
6932
- * @returns Created NavigableIndex
7948
+ * @returns Created NavigableIndex (or LazyNavigableIndex)
6933
7949
  */
6934
7950
  addNavigableIndex(attribute, comparator) {
7951
+ if (this.options.lazyIndexBuilding) {
7952
+ const index2 = new LazyNavigableIndex(attribute, comparator, {
7953
+ onProgress: this.options.onIndexBuilding
7954
+ });
7955
+ this.indexRegistry.addIndex(index2);
7956
+ this.buildIndex(index2);
7957
+ return index2;
7958
+ }
6935
7959
  const index = new NavigableIndex(attribute, comparator);
6936
7960
  this.indexRegistry.addIndex(index);
6937
7961
  this.buildIndex(index);
@@ -6940,10 +7964,11 @@ var IndexedLWWMap = class extends LWWMap {
6940
7964
  /**
6941
7965
  * Add an inverted index for full-text search on an attribute.
6942
7966
  * Inverted indexes support text search queries (contains, containsAll, containsAny).
7967
+ * If lazyIndexBuilding is enabled, creates a LazyInvertedIndex instead.
6943
7968
  *
6944
7969
  * @param attribute - Text attribute to index
6945
7970
  * @param pipeline - Optional custom tokenization pipeline
6946
- * @returns Created InvertedIndex
7971
+ * @returns Created InvertedIndex (or LazyInvertedIndex)
6947
7972
  *
6948
7973
  * @example
6949
7974
  * ```typescript
@@ -6955,6 +7980,14 @@ var IndexedLWWMap = class extends LWWMap {
6955
7980
  * ```
6956
7981
  */
6957
7982
  addInvertedIndex(attribute, pipeline) {
7983
+ if (this.options.lazyIndexBuilding) {
7984
+ const index2 = new LazyInvertedIndex(attribute, pipeline, {
7985
+ onProgress: this.options.onIndexBuilding
7986
+ });
7987
+ this.indexRegistry.addIndex(index2);
7988
+ this.buildIndex(index2);
7989
+ return index2;
7990
+ }
6958
7991
  const index = new InvertedIndex(attribute, pipeline);
6959
7992
  this.indexRegistry.addIndex(index);
6960
7993
  this.buildIndex(index);
@@ -7459,6 +8492,58 @@ var IndexedLWWMap = class extends LWWMap {
7459
8492
  isAutoIndexingEnabled() {
7460
8493
  return this.autoIndexManager !== null;
7461
8494
  }
8495
+ // ==================== Lazy Indexing (Phase 9.01) ====================
8496
+ /**
8497
+ * Check if lazy index building is enabled.
8498
+ */
8499
+ isLazyIndexingEnabled() {
8500
+ return this.options.lazyIndexBuilding === true;
8501
+ }
8502
+ /**
8503
+ * Force materialization of all lazy indexes.
8504
+ * Useful to pre-warm indexes before critical operations.
8505
+ *
8506
+ * @param progressCallback - Optional progress callback
8507
+ */
8508
+ materializeAllIndexes(progressCallback) {
8509
+ const callback = progressCallback ?? this.options.onIndexBuilding;
8510
+ for (const index of this.indexRegistry.getAllIndexes()) {
8511
+ if ("isLazy" in index && index.isLazy) {
8512
+ const lazyIndex = index;
8513
+ if (!lazyIndex.isBuilt) {
8514
+ lazyIndex.materialize(callback);
8515
+ }
8516
+ }
8517
+ }
8518
+ }
8519
+ /**
8520
+ * Get count of pending records across all lazy indexes.
8521
+ * Returns 0 if no lazy indexes or all are materialized.
8522
+ */
8523
+ getPendingIndexCount() {
8524
+ let total = 0;
8525
+ for (const index of this.indexRegistry.getAllIndexes()) {
8526
+ if ("isLazy" in index && index.isLazy) {
8527
+ const lazyIndex = index;
8528
+ total += lazyIndex.pendingCount;
8529
+ }
8530
+ }
8531
+ return total;
8532
+ }
8533
+ /**
8534
+ * Check if any lazy indexes are still pending (not built).
8535
+ */
8536
+ hasUnbuiltIndexes() {
8537
+ for (const index of this.indexRegistry.getAllIndexes()) {
8538
+ if ("isLazy" in index && index.isLazy) {
8539
+ const lazyIndex = index;
8540
+ if (!lazyIndex.isBuilt) {
8541
+ return true;
8542
+ }
8543
+ }
8544
+ }
8545
+ return false;
8546
+ }
7462
8547
  /**
7463
8548
  * Track query pattern for adaptive indexing.
7464
8549
  */