Skip to main content

lance_index/scalar/
btree.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright The Lance Authors
3
4use std::{
5    any::Any,
6    cmp::Ordering,
7    collections::{BTreeMap, BinaryHeap, HashMap, HashSet},
8    fmt::{Debug, Display},
9    ops::Bound,
10    sync::Arc,
11};
12
13use super::{
14    AnyQuery, BuiltinIndexType, IndexReader, IndexStore, IndexWriter, MetricsCollector,
15    SargableQuery, ScalarIndex, ScalarIndexParams, SearchResult,
16};
17use crate::{Index, IndexType};
18use crate::{
19    frag_reuse::FragReuseIndex,
20    scalar::{
21        CreatedIndex, UpdateCriteria,
22        expression::{SargableQueryParser, ScalarQueryParser},
23        registry::{ScalarIndexPlugin, TrainingOrdering, TrainingRequest, VALUE_COLUMN_NAME},
24    },
25};
26use crate::{metrics::NoOpMetricsCollector, scalar::registry::TrainingCriteria};
27use crate::{pbold, scalar::btree::flat::FlatIndex};
28use arrow_arith::numeric::add;
29use arrow_array::{Array, RecordBatch, UInt32Array, new_empty_array};
30use arrow_schema::{DataType, Field, Schema, SortOptions};
31use async_trait::async_trait;
32use datafusion::physical_plan::{
33    ExecutionPlan, SendableRecordBatchStream,
34    sorts::sort_preserving_merge::SortPreservingMergeExec, stream::RecordBatchStreamAdapter,
35    union::UnionExec,
36};
37use datafusion_common::{DataFusionError, ScalarValue};
38use datafusion_physical_expr::{PhysicalSortExpr, expressions::Column};
39use deepsize::DeepSizeOf;
40use futures::{
41    FutureExt, Stream, StreamExt, TryFutureExt, TryStreamExt,
42    future::BoxFuture,
43    stream::{self},
44};
45use lance_core::{
46    Error, ROW_ID, Result,
47    cache::{CacheKey, LanceCache, WeakLanceCache},
48    error::LanceOptionExt,
49    utils::{
50        mask::NullableRowAddrSet,
51        tokio::get_num_compute_intensive_cpus,
52        tracing::{IO_TYPE_LOAD_SCALAR_PART, TRACE_IO_EVENTS},
53    },
54};
55use lance_datafusion::{
56    chunker::chunk_concat_stream,
57    exec::{LanceExecutionOptions, OneShotExec, execute_plan},
58};
59use lance_io::object_store::ObjectStore;
60use log::{debug, warn};
61use object_store::path::Path;
62use rangemap::RangeInclusiveMap;
63use roaring::RoaringBitmap;
64use serde::{Deserialize, Serialize, Serializer};
65use tracing::{info, instrument};
66
67mod flat;
68
69const BTREE_LOOKUP_NAME: &str = "page_lookup.lance";
70const BTREE_PAGES_NAME: &str = "page_data.lance";
71pub const DEFAULT_BTREE_BATCH_SIZE: u64 = 4096;
72const BATCH_SIZE_META_KEY: &str = "batch_size";
73const DEFAULT_RANGE_PARTITIONED: bool = false;
74const RANGE_PARTITIONED_META_KEY: &str = "range_partitioned";
75const PAGE_NUM_PER_RANGE_PARTITION_META_KEY: &str = "page_num_per_range_partition";
76const BTREE_INDEX_VERSION: u32 = 0;
77pub(crate) const BTREE_VALUES_COLUMN: &str = "values";
78pub(crate) const BTREE_IDS_COLUMN: &str = "ids";
79
80/// Wraps a ScalarValue and implements Ord (ScalarValue only implements PartialOrd)
81#[derive(Clone, Debug)]
82pub struct OrderableScalarValue(pub ScalarValue);
83
84impl DeepSizeOf for OrderableScalarValue {
85    fn deep_size_of_children(&self, _context: &mut deepsize::Context) -> usize {
86        // deepsize and size both factor in the size of the ScalarValue
87        self.0.size() - std::mem::size_of::<ScalarValue>()
88    }
89}
90
91impl Display for OrderableScalarValue {
92    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
93        std::fmt::Display::fmt(&self.0, f)
94    }
95}
96
97impl PartialEq for OrderableScalarValue {
98    fn eq(&self, other: &Self) -> bool {
99        self.0.eq(&other.0)
100    }
101}
102
103impl Eq for OrderableScalarValue {}
104
105impl PartialOrd for OrderableScalarValue {
106    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
107        Some(self.cmp(other))
108    }
109}
110
111// manual implementation of `Ord` that panics when asked to compare scalars of different type
112// and always puts nulls before non-nulls (this is consistent with Option<T>'s implementation
113// of Ord)
114//
115// TODO: Consider upstreaming this
116impl Ord for OrderableScalarValue {
117    fn cmp(&self, other: &Self) -> Ordering {
118        use ScalarValue::*;
119        // This purposely doesn't have a catch-all "(_, _)" so that
120        // any newly added enum variant will require editing this list
121        // or else face a compile error
122        match (&self.0, &other.0) {
123            (Decimal32(v1, p1, s1), Decimal32(v2, p2, s2)) => {
124                if p1.eq(p2) && s1.eq(s2) {
125                    v1.cmp(v2)
126                } else {
127                    // Two decimal values can only be compared if they have the same precision and scale.
128                    panic!("Attempt to compare decimals with unequal precision / scale")
129                }
130            }
131            (Decimal32(v1, _, _), Null) => {
132                if v1.is_none() {
133                    Ordering::Equal
134                } else {
135                    Ordering::Greater
136                }
137            }
138            (Decimal32(_, _, _), _) => panic!("Attempt to compare decimal with non-decimal"),
139            (Decimal64(v1, p1, s1), Decimal64(v2, p2, s2)) => {
140                if p1.eq(p2) && s1.eq(s2) {
141                    v1.cmp(v2)
142                } else {
143                    // Two decimal values can only be compared if they have the same precision and scale.
144                    panic!("Attempt to compare decimals with unequal precision / scale")
145                }
146            }
147            (Decimal64(v1, _, _), Null) => {
148                if v1.is_none() {
149                    Ordering::Equal
150                } else {
151                    Ordering::Greater
152                }
153            }
154            (Decimal64(_, _, _), _) => panic!("Attempt to compare decimal with non-decimal"),
155            (Decimal128(v1, p1, s1), Decimal128(v2, p2, s2)) => {
156                if p1.eq(p2) && s1.eq(s2) {
157                    v1.cmp(v2)
158                } else {
159                    // Two decimal values can only be compared if they have the same precision and scale.
160                    panic!("Attempt to compare decimals with unequal precision / scale")
161                }
162            }
163            (Decimal128(v1, _, _), Null) => {
164                if v1.is_none() {
165                    Ordering::Equal
166                } else {
167                    Ordering::Greater
168                }
169            }
170            (Decimal128(_, _, _), _) => panic!("Attempt to compare decimal with non-decimal"),
171            (Decimal256(v1, p1, s1), Decimal256(v2, p2, s2)) => {
172                if p1.eq(p2) && s1.eq(s2) {
173                    v1.cmp(v2)
174                } else {
175                    // Two decimal values can only be compared if they have the same precision and scale.
176                    panic!("Attempt to compare decimals with unequal precision / scale")
177                }
178            }
179            (Decimal256(v1, _, _), Null) => {
180                if v1.is_none() {
181                    Ordering::Equal
182                } else {
183                    Ordering::Greater
184                }
185            }
186            (Decimal256(_, _, _), _) => panic!("Attempt to compare decimal with non-decimal"),
187
188            (Boolean(v1), Boolean(v2)) => v1.cmp(v2),
189            (Boolean(v1), Null) => {
190                if v1.is_none() {
191                    Ordering::Equal
192                } else {
193                    Ordering::Greater
194                }
195            }
196            (Boolean(_), _) => panic!("Attempt to compare boolean with non-boolean"),
197            (Float32(v1), Float32(v2)) => match (v1, v2) {
198                (Some(f1), Some(f2)) => f1.total_cmp(f2),
199                (None, Some(_)) => Ordering::Less,
200                (Some(_), None) => Ordering::Greater,
201                (None, None) => Ordering::Equal,
202            },
203            (Float32(v1), Null) => {
204                if v1.is_none() {
205                    Ordering::Equal
206                } else {
207                    Ordering::Greater
208                }
209            }
210            (Float32(_), _) => panic!("Attempt to compare f32 with non-f32"),
211            (Float64(v1), Float64(v2)) => match (v1, v2) {
212                (Some(f1), Some(f2)) => f1.total_cmp(f2),
213                (None, Some(_)) => Ordering::Less,
214                (Some(_), None) => Ordering::Greater,
215                (None, None) => Ordering::Equal,
216            },
217            (Float64(v1), Null) => {
218                if v1.is_none() {
219                    Ordering::Equal
220                } else {
221                    Ordering::Greater
222                }
223            }
224            (Float64(_), _) => panic!("Attempt to compare f64 with non-f64"),
225            (Float16(v1), Float16(v2)) => match (v1, v2) {
226                (Some(f1), Some(f2)) => f1.total_cmp(f2),
227                (None, Some(_)) => Ordering::Less,
228                (Some(_), None) => Ordering::Greater,
229                (None, None) => Ordering::Equal,
230            },
231            (Float16(v1), Null) => {
232                if v1.is_none() {
233                    Ordering::Equal
234                } else {
235                    Ordering::Greater
236                }
237            }
238            (Float16(_), _) => panic!("Attempt to compare f16 with non-f16"),
239            (Int8(v1), Int8(v2)) => v1.cmp(v2),
240            (Int8(v1), Null) => {
241                if v1.is_none() {
242                    Ordering::Equal
243                } else {
244                    Ordering::Greater
245                }
246            }
247            (Int8(_), _) => panic!("Attempt to compare Int8 with non-Int8"),
248            (Int16(v1), Int16(v2)) => v1.cmp(v2),
249            (Int16(v1), Null) => {
250                if v1.is_none() {
251                    Ordering::Equal
252                } else {
253                    Ordering::Greater
254                }
255            }
256            (Int16(_), _) => panic!("Attempt to compare Int16 with non-Int16"),
257            (Int32(v1), Int32(v2)) => v1.cmp(v2),
258            (Int32(v1), Null) => {
259                if v1.is_none() {
260                    Ordering::Equal
261                } else {
262                    Ordering::Greater
263                }
264            }
265            (Int32(_), _) => panic!("Attempt to compare Int32 with non-Int32"),
266            (Int64(v1), Int64(v2)) => v1.cmp(v2),
267            (Int64(v1), Null) => {
268                if v1.is_none() {
269                    Ordering::Equal
270                } else {
271                    Ordering::Greater
272                }
273            }
274            (Int64(_), _) => panic!("Attempt to compare Int64 with non-Int64"),
275            (UInt8(v1), UInt8(v2)) => v1.cmp(v2),
276            (UInt8(v1), Null) => {
277                if v1.is_none() {
278                    Ordering::Equal
279                } else {
280                    Ordering::Greater
281                }
282            }
283            (UInt8(_), _) => panic!("Attempt to compare UInt8 with non-UInt8"),
284            (UInt16(v1), UInt16(v2)) => v1.cmp(v2),
285            (UInt16(v1), Null) => {
286                if v1.is_none() {
287                    Ordering::Equal
288                } else {
289                    Ordering::Greater
290                }
291            }
292            (UInt16(_), _) => panic!("Attempt to compare UInt16 with non-UInt16"),
293            (UInt32(v1), UInt32(v2)) => v1.cmp(v2),
294            (UInt32(v1), Null) => {
295                if v1.is_none() {
296                    Ordering::Equal
297                } else {
298                    Ordering::Greater
299                }
300            }
301            (UInt32(_), _) => panic!("Attempt to compare UInt32 with non-UInt32"),
302            (UInt64(v1), UInt64(v2)) => v1.cmp(v2),
303            (UInt64(v1), Null) => {
304                if v1.is_none() {
305                    Ordering::Equal
306                } else {
307                    Ordering::Greater
308                }
309            }
310            (UInt64(_), _) => panic!("Attempt to compare UInt64 with non-UInt64"),
311            (Utf8(v1) | Utf8View(v1) | LargeUtf8(v1), Utf8(v2) | Utf8View(v2) | LargeUtf8(v2)) => {
312                v1.cmp(v2)
313            }
314            (Utf8(v1) | Utf8View(v1) | LargeUtf8(v1), Null) => {
315                if v1.is_none() {
316                    Ordering::Equal
317                } else {
318                    Ordering::Greater
319                }
320            }
321            (Utf8(_) | Utf8View(_) | LargeUtf8(_), _) => {
322                panic!("Attempt to compare Utf8 with non-Utf8")
323            }
324            (
325                Binary(v1) | LargeBinary(v1) | BinaryView(v1),
326                Binary(v2) | LargeBinary(v2) | BinaryView(v2),
327            ) => v1.cmp(v2),
328            (Binary(v1) | LargeBinary(v1) | BinaryView(v1), Null) => {
329                if v1.is_none() {
330                    Ordering::Equal
331                } else {
332                    Ordering::Greater
333                }
334            }
335            (Binary(_) | LargeBinary(_) | BinaryView(_), _) => {
336                panic!("Attempt to compare Binary with non-Binary")
337            }
338            (FixedSizeBinary(_, v1), FixedSizeBinary(_, v2)) => v1.cmp(v2),
339            (FixedSizeBinary(_, v1), Null) => {
340                if v1.is_none() {
341                    Ordering::Equal
342                } else {
343                    Ordering::Greater
344                }
345            }
346            (FixedSizeBinary(_, _), _) => {
347                panic!("Attempt to compare FixedSizeBinary with non-FixedSizeBinary")
348            }
349            (FixedSizeList(left), FixedSizeList(right)) => {
350                if left.eq(right) {
351                    todo!()
352                } else {
353                    panic!(
354                        "Attempt to compare fixed size list elements with different widths/fields"
355                    )
356                }
357            }
358            (FixedSizeList(left), Null) => {
359                if left.is_null(0) {
360                    Ordering::Equal
361                } else {
362                    Ordering::Greater
363                }
364            }
365            (FixedSizeList(_), _) => {
366                panic!("Attempt to compare FixedSizeList with non-FixedSizeList")
367            }
368            (List(_), List(_)) => todo!(),
369            (List(left), Null) => {
370                if left.is_null(0) {
371                    Ordering::Equal
372                } else {
373                    Ordering::Greater
374                }
375            }
376            (List(_), _) => {
377                panic!("Attempt to compare List with non-List")
378            }
379            (LargeList(_), _) => todo!(),
380            (Map(_), Map(_)) => todo!(),
381            (Map(left), Null) => {
382                if left.is_null(0) {
383                    Ordering::Equal
384                } else {
385                    Ordering::Greater
386                }
387            }
388            (Map(_), _) => {
389                panic!("Attempt to compare Map with non-Map")
390            }
391            (Date32(v1), Date32(v2)) => v1.cmp(v2),
392            (Date32(v1), Null) => {
393                if v1.is_none() {
394                    Ordering::Equal
395                } else {
396                    Ordering::Greater
397                }
398            }
399            (Date32(_), _) => panic!("Attempt to compare Date32 with non-Date32"),
400            (Date64(v1), Date64(v2)) => v1.cmp(v2),
401            (Date64(v1), Null) => {
402                if v1.is_none() {
403                    Ordering::Equal
404                } else {
405                    Ordering::Greater
406                }
407            }
408            (Date64(_), _) => panic!("Attempt to compare Date64 with non-Date64"),
409            (Time32Second(v1), Time32Second(v2)) => v1.cmp(v2),
410            (Time32Second(v1), Null) => {
411                if v1.is_none() {
412                    Ordering::Equal
413                } else {
414                    Ordering::Greater
415                }
416            }
417            (Time32Second(_), _) => panic!("Attempt to compare Time32Second with non-Time32Second"),
418            (Time32Millisecond(v1), Time32Millisecond(v2)) => v1.cmp(v2),
419            (Time32Millisecond(v1), Null) => {
420                if v1.is_none() {
421                    Ordering::Equal
422                } else {
423                    Ordering::Greater
424                }
425            }
426            (Time32Millisecond(_), _) => {
427                panic!("Attempt to compare Time32Millisecond with non-Time32Millisecond")
428            }
429            (Time64Microsecond(v1), Time64Microsecond(v2)) => v1.cmp(v2),
430            (Time64Microsecond(v1), Null) => {
431                if v1.is_none() {
432                    Ordering::Equal
433                } else {
434                    Ordering::Greater
435                }
436            }
437            (Time64Microsecond(_), _) => {
438                panic!("Attempt to compare Time64Microsecond with non-Time64Microsecond")
439            }
440            (Time64Nanosecond(v1), Time64Nanosecond(v2)) => v1.cmp(v2),
441            (Time64Nanosecond(v1), Null) => {
442                if v1.is_none() {
443                    Ordering::Equal
444                } else {
445                    Ordering::Greater
446                }
447            }
448            (Time64Nanosecond(_), _) => {
449                panic!("Attempt to compare Time64Nanosecond with non-Time64Nanosecond")
450            }
451            (TimestampSecond(v1, _), TimestampSecond(v2, _)) => v1.cmp(v2),
452            (TimestampSecond(v1, _), Null) => {
453                if v1.is_none() {
454                    Ordering::Equal
455                } else {
456                    Ordering::Greater
457                }
458            }
459            (TimestampSecond(_, _), _) => {
460                panic!("Attempt to compare TimestampSecond with non-TimestampSecond")
461            }
462            (TimestampMillisecond(v1, _), TimestampMillisecond(v2, _)) => v1.cmp(v2),
463            (TimestampMillisecond(v1, _), Null) => {
464                if v1.is_none() {
465                    Ordering::Equal
466                } else {
467                    Ordering::Greater
468                }
469            }
470            (TimestampMillisecond(_, _), _) => {
471                panic!("Attempt to compare TimestampMillisecond with non-TimestampMillisecond")
472            }
473            (TimestampMicrosecond(v1, _), TimestampMicrosecond(v2, _)) => v1.cmp(v2),
474            (TimestampMicrosecond(v1, _), Null) => {
475                if v1.is_none() {
476                    Ordering::Equal
477                } else {
478                    Ordering::Greater
479                }
480            }
481            (TimestampMicrosecond(_, _), _) => {
482                panic!("Attempt to compare TimestampMicrosecond with non-TimestampMicrosecond")
483            }
484            (TimestampNanosecond(v1, _), TimestampNanosecond(v2, _)) => v1.cmp(v2),
485            (TimestampNanosecond(v1, _), Null) => {
486                if v1.is_none() {
487                    Ordering::Equal
488                } else {
489                    Ordering::Greater
490                }
491            }
492            (TimestampNanosecond(_, _), _) => {
493                panic!("Attempt to compare TimestampNanosecond with non-TimestampNanosecond")
494            }
495            (IntervalYearMonth(v1), IntervalYearMonth(v2)) => v1.cmp(v2),
496            (IntervalYearMonth(v1), Null) => {
497                if v1.is_none() {
498                    Ordering::Equal
499                } else {
500                    Ordering::Greater
501                }
502            }
503            (IntervalYearMonth(_), _) => {
504                panic!("Attempt to compare IntervalYearMonth with non-IntervalYearMonth")
505            }
506            (IntervalDayTime(v1), IntervalDayTime(v2)) => v1.cmp(v2),
507            (IntervalDayTime(v1), Null) => {
508                if v1.is_none() {
509                    Ordering::Equal
510                } else {
511                    Ordering::Greater
512                }
513            }
514            (IntervalDayTime(_), _) => {
515                panic!("Attempt to compare IntervalDayTime with non-IntervalDayTime")
516            }
517            (IntervalMonthDayNano(v1), IntervalMonthDayNano(v2)) => v1.cmp(v2),
518            (IntervalMonthDayNano(v1), Null) => {
519                if v1.is_none() {
520                    Ordering::Equal
521                } else {
522                    Ordering::Greater
523                }
524            }
525            (IntervalMonthDayNano(_), _) => {
526                panic!("Attempt to compare IntervalMonthDayNano with non-IntervalMonthDayNano")
527            }
528            (DurationSecond(v1), DurationSecond(v2)) => v1.cmp(v2),
529            (DurationSecond(v1), Null) => {
530                if v1.is_none() {
531                    Ordering::Equal
532                } else {
533                    Ordering::Greater
534                }
535            }
536            (DurationSecond(_), _) => {
537                panic!("Attempt to compare DurationSecond with non-DurationSecond")
538            }
539            (DurationMillisecond(v1), DurationMillisecond(v2)) => v1.cmp(v2),
540            (DurationMillisecond(v1), Null) => {
541                if v1.is_none() {
542                    Ordering::Equal
543                } else {
544                    Ordering::Greater
545                }
546            }
547            (DurationMillisecond(_), _) => {
548                panic!("Attempt to compare DurationMillisecond with non-DurationMillisecond")
549            }
550            (DurationMicrosecond(v1), DurationMicrosecond(v2)) => v1.cmp(v2),
551            (DurationMicrosecond(v1), Null) => {
552                if v1.is_none() {
553                    Ordering::Equal
554                } else {
555                    Ordering::Greater
556                }
557            }
558            (DurationMicrosecond(_), _) => {
559                panic!("Attempt to compare DurationMicrosecond with non-DurationMicrosecond")
560            }
561            (DurationNanosecond(v1), DurationNanosecond(v2)) => v1.cmp(v2),
562            (DurationNanosecond(v1), Null) => {
563                if v1.is_none() {
564                    Ordering::Equal
565                } else {
566                    Ordering::Greater
567                }
568            }
569            (DurationNanosecond(_), _) => {
570                panic!("Attempt to compare DurationNanosecond with non-DurationNanosecond")
571            }
572            (Struct(_arr), Struct(_arr2)) => todo!(),
573            (Struct(arr), Null) => {
574                if arr.is_empty() {
575                    Ordering::Equal
576                } else {
577                    Ordering::Greater
578                }
579            }
580            (Struct(_arr), _) => panic!("Attempt to compare Struct with non-Struct"),
581            (Dictionary(_k1, _v1), Dictionary(_k2, _v2)) => todo!(),
582            (Dictionary(_, v1), Null) => Self(*v1.clone()).cmp(&Self(ScalarValue::Null)),
583            (Dictionary(_, _), _) => panic!("Attempt to compare Dictionary with non-Dictionary"),
584            // What would a btree of unions even look like?  May not be possible.
585            (Union(_, _, _), _) => todo!("Support for union scalars"),
586            (Null, Null) => Ordering::Equal,
587            (Null, _) => todo!(),
588        }
589    }
590}
591
592#[derive(Debug, DeepSizeOf, PartialEq, Eq)]
593struct PageRecord {
594    max: OrderableScalarValue,
595    page_number: u32,
596}
597
598trait BTreeMapExt<K, V> {
599    fn largest_node_less(&self, key: &K) -> Option<(&K, &V)>;
600}
601
602impl<K: Ord, V> BTreeMapExt<K, V> for BTreeMap<K, V> {
603    fn largest_node_less(&self, key: &K) -> Option<(&K, &V)> {
604        self.range((Bound::Unbounded, Bound::Excluded(key)))
605            .next_back()
606    }
607}
608
609/// An in-memory structure that can quickly satisfy scalar queries using a btree of ScalarValue
610#[derive(Debug, DeepSizeOf, PartialEq, Eq)]
611pub struct BTreeLookup {
612    tree: BTreeMap<OrderableScalarValue, Vec<PageRecord>>,
613    /// Pages where the value may be null (does not include all_null_pages)
614    null_pages: Vec<u32>,
615    /// Pages that are entirely null
616    all_null_pages: Vec<u32>,
617}
618
619impl BTreeLookup {
620    fn empty() -> Self {
621        Self {
622            tree: BTreeMap::new(),
623            null_pages: Vec::new(),
624            all_null_pages: Vec::new(),
625        }
626    }
627}
628
629#[derive(Debug, Copy, Clone)]
630enum Matches {
631    Some(u32),
632    All(u32),
633}
634
635impl Matches {
636    fn page_id(&self) -> u32 {
637        match self {
638            Self::Some(page_id) => *page_id,
639            Self::All(page_id) => *page_id,
640        }
641    }
642}
643
644impl BTreeLookup {
645    fn new(
646        tree: BTreeMap<OrderableScalarValue, Vec<PageRecord>>,
647        null_pages: Vec<u32>,
648        all_null_pages: Vec<u32>,
649    ) -> Self {
650        Self {
651            tree,
652            null_pages,
653            all_null_pages,
654        }
655    }
656
657    // All pages that could have a value equal to val
658    fn pages_eq(&self, query: &OrderableScalarValue) -> Vec<Matches> {
659        if query.0.is_null() {
660            self.pages_null()
661        } else {
662            self.pages_between((Bound::Included(query), Bound::Excluded(query)))
663        }
664    }
665
666    // All pages that could have a value equal to one of the values
667    fn pages_in(&self, values: impl IntoIterator<Item = OrderableScalarValue>) -> Vec<Matches> {
668        // TODO: Right now we convert all Matches::All into Matches::Some.  We could refine this.
669        // It would improve performance on low cardinality data.
670        let page_lists = values
671            .into_iter()
672            .map(|val| {
673                self.pages_eq(&val)
674                    .into_iter()
675                    .map(|matches| matches.page_id())
676            })
677            .collect::<Vec<_>>();
678        let total_size = page_lists.iter().map(|set| set.len()).sum();
679        let mut heap = BinaryHeap::with_capacity(total_size);
680        for page_list in page_lists {
681            heap.extend(page_list);
682        }
683        let mut all_pages = heap.into_sorted_vec();
684        all_pages.dedup();
685        all_pages.into_iter().map(Matches::Some).collect()
686    }
687
688    // All pages that could have a value in the range
689    fn pages_between(
690        &self,
691        range: (Bound<&OrderableScalarValue>, Bound<&OrderableScalarValue>),
692    ) -> Vec<Matches> {
693        // We need to grab a little bit left of the given range because the query might be 7
694        // and the first page might be something like 5-10.
695        let lower_bound = match range.0 {
696            Bound::Unbounded => Bound::Unbounded,
697            // It doesn't matter if the bound is exclusive or inclusive.  We are going to grab
698            // the first node whose min is strictly less than the given bound.  Then we grab
699            // all nodes greater than or equal to that
700            //
701            // We have to peek a bit to the left because we might have something like a lower
702            // bound of 7 and there is a page [5-10] we want to search for.
703            Bound::Included(lower) => self
704                .tree
705                .largest_node_less(lower)
706                .map(|val| Bound::Included(val.0))
707                .unwrap_or(Bound::Unbounded),
708            Bound::Excluded(lower) => self
709                .tree
710                .largest_node_less(lower)
711                .map(|val| Bound::Included(val.0))
712                .unwrap_or(Bound::Unbounded),
713        };
714        let upper_bound = match range.1 {
715            Bound::Unbounded => Bound::Unbounded,
716            Bound::Included(upper) => Bound::Included(upper),
717            // Even if the upper bound is excluded we need to include it on an [x, x) query.  This is because the
718            // query might be [x, x).  Our lower bound might find some [a-x] bucket and we still
719            // want to include any [x, z] bucket.
720            //
721            // We could be slightly more accurate here and only include the upper bound if the lower bound
722            // is defined, inclusive, and equal to the upper bound.  However, let's keep it simple for now.  This
723            // should only affect the probably rare case that our query is a true range query and the value
724            // matches an upper bound.  This will all be moot if/when we merge pages.
725            Bound::Excluded(upper) => Bound::Included(upper),
726        };
727
728        match (lower_bound, upper_bound) {
729            (Bound::Excluded(lower), Bound::Excluded(upper))
730            | (Bound::Excluded(lower), Bound::Included(upper))
731            | (Bound::Included(lower), Bound::Excluded(upper)) => {
732                // It's not really clear what (Included(5), Excluded(5)) would mean so we
733                // interpret it as an empty range which matches rust's BTreeMap behavior
734                if lower >= upper {
735                    return vec![];
736                }
737            }
738            (Bound::Included(lower), Bound::Included(upper)) => {
739                if lower > upper {
740                    return vec![];
741                }
742            }
743            _ => {}
744        }
745
746        let mut matches = Vec::new();
747
748        for (min, page_records) in self.tree.range((lower_bound, upper_bound)) {
749            for page_record in page_records {
750                match lower_bound {
751                    Bound::Unbounded => {}
752                    Bound::Included(lower) => {
753                        if page_record.max.cmp(lower) == Ordering::Less {
754                            continue;
755                        }
756                    }
757                    Bound::Excluded(lower) => {
758                        if page_record.max.cmp(lower) != Ordering::Greater {
759                            continue;
760                        }
761                    }
762                }
763                // At this point we know the page record matches at least some values.
764                // We should test to see if ALL values are a match.
765
766                if min.0.is_null() || page_record.max.0.is_null() {
767                    // If there are nulls then we just use Matches::Some
768                    matches.push(Matches::Some(page_record.page_number));
769                    continue;
770                }
771
772                match range.0 {
773                    // range.0 < X therefore if the smallest value is not strictly greater than
774                    // the lower bound we only have partial match
775                    Bound::Excluded(lower) => {
776                        if min.cmp(lower) != Ordering::Greater {
777                            matches.push(Matches::Some(page_record.page_number));
778                            continue;
779                        }
780                    }
781                    // range.0 <= X therefore if the smallest value is not greater than or equal
782                    // to the lower bound we only have partial match
783                    Bound::Included(lower) => {
784                        if min.cmp(lower) == Ordering::Less {
785                            matches.push(Matches::Some(page_record.page_number));
786                            continue;
787                        }
788                    }
789                    Bound::Unbounded => {}
790                }
791                match range.1 {
792                    // X < range.1 therefore if the largest value is not strictly less than
793                    // the upper bound we only have partial match
794                    Bound::Excluded(upper) => {
795                        if page_record.max.cmp(upper) != Ordering::Less {
796                            matches.push(Matches::Some(page_record.page_number));
797                            continue;
798                        }
799                    }
800                    // X <= range.1 therefore if the largest value is not less than or equal to
801                    // the upper bound we only have partial match
802                    Bound::Included(upper) => {
803                        if page_record.max.cmp(upper) == Ordering::Greater {
804                            matches.push(Matches::Some(page_record.page_number));
805                            continue;
806                        }
807                    }
808                    Bound::Unbounded => {}
809                }
810                // The min is greater than the lower bound and the max is less than the upper bound
811                // so we have a full match
812                matches.push(Matches::All(page_record.page_number));
813            }
814        }
815
816        matches
817    }
818
819    fn pages_null(&self) -> Vec<Matches> {
820        self.null_pages
821            .iter()
822            .map(|page_id| Matches::Some(*page_id))
823            .chain(self.all_null_pages.iter().copied().map(Matches::All))
824            .collect()
825    }
826}
827
828// We only need to open a file reader for pages if we need to load a page.  If all
829// pages are cached we don't open it.  If we do open it we should only open it once.
830#[derive(Clone)]
831struct LazyIndexReader {
832    index_reader: Arc<tokio::sync::Mutex<Option<Arc<dyn IndexReader>>>>,
833    store: Arc<dyn IndexStore>,
834    ranges_to_files: Option<Arc<RangeInclusiveMap<u32, (String, u32)>>>,
835}
836
837impl LazyIndexReader {
838    fn new(
839        store: Arc<dyn IndexStore>,
840        ranges_to_files: Option<Arc<RangeInclusiveMap<u32, (String, u32)>>>,
841    ) -> Self {
842        Self {
843            index_reader: Arc::new(tokio::sync::Mutex::new(None)),
844            store,
845            ranges_to_files,
846        }
847    }
848
849    async fn get(&self) -> Result<Arc<dyn IndexReader>> {
850        let mut reader = self.index_reader.lock().await;
851        if reader.is_none() {
852            let index_reader = if let Some(ranges_to_files) = &self.ranges_to_files {
853                Arc::new(LazyRangedIndexReader::new(
854                    self.store.clone(),
855                    ranges_to_files.clone(),
856                ))
857            } else {
858                self.store.open_index_file(BTREE_PAGES_NAME).await?
859            };
860            *reader = Some(index_reader);
861        }
862        Ok(reader.as_ref().unwrap().clone())
863    }
864}
865
866/// Index reader to dispatch page query to corresponding ranged page-files.
867struct LazyRangedIndexReader {
868    #[allow(clippy::type_complexity)]
869    readers:
870        Arc<tokio::sync::Mutex<HashMap<String, Arc<tokio::sync::OnceCell<Arc<dyn IndexReader>>>>>>,
871    store: Arc<dyn IndexStore>,
872    ranges_to_files: Arc<RangeInclusiveMap<u32, (String, u32)>>,
873}
874
875impl LazyRangedIndexReader {
876    fn new(
877        store: Arc<dyn IndexStore>,
878        ranges_to_files: Arc<RangeInclusiveMap<u32, (String, u32)>>,
879    ) -> Self {
880        Self {
881            readers: Arc::new(tokio::sync::Mutex::new(HashMap::new())),
882            store,
883            ranges_to_files,
884        }
885    }
886
887    async fn get_reader(&self, file_name: &str) -> Result<Arc<dyn IndexReader>> {
888        let reader_cell = {
889            let mut guard = self.readers.lock().await;
890            guard
891                .entry(file_name.to_string())
892                .or_insert_with(|| Arc::new(tokio::sync::OnceCell::new()))
893                .clone()
894        };
895        let reader = reader_cell
896            .get_or_try_init(|| async { self.store.open_index_file(file_name).await })
897            .await?;
898        Ok(reader.clone())
899    }
900
901    async fn get_reader_and_local_page_idx(
902        &self,
903        page_idx: u32,
904    ) -> Result<(Arc<dyn IndexReader>, u32)> {
905        let (page_file_name, offset) = self.ranges_to_files.get(&page_idx).ok_or_else(|| {
906            Error::internal(format!(
907                "Unexpected page index, index {} is out of range.",
908                page_idx
909            ))
910        })?;
911        let reader = self.get_reader(page_file_name).await?;
912        Ok((reader.clone(), page_idx - *offset))
913    }
914}
915
916#[async_trait]
917impl IndexReader for LazyRangedIndexReader {
918    async fn read_record_batch(&self, n: u64, batch_size: u64) -> Result<RecordBatch> {
919        let (reader, local_page_idx) = self.get_reader_and_local_page_idx(n as u32).await?;
920        reader
921            .read_record_batch(local_page_idx as u64, batch_size)
922            .await
923    }
924
925    async fn read_range(
926        &self,
927        _range: std::ops::Range<usize>,
928        _projection: Option<&[&str]>,
929    ) -> Result<RecordBatch> {
930        unimplemented!("Read range is not implemented for lazy page file reader.");
931    }
932
933    async fn num_batches(&self, batch_size: u64) -> u32 {
934        let mut total_batches = 0;
935        for (_, (file_name, _)) in self.ranges_to_files.iter() {
936            let reader = self
937                .get_reader(file_name)
938                .await
939                .unwrap_or_else(|_| panic!("Cannot open page file {}.", file_name));
940            total_batches += reader.as_ref().num_batches(batch_size).await;
941        }
942        total_batches
943    }
944
945    fn num_rows(&self) -> usize {
946        unimplemented!("only async functions are available for lazy page index reader.");
947    }
948
949    fn schema(&self) -> &lance_core::datatypes::Schema {
950        unimplemented!("only async functions are available for lazy page index reader.");
951    }
952}
953
954/// A btree index satisfies scalar queries using a b tree
955///
956/// The upper layers of the btree are expected to be cached and, when unloaded,
957/// are stored in a btree structure in memory.  The leaves of the btree are left
958/// to be searched by some other kind of index (currently a flat search).
959///
960/// This strikes a balance between an expensive memory structure containing all
961/// of the values and an expensive disk structure that can't be efficiently searched.
962///
963/// For example, given 1Bi values we can store 256Ki leaves of size 4Ki.  We only
964/// need memory space for 256Ki leaves (depends on the data type but usually a few MiB
965/// at most) and can narrow our search to 4Ki values.
966///
967// Cache key implementation for type-safe cache access
968#[derive(Debug, Clone, DeepSizeOf)]
969pub struct CachedScalarIndex(Arc<dyn ScalarIndex>);
970
971impl CachedScalarIndex {
972    pub fn new(index: Arc<dyn ScalarIndex>) -> Self {
973        Self(index)
974    }
975
976    pub fn into_inner(self) -> Arc<dyn ScalarIndex> {
977        self.0
978    }
979}
980
981#[derive(Debug, Clone)]
982pub struct BTreePageKey {
983    pub page_number: u32,
984}
985
986impl CacheKey for BTreePageKey {
987    type ValueType = FlatIndex;
988
989    fn key(&self) -> std::borrow::Cow<'_, str> {
990        format!("page-{}", self.page_number).into()
991    }
992}
993
994/// Note: this is very similar to the IVF index except we store the IVF part in a btree
995/// for faster lookup
996#[derive(Clone, Debug)]
997pub struct BTreeIndex {
998    page_lookup: Arc<BTreeLookup>,
999    index_cache: WeakLanceCache,
1000    store: Arc<dyn IndexStore>,
1001    data_type: DataType,
1002    batch_size: u64,
1003
1004    /// A map that translates a global_page_idx stored in the single lookup file into the
1005    /// specific page file and local_page_idx.
1006    ///
1007    /// This is the key data structure used for efficiently reading data from a merged,
1008    /// range-partitioned index. It stores mappings from a contiguous range of global page
1009    /// indices to a tuple containing:
1010    ///
1011    /// 1. The path to the corresponding page file (e.g., `part_i_page_file.lance`).
1012    /// 2. The start offset that was used to calculate the local_page_idx for that partition.
1013    ///
1014    /// When a query needs to access a specific page using its `global_page_idx`:
1015    ///
1016    /// 1. The `global_page_idx` is used to look up its range in this `RangeInclusiveMap`,
1017    ///    and the map returns the `(file_path, start_offset)` tuple for that range.
1018    /// 3. The `local_page_idx` is calculated using the formula:
1019    ///    `local_page_idx = global_page_idx - start_offset`.
1020    /// 4. With the `file_path` and `local_page_idx`, the system can directly open the
1021    ///    correct partition file and read the specific page.
1022    ///
1023    /// # Example
1024    ///
1025    /// If the map contains an entry `(100..=199) => ("part_2_page_file.lance", 100)`, and we
1026    /// need to find `global_page_idx = 142`:
1027    ///
1028    /// - The map finds that 142 falls within the range `100..=199`, and it returns
1029    ///   `("part_2_page_file.lance", 100)`.
1030    /// - The local page_idx is calculated: `142 - 100 = 42`.
1031    /// - The system now knows to read page `42` from the file `part_2_page_file.lance`.
1032    ranges_to_files: Option<Arc<RangeInclusiveMap<u32, (String, u32)>>>,
1033    frag_reuse_index: Option<Arc<FragReuseIndex>>,
1034}
1035
1036impl DeepSizeOf for BTreeIndex {
1037    fn deep_size_of_children(&self, context: &mut deepsize::Context) -> usize {
1038        // We don't include the index cache, or anything stored in it. For example:
1039        // sub_index and fri.
1040        self.page_lookup.deep_size_of_children(context) + self.store.deep_size_of_children(context)
1041    }
1042}
1043
1044impl BTreeIndex {
1045    #[allow(clippy::too_many_arguments)]
1046    fn new(
1047        page_lookup: Arc<BTreeLookup>,
1048        store: Arc<dyn IndexStore>,
1049        data_type: DataType,
1050        index_cache: WeakLanceCache,
1051        batch_size: u64,
1052        ranges_to_files: Option<Arc<RangeInclusiveMap<u32, (String, u32)>>>,
1053        frag_reuse_index: Option<Arc<FragReuseIndex>>,
1054    ) -> Self {
1055        Self {
1056            page_lookup,
1057            store,
1058            data_type,
1059            index_cache,
1060            batch_size,
1061            ranges_to_files,
1062            frag_reuse_index,
1063        }
1064    }
1065
1066    async fn lookup_page(
1067        &self,
1068        page_number: u32,
1069        index_reader: LazyIndexReader,
1070        metrics: &dyn MetricsCollector,
1071    ) -> Result<Arc<FlatIndex>> {
1072        self.index_cache
1073            .get_or_insert_with_key(BTreePageKey { page_number }, move || async move {
1074                self.read_page(page_number, index_reader, metrics).await
1075            })
1076            .await
1077    }
1078
1079    #[instrument(level = "debug", skip_all)]
1080    async fn read_page(
1081        &self,
1082        page_number: u32,
1083        index_reader: LazyIndexReader,
1084        metrics: &dyn MetricsCollector,
1085    ) -> Result<FlatIndex> {
1086        metrics.record_part_load();
1087        info!(target: TRACE_IO_EVENTS, r#type=IO_TYPE_LOAD_SCALAR_PART, index_type="btree", part_id=page_number);
1088        let index_reader = index_reader.get().await?;
1089        let mut serialized_page = index_reader
1090            .read_record_batch(page_number as u64, self.batch_size)
1091            .await?;
1092        if let Some(frag_reuse_index_ref) = self.frag_reuse_index.as_ref() {
1093            serialized_page =
1094                frag_reuse_index_ref.remap_row_ids_record_batch(serialized_page, 1)?;
1095        }
1096        FlatIndex::try_new(serialized_page)
1097    }
1098
1099    async fn search_page(
1100        &self,
1101        query: &SargableQuery,
1102        matches: Matches,
1103        index_reader: LazyIndexReader,
1104        metrics: &dyn MetricsCollector,
1105    ) -> Result<NullableRowAddrSet> {
1106        let subindex = self
1107            .lookup_page(matches.page_id(), index_reader, metrics)
1108            .await?;
1109
1110        match matches {
1111            Matches::Some(_) => {
1112                // TODO: If this is an IN query we can perhaps simplify the subindex query by restricting it to the
1113                // values that might be in the page.  E.g. if we are searching for X IN [5, 3, 7] and five is in pages
1114                // 1 and 2 and three is in page 2 and seven is in pages 8 and 9, then when searching page 2 we only need
1115                // to search for X IN [5, 3]
1116                subindex.search(query, metrics)
1117            }
1118            Matches::All(_) => Ok(match query {
1119                // This means we hit an all-null page so just grab all row ids as true
1120                SargableQuery::IsNull() => subindex.all_ignore_nulls(),
1121                _ => subindex.all(),
1122            }),
1123        }
1124    }
1125
1126    #[instrument(level = "debug", skip_all)]
1127    fn try_from_serialized(
1128        data: RecordBatch,
1129        store: Arc<dyn IndexStore>,
1130        index_cache: &LanceCache,
1131        batch_size: u64,
1132        ranges_to_files: Option<Arc<RangeInclusiveMap<u32, (String, u32)>>>,
1133        frag_reuse_index: Option<Arc<FragReuseIndex>>,
1134    ) -> Result<Self> {
1135        let mut map = BTreeMap::<OrderableScalarValue, Vec<PageRecord>>::new();
1136        // Pages that have at least one null value
1137        let mut null_pages = Vec::<u32>::new();
1138        // Pages that are entirely null
1139        let mut all_null_pages = Vec::<u32>::new();
1140
1141        if data.num_rows() == 0 {
1142            let data_type = data.column(0).data_type().clone();
1143            let page_lookup = Arc::new(BTreeLookup::empty());
1144            return Ok(Self::new(
1145                page_lookup,
1146                store,
1147                data_type,
1148                WeakLanceCache::from(index_cache),
1149                batch_size,
1150                ranges_to_files,
1151                frag_reuse_index,
1152            ));
1153        }
1154
1155        let mins = data.column(0);
1156        let maxs = data.column(1);
1157        let null_counts = data
1158            .column(2)
1159            .as_any()
1160            .downcast_ref::<UInt32Array>()
1161            .unwrap();
1162        let page_numbers = data
1163            .column(3)
1164            .as_any()
1165            .downcast_ref::<UInt32Array>()
1166            .unwrap();
1167
1168        for idx in 0..data.num_rows() {
1169            let min = OrderableScalarValue(ScalarValue::try_from_array(&mins, idx)?);
1170            let max = OrderableScalarValue(ScalarValue::try_from_array(&maxs, idx)?);
1171            let null_count = null_counts.values()[idx];
1172            let page_number = page_numbers.values()[idx];
1173
1174            // If the page is entirely null don't even bother putting it in the tree
1175            if max.0.is_null() {
1176                all_null_pages.push(page_number);
1177                // continue so we don't add it to the null_pages
1178                continue;
1179            } else {
1180                map.entry(min)
1181                    .or_default()
1182                    .push(PageRecord { max, page_number });
1183            }
1184
1185            if null_count > 0 {
1186                null_pages.push(page_number);
1187            }
1188        }
1189
1190        let last_max = ScalarValue::try_from_array(&maxs, data.num_rows() - 1)?;
1191        map.entry(OrderableScalarValue(last_max)).or_default();
1192
1193        let data_type = mins.data_type();
1194
1195        let page_lookup = Arc::new(BTreeLookup::new(map, null_pages, all_null_pages));
1196
1197        Ok(Self::new(
1198            page_lookup,
1199            store,
1200            data_type.clone(),
1201            WeakLanceCache::from(index_cache),
1202            batch_size,
1203            ranges_to_files,
1204            frag_reuse_index,
1205        ))
1206    }
1207
1208    async fn load(
1209        store: Arc<dyn IndexStore>,
1210        frag_reuse_index: Option<Arc<FragReuseIndex>>,
1211        index_cache: &LanceCache,
1212    ) -> Result<Arc<Self>> {
1213        let page_lookup_file = store.open_index_file(BTREE_LOOKUP_NAME).await?;
1214        let num_rows_in_lookup = page_lookup_file.num_rows();
1215        let serialized_lookup = page_lookup_file
1216            .read_range(0..num_rows_in_lookup, None)
1217            .await?;
1218        let file_schema = page_lookup_file.schema();
1219        let batch_size = file_schema
1220            .metadata
1221            .get(BATCH_SIZE_META_KEY)
1222            .map(|bs| bs.parse().unwrap_or(DEFAULT_BTREE_BATCH_SIZE))
1223            .unwrap_or(DEFAULT_BTREE_BATCH_SIZE);
1224
1225        let range_partitioned = file_schema
1226            .metadata
1227            .get(RANGE_PARTITIONED_META_KEY)
1228            .map(|bs| bs.parse().unwrap_or(DEFAULT_RANGE_PARTITIONED))
1229            .unwrap_or(DEFAULT_RANGE_PARTITIONED);
1230        // For range-partitioned indices, construct the `ranges_to_files` map.
1231        // This converts the list of (partition ID, page count) from metadata into a map
1232        // from a global page range to its corresponding file and starting offset.
1233        let ranges_to_files = if range_partitioned {
1234            let part_sizes_str = file_schema
1235            .metadata
1236            .get(PAGE_NUM_PER_RANGE_PARTITION_META_KEY)
1237            .expect("Range-partitioned Btree lookup file must have page-number-per-range-file metadata!");
1238            let part_sizes_vec: Vec<(u64, u32)> = serde_json::from_str(part_sizes_str)?;
1239            let mut offset: u32 = 0;
1240
1241            let range_map = part_sizes_vec
1242                .into_iter()
1243                .map(|(id, size)| {
1244                    let range = offset..=(offset + size - 1);
1245                    let file_with_size = (part_page_data_file_path(id), offset);
1246                    offset += size;
1247                    (range, file_with_size)
1248                })
1249                .collect();
1250
1251            Some(Arc::new(range_map))
1252        } else {
1253            None
1254        };
1255
1256        Ok(Arc::new(Self::try_from_serialized(
1257            serialized_lookup,
1258            store,
1259            index_cache,
1260            batch_size,
1261            ranges_to_files,
1262            frag_reuse_index,
1263        )?))
1264    }
1265
1266    // For legacy reasons a btree index expects the training input to use value/_rowid
1267    fn train_schema(&self) -> Schema {
1268        let value_field = Field::new(VALUE_COLUMN_NAME, self.data_type.clone(), true);
1269        let row_id_field = Field::new(ROW_ID, DataType::UInt64, false);
1270        Schema::new(vec![value_field, row_id_field])
1271    }
1272
1273    /// Create a stream of all the data in the index, in the same format used to train the index
1274    async fn into_data_stream(self) -> Result<SendableRecordBatchStream> {
1275        let lazy_reader = LazyIndexReader::new(self.store.clone(), self.ranges_to_files.clone());
1276        let reader = lazy_reader.get().await?;
1277        let new_schema = Arc::new(self.train_schema());
1278        let new_schema_clone = new_schema.clone();
1279        let reader_stream = IndexReaderStream::new(reader, self.batch_size).await;
1280        let batches = reader_stream
1281            .map(|fut| fut.map_err(DataFusionError::from))
1282            .buffered(self.store.io_parallelism())
1283            .map_ok(move |batch| {
1284                RecordBatch::try_new(
1285                    new_schema.clone(),
1286                    vec![batch.column(0).clone(), batch.column(1).clone()],
1287                )
1288                .unwrap()
1289            })
1290            .boxed();
1291        Ok(Box::pin(RecordBatchStreamAdapter::new(
1292            new_schema_clone,
1293            batches,
1294        )))
1295    }
1296
1297    async fn combine_old_new(
1298        self,
1299        new_data: SendableRecordBatchStream,
1300        chunk_size: u64,
1301        valid_old_fragments: Option<RoaringBitmap>,
1302    ) -> Result<SendableRecordBatchStream> {
1303        let value_column_index = new_data.schema().index_of(VALUE_COLUMN_NAME)?;
1304
1305        let new_input = Arc::new(OneShotExec::new(new_data));
1306        let old_stream = self.into_data_stream().await?;
1307        let old_stream = match valid_old_fragments {
1308            Some(valid_frags) => filter_row_ids_by_fragments(old_stream, valid_frags),
1309            None => old_stream,
1310        };
1311        let old_input = Arc::new(OneShotExec::new(old_stream));
1312        debug_assert_eq!(
1313            old_input.schema().flattened_fields().len(),
1314            new_input.schema().flattened_fields().len()
1315        );
1316
1317        let sort_expr = PhysicalSortExpr {
1318            expr: Arc::new(Column::new(VALUE_COLUMN_NAME, value_column_index)),
1319            options: SortOptions {
1320                descending: false,
1321                nulls_first: true,
1322            },
1323        };
1324        // The UnionExec creates multiple partitions but the SortPreservingMergeExec merges
1325        // them back into a single partition.
1326        let all_data = UnionExec::try_new(vec![old_input, new_input])?;
1327        let ordered = Arc::new(SortPreservingMergeExec::new([sort_expr].into(), all_data));
1328
1329        let unchunked = execute_plan(
1330            ordered,
1331            LanceExecutionOptions {
1332                use_spilling: true,
1333                ..Default::default()
1334            },
1335        )?;
1336        Ok(chunk_concat_stream(unchunked, chunk_size as usize))
1337    }
1338}
1339
1340/// Filter a stream of record batches to only include rows whose row address
1341/// belongs to a fragment in `valid_fragments`. Row addresses encode the fragment
1342/// ID in the upper 32 bits.
1343fn filter_row_ids_by_fragments(
1344    stream: SendableRecordBatchStream,
1345    valid_fragments: RoaringBitmap,
1346) -> SendableRecordBatchStream {
1347    let schema = stream.schema();
1348    let filtered = stream.map(move |batch_result| {
1349        let batch = batch_result?;
1350        let row_ids = batch[ROW_ID]
1351            .as_any()
1352            .downcast_ref::<arrow_array::UInt64Array>()
1353            .expect("expected UInt64Array for row_id column");
1354        let mask: arrow_array::BooleanArray = row_ids
1355            .iter()
1356            .map(|id| id.map(|id| valid_fragments.contains((id >> 32) as u32)))
1357            .collect();
1358        Ok(arrow_select::filter::filter_record_batch(&batch, &mask)?)
1359    });
1360    Box::pin(RecordBatchStreamAdapter::new(schema, filtered))
1361}
1362
1363fn wrap_bound(bound: &Bound<ScalarValue>) -> Bound<OrderableScalarValue> {
1364    match bound {
1365        Bound::Unbounded => Bound::Unbounded,
1366        Bound::Included(val) => Bound::Included(OrderableScalarValue(val.clone())),
1367        Bound::Excluded(val) => Bound::Excluded(OrderableScalarValue(val.clone())),
1368    }
1369}
1370
1371fn serialize_with_display<T: Display, S: Serializer>(
1372    value: &Option<T>,
1373    serializer: S,
1374) -> std::result::Result<S::Ok, S::Error> {
1375    if let Some(value) = value {
1376        serializer.collect_str(value)
1377    } else {
1378        serializer.collect_str("N/A")
1379    }
1380}
1381
1382#[derive(Serialize)]
1383struct BTreeStatistics {
1384    #[serde(serialize_with = "serialize_with_display")]
1385    min: Option<OrderableScalarValue>,
1386    #[serde(serialize_with = "serialize_with_display")]
1387    max: Option<OrderableScalarValue>,
1388    num_pages: u32,
1389}
1390
1391#[async_trait]
1392impl Index for BTreeIndex {
1393    fn as_any(&self) -> &dyn Any {
1394        self
1395    }
1396
1397    fn as_index(self: Arc<Self>) -> Arc<dyn Index> {
1398        self
1399    }
1400
1401    fn as_vector_index(self: Arc<Self>) -> Result<Arc<dyn crate::vector::VectorIndex>> {
1402        Err(Error::not_supported_source(
1403            "BTreeIndex is not vector index".into(),
1404        ))
1405    }
1406
1407    async fn prewarm(&self) -> Result<()> {
1408        let index_reader = LazyIndexReader::new(self.store.clone(), self.ranges_to_files.clone());
1409        let reader = index_reader.get().await?;
1410        let num_pages = reader.num_batches(self.batch_size).await;
1411        let mut pages = stream::iter(0..num_pages)
1412            .map(|page_idx| {
1413                let index_reader = index_reader.clone();
1414                async move {
1415                    let page = self
1416                        .read_page(page_idx, index_reader, &NoOpMetricsCollector)
1417                        .await?;
1418                    Result::Ok((page_idx, page))
1419                }
1420            })
1421            .buffer_unordered(get_num_compute_intensive_cpus());
1422
1423        while let Some((page_idx, page)) = pages.try_next().await? {
1424            let inserted = self
1425                .index_cache
1426                .insert_with_key(
1427                    &BTreePageKey {
1428                        page_number: page_idx,
1429                    },
1430                    Arc::new(page),
1431                )
1432                .await;
1433
1434            if !inserted {
1435                return Err(Error::internal(
1436                    "Failed to prewarm index: cache is no longer available".to_string(),
1437                ));
1438            }
1439        }
1440
1441        Ok(())
1442    }
1443
1444    fn index_type(&self) -> IndexType {
1445        IndexType::BTree
1446    }
1447
1448    fn statistics(&self) -> Result<serde_json::Value> {
1449        let min = self
1450            .page_lookup
1451            .tree
1452            .first_key_value()
1453            .map(|(k, _)| k.clone());
1454        let max = self
1455            .page_lookup
1456            .tree
1457            .last_key_value()
1458            .map(|(k, _)| k.clone());
1459        serde_json::to_value(&BTreeStatistics {
1460            num_pages: self.page_lookup.tree.len() as u32,
1461            min,
1462            max,
1463        })
1464        .map_err(|err| err.into())
1465    }
1466
1467    async fn calculate_included_frags(&self) -> Result<RoaringBitmap> {
1468        let mut frag_ids = RoaringBitmap::default();
1469
1470        let lazy_reader = LazyIndexReader::new(self.store.clone(), self.ranges_to_files.clone());
1471        let sub_index_reader = lazy_reader.get().await?;
1472        let mut reader_stream = IndexReaderStream::new(sub_index_reader, self.batch_size)
1473            .await
1474            .buffered(self.store.io_parallelism());
1475        while let Some(serialized) = reader_stream.try_next().await? {
1476            let page = FlatIndex::try_new(serialized)?;
1477            frag_ids |= page.calculate_included_frags()?;
1478        }
1479
1480        Ok(frag_ids)
1481    }
1482}
1483
1484#[async_trait]
1485impl ScalarIndex for BTreeIndex {
1486    async fn search(
1487        &self,
1488        query: &dyn AnyQuery,
1489        metrics: &dyn MetricsCollector,
1490    ) -> Result<SearchResult> {
1491        let query = query.as_any().downcast_ref::<SargableQuery>().unwrap();
1492        let mut pages = match query {
1493            SargableQuery::Equals(val) => self
1494                .page_lookup
1495                .pages_eq(&OrderableScalarValue(val.clone())),
1496            SargableQuery::Range(start, end) => self
1497                .page_lookup
1498                .pages_between((wrap_bound(start).as_ref(), wrap_bound(end).as_ref())),
1499            SargableQuery::IsIn(values) => self
1500                .page_lookup
1501                .pages_in(values.iter().map(|val| OrderableScalarValue(val.clone()))),
1502            SargableQuery::FullTextSearch(_) => {
1503                return Err(Error::invalid_input(
1504                    "full text search is not supported for BTree index, build a inverted index for it",
1505                ));
1506            }
1507            SargableQuery::IsNull() => self.page_lookup.pages_null(),
1508        };
1509
1510        // For non-IsNull queries, also include null pages so that null row IDs
1511        // are tracked in the result. Any comparison with NULL yields NULL, and
1512        // we need this information for correct three-valued logic (e.g. NOT,
1513        // OR). Without this, a query like `NOT(x = 0)` on data where 0 doesn't
1514        // exist would incorrectly include NULL rows.
1515        //
1516        // We add them as Matches::Some (not Matches::All) so that
1517        // FlatIndex::search() evaluates the predicate and correctly marks
1518        // the rows as NULL rather than TRUE.
1519        if !matches!(query, SargableQuery::IsNull()) {
1520            let existing: HashSet<u32> = pages.iter().map(|m| m.page_id()).collect();
1521            for &page_id in self
1522                .page_lookup
1523                .null_pages
1524                .iter()
1525                .chain(self.page_lookup.all_null_pages.iter())
1526            {
1527                if !existing.contains(&page_id) {
1528                    pages.push(Matches::Some(page_id));
1529                }
1530            }
1531        }
1532
1533        let lazy_index_reader =
1534            LazyIndexReader::new(self.store.clone(), self.ranges_to_files.clone());
1535        let page_tasks = pages
1536            .into_iter()
1537            .map(|page_index| {
1538                self.search_page(query, page_index, lazy_index_reader.clone(), metrics)
1539                    .boxed()
1540            })
1541            .collect::<Vec<_>>();
1542        debug!("Searching {} btree pages", page_tasks.len());
1543
1544        // Collect both matching row IDs and null row IDs from all pages
1545        let results: Vec<NullableRowAddrSet> = stream::iter(page_tasks)
1546            // I/O and compute mixed here but important case is index in cache so
1547            // use compute intensive thread count
1548            .buffered(get_num_compute_intensive_cpus())
1549            .try_collect()
1550            .await?;
1551
1552        // Merge matching row IDs
1553        let selection = NullableRowAddrSet::union_all(&results);
1554
1555        Ok(SearchResult::Exact(selection))
1556    }
1557
1558    fn can_remap(&self) -> bool {
1559        true
1560    }
1561
1562    async fn remap(
1563        &self,
1564        mapping: &HashMap<u64, Option<u64>>,
1565        dest_store: &dyn IndexStore,
1566    ) -> Result<CreatedIndex> {
1567        // (part_id, path)
1568        // The part_id is None for a basic index
1569        // For a range-based index we use Some(0), Some(1), ...
1570        //   even if those weren't the original part ids
1571        let part_page_files: Vec<(Option<u32>, &str)> =
1572            if let Some(ranges_to_files) = &self.ranges_to_files {
1573                // Range-based Index: Directly collect references to the file paths.
1574                ranges_to_files
1575                    .iter()
1576                    .enumerate()
1577                    .map(|(part_id, (_, (path, _)))| (Some(part_id as u32), path.as_str()))
1578                    .collect()
1579            } else {
1580                // Basic Index: There is only one source page file.
1581                vec![(None, BTREE_PAGES_NAME)]
1582            };
1583
1584        let mapping = Arc::new(mapping.clone());
1585        let train_schema = Arc::new(self.train_schema());
1586
1587        // TODO: Could potentially parallelize this across parts, unclear it would be worth it
1588        for (part_id, page_file) in part_page_files {
1589            // Retrain on the remapped pages
1590            let sub_index_reader = self.store.open_index_file(page_file).await?;
1591            let mapping = mapping.clone();
1592
1593            let train_schema_clone = train_schema.clone();
1594            let train_schema = train_schema.clone();
1595
1596            let remapped_stream = IndexReaderStream::new(sub_index_reader, self.batch_size)
1597                .await
1598                .buffered(self.store.io_parallelism())
1599                .map_err(DataFusionError::from)
1600                .and_then(move |batch| {
1601                    // Remap the batch and then convert from the serialized schema to the training input schema
1602                    let remapped =
1603                        FlatIndex::remap_batch(batch, &mapping).map_err(DataFusionError::from);
1604                    let with_train_schema = remapped.and_then(|batch| {
1605                        RecordBatch::try_new(train_schema.clone(), batch.columns().to_vec())
1606                            .map_err(DataFusionError::from)
1607                    });
1608                    std::future::ready(with_train_schema)
1609                });
1610
1611            let remapped_stream = Box::pin(RecordBatchStreamAdapter::new(
1612                train_schema_clone,
1613                remapped_stream,
1614            ));
1615
1616            train_btree_index(remapped_stream, dest_store, self.batch_size, None, part_id).await?;
1617        }
1618
1619        if let Some(ranges_to_files) = &self.ranges_to_files {
1620            let num_parts = ranges_to_files.len();
1621            // Merge the lookups if we are a range-based index
1622            let page_files = (0..num_parts)
1623                .map(|part_id| part_page_data_file_path((part_id as u64) << 32))
1624                .collect::<Vec<_>>();
1625            let lookup_files = (0..num_parts)
1626                .map(|part_id| part_lookup_file_path((part_id as u64) << 32))
1627                .collect::<Vec<_>>();
1628            merge_metadata_files(dest_store, &page_files, &lookup_files, None).await?;
1629        }
1630
1631        Ok(CreatedIndex {
1632            index_details: prost_types::Any::from_msg(&pbold::BTreeIndexDetails::default())
1633                .unwrap(),
1634            index_version: BTREE_INDEX_VERSION,
1635        })
1636    }
1637
1638    async fn update(
1639        &self,
1640        new_data: SendableRecordBatchStream,
1641        dest_store: &dyn IndexStore,
1642        valid_old_fragments: Option<&RoaringBitmap>,
1643    ) -> Result<CreatedIndex> {
1644        // Merge the existing index data with the new data and then retrain the index on the merged stream
1645        let merged_data_source = self
1646            .clone()
1647            .combine_old_new(new_data, self.batch_size, valid_old_fragments.cloned())
1648            .await?;
1649        train_btree_index(merged_data_source, dest_store, self.batch_size, None, None).await?;
1650
1651        Ok(CreatedIndex {
1652            index_details: prost_types::Any::from_msg(&pbold::BTreeIndexDetails::default())
1653                .unwrap(),
1654            index_version: BTREE_INDEX_VERSION,
1655        })
1656    }
1657
1658    fn update_criteria(&self) -> UpdateCriteria {
1659        UpdateCriteria::only_new_data(TrainingCriteria::new(TrainingOrdering::Values).with_row_id())
1660    }
1661
1662    fn derive_index_params(&self) -> Result<ScalarIndexParams> {
1663        let params = serde_json::to_value(BTreeParameters {
1664            zone_size: Some(self.batch_size),
1665            range_id: None,
1666        })?;
1667        Ok(ScalarIndexParams::for_builtin(BuiltinIndexType::BTree).with_params(&params))
1668    }
1669}
1670
1671struct BatchStats {
1672    min: ScalarValue,
1673    max: ScalarValue,
1674    null_count: u32,
1675}
1676
1677fn analyze_batch(batch: &RecordBatch) -> Result<BatchStats> {
1678    let values = batch.column_by_name(VALUE_COLUMN_NAME).expect_ok()?;
1679    if values.is_empty() {
1680        return Err(Error::internal(
1681            "received an empty batch in btree training".to_string(),
1682        ));
1683    }
1684    let min = ScalarValue::try_from_array(&values, 0)
1685        .map_err(|e| Error::internal(format!("failed to get min value from batch: {}", e)))?;
1686    let max = ScalarValue::try_from_array(&values, values.len() - 1)
1687        .map_err(|e| Error::internal(format!("failed to get max value from batch: {}", e)))?;
1688
1689    Ok(BatchStats {
1690        min,
1691        max,
1692        null_count: values.null_count() as u32,
1693    })
1694}
1695
1696/// A trait that must be implemented by anything that wishes to act as a btree subindex
1697#[async_trait]
1698pub trait BTreeSubIndex: Debug + Send + Sync + DeepSizeOf {
1699    /// Trains the subindex on a single batch of data and serializes it to Arrow
1700    async fn train(&self, batch: RecordBatch) -> Result<RecordBatch>;
1701
1702    /// Deserialize a subindex from Arrow
1703    async fn load_subindex(&self, serialized: RecordBatch) -> Result<Arc<dyn ScalarIndex>>;
1704
1705    /// Retrieve the data used to originally train this page
1706    ///
1707    /// In order to perform an update we need to merge the old data in with the new data which
1708    /// means we need to access the new data.  Right now this is convenient for flat indices but
1709    /// we may need to take a different approach if we ever decide to use a sub-index other than
1710    /// flat
1711    async fn retrieve_data(&self, serialized: RecordBatch) -> Result<RecordBatch>;
1712
1713    /// The schema of the subindex when serialized to Arrow
1714    fn schema(&self) -> &Arc<Schema>;
1715
1716    /// Given a serialized page, deserialize it, remap the row ids, and re-serialize it
1717    async fn remap_subindex(
1718        &self,
1719        serialized: RecordBatch,
1720        mapping: &HashMap<u64, Option<u64>>,
1721    ) -> Result<RecordBatch>;
1722}
1723
1724struct EncodedBatch {
1725    stats: BatchStats,
1726    page_number: u32,
1727}
1728
1729async fn train_btree_page(
1730    batch: RecordBatch,
1731    batch_idx: u32,
1732    writer: &mut dyn IndexWriter,
1733    schema: Arc<Schema>,
1734) -> Result<EncodedBatch> {
1735    let stats = analyze_batch(&batch)?;
1736
1737    // Renames from value/_rowid to values/ids
1738    let trained = RecordBatch::try_new(
1739        schema.clone(),
1740        vec![
1741            batch.column_by_name(VALUE_COLUMN_NAME).expect_ok()?.clone(),
1742            batch.column_by_name(ROW_ID).expect_ok()?.clone(),
1743        ],
1744    )?;
1745
1746    writer.write_record_batch(trained).await?;
1747    Ok(EncodedBatch {
1748        stats,
1749        page_number: batch_idx,
1750    })
1751}
1752
1753fn btree_stats_as_batch(stats: Vec<EncodedBatch>, value_type: &DataType) -> Result<RecordBatch> {
1754    let mins = if stats.is_empty() {
1755        new_empty_array(value_type)
1756    } else {
1757        ScalarValue::iter_to_array(stats.iter().map(|stat| stat.stats.min.clone()))?
1758    };
1759    let maxs = if stats.is_empty() {
1760        new_empty_array(value_type)
1761    } else {
1762        ScalarValue::iter_to_array(stats.iter().map(|stat| stat.stats.max.clone()))?
1763    };
1764    let null_counts = UInt32Array::from_iter_values(stats.iter().map(|stat| stat.stats.null_count));
1765    let page_numbers = UInt32Array::from_iter_values(stats.iter().map(|stat| stat.page_number));
1766
1767    let schema = Arc::new(Schema::new(vec![
1768        // min and max can be null if the entire batch is null values
1769        Field::new("min", mins.data_type().clone(), true),
1770        Field::new("max", maxs.data_type().clone(), true),
1771        Field::new("null_count", null_counts.data_type().clone(), false),
1772        Field::new("page_idx", page_numbers.data_type().clone(), false),
1773    ]));
1774
1775    let columns = vec![
1776        mins,
1777        maxs,
1778        Arc::new(null_counts) as Arc<dyn Array>,
1779        Arc::new(page_numbers) as Arc<dyn Array>,
1780    ];
1781
1782    Ok(RecordBatch::try_new(schema, columns)?)
1783}
1784
1785/// Train a btree index from a stream of sorted page-size batches of values and row ids
1786pub async fn train_btree_index(
1787    batches_source: SendableRecordBatchStream,
1788    index_store: &dyn IndexStore,
1789    batch_size: u64,
1790    fragment_ids: Option<Vec<u32>>,
1791    range_id: Option<u32>,
1792) -> Result<()> {
1793    // Create `partition_id` for distributed index building.
1794    // This ID serves as a high-level mask (first 32 bits of a u64) to ensure
1795    // that index partitions generated by different workers do not conflict.
1796    // Lance supports two strategies for distributed training: fragment-based and range-based.
1797    let partition_id = fragment_ids
1798        .as_ref()
1799        // --- Fragment-based Partitioning ---
1800        // Used when training sub-indexes on a fragment-level-split basis. The `partition_id` is
1801        // derived from `fragment_ids` to associate the index pages with their source fragment.
1802        .and_then(|frag_ids| frag_ids.first())
1803        .map(|&first_frag_id| (first_frag_id as u64) << 32)
1804        // --- Range-based Partitioning ---
1805        // Built upon data globally sorted by an external compute engine. The `range_id` creates
1806        // a unique name for the index pages generated by each worker.
1807        .or_else(|| range_id.map(|id| (id as u64) << 32));
1808
1809    let flat_schema = Arc::new(Schema::new(vec![
1810        Field::new(
1811            BTREE_VALUES_COLUMN,
1812            batches_source.schema().field(0).data_type().clone(),
1813            true,
1814        ),
1815        Field::new(BTREE_IDS_COLUMN, DataType::UInt64, false),
1816    ]));
1817
1818    let mut sub_index_file = match partition_id {
1819        None => {
1820            index_store
1821                .new_index_file(BTREE_PAGES_NAME, flat_schema.clone())
1822                .await?
1823        }
1824        Some(partition_id) => {
1825            index_store
1826                .new_index_file(
1827                    part_page_data_file_path(partition_id).as_str(),
1828                    flat_schema.clone(),
1829                )
1830                .await?
1831        }
1832    };
1833
1834    let mut encoded_batches = Vec::new();
1835    let mut batch_idx = 0;
1836
1837    let value_type = batches_source
1838        .schema()
1839        .field_with_name(VALUE_COLUMN_NAME)?
1840        .data_type()
1841        .clone();
1842
1843    let mut batches_source = chunk_concat_stream(batches_source, batch_size as usize);
1844
1845    while let Some(batch) = batches_source.try_next().await? {
1846        encoded_batches.push(
1847            train_btree_page(
1848                batch,
1849                batch_idx,
1850                sub_index_file.as_mut(),
1851                flat_schema.clone(),
1852            )
1853            .await?,
1854        );
1855        batch_idx += 1;
1856    }
1857    sub_index_file.finish().await?;
1858    let record_batch = btree_stats_as_batch(encoded_batches, &value_type)?;
1859    let mut file_schema = record_batch.schema().as_ref().clone();
1860    file_schema
1861        .metadata
1862        .insert(BATCH_SIZE_META_KEY.to_string(), batch_size.to_string());
1863    file_schema.metadata.insert(
1864        RANGE_PARTITIONED_META_KEY.to_string(),
1865        range_id.is_some().to_string(),
1866    );
1867    let mut btree_index_file = match partition_id {
1868        None => {
1869            index_store
1870                .new_index_file(BTREE_LOOKUP_NAME, Arc::new(file_schema))
1871                .await?
1872        }
1873        Some(partition_id) => {
1874            index_store
1875                .new_index_file(
1876                    part_lookup_file_path(partition_id).as_str(),
1877                    Arc::new(file_schema),
1878                )
1879                .await?
1880        }
1881    };
1882    btree_index_file.write_record_batch(record_batch).await?;
1883    btree_index_file.finish().await?;
1884    Ok(())
1885}
1886
1887pub async fn merge_index_files(
1888    object_store: &ObjectStore,
1889    index_dir: &Path,
1890    store: Arc<dyn IndexStore>,
1891    batch_readhead: Option<usize>,
1892) -> Result<()> {
1893    // List all partition page / lookup files in the index directory
1894    let (part_page_files, part_lookup_files) =
1895        list_page_lookup_files(object_store, index_dir).await?;
1896    merge_metadata_files(
1897        store.as_ref(),
1898        &part_page_files,
1899        &part_lookup_files,
1900        batch_readhead,
1901    )
1902    .await
1903}
1904
1905/// List and filter files from the index directory
1906/// Returns (page_files, lookup_files)
1907async fn list_page_lookup_files(
1908    object_store: &ObjectStore,
1909    index_dir: &Path,
1910) -> Result<(Vec<String>, Vec<String>)> {
1911    let mut part_page_files = Vec::new();
1912    let mut part_lookup_files = Vec::new();
1913
1914    let mut list_stream = object_store.list(Some(index_dir.clone()));
1915
1916    while let Some(item) = list_stream.next().await {
1917        match item {
1918            Ok(meta) => {
1919                let file_name = meta.location.filename().unwrap_or_default();
1920                // Filter files matching the pattern part_*_page_data.lance
1921                if file_name.starts_with("part_") && file_name.ends_with("_page_data.lance") {
1922                    part_page_files.push(file_name.to_string());
1923                }
1924                // Filter files matching the pattern part_*_page_lookup.lance
1925                if file_name.starts_with("part_") && file_name.ends_with("_page_lookup.lance") {
1926                    part_lookup_files.push(file_name.to_string());
1927                }
1928            }
1929            Err(_) => continue,
1930        }
1931    }
1932
1933    if part_page_files.is_empty() || part_lookup_files.is_empty() {
1934        return Err(Error::internal(format!(
1935            "No partition metadata files found in index directory: {} (page_files: {}, lookup_files: {})",
1936            index_dir,
1937            part_page_files.len(),
1938            part_lookup_files.len()
1939        )));
1940    }
1941
1942    Ok((part_page_files, part_lookup_files))
1943}
1944
1945/// Merge multiple partition page / lookup files into a complete metadata file
1946///
1947/// In a distributed environment, each worker node writes partition page / lookup file for the partitions it processes,
1948/// and this function merges these files into a final metadata file.
1949/// - For fragment-based indices, it performs a full K-way sort-merge of page files to create new global page and lookup files.
1950/// - For range-based indices, it concatenates lookup files, as data is already globally sorted.
1951async fn merge_metadata_files(
1952    store: &dyn IndexStore,
1953    part_page_files: &[String],
1954    part_lookup_files: &[String],
1955    batch_readhead: Option<usize>,
1956) -> Result<()> {
1957    if part_lookup_files.is_empty() || part_page_files.is_empty() {
1958        return Err(Error::internal(
1959            "No partition files provided for merging".to_string(),
1960        ));
1961    }
1962
1963    // Step 1: Create lookup map for page files by partition ID
1964    if part_lookup_files.len() != part_page_files.len() {
1965        return Err(Error::internal(format!(
1966            "Number of partition lookup files ({}) does not match number of partition page files ({})",
1967            part_lookup_files.len(),
1968            part_page_files.len()
1969        )));
1970    }
1971    let mut page_files_map = HashMap::new();
1972    for page_file in part_page_files {
1973        let partition_id = extract_partition_id(page_file)?;
1974        page_files_map.insert(partition_id, page_file);
1975    }
1976
1977    // Step 2: Validate that all lookup files have corresponding page files
1978    for lookup_file in part_lookup_files {
1979        let partition_id = extract_partition_id(lookup_file)?;
1980        if !page_files_map.contains_key(&partition_id) {
1981            return Err(Error::internal(format!(
1982                "No corresponding page file found for lookup file: {} (partition_id: {})",
1983                lookup_file, partition_id
1984            )));
1985        }
1986    }
1987
1988    // Step 3: Extract shared metadata and generate lookup_schema
1989    let first_lookup_reader = store.open_index_file(&part_lookup_files[0]).await?;
1990    let batch_size = first_lookup_reader
1991        .schema()
1992        .metadata
1993        .get(BATCH_SIZE_META_KEY)
1994        .map(|bs| bs.parse().unwrap_or(DEFAULT_BTREE_BATCH_SIZE))
1995        .unwrap_or(DEFAULT_BTREE_BATCH_SIZE);
1996    let range_partitioned = first_lookup_reader
1997        .schema()
1998        .metadata
1999        .get(RANGE_PARTITIONED_META_KEY)
2000        .map(|bs| bs.parse().unwrap_or(DEFAULT_RANGE_PARTITIONED))
2001        .unwrap_or(DEFAULT_RANGE_PARTITIONED);
2002
2003    // Get the value type from lookup schema (min column)
2004    let value_type = first_lookup_reader
2005        .schema()
2006        .fields
2007        .first()
2008        .unwrap()
2009        .data_type();
2010
2011    let mut metadata = HashMap::new();
2012    metadata.insert(BATCH_SIZE_META_KEY.to_string(), batch_size.to_string());
2013    let lookup_schema = Arc::new(Schema::new(vec![
2014        Field::new("min", value_type.clone(), true),
2015        Field::new("max", value_type.clone(), true),
2016        Field::new("null_count", DataType::UInt32, false),
2017        Field::new("page_idx", DataType::UInt32, false),
2018    ]));
2019
2020    // Step 4: Merge pages and lookups and generate new index files
2021    if range_partitioned {
2022        merge_range_partitioned_lookups(
2023            store,
2024            part_lookup_files,
2025            lookup_schema,
2026            metadata,
2027            batch_size,
2028            batch_readhead,
2029        )
2030        .await
2031    } else {
2032        merge_pages_and_lookups(
2033            store,
2034            part_page_files,
2035            part_lookup_files,
2036            &page_files_map,
2037            lookup_schema,
2038            metadata,
2039            batch_size,
2040            batch_readhead,
2041        )
2042        .await
2043    }
2044}
2045
2046/// Merges multiple lookup files from a range-partitioned index into a single, unified lookup file.
2047///
2048/// A range-partitioned B-Tree index creates a separate `page_lookup.lance` file for
2049/// each partition. Each of these files has its own local `page_idx` column, where the indices
2050/// start from 0.
2051///
2052/// This function's primary goal is to combine these separate files into one large
2053/// `page_lookup.lance` file. To do this, it remaps the local `page_idx` from each partition
2054/// file into a contiguous, global `page_idx` space. It processes partition files sequentially,
2055/// calculating an offset based on the number of pages in all previously processed partitions.
2056///
2057/// **The reverse operation occurs when the B-Tree index is loaded**: a global `page_idx` is translated
2058/// back into a `(partition_id, local_page_idx)` tuple. This translation is made possible by the
2059/// metadata stored under the `PAGE_NUM_PER_RANGE_PARTITION_META_KEY`, which this function
2060/// is responsible for writing.
2061///
2062/// # Examples
2063///
2064/// If we have two partition lookup files:
2065/// - `part_0_page_lookup.lance`: Contains 3 pages. Its `page_idx` column is `[0, 1, 2]`.
2066/// - `part_1_page_lookup.lance`: Contains 4 pages. Its `page_idx` column is `[0, 1, 2, 3]`.
2067///
2068/// The merge process works as follows:
2069/// 1. Process `part_0`: The offset is 0. The indices `[0, 1, 2]` are written as is.
2070/// 2. Process `part_1`: The offset is 3 and the local indices `[0, 1, 2, 3]` are remapped
2071///    by adding the offset, resulting in `[3, 4, 5, 6]`.
2072///
2073/// The final, merged `_page_lookup.lance` will have a single `page_idx` column containing
2074/// `[0, 1, 2, 3, 4, 5, 6]`.
2075async fn merge_range_partitioned_lookups(
2076    store: &dyn IndexStore,
2077    part_lookup_files: &[String],
2078    lookup_schema: Arc<Schema>,
2079    mut metadata: HashMap<String, String>,
2080    batch_size: u64,
2081    batch_readhead: Option<usize>,
2082) -> Result<()> {
2083    let sorted_part_lookup_files = sort_files_by_partition_id(part_lookup_files)?;
2084    let mut lookup_file = store
2085        .new_index_file(BTREE_LOOKUP_NAME, lookup_schema)
2086        .await?;
2087
2088    // stores partition id and the number of pages in that partition
2089    let mut pages_per_file: Vec<(u64, u32)> = Vec::with_capacity(sorted_part_lookup_files.len());
2090    let mut num_pages_written = 0u32;
2091
2092    for (part_id, part_lookup_file) in sorted_part_lookup_files {
2093        let lookup_reader = store.open_index_file(&part_lookup_file).await?;
2094        let reader_stream = IndexReaderStream::new(lookup_reader.clone(), batch_size).await;
2095        let mut stream = reader_stream.buffered(batch_readhead.unwrap_or(1)).boxed();
2096        while let Some(batch) = stream.next().await {
2097            let original_batch = batch?;
2098            let modified_batch = add_offset_to_page_idx(&original_batch, num_pages_written)?;
2099            lookup_file.write_record_batch(modified_batch).await?;
2100        }
2101        pages_per_file.push((part_id, lookup_reader.num_rows() as u32));
2102        num_pages_written += lookup_reader.num_rows() as u32;
2103    }
2104
2105    metadata.insert(RANGE_PARTITIONED_META_KEY.to_string(), "true".to_string());
2106    metadata.insert(
2107        PAGE_NUM_PER_RANGE_PARTITION_META_KEY.to_string(),
2108        serde_json::to_string(&pages_per_file)?,
2109    );
2110
2111    lookup_file.finish_with_metadata(metadata).await?;
2112
2113    // In this mode, we only clean up lookup files, and page files are untouched.
2114    cleanup_partition_files(store, part_lookup_files, &[]).await;
2115    Ok(())
2116}
2117
2118/// Merges partition files using a K-way sort-merge algorithm.
2119///
2120/// This function assumes its inputs have been pre-validated. It reads from all
2121/// partitioned page files simultaneously, merges them into a single sorted stream,
2122/// writes a new global page file, and generates a corresponding global lookup file.
2123#[allow(clippy::too_many_arguments)]
2124async fn merge_pages_and_lookups(
2125    store: &dyn IndexStore,
2126    part_page_files: &[String],
2127    part_lookup_files: &[String],
2128    page_files_map: &HashMap<u64, &String>,
2129    lookup_schema: Arc<Schema>,
2130    metadata: HashMap<String, String>,
2131    batch_size: u64,
2132    batch_readhead: Option<usize>,
2133) -> Result<()> {
2134    // Create a new global page file
2135    let partition_id = extract_partition_id(part_lookup_files[0].as_str())?;
2136    let page_file = page_files_map.get(&partition_id).unwrap();
2137    let page_reader = store.open_index_file(page_file).await?;
2138    let page_schema = page_reader.schema().clone();
2139
2140    let arrow_schema = Arc::new(Schema::from(&page_schema));
2141    let mut page_file = store
2142        .new_index_file(BTREE_PAGES_NAME, arrow_schema.clone())
2143        .await?;
2144
2145    let lookup_entries = merge_pages(
2146        part_lookup_files,
2147        page_files_map,
2148        store,
2149        batch_size,
2150        &mut page_file,
2151        arrow_schema.clone(),
2152        batch_readhead,
2153    )
2154    .await?;
2155    page_file.finish().await?;
2156
2157    let lookup_batch = RecordBatch::try_new(
2158        lookup_schema.clone(),
2159        vec![
2160            ScalarValue::iter_to_array(lookup_entries.iter().map(|(min, _, _, _)| min.clone()))?,
2161            ScalarValue::iter_to_array(lookup_entries.iter().map(|(_, max, _, _)| max.clone()))?,
2162            Arc::new(UInt32Array::from_iter_values(
2163                lookup_entries
2164                    .iter()
2165                    .map(|(_, _, null_count, _)| *null_count),
2166            )),
2167            Arc::new(UInt32Array::from_iter_values(
2168                lookup_entries.iter().map(|(_, _, _, page_idx)| *page_idx),
2169            )),
2170        ],
2171    )?;
2172    let mut lookup_file = store
2173        .new_index_file(BTREE_LOOKUP_NAME, lookup_schema)
2174        .await?;
2175    lookup_file.write_record_batch(lookup_batch).await?;
2176    lookup_file.finish_with_metadata(metadata).await?;
2177
2178    // After successfully writing the merged files, delete all partition files
2179    // Only perform deletion after files are successfully written, ensuring debug information is not lost in case of failure
2180    cleanup_partition_files(store, part_lookup_files, part_page_files).await;
2181
2182    Ok(())
2183}
2184
2185// Adjust local_page_idx_ in each look-up file to create a contiguous global_page_idx
2186fn add_offset_to_page_idx(batch: &RecordBatch, offset: u32) -> Result<RecordBatch> {
2187    let (page_idx_pos, _) = batch.schema().column_with_name("page_idx").ok_or_else(|| {
2188        Error::internal("Column 'page_idx' not found in RecordBatch schema".to_string())
2189    })?;
2190    let page_idx_array = batch
2191        .column(page_idx_pos)
2192        .as_any()
2193        .downcast_ref::<UInt32Array>()
2194        .ok_or_else(|| {
2195            Error::internal("Failed to downcast 'page_idx' column to UInt32Array".to_string())
2196        })?;
2197    let offset_array = UInt32Array::from(vec![offset; page_idx_array.len()]);
2198    let new_page_idx_array_ref = add(page_idx_array, &offset_array)?;
2199    let mut new_columns = batch.columns().to_vec();
2200    new_columns[page_idx_pos] = new_page_idx_array_ref;
2201    let new_batch = RecordBatch::try_new(batch.schema(), new_columns)?;
2202    Ok(new_batch)
2203}
2204
2205/// Merge pages using Datafusion's SortPreservingMergeExec
2206/// which implements a K-way merge algorithm with fixed-size output batches
2207async fn merge_pages(
2208    part_lookup_files: &[String],
2209    page_files_map: &HashMap<u64, &String>,
2210    store: &dyn IndexStore,
2211    batch_size: u64,
2212    page_file: &mut Box<dyn IndexWriter>,
2213    arrow_schema: Arc<Schema>,
2214    batch_readhead: Option<usize>,
2215) -> Result<Vec<(ScalarValue, ScalarValue, u32, u32)>> {
2216    let mut lookup_entries = Vec::new();
2217    let mut page_idx = 0u32;
2218
2219    debug!(
2220        "Starting SortPreservingMerge with {} partitions",
2221        part_lookup_files.len()
2222    );
2223
2224    let value_field = arrow_schema.field(0).clone().with_name(VALUE_COLUMN_NAME);
2225    let row_id_field = arrow_schema.field(1).clone().with_name(ROW_ID);
2226    let stream_schema = Arc::new(Schema::new(vec![value_field, row_id_field]));
2227
2228    // Create execution plans for each stream
2229    let mut inputs: Vec<Arc<dyn ExecutionPlan>> = Vec::new();
2230    for lookup_file in part_lookup_files {
2231        let partition_id = extract_partition_id(lookup_file)?;
2232        let page_file_name = (*page_files_map.get(&partition_id).ok_or_else(|| {
2233            Error::internal(format!(
2234                "Page file not found for partition ID: {}",
2235                partition_id
2236            ))
2237        })?)
2238        .clone();
2239
2240        let reader = store.open_index_file(&page_file_name).await?;
2241
2242        let reader_stream = IndexReaderStream::new(reader, batch_size).await;
2243
2244        let stream = reader_stream
2245            .map(|fut| fut.map_err(DataFusionError::from))
2246            .buffered(batch_readhead.unwrap_or(1))
2247            .boxed();
2248
2249        let sendable_stream =
2250            Box::pin(RecordBatchStreamAdapter::new(stream_schema.clone(), stream));
2251        inputs.push(Arc::new(OneShotExec::new(sendable_stream)));
2252    }
2253
2254    // Create Union execution plan to combine all partitions
2255    let union_inputs = UnionExec::try_new(inputs)?;
2256
2257    // Create SortPreservingMerge execution plan
2258    let value_column_index = stream_schema.index_of(VALUE_COLUMN_NAME)?;
2259    let sort_expr = PhysicalSortExpr {
2260        expr: Arc::new(Column::new(VALUE_COLUMN_NAME, value_column_index)),
2261        options: SortOptions {
2262            descending: false,
2263            nulls_first: true,
2264        },
2265    };
2266
2267    let merge_exec = Arc::new(SortPreservingMergeExec::new(
2268        [sort_expr].into(),
2269        union_inputs,
2270    ));
2271
2272    let unchunked = execute_plan(
2273        merge_exec,
2274        LanceExecutionOptions {
2275            use_spilling: false,
2276            ..Default::default()
2277        },
2278    )?;
2279
2280    // Use chunk_concat_stream to ensure fixed batch sizes
2281    let mut chunked_stream = chunk_concat_stream(unchunked, batch_size as usize);
2282
2283    // Process chunked stream
2284    while let Some(batch) = chunked_stream.try_next().await? {
2285        let writer_batch = RecordBatch::try_new(
2286            arrow_schema.clone(),
2287            vec![batch.column(0).clone(), batch.column(1).clone()],
2288        )?;
2289
2290        page_file.write_record_batch(writer_batch).await?;
2291
2292        let min_val = ScalarValue::try_from_array(batch.column(0), 0)?;
2293        let max_val = ScalarValue::try_from_array(batch.column(0), batch.num_rows() - 1)?;
2294        let null_count = batch.column(0).null_count() as u32;
2295
2296        lookup_entries.push((min_val, max_val, null_count, page_idx));
2297        page_idx += 1;
2298    }
2299
2300    Ok(lookup_entries)
2301}
2302
2303// Sorts file paths by the partition ID extracted from file name.
2304fn sort_files_by_partition_id(part_files: &[String]) -> Result<Vec<(u64, String)>> {
2305    let mut files_with_ids: Vec<(u64, &String)> = part_files
2306        .iter()
2307        .map(|file| extract_partition_id(file).map(|id| (id, file)))
2308        .collect::<Result<Vec<_>>>()?;
2309
2310    files_with_ids.sort_unstable_by_key(|k| k.0);
2311
2312    let sorted_files = files_with_ids
2313        .into_iter()
2314        .map(|(id, file)| (id, file.clone()))
2315        .collect();
2316
2317    Ok(sorted_files)
2318}
2319
2320/// Extract partition ID from partition file name
2321/// Expected format: "part_{partition_id}_{suffix}.lance"
2322fn extract_partition_id(filename: &str) -> Result<u64> {
2323    if !filename.starts_with("part_") {
2324        return Err(Error::internal(format!(
2325            "Invalid partition file name format: {}",
2326            filename
2327        )));
2328    }
2329
2330    let parts: Vec<&str> = filename.split('_').collect();
2331    if parts.len() < 3 {
2332        return Err(Error::internal(format!(
2333            "Invalid partition file name format: {}",
2334            filename
2335        )));
2336    }
2337
2338    parts[1].parse::<u64>().map_err(|_| {
2339        Error::internal(format!(
2340            "Failed to parse partition ID from filename: {}",
2341            filename
2342        ))
2343    })
2344}
2345
2346/// Clean up partition files after successful merge
2347///
2348/// This function safely deletes partition lookup and page files after a successful merge operation.
2349/// File deletion failures are logged but do not affect the overall success of the merge operation.
2350async fn cleanup_partition_files(
2351    store: &dyn IndexStore,
2352    part_lookup_files: &[String],
2353    part_page_files: &[String],
2354) {
2355    // Clean up partition lookup files
2356    for file_name in part_lookup_files {
2357        cleanup_single_file(
2358            store,
2359            file_name,
2360            "part_",
2361            "_page_lookup.lance",
2362            "partition lookup",
2363        )
2364        .await;
2365    }
2366
2367    // Clean up partition page files
2368    for file_name in part_page_files {
2369        cleanup_single_file(
2370            store,
2371            file_name,
2372            "part_",
2373            "_page_data.lance",
2374            "partition page",
2375        )
2376        .await;
2377    }
2378}
2379
2380/// Helper function to clean up a single partition file
2381///
2382/// Performs safety checks on the filename pattern before attempting deletion.
2383async fn cleanup_single_file(
2384    store: &dyn IndexStore,
2385    file_name: &str,
2386    expected_prefix: &str,
2387    expected_suffix: &str,
2388    file_type: &str,
2389) {
2390    if file_name.starts_with(expected_prefix) && file_name.ends_with(expected_suffix) {
2391        match store.delete_index_file(file_name).await {
2392            Ok(()) => {
2393                debug!("Successfully deleted {} file: {}", file_type, file_name);
2394            }
2395            Err(e) => {
2396                warn!(
2397                    "Failed to delete {} file '{}': {}. \
2398                    This does not affect the merge operation, but may leave \
2399                    partition files that should be cleaned up manually.",
2400                    file_type, file_name, e
2401                );
2402            }
2403        }
2404    } else {
2405        // If the filename doesn't match the expected format, log a warning but don't attempt deletion
2406        warn!(
2407            "Skipping deletion of file '{}' as it does not match the expected \
2408            {} file pattern ({}*{})",
2409            file_name, file_type, expected_prefix, expected_suffix
2410        );
2411    }
2412}
2413
2414pub(crate) fn part_page_data_file_path(partition_id: u64) -> String {
2415    format!("part_{}_{}", partition_id, BTREE_PAGES_NAME)
2416}
2417
2418pub(crate) fn part_lookup_file_path(partition_id: u64) -> String {
2419    format!("part_{}_{}", partition_id, BTREE_LOOKUP_NAME)
2420}
2421
2422/// A stream that reads the original training data back out of the index
2423///
2424/// This is used for updating the index
2425struct IndexReaderStream {
2426    reader: Arc<dyn IndexReader>,
2427    batch_size: u64,
2428    num_batches: u32,
2429    batch_idx: u32,
2430}
2431
2432impl IndexReaderStream {
2433    async fn new(reader: Arc<dyn IndexReader>, batch_size: u64) -> Self {
2434        let num_batches = reader.num_batches(batch_size).await;
2435        Self {
2436            reader,
2437            batch_size,
2438            num_batches,
2439            batch_idx: 0,
2440        }
2441    }
2442}
2443
2444impl Stream for IndexReaderStream {
2445    type Item = BoxFuture<'static, Result<RecordBatch>>;
2446
2447    fn poll_next(
2448        self: std::pin::Pin<&mut Self>,
2449        _cx: &mut std::task::Context<'_>,
2450    ) -> std::task::Poll<Option<Self::Item>> {
2451        let this = self.get_mut();
2452        if this.batch_idx >= this.num_batches {
2453            return std::task::Poll::Ready(None);
2454        }
2455        let batch_num = this.batch_idx;
2456        this.batch_idx += 1;
2457        let reader_copy = this.reader.clone();
2458        let batch_size = this.batch_size;
2459        let read_task = async move {
2460            reader_copy
2461                .read_record_batch(batch_num as u64, batch_size)
2462                .await
2463        }
2464        .boxed();
2465        std::task::Poll::Ready(Some(read_task))
2466    }
2467}
2468
2469/// Parameters for a btree index
2470#[derive(Debug, Serialize, Deserialize)]
2471pub struct BTreeParameters {
2472    /// The number of rows to include in each zone
2473    pub zone_size: Option<u64>,
2474
2475    /// The ordinal ID of a data partition for building a large, distributed BTree index.
2476    ///
2477    /// When building an index from multiple, pre-partitioned data chunks (for example,
2478    /// in a distributed environment), this ID specifies which partition this particular
2479    /// build operation corresponds to.
2480    ///
2481    /// # Data Distribution Requirements
2482    ///
2483    /// If this parameter is `Some(id)`, the caller **must** guarantee that the input data
2484    /// is strictly global sorted. The input data, when considered as a whole across all
2485    /// partitions ordered by `range_id`, must be sorted.
2486    ///
2487    /// Concretely, this means:
2488    ///
2489    /// All values in the data provided for `range_id: N` must be **less than or equal to**
2490    /// all values in the data for `range_id: N+1`.
2491    ///
2492    /// Lance relies on this precondition to ensure the final, merged index is valid and
2493    /// correctly ordered.
2494    ///
2495    /// # `None` Case
2496    ///
2497    /// If `range_id` is `None`, a single, monolithic index is built over the provided dataset.
2498    pub range_id: Option<u32>,
2499}
2500
2501struct BTreeTrainingRequest {
2502    parameters: BTreeParameters,
2503    criteria: TrainingCriteria,
2504}
2505
2506impl BTreeTrainingRequest {
2507    pub fn new(parameters: BTreeParameters) -> Self {
2508        Self {
2509            parameters,
2510            // BTree indexes need data sorted by the value column
2511            criteria: TrainingCriteria::new(TrainingOrdering::Values).with_row_id(),
2512        }
2513    }
2514}
2515
2516impl TrainingRequest for BTreeTrainingRequest {
2517    fn as_any(&self) -> &dyn std::any::Any {
2518        self
2519    }
2520
2521    fn criteria(&self) -> &TrainingCriteria {
2522        &self.criteria
2523    }
2524}
2525
2526#[derive(Debug, Default)]
2527pub struct BTreeIndexPlugin;
2528
2529#[async_trait]
2530impl ScalarIndexPlugin for BTreeIndexPlugin {
2531    fn name(&self) -> &str {
2532        "BTree"
2533    }
2534
2535    fn new_training_request(
2536        &self,
2537        params: &str,
2538        field: &Field,
2539    ) -> Result<Box<dyn TrainingRequest>> {
2540        if field.data_type().is_nested() {
2541            return Err(Error::invalid_input_source(
2542                "A btree index can only be created on a non-nested field.".into(),
2543            ));
2544        }
2545
2546        let params = serde_json::from_str::<BTreeParameters>(params)?;
2547        Ok(Box::new(BTreeTrainingRequest::new(params)))
2548    }
2549
2550    fn provides_exact_answer(&self) -> bool {
2551        true
2552    }
2553
2554    fn version(&self) -> u32 {
2555        BTREE_INDEX_VERSION
2556    }
2557
2558    fn new_query_parser(
2559        &self,
2560        index_name: String,
2561        _index_details: &prost_types::Any,
2562    ) -> Option<Box<dyn ScalarQueryParser>> {
2563        Some(Box::new(SargableQueryParser::new(index_name, false)))
2564    }
2565
2566    async fn train_index(
2567        &self,
2568        data: SendableRecordBatchStream,
2569        index_store: &dyn IndexStore,
2570        request: Box<dyn TrainingRequest>,
2571        fragment_ids: Option<Vec<u32>>,
2572        _progress: Arc<dyn crate::progress::IndexBuildProgress>,
2573    ) -> Result<CreatedIndex> {
2574        let request = request
2575            .as_any()
2576            .downcast_ref::<BTreeTrainingRequest>()
2577            .unwrap();
2578        train_btree_index(
2579            data,
2580            index_store,
2581            request
2582                .parameters
2583                .zone_size
2584                .unwrap_or(DEFAULT_BTREE_BATCH_SIZE),
2585            fragment_ids,
2586            request.parameters.range_id,
2587        )
2588        .await?;
2589        Ok(CreatedIndex {
2590            index_details: prost_types::Any::from_msg(&pbold::BTreeIndexDetails::default())
2591                .unwrap(),
2592            index_version: BTREE_INDEX_VERSION,
2593        })
2594    }
2595
2596    async fn load_index(
2597        &self,
2598        index_store: Arc<dyn IndexStore>,
2599        _index_details: &prost_types::Any,
2600        frag_reuse_index: Option<Arc<FragReuseIndex>>,
2601        cache: &LanceCache,
2602    ) -> Result<Arc<dyn ScalarIndex>> {
2603        Ok(BTreeIndex::load(index_store, frag_reuse_index, cache).await? as Arc<dyn ScalarIndex>)
2604    }
2605}
2606
2607#[cfg(test)]
2608mod tests {
2609    use std::sync::atomic::Ordering;
2610    use std::{collections::HashMap, sync::Arc};
2611
2612    use arrow::datatypes::{Float32Type, Float64Type, Int32Type, UInt64Type};
2613    use arrow_array::{FixedSizeListArray, record_batch};
2614    use datafusion::{
2615        execution::{SendableRecordBatchStream, TaskContext},
2616        physical_plan::{ExecutionPlan, sorts::sort::SortExec, stream::RecordBatchStreamAdapter},
2617    };
2618    use datafusion_common::{DataFusionError, ScalarValue};
2619    use datafusion_physical_expr::{PhysicalSortExpr, expressions::col};
2620    use deepsize::DeepSizeOf;
2621    use futures::TryStreamExt;
2622    use futures::stream;
2623    use lance_core::utils::mask::RowSetOps;
2624    use lance_core::utils::tempfile::TempObjDir;
2625    use lance_core::{cache::LanceCache, utils::mask::RowAddrTreeMap};
2626    use lance_datafusion::{chunker::break_stream, datagen::DatafusionDatagenExt};
2627    use lance_datagen::{ArrayGeneratorExt, BatchCount, RowCount, array, gen_batch};
2628    use lance_io::object_store::ObjectStore;
2629    use object_store::path::Path;
2630
2631    use crate::metrics::LocalMetricsCollector;
2632    use crate::{
2633        metrics::NoOpMetricsCollector,
2634        scalar::{
2635            IndexStore, SargableQuery, ScalarIndex, SearchResult,
2636            btree::{BTREE_PAGES_NAME, BTreeIndex},
2637            lance_format::LanceIndexStore,
2638        },
2639    };
2640
2641    use super::{
2642        DEFAULT_BTREE_BATCH_SIZE, OrderableScalarValue, part_lookup_file_path,
2643        part_page_data_file_path, train_btree_index,
2644    };
2645    #[test]
2646    fn test_scalar_value_size() {
2647        let size_of_i32 = OrderableScalarValue(ScalarValue::Int32(Some(0))).deep_size_of();
2648        let size_of_many_i32 = OrderableScalarValue(ScalarValue::FixedSizeList(Arc::new(
2649            FixedSizeListArray::from_iter_primitive::<Int32Type, _, _>(
2650                vec![Some(vec![Some(0); 128])],
2651                128,
2652            ),
2653        )))
2654        .deep_size_of();
2655
2656        // deep_size_of should account for the rust type overhead
2657        assert!(size_of_i32 > 4);
2658        assert!(size_of_many_i32 > 128 * 4);
2659    }
2660
2661    #[tokio::test]
2662    async fn test_null_ids() {
2663        let tmpdir = TempObjDir::default();
2664        let test_store = Arc::new(LanceIndexStore::new(
2665            Arc::new(ObjectStore::local()),
2666            tmpdir.clone(),
2667            Arc::new(LanceCache::no_cache()),
2668        ));
2669
2670        // Generate 50,000 rows of random data with 80% nulls
2671        let stream = gen_batch()
2672            .col(
2673                "value",
2674                array::rand::<Float32Type>().with_nulls(&[true, false, false, false, false]),
2675            )
2676            .col("_rowid", array::step::<UInt64Type>())
2677            .into_df_stream(RowCount::from(5000), BatchCount::from(10));
2678
2679        train_btree_index(stream, test_store.as_ref(), 5000, None, None)
2680            .await
2681            .unwrap();
2682
2683        let index = BTreeIndex::load(test_store.clone(), None, &LanceCache::no_cache())
2684            .await
2685            .unwrap();
2686
2687        assert_eq!(index.page_lookup.null_pages.len(), 10);
2688
2689        let remap_dir = TempObjDir::default();
2690        let remap_store = Arc::new(LanceIndexStore::new(
2691            Arc::new(ObjectStore::local()),
2692            remap_dir.clone(),
2693            Arc::new(LanceCache::no_cache()),
2694        ));
2695
2696        // Remap with a no-op mapping.  The remapped index should be identical to the original
2697        index
2698            .remap(&HashMap::default(), remap_store.as_ref())
2699            .await
2700            .unwrap();
2701
2702        let remap_index = BTreeIndex::load(remap_store.clone(), None, &LanceCache::no_cache())
2703            .await
2704            .unwrap();
2705
2706        assert_eq!(remap_index.page_lookup, index.page_lookup);
2707
2708        let original_pages = test_store.open_index_file(BTREE_PAGES_NAME).await.unwrap();
2709        let remapped_pages = remap_store.open_index_file(BTREE_PAGES_NAME).await.unwrap();
2710
2711        assert_eq!(original_pages.num_rows(), remapped_pages.num_rows());
2712
2713        let original_data = original_pages
2714            .read_record_batch(0, original_pages.num_rows() as u64)
2715            .await
2716            .unwrap();
2717        let remapped_data = remapped_pages
2718            .read_record_batch(0, remapped_pages.num_rows() as u64)
2719            .await
2720            .unwrap();
2721
2722        assert_eq!(original_data, remapped_data);
2723    }
2724
2725    #[tokio::test]
2726    async fn test_nan_ordering() {
2727        let tmpdir = TempObjDir::default();
2728        let test_store = Arc::new(LanceIndexStore::new(
2729            Arc::new(ObjectStore::local()),
2730            tmpdir.clone(),
2731            Arc::new(LanceCache::no_cache()),
2732        ));
2733
2734        let values = vec![
2735            0.0,
2736            1.0,
2737            2.0,
2738            3.0,
2739            f64::NAN,
2740            f64::NEG_INFINITY,
2741            f64::INFINITY,
2742        ];
2743
2744        // This is a bit overkill but we've had bugs in the past where DF's sort
2745        // didn't agree with Arrow's sort so we do an end-to-end test here
2746        // and use DF to sort the data like we would in a real dataset.
2747        let data = gen_batch()
2748            .col("value", array::cycle::<Float64Type>(values.clone()))
2749            .col("_rowid", array::step::<UInt64Type>())
2750            .into_df_exec(RowCount::from(10), BatchCount::from(100));
2751        let schema = data.schema();
2752        let sort_expr = PhysicalSortExpr::new_default(col("value", schema.as_ref()).unwrap());
2753        let plan = Arc::new(SortExec::new([sort_expr].into(), data));
2754        let stream = plan.execute(0, Arc::new(TaskContext::default())).unwrap();
2755        let stream = break_stream(stream, 64);
2756        let stream = stream.map_err(DataFusionError::from);
2757        let stream =
2758            Box::pin(RecordBatchStreamAdapter::new(schema, stream)) as SendableRecordBatchStream;
2759
2760        train_btree_index(stream, test_store.as_ref(), 64, None, None)
2761            .await
2762            .unwrap();
2763
2764        let index = BTreeIndex::load(test_store, None, &LanceCache::no_cache())
2765            .await
2766            .unwrap();
2767
2768        for (idx, value) in values.into_iter().enumerate() {
2769            let query = SargableQuery::Equals(ScalarValue::Float64(Some(value)));
2770            let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
2771            assert_eq!(
2772                result,
2773                SearchResult::exact(RowAddrTreeMap::from_iter(((idx as u64)..1000).step_by(7)))
2774            );
2775        }
2776    }
2777
2778    #[tokio::test]
2779    async fn test_page_cache() {
2780        let tmpdir = TempObjDir::default();
2781        let test_store = Arc::new(LanceIndexStore::new(
2782            Arc::new(ObjectStore::local()),
2783            tmpdir.clone(),
2784            Arc::new(LanceCache::no_cache()),
2785        ));
2786
2787        let data = gen_batch()
2788            .col("value", array::step::<Float32Type>())
2789            .col("_rowid", array::step::<UInt64Type>())
2790            .into_df_exec(RowCount::from(1000), BatchCount::from(10));
2791        let schema = data.schema();
2792        let sort_expr = PhysicalSortExpr::new_default(col("value", schema.as_ref()).unwrap());
2793        let plan = Arc::new(SortExec::new([sort_expr].into(), data));
2794        let stream = plan.execute(0, Arc::new(TaskContext::default())).unwrap();
2795        let stream = break_stream(stream, 64);
2796        let stream = stream.map_err(DataFusionError::from);
2797        let stream =
2798            Box::pin(RecordBatchStreamAdapter::new(schema, stream)) as SendableRecordBatchStream;
2799
2800        train_btree_index(stream, test_store.as_ref(), 64, None, None)
2801            .await
2802            .unwrap();
2803
2804        let cache = Arc::new(LanceCache::with_capacity(100 * 1024 * 1024));
2805        let index = BTreeIndex::load(test_store, None, cache.as_ref())
2806            .await
2807            .unwrap();
2808
2809        let query = SargableQuery::Equals(ScalarValue::Float32(Some(0.0)));
2810        let metrics = LocalMetricsCollector::default();
2811        let query1 = index.search(&query, &metrics);
2812        let query2 = index.search(&query, &metrics);
2813        tokio::join!(query1, query2).0.unwrap();
2814        assert_eq!(metrics.parts_loaded.load(Ordering::Relaxed), 1);
2815    }
2816
2817    #[tokio::test]
2818    async fn test_fragment_btree_index_consistency() {
2819        // Setup stores for both indexes
2820        let full_tmpdir = TempObjDir::default();
2821        let full_store = Arc::new(LanceIndexStore::new(
2822            Arc::new(ObjectStore::local()),
2823            full_tmpdir.clone(),
2824            Arc::new(LanceCache::no_cache()),
2825        ));
2826
2827        let fragment_tmpdir = TempObjDir::default();
2828        let fragment_store = Arc::new(LanceIndexStore::new(
2829            Arc::new(ObjectStore::local()),
2830            fragment_tmpdir.clone(),
2831            Arc::new(LanceCache::no_cache()),
2832        ));
2833
2834        // Method 1: Build complete index directly using the same data
2835        // Create deterministic data for comparison - use 2 * DEFAULT_BTREE_BATCH_SIZE for testing
2836        let total_count = 2 * DEFAULT_BTREE_BATCH_SIZE;
2837        let full_data_gen = gen_batch()
2838            .col("value", array::step::<Int32Type>())
2839            .col("_rowid", array::step::<UInt64Type>())
2840            .into_df_stream(RowCount::from(total_count / 2), BatchCount::from(2));
2841        let full_data_source = Box::pin(RecordBatchStreamAdapter::new(
2842            full_data_gen.schema(),
2843            full_data_gen,
2844        ));
2845
2846        train_btree_index(
2847            full_data_source,
2848            full_store.as_ref(),
2849            DEFAULT_BTREE_BATCH_SIZE,
2850            None,
2851            None,
2852        )
2853        .await
2854        .unwrap();
2855
2856        // Method 2: Build fragment-based index using the same data split into fragments
2857        // Create fragment 1 index - first half of the data (0 to DEFAULT_BTREE_BATCH_SIZE-1)
2858        let half_count = DEFAULT_BTREE_BATCH_SIZE;
2859        let fragment1_gen = gen_batch()
2860            .col("value", array::step::<Int32Type>())
2861            .col("_rowid", array::step::<UInt64Type>())
2862            .into_df_stream(RowCount::from(half_count), BatchCount::from(1));
2863        let fragment1_data_source = Box::pin(RecordBatchStreamAdapter::new(
2864            fragment1_gen.schema(),
2865            fragment1_gen,
2866        ));
2867
2868        train_btree_index(
2869            fragment1_data_source,
2870            fragment_store.as_ref(),
2871            DEFAULT_BTREE_BATCH_SIZE,
2872            Some(vec![1]), // fragment_id = 1
2873            None,
2874        )
2875        .await
2876        .unwrap();
2877
2878        // Create fragment 2 index - second half of the data (DEFAULT_BTREE_BATCH_SIZE to 2*DEFAULT_BTREE_BATCH_SIZE-1)
2879        let start_val = DEFAULT_BTREE_BATCH_SIZE as i32;
2880        let end_val = (2 * DEFAULT_BTREE_BATCH_SIZE) as i32;
2881        let values_second_half: Vec<i32> = (start_val..end_val).collect();
2882        let row_ids_second_half: Vec<u64> = (start_val as u64..end_val as u64).collect();
2883        let fragment2_gen = gen_batch()
2884            .col("value", array::cycle::<Int32Type>(values_second_half))
2885            .col("_rowid", array::cycle::<UInt64Type>(row_ids_second_half))
2886            .into_df_stream(RowCount::from(half_count), BatchCount::from(1));
2887        let fragment2_data_source = Box::pin(RecordBatchStreamAdapter::new(
2888            fragment2_gen.schema(),
2889            fragment2_gen,
2890        ));
2891
2892        train_btree_index(
2893            fragment2_data_source,
2894            fragment_store.as_ref(),
2895            DEFAULT_BTREE_BATCH_SIZE,
2896            Some(vec![2]), // fragment_id = 2
2897            None,
2898        )
2899        .await
2900        .unwrap();
2901
2902        // Merge the fragment files
2903        let part_page_files = vec![
2904            part_page_data_file_path(1 << 32),
2905            part_page_data_file_path(2 << 32),
2906        ];
2907
2908        let part_lookup_files = vec![
2909            part_lookup_file_path(1 << 32),
2910            part_lookup_file_path(2 << 32),
2911        ];
2912
2913        super::merge_metadata_files(
2914            fragment_store.as_ref(),
2915            &part_page_files,
2916            &part_lookup_files,
2917            Option::from(1usize),
2918        )
2919        .await
2920        .unwrap();
2921
2922        // Load both indexes
2923        let full_index = BTreeIndex::load(full_store.clone(), None, &LanceCache::no_cache())
2924            .await
2925            .unwrap();
2926
2927        let merged_index = BTreeIndex::load(fragment_store.clone(), None, &LanceCache::no_cache())
2928            .await
2929            .unwrap();
2930
2931        // Test queries one by one to identify the exact problem
2932
2933        // Test 1: Query for value 0 (should be in first page)
2934        let query_0 = SargableQuery::Equals(ScalarValue::Int32(Some(0)));
2935        let full_result_0 = full_index
2936            .search(&query_0, &NoOpMetricsCollector)
2937            .await
2938            .unwrap();
2939        let merged_result_0 = merged_index
2940            .search(&query_0, &NoOpMetricsCollector)
2941            .await
2942            .unwrap();
2943        assert_eq!(full_result_0, merged_result_0, "Query for value 0 failed");
2944
2945        // Test 2: Query for value in middle of first batch (should be in first page)
2946        let mid_first_batch = (DEFAULT_BTREE_BATCH_SIZE / 2) as i32;
2947        let query_mid_first = SargableQuery::Equals(ScalarValue::Int32(Some(mid_first_batch)));
2948        let full_result_mid_first = full_index
2949            .search(&query_mid_first, &NoOpMetricsCollector)
2950            .await
2951            .unwrap();
2952        let merged_result_mid_first = merged_index
2953            .search(&query_mid_first, &NoOpMetricsCollector)
2954            .await
2955            .unwrap();
2956        assert_eq!(
2957            full_result_mid_first, merged_result_mid_first,
2958            "Query for value {} failed",
2959            mid_first_batch
2960        );
2961
2962        // Test 3: Query for first value in second batch (should be in second page)
2963        let first_second_batch = DEFAULT_BTREE_BATCH_SIZE as i32;
2964        let query_first_second =
2965            SargableQuery::Equals(ScalarValue::Int32(Some(first_second_batch)));
2966        let full_result_first_second = full_index
2967            .search(&query_first_second, &NoOpMetricsCollector)
2968            .await
2969            .unwrap();
2970        let merged_result_first_second = merged_index
2971            .search(&query_first_second, &NoOpMetricsCollector)
2972            .await
2973            .unwrap();
2974        assert_eq!(
2975            full_result_first_second, merged_result_first_second,
2976            "Query for value {} failed",
2977            first_second_batch
2978        );
2979
2980        // Test 4: Query for value in middle of second batch (should be in second page)
2981        let mid_second_batch = (DEFAULT_BTREE_BATCH_SIZE + DEFAULT_BTREE_BATCH_SIZE / 2) as i32;
2982        let query_mid_second = SargableQuery::Equals(ScalarValue::Int32(Some(mid_second_batch)));
2983
2984        let full_result_mid_second = full_index
2985            .search(&query_mid_second, &NoOpMetricsCollector)
2986            .await
2987            .unwrap();
2988        let merged_result_mid_second = merged_index
2989            .search(&query_mid_second, &NoOpMetricsCollector)
2990            .await
2991            .unwrap();
2992        assert_eq!(
2993            full_result_mid_second, merged_result_mid_second,
2994            "Query for value {} failed",
2995            mid_second_batch
2996        );
2997    }
2998
2999    #[tokio::test]
3000    async fn test_fragment_btree_index_boundary_queries() {
3001        // Setup stores for both indexes
3002        let full_tmpdir = TempObjDir::default();
3003        let full_store = Arc::new(LanceIndexStore::new(
3004            Arc::new(ObjectStore::local()),
3005            full_tmpdir.clone(),
3006            Arc::new(LanceCache::no_cache()),
3007        ));
3008
3009        let fragment_tmpdir = TempObjDir::default();
3010        let fragment_store = Arc::new(LanceIndexStore::new(
3011            Arc::new(ObjectStore::local()),
3012            fragment_tmpdir.clone(),
3013            Arc::new(LanceCache::no_cache()),
3014        ));
3015
3016        // Use 3 * DEFAULT_BTREE_BATCH_SIZE for more comprehensive boundary testing
3017        let total_count = 3 * DEFAULT_BTREE_BATCH_SIZE;
3018
3019        // Method 1: Build complete index directly
3020        let full_data_gen = gen_batch()
3021            .col("value", array::step::<Int32Type>())
3022            .col("_rowid", array::step::<UInt64Type>())
3023            .into_df_stream(RowCount::from(total_count / 3), BatchCount::from(3));
3024        let full_data_source = Box::pin(RecordBatchStreamAdapter::new(
3025            full_data_gen.schema(),
3026            full_data_gen,
3027        ));
3028
3029        train_btree_index(
3030            full_data_source,
3031            full_store.as_ref(),
3032            DEFAULT_BTREE_BATCH_SIZE,
3033            None,
3034            None,
3035        )
3036        .await
3037        .unwrap();
3038
3039        // Method 2: Build fragment-based index using 3 fragments
3040        // Fragment 1: 0 to DEFAULT_BTREE_BATCH_SIZE-1
3041        let fragment_size = DEFAULT_BTREE_BATCH_SIZE;
3042        let fragment1_gen = gen_batch()
3043            .col("value", array::step::<Int32Type>())
3044            .col("_rowid", array::step::<UInt64Type>())
3045            .into_df_stream(RowCount::from(fragment_size), BatchCount::from(1));
3046        let fragment1_data_source = Box::pin(RecordBatchStreamAdapter::new(
3047            fragment1_gen.schema(),
3048            fragment1_gen,
3049        ));
3050
3051        train_btree_index(
3052            fragment1_data_source,
3053            fragment_store.as_ref(),
3054            DEFAULT_BTREE_BATCH_SIZE,
3055            Some(vec![1]),
3056            None,
3057        )
3058        .await
3059        .unwrap();
3060
3061        // Fragment 2: DEFAULT_BTREE_BATCH_SIZE to 2*DEFAULT_BTREE_BATCH_SIZE-1
3062        let start_val2 = DEFAULT_BTREE_BATCH_SIZE as i32;
3063        let end_val2 = (2 * DEFAULT_BTREE_BATCH_SIZE) as i32;
3064        let values_fragment2: Vec<i32> = (start_val2..end_val2).collect();
3065        let row_ids_fragment2: Vec<u64> = (start_val2 as u64..end_val2 as u64).collect();
3066        let fragment2_gen = gen_batch()
3067            .col("value", array::cycle::<Int32Type>(values_fragment2))
3068            .col("_rowid", array::cycle::<UInt64Type>(row_ids_fragment2))
3069            .into_df_stream(RowCount::from(fragment_size), BatchCount::from(1));
3070        let fragment2_data_source = Box::pin(RecordBatchStreamAdapter::new(
3071            fragment2_gen.schema(),
3072            fragment2_gen,
3073        ));
3074
3075        train_btree_index(
3076            fragment2_data_source,
3077            fragment_store.as_ref(),
3078            DEFAULT_BTREE_BATCH_SIZE,
3079            Some(vec![2]),
3080            None,
3081        )
3082        .await
3083        .unwrap();
3084
3085        // Fragment 3: 2*DEFAULT_BTREE_BATCH_SIZE to 3*DEFAULT_BTREE_BATCH_SIZE-1
3086        let start_val3 = (2 * DEFAULT_BTREE_BATCH_SIZE) as i32;
3087        let end_val3 = (3 * DEFAULT_BTREE_BATCH_SIZE) as i32;
3088        let values_fragment3: Vec<i32> = (start_val3..end_val3).collect();
3089        let row_ids_fragment3: Vec<u64> = (start_val3 as u64..end_val3 as u64).collect();
3090        let fragment3_gen = gen_batch()
3091            .col("value", array::cycle::<Int32Type>(values_fragment3))
3092            .col("_rowid", array::cycle::<UInt64Type>(row_ids_fragment3))
3093            .into_df_stream(RowCount::from(fragment_size), BatchCount::from(1));
3094        let fragment3_data_source = Box::pin(RecordBatchStreamAdapter::new(
3095            fragment3_gen.schema(),
3096            fragment3_gen,
3097        ));
3098
3099        train_btree_index(
3100            fragment3_data_source,
3101            fragment_store.as_ref(),
3102            DEFAULT_BTREE_BATCH_SIZE,
3103            Some(vec![3]),
3104            None,
3105        )
3106        .await
3107        .unwrap();
3108
3109        // Merge all fragment files
3110        let part_page_files = vec![
3111            part_page_data_file_path(1 << 32),
3112            part_page_data_file_path(2 << 32),
3113            part_page_data_file_path(3 << 32),
3114        ];
3115
3116        let part_lookup_files = vec![
3117            part_lookup_file_path(1 << 32),
3118            part_lookup_file_path(2 << 32),
3119            part_lookup_file_path(3 << 32),
3120        ];
3121
3122        super::merge_metadata_files(
3123            fragment_store.as_ref(),
3124            &part_page_files,
3125            &part_lookup_files,
3126            Option::from(1usize),
3127        )
3128        .await
3129        .unwrap();
3130
3131        // Load both indexes
3132        let full_index = BTreeIndex::load(full_store.clone(), None, &LanceCache::no_cache())
3133            .await
3134            .unwrap();
3135
3136        let merged_index = BTreeIndex::load(fragment_store.clone(), None, &LanceCache::no_cache())
3137            .await
3138            .unwrap();
3139
3140        // === Boundary Value Tests ===
3141
3142        // Test 1: Query minimum value (boundary: data start)
3143        let query_min = SargableQuery::Equals(ScalarValue::Int32(Some(0)));
3144        let full_result_min = full_index
3145            .search(&query_min, &NoOpMetricsCollector)
3146            .await
3147            .unwrap();
3148        let merged_result_min = merged_index
3149            .search(&query_min, &NoOpMetricsCollector)
3150            .await
3151            .unwrap();
3152        assert_eq!(
3153            full_result_min, merged_result_min,
3154            "Query for minimum value 0 failed"
3155        );
3156
3157        // Test 2: Query maximum value (boundary: data end)
3158        let max_val = (3 * DEFAULT_BTREE_BATCH_SIZE - 1) as i32;
3159        let query_max = SargableQuery::Equals(ScalarValue::Int32(Some(max_val)));
3160        let full_result_max = full_index
3161            .search(&query_max, &NoOpMetricsCollector)
3162            .await
3163            .unwrap();
3164        let merged_result_max = merged_index
3165            .search(&query_max, &NoOpMetricsCollector)
3166            .await
3167            .unwrap();
3168        assert_eq!(
3169            full_result_max, merged_result_max,
3170            "Query for maximum value {} failed",
3171            max_val
3172        );
3173
3174        // Test 3: Query fragment boundary value (last value of first fragment)
3175        let fragment1_last = (DEFAULT_BTREE_BATCH_SIZE - 1) as i32;
3176        let query_frag1_last = SargableQuery::Equals(ScalarValue::Int32(Some(fragment1_last)));
3177        let full_result_frag1_last = full_index
3178            .search(&query_frag1_last, &NoOpMetricsCollector)
3179            .await
3180            .unwrap();
3181        let merged_result_frag1_last = merged_index
3182            .search(&query_frag1_last, &NoOpMetricsCollector)
3183            .await
3184            .unwrap();
3185        assert_eq!(
3186            full_result_frag1_last, merged_result_frag1_last,
3187            "Query for fragment 1 last value {} failed",
3188            fragment1_last
3189        );
3190
3191        // Test 4: Query fragment boundary value (first value of second fragment)
3192        let fragment2_first = DEFAULT_BTREE_BATCH_SIZE as i32;
3193        let query_frag2_first = SargableQuery::Equals(ScalarValue::Int32(Some(fragment2_first)));
3194        let full_result_frag2_first = full_index
3195            .search(&query_frag2_first, &NoOpMetricsCollector)
3196            .await
3197            .unwrap();
3198        let merged_result_frag2_first = merged_index
3199            .search(&query_frag2_first, &NoOpMetricsCollector)
3200            .await
3201            .unwrap();
3202        assert_eq!(
3203            full_result_frag2_first, merged_result_frag2_first,
3204            "Query for fragment 2 first value {} failed",
3205            fragment2_first
3206        );
3207
3208        // Test 5: Query fragment boundary value (last value of second fragment)
3209        let fragment2_last = (2 * DEFAULT_BTREE_BATCH_SIZE - 1) as i32;
3210        let query_frag2_last = SargableQuery::Equals(ScalarValue::Int32(Some(fragment2_last)));
3211        let full_result_frag2_last = full_index
3212            .search(&query_frag2_last, &NoOpMetricsCollector)
3213            .await
3214            .unwrap();
3215        let merged_result_frag2_last = merged_index
3216            .search(&query_frag2_last, &NoOpMetricsCollector)
3217            .await
3218            .unwrap();
3219        assert_eq!(
3220            full_result_frag2_last, merged_result_frag2_last,
3221            "Query for fragment 2 last value {} failed",
3222            fragment2_last
3223        );
3224
3225        // Test 6: Query fragment boundary value (first value of third fragment)
3226        let fragment3_first = (2 * DEFAULT_BTREE_BATCH_SIZE) as i32;
3227        let query_frag3_first = SargableQuery::Equals(ScalarValue::Int32(Some(fragment3_first)));
3228        let full_result_frag3_first = full_index
3229            .search(&query_frag3_first, &NoOpMetricsCollector)
3230            .await
3231            .unwrap();
3232        let merged_result_frag3_first = merged_index
3233            .search(&query_frag3_first, &NoOpMetricsCollector)
3234            .await
3235            .unwrap();
3236        assert_eq!(
3237            full_result_frag3_first, merged_result_frag3_first,
3238            "Query for fragment 3 first value {} failed",
3239            fragment3_first
3240        );
3241
3242        // === Non-existent Value Tests ===
3243
3244        // Test 7: Query value below minimum
3245        let query_below_min = SargableQuery::Equals(ScalarValue::Int32(Some(-1)));
3246        let full_result_below = full_index
3247            .search(&query_below_min, &NoOpMetricsCollector)
3248            .await
3249            .unwrap();
3250        let merged_result_below = merged_index
3251            .search(&query_below_min, &NoOpMetricsCollector)
3252            .await
3253            .unwrap();
3254        assert_eq!(
3255            full_result_below, merged_result_below,
3256            "Query for value below minimum (-1) failed"
3257        );
3258
3259        // Test 8: Query value above maximum
3260        let query_above_max = SargableQuery::Equals(ScalarValue::Int32(Some(max_val + 1)));
3261        let full_result_above = full_index
3262            .search(&query_above_max, &NoOpMetricsCollector)
3263            .await
3264            .unwrap();
3265        let merged_result_above = merged_index
3266            .search(&query_above_max, &NoOpMetricsCollector)
3267            .await
3268            .unwrap();
3269        assert_eq!(
3270            full_result_above,
3271            merged_result_above,
3272            "Query for value above maximum ({}) failed",
3273            max_val + 1
3274        );
3275
3276        // === Range Query Tests ===
3277
3278        // Test 9: Cross-fragment range query (from first fragment to second fragment)
3279        let range_start = (DEFAULT_BTREE_BATCH_SIZE - 100) as i32;
3280        let range_end = (DEFAULT_BTREE_BATCH_SIZE + 100) as i32;
3281        let query_cross_frag = SargableQuery::Range(
3282            std::collections::Bound::Included(ScalarValue::Int32(Some(range_start))),
3283            std::collections::Bound::Excluded(ScalarValue::Int32(Some(range_end))),
3284        );
3285        let full_result_cross = full_index
3286            .search(&query_cross_frag, &NoOpMetricsCollector)
3287            .await
3288            .unwrap();
3289        let merged_result_cross = merged_index
3290            .search(&query_cross_frag, &NoOpMetricsCollector)
3291            .await
3292            .unwrap();
3293        assert_eq!(
3294            full_result_cross, merged_result_cross,
3295            "Cross-fragment range query [{}, {}] failed",
3296            range_start, range_end
3297        );
3298
3299        // Test 10: Range query within single fragment
3300        let single_frag_start = 100i32;
3301        let single_frag_end = 200i32;
3302        let query_single_frag = SargableQuery::Range(
3303            std::collections::Bound::Included(ScalarValue::Int32(Some(single_frag_start))),
3304            std::collections::Bound::Excluded(ScalarValue::Int32(Some(single_frag_end))),
3305        );
3306        let full_result_single = full_index
3307            .search(&query_single_frag, &NoOpMetricsCollector)
3308            .await
3309            .unwrap();
3310        let merged_result_single = merged_index
3311            .search(&query_single_frag, &NoOpMetricsCollector)
3312            .await
3313            .unwrap();
3314        assert_eq!(
3315            full_result_single, merged_result_single,
3316            "Single fragment range query [{}, {}] failed",
3317            single_frag_start, single_frag_end
3318        );
3319
3320        // Test 11: Large range query spanning all fragments
3321        let large_range_start = 100i32;
3322        let large_range_end = (3 * DEFAULT_BTREE_BATCH_SIZE - 100) as i32;
3323        let query_large_range = SargableQuery::Range(
3324            std::collections::Bound::Included(ScalarValue::Int32(Some(large_range_start))),
3325            std::collections::Bound::Excluded(ScalarValue::Int32(Some(large_range_end))),
3326        );
3327        let full_result_large = full_index
3328            .search(&query_large_range, &NoOpMetricsCollector)
3329            .await
3330            .unwrap();
3331        let merged_result_large = merged_index
3332            .search(&query_large_range, &NoOpMetricsCollector)
3333            .await
3334            .unwrap();
3335        assert_eq!(
3336            full_result_large, merged_result_large,
3337            "Large range query [{}, {}] failed",
3338            large_range_start, large_range_end
3339        );
3340
3341        // === Range Boundary Query Tests ===
3342
3343        // Test 12: Less than query (implemented using range query, from minimum to specified value)
3344        let lt_val = (DEFAULT_BTREE_BATCH_SIZE / 2) as i32;
3345        let query_lt = SargableQuery::Range(
3346            std::collections::Bound::Included(ScalarValue::Int32(Some(0))),
3347            std::collections::Bound::Excluded(ScalarValue::Int32(Some(lt_val))),
3348        );
3349        let full_result_lt = full_index
3350            .search(&query_lt, &NoOpMetricsCollector)
3351            .await
3352            .unwrap();
3353        let merged_result_lt = merged_index
3354            .search(&query_lt, &NoOpMetricsCollector)
3355            .await
3356            .unwrap();
3357        assert_eq!(
3358            full_result_lt, merged_result_lt,
3359            "Less than query (<{}) failed",
3360            lt_val
3361        );
3362
3363        // Test 13: Greater than query (implemented using range query, from specified value to maximum)
3364        let gt_val = (2 * DEFAULT_BTREE_BATCH_SIZE) as i32;
3365        let max_range_val = (3 * DEFAULT_BTREE_BATCH_SIZE) as i32;
3366        let query_gt = SargableQuery::Range(
3367            std::collections::Bound::Excluded(ScalarValue::Int32(Some(gt_val))),
3368            std::collections::Bound::Excluded(ScalarValue::Int32(Some(max_range_val))),
3369        );
3370        let full_result_gt = full_index
3371            .search(&query_gt, &NoOpMetricsCollector)
3372            .await
3373            .unwrap();
3374        let merged_result_gt = merged_index
3375            .search(&query_gt, &NoOpMetricsCollector)
3376            .await
3377            .unwrap();
3378        assert_eq!(
3379            full_result_gt, merged_result_gt,
3380            "Greater than query (>{}) failed",
3381            gt_val
3382        );
3383
3384        // Test 14: Less than or equal query (implemented using range query, including boundary value)
3385        let lte_val = (DEFAULT_BTREE_BATCH_SIZE - 1) as i32;
3386        let query_lte = SargableQuery::Range(
3387            std::collections::Bound::Included(ScalarValue::Int32(Some(0))),
3388            std::collections::Bound::Included(ScalarValue::Int32(Some(lte_val))),
3389        );
3390        let full_result_lte = full_index
3391            .search(&query_lte, &NoOpMetricsCollector)
3392            .await
3393            .unwrap();
3394        let merged_result_lte = merged_index
3395            .search(&query_lte, &NoOpMetricsCollector)
3396            .await
3397            .unwrap();
3398        assert_eq!(
3399            full_result_lte, merged_result_lte,
3400            "Less than or equal query (<={}) failed",
3401            lte_val
3402        );
3403
3404        // Test 15: Greater than or equal query (implemented using range query, including boundary value)
3405        let gte_val = (2 * DEFAULT_BTREE_BATCH_SIZE) as i32;
3406        let query_gte = SargableQuery::Range(
3407            std::collections::Bound::Included(ScalarValue::Int32(Some(gte_val))),
3408            std::collections::Bound::Excluded(ScalarValue::Int32(Some(max_range_val))),
3409        );
3410        let full_result_gte = full_index
3411            .search(&query_gte, &NoOpMetricsCollector)
3412            .await
3413            .unwrap();
3414        let merged_result_gte = merged_index
3415            .search(&query_gte, &NoOpMetricsCollector)
3416            .await
3417            .unwrap();
3418        assert_eq!(
3419            full_result_gte, merged_result_gte,
3420            "Greater than or equal query (>={}) failed",
3421            gte_val
3422        );
3423    }
3424
3425    #[test]
3426    fn test_extract_partition_id() {
3427        // Test valid partition file names
3428        assert_eq!(
3429            super::extract_partition_id("part_123_page_data.lance").unwrap(),
3430            123
3431        );
3432        assert_eq!(
3433            super::extract_partition_id("part_456_page_lookup.lance").unwrap(),
3434            456
3435        );
3436        assert_eq!(
3437            super::extract_partition_id("part_4294967296_page_data.lance").unwrap(),
3438            4294967296
3439        );
3440
3441        // Test invalid file names
3442        assert!(super::extract_partition_id("invalid_filename.lance").is_err());
3443        assert!(super::extract_partition_id("part_abc_page_data.lance").is_err());
3444        assert!(super::extract_partition_id("part_123").is_err());
3445        assert!(super::extract_partition_id("part_").is_err());
3446    }
3447
3448    #[tokio::test]
3449    async fn test_cleanup_partition_files() {
3450        // Create a test store
3451        let tmpdir = TempObjDir::default();
3452        let test_store: Arc<dyn crate::scalar::IndexStore> = Arc::new(LanceIndexStore::new(
3453            Arc::new(ObjectStore::local()),
3454            tmpdir.clone(),
3455            Arc::new(LanceCache::no_cache()),
3456        ));
3457
3458        // Test files with different patterns
3459        let lookup_files = vec![
3460            "part_123_page_lookup.lance".to_string(),
3461            "invalid_lookup_file.lance".to_string(),
3462            "part_456_page_lookup.lance".to_string(),
3463        ];
3464
3465        let page_files = vec![
3466            "part_123_page_data.lance".to_string(),
3467            "invalid_page_file.lance".to_string(),
3468            "part_456_page_data.lance".to_string(),
3469        ];
3470
3471        // The cleanup function should handle both valid and invalid file patterns gracefully
3472        // This test mainly verifies that the function doesn't panic and handles edge cases
3473        super::cleanup_partition_files(test_store.as_ref(), &lookup_files, &page_files).await;
3474    }
3475
3476    #[tokio::test]
3477    async fn test_btree_null_handling_in_queries() {
3478        let store = Arc::new(LanceIndexStore::new(
3479            Arc::new(ObjectStore::memory()),
3480            Path::default(),
3481            Arc::new(LanceCache::no_cache()),
3482        ));
3483
3484        // Create test data: [null, 0, 5] at row IDs [0, 1, 2]
3485        // BTree expects sorted data with nulls first (or filtered out)
3486        let batch = record_batch!(
3487            ("value", Int32, [None, Some(0), Some(5)]),
3488            ("_rowid", UInt64, [0, 1, 2])
3489        )
3490        .unwrap();
3491        let stream = stream::once(futures::future::ok(batch.clone()));
3492        let stream = Box::pin(RecordBatchStreamAdapter::new(batch.schema(), stream));
3493
3494        // Train the btree index with FlatIndexMetadata as sub-index
3495        super::train_btree_index(stream, store.as_ref(), 256, None, None)
3496            .await
3497            .unwrap();
3498
3499        let cache = LanceCache::with_capacity(1024 * 1024);
3500        let index = super::BTreeIndex::load(store.clone(), None, &cache)
3501            .await
3502            .unwrap();
3503
3504        // Test 1: Search for value 5 - should return allow=[2], null=[0]
3505        let query = SargableQuery::Equals(ScalarValue::Int32(Some(5)));
3506        let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
3507
3508        match result {
3509            SearchResult::Exact(row_ids) => {
3510                let actual_rows: Vec<u64> = row_ids
3511                    .true_rows()
3512                    .row_addrs()
3513                    .unwrap()
3514                    .map(u64::from)
3515                    .collect();
3516                assert_eq!(actual_rows, vec![2], "Should find row 2 where value == 5");
3517
3518                // Check that null_row_ids contains row 0
3519                let null_row_ids = row_ids.null_rows();
3520                assert!(!null_row_ids.is_empty(), "null_row_ids should be non-empty");
3521                let null_rows: Vec<u64> =
3522                    null_row_ids.row_addrs().unwrap().map(u64::from).collect();
3523                assert_eq!(null_rows, vec![0], "Should report row 0 as null");
3524            }
3525            _ => panic!("Expected Exact search result"),
3526        }
3527
3528        // Test 2: Range query [0, 3] - should return allow=[1], null=[0]
3529        let query = SargableQuery::Range(
3530            std::ops::Bound::Included(ScalarValue::Int32(Some(0))),
3531            std::ops::Bound::Included(ScalarValue::Int32(Some(3))),
3532        );
3533        let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
3534
3535        match result {
3536            SearchResult::Exact(row_ids) => {
3537                let actual_rows: Vec<u64> = row_ids
3538                    .true_rows()
3539                    .row_addrs()
3540                    .unwrap()
3541                    .map(u64::from)
3542                    .collect();
3543                assert_eq!(actual_rows, vec![1], "Should find row 1 where value == 0");
3544
3545                // Should report row 0 as null
3546                let null_row_ids = row_ids.null_rows();
3547                assert!(!null_row_ids.is_empty(), "null_row_ids should be non-empty");
3548                let null_rows: Vec<u64> =
3549                    null_row_ids.row_addrs().unwrap().map(u64::from).collect();
3550                assert_eq!(null_rows, vec![0], "Should report row 0 as null");
3551            }
3552            _ => panic!("Expected Exact search result"),
3553        }
3554
3555        // Test 3: IsIn query [0, 5] - should return allow=[1, 2], null=[0]
3556        let query = SargableQuery::IsIn(vec![
3557            ScalarValue::Int32(Some(0)),
3558            ScalarValue::Int32(Some(5)),
3559        ]);
3560        let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
3561
3562        match result {
3563            SearchResult::Exact(row_ids) => {
3564                let mut actual_rows: Vec<u64> = row_ids
3565                    .true_rows()
3566                    .row_addrs()
3567                    .unwrap()
3568                    .map(u64::from)
3569                    .collect();
3570                actual_rows.sort();
3571                assert_eq!(
3572                    actual_rows,
3573                    vec![1, 2],
3574                    "Should find rows 1 and 2 where value in [0, 5]"
3575                );
3576
3577                // Should report row 0 as null
3578                let null_row_ids = row_ids.null_rows();
3579                assert!(!null_row_ids.is_empty(), "null_row_ids should be non-empty");
3580                let null_rows: Vec<u64> =
3581                    null_row_ids.row_addrs().unwrap().map(u64::from).collect();
3582                assert_eq!(null_rows, vec![0], "Should report row 0 as null");
3583            }
3584            _ => panic!("Expected Exact search result"),
3585        }
3586    }
3587
3588    #[tokio::test]
3589    async fn test_range_btree_index_consistency() {
3590        // Setup stores for both indexes
3591        let full_tmpdir = TempObjDir::default();
3592        let full_store = Arc::new(LanceIndexStore::new(
3593            Arc::new(ObjectStore::local()),
3594            full_tmpdir.clone(),
3595            Arc::new(LanceCache::no_cache()),
3596        ));
3597
3598        let range_tmpdir = TempObjDir::default();
3599        let range_store = Arc::new(LanceIndexStore::new(
3600            Arc::new(ObjectStore::local()),
3601            range_tmpdir.clone(),
3602            Arc::new(LanceCache::no_cache()),
3603        ));
3604
3605        // Method 1: Build complete index directly using the same data
3606        // Create deterministic data for comparison - use 4 * DEFAULT_BTREE_BATCH_SIZE for testing
3607        let total_count = 4 * DEFAULT_BTREE_BATCH_SIZE;
3608        let full_data_gen = gen_batch()
3609            .col("value", array::step::<Int32Type>())
3610            .col("_rowid", array::step::<UInt64Type>())
3611            .into_df_stream(RowCount::from(total_count / 4), BatchCount::from(4));
3612        let full_data_source = Box::pin(RecordBatchStreamAdapter::new(
3613            full_data_gen.schema(),
3614            full_data_gen,
3615        ));
3616
3617        train_btree_index(
3618            full_data_source,
3619            full_store.as_ref(),
3620            DEFAULT_BTREE_BATCH_SIZE,
3621            None,
3622            None,
3623        )
3624        .await
3625        .unwrap();
3626
3627        // Method 2: Build range-based index using the same data split into ranges
3628        // Create range 1 index, intentionally make it not divisible by DEFAULT_BTREE_BATCH_SIZE
3629        let range1_gen = gen_batch()
3630            .col("value", array::step::<Int32Type>())
3631            .col("_rowid", array::step::<UInt64Type>())
3632            .into_df_stream(
3633                RowCount::from(DEFAULT_BTREE_BATCH_SIZE / 2),
3634                BatchCount::from(5),
3635            );
3636        let range1_data_source = Box::pin(RecordBatchStreamAdapter::new(
3637            range1_gen.schema(),
3638            range1_gen,
3639        ));
3640
3641        train_btree_index(
3642            range1_data_source,
3643            range_store.as_ref(),
3644            DEFAULT_BTREE_BATCH_SIZE,
3645            None,
3646            Option::from(0u32),
3647        )
3648        .await
3649        .unwrap();
3650
3651        // Create range 2 index, also intentionally make it not divisible by DEFAULT_BTREE_BATCH_SIZE
3652        let start_val = (DEFAULT_BTREE_BATCH_SIZE * 2 + DEFAULT_BTREE_BATCH_SIZE / 2) as i32;
3653        let end_val = (4 * DEFAULT_BTREE_BATCH_SIZE) as i32;
3654        let values_second_half: Vec<i32> = (start_val..end_val).collect();
3655        let row_ids_second_half: Vec<u64> = (start_val as u64..end_val as u64).collect();
3656        let range2_gen = gen_batch()
3657            .col("value", array::cycle::<Int32Type>(values_second_half))
3658            .col("_rowid", array::cycle::<UInt64Type>(row_ids_second_half))
3659            .into_df_stream(
3660                RowCount::from(DEFAULT_BTREE_BATCH_SIZE / 2),
3661                BatchCount::from(3),
3662            );
3663        let range2_data_source = Box::pin(RecordBatchStreamAdapter::new(
3664            range2_gen.schema(),
3665            range2_gen,
3666        ));
3667
3668        train_btree_index(
3669            range2_data_source,
3670            range_store.as_ref(),
3671            DEFAULT_BTREE_BATCH_SIZE,
3672            None,
3673            Option::from(1u32),
3674        )
3675        .await
3676        .unwrap();
3677
3678        // Merge the fragment files
3679        let part_page_files = vec![
3680            part_page_data_file_path(0 << 32),
3681            part_page_data_file_path(1 << 32),
3682        ];
3683
3684        let part_lookup_files = vec![
3685            part_lookup_file_path(0 << 32),
3686            part_lookup_file_path(1 << 32),
3687        ];
3688
3689        super::merge_metadata_files(
3690            range_store.as_ref(),
3691            &part_page_files,
3692            &part_lookup_files,
3693            Option::from(1usize),
3694        )
3695        .await
3696        .unwrap();
3697
3698        let full_index = BTreeIndex::load(full_store.clone(), None, &LanceCache::no_cache())
3699            .await
3700            .unwrap();
3701
3702        let ranged_index = BTreeIndex::load(range_store.clone(), None, &LanceCache::no_cache())
3703            .await
3704            .unwrap();
3705
3706        // Equality Tests
3707
3708        // Test 1: Query for value 0
3709        let query_0 = SargableQuery::Equals(ScalarValue::Int32(Some(0)));
3710        let full_result_0 = full_index
3711            .search(&query_0, &NoOpMetricsCollector)
3712            .await
3713            .unwrap();
3714        let ranged_result_0 = ranged_index
3715            .search(&query_0, &NoOpMetricsCollector)
3716            .await
3717            .unwrap();
3718        assert_eq!(full_result_0, ranged_result_0, "Query for value 0 failed");
3719
3720        // Test 2: Query for value in middle of first batch (should be in first page)
3721        let mid_first_batch = (DEFAULT_BTREE_BATCH_SIZE / 2) as i32;
3722        let query_mid_first = SargableQuery::Equals(ScalarValue::Int32(Some(mid_first_batch)));
3723        let full_result_mid_first = full_index
3724            .search(&query_mid_first, &NoOpMetricsCollector)
3725            .await
3726            .unwrap();
3727        let ranged_result_mid_first = ranged_index
3728            .search(&query_mid_first, &NoOpMetricsCollector)
3729            .await
3730            .unwrap();
3731        assert_eq!(
3732            full_result_mid_first, ranged_result_mid_first,
3733            "Query for value {} failed",
3734            mid_first_batch
3735        );
3736
3737        // Test 3: Query for value in the last batch (should be in the second range file)
3738        let mid_last_batch = (DEFAULT_BTREE_BATCH_SIZE * 3 + (DEFAULT_BTREE_BATCH_SIZE / 2)) as i32;
3739        let query_mid_last = SargableQuery::Equals(ScalarValue::Int32(Some(mid_last_batch)));
3740        let full_result_mid_last = full_index
3741            .search(&query_mid_last, &NoOpMetricsCollector)
3742            .await
3743            .unwrap();
3744        let ranged_result_mid_last = ranged_index
3745            .search(&query_mid_last, &NoOpMetricsCollector)
3746            .await
3747            .unwrap();
3748        assert_eq!(
3749            full_result_mid_last, ranged_result_mid_last,
3750            "Query for value {} failed",
3751            mid_last_batch
3752        );
3753
3754        // Test 4: Query upper bound.
3755        let max_val = (4 * DEFAULT_BTREE_BATCH_SIZE - 1) as i32;
3756        let query_max = SargableQuery::Equals(ScalarValue::Int32(Some(max_val)));
3757        let full_result_max = full_index
3758            .search(&query_max, &NoOpMetricsCollector)
3759            .await
3760            .unwrap();
3761        let ranged_result_max = ranged_index
3762            .search(&query_max, &NoOpMetricsCollector)
3763            .await
3764            .unwrap();
3765        assert_eq!(
3766            full_result_max, ranged_result_max,
3767            "Query for maximum value {} failed",
3768            max_val
3769        );
3770
3771        // Test 5: Query first value of the second page file.
3772        let second_first_val = (DEFAULT_BTREE_BATCH_SIZE * 2 + DEFAULT_BTREE_BATCH_SIZE / 2) as i32;
3773        let query_second_first = SargableQuery::Equals(ScalarValue::Int32(Some(second_first_val)));
3774        let full_result_second_first = full_index
3775            .search(&query_second_first, &NoOpMetricsCollector)
3776            .await
3777            .unwrap();
3778        let ranged_result_second_first = ranged_index
3779            .search(&query_second_first, &NoOpMetricsCollector)
3780            .await
3781            .unwrap();
3782        assert_eq!(
3783            full_result_second_first, ranged_result_second_first,
3784            "Query for first value of the second page file {} failed",
3785            second_first_val
3786        );
3787
3788        // Test 6: Query value below the minimum
3789        let query_below_min = SargableQuery::Equals(ScalarValue::Int32(Some(-1)));
3790        let full_result_below = full_index
3791            .search(&query_below_min, &NoOpMetricsCollector)
3792            .await
3793            .unwrap();
3794        let ranged_result_below = ranged_index
3795            .search(&query_below_min, &NoOpMetricsCollector)
3796            .await
3797            .unwrap();
3798        assert_eq!(
3799            full_result_below, ranged_result_below,
3800            "Query for value below minimum (-1) failed"
3801        );
3802
3803        // Test 7: Query value above the maximum
3804        let query_above_max = SargableQuery::Equals(ScalarValue::Int32(Some(max_val + 1)));
3805        let full_result_above = full_index
3806            .search(&query_above_max, &NoOpMetricsCollector)
3807            .await
3808            .unwrap();
3809        let ranged_result_above = ranged_index
3810            .search(&query_above_max, &NoOpMetricsCollector)
3811            .await
3812            .unwrap();
3813        assert_eq!(
3814            full_result_above,
3815            ranged_result_above,
3816            "Query for value above maximum ({}) failed",
3817            max_val + 1
3818        );
3819
3820        // Range Tests
3821
3822        // Test 8: Cross-range query: One range including different values from adjacent range files.
3823        let range_start =
3824            (DEFAULT_BTREE_BATCH_SIZE * 2 + DEFAULT_BTREE_BATCH_SIZE / 2 - 100) as i32;
3825        let range_end = range_start + 200;
3826        let query_cross_range = SargableQuery::Range(
3827            std::collections::Bound::Included(ScalarValue::Int32(Some(range_start))),
3828            std::collections::Bound::Excluded(ScalarValue::Int32(Some(range_end))),
3829        );
3830        let full_result_cross = full_index
3831            .search(&query_cross_range, &NoOpMetricsCollector)
3832            .await
3833            .unwrap();
3834        let ranged_result_cross = ranged_index
3835            .search(&query_cross_range, &NoOpMetricsCollector)
3836            .await
3837            .unwrap();
3838        assert_eq!(
3839            full_result_cross, ranged_result_cross,
3840            "Cross-range range query [{}, {}] failed",
3841            range_start, range_end
3842        );
3843
3844        // Test 9 Test simple range within a single page file
3845        let single_range_start = (DEFAULT_BTREE_BATCH_SIZE * 4 - 300) as i32;
3846        let single_range_end = single_range_start + 200;
3847        let query_single_range = SargableQuery::Range(
3848            std::collections::Bound::Included(ScalarValue::Int32(Some(single_range_start))),
3849            std::collections::Bound::Excluded(ScalarValue::Int32(Some(single_range_end))),
3850        );
3851        let full_result_single = full_index
3852            .search(&query_single_range, &NoOpMetricsCollector)
3853            .await
3854            .unwrap();
3855        let ranged_result_single = ranged_index
3856            .search(&query_single_range, &NoOpMetricsCollector)
3857            .await
3858            .unwrap();
3859        assert_eq!(
3860            full_result_single, ranged_result_single,
3861            "Single range query [{}, {}] failed",
3862            single_range_start, single_range_end
3863        );
3864
3865        // Test 10: Large range query spanning almost all values
3866        let large_range_start = 100_i32;
3867        let large_range_end = (DEFAULT_BTREE_BATCH_SIZE * 4 - 100) as i32;
3868        let query_large_range = SargableQuery::Range(
3869            std::collections::Bound::Included(ScalarValue::Int32(Some(large_range_start))),
3870            std::collections::Bound::Excluded(ScalarValue::Int32(Some(large_range_end))),
3871        );
3872        let full_result_single = full_index
3873            .search(&query_large_range, &NoOpMetricsCollector)
3874            .await
3875            .unwrap();
3876        let ranged_result_single = ranged_index
3877            .search(&query_large_range, &NoOpMetricsCollector)
3878            .await
3879            .unwrap();
3880        assert_eq!(
3881            full_result_single, ranged_result_single,
3882            "Single fragment range query [{}, {}] failed",
3883            large_range_start, large_range_end
3884        );
3885
3886        let remap_dir = TempObjDir::default();
3887        let remap_store = Arc::new(LanceIndexStore::new(
3888            Arc::new(ObjectStore::local()),
3889            remap_dir.clone(),
3890            Arc::new(LanceCache::no_cache()),
3891        ));
3892
3893        // Remap with a no-op mapping.  The remapped index should be identical to the original
3894        ranged_index
3895            .remap(&HashMap::default(), remap_store.as_ref())
3896            .await
3897            .unwrap();
3898
3899        let remap_index = BTreeIndex::load(remap_store.clone(), None, &LanceCache::no_cache())
3900            .await
3901            .unwrap();
3902
3903        assert_eq!(remap_index.page_lookup, ranged_index.page_lookup);
3904
3905        let ranged_pages = range_store
3906            .open_index_file(part_page_data_file_path(1 << 32).as_str())
3907            .await
3908            .unwrap();
3909        let remapped_pages = remap_store
3910            .open_index_file(part_page_data_file_path(1 << 32).as_str())
3911            .await
3912            .unwrap();
3913
3914        assert_eq!(ranged_pages.num_rows(), remapped_pages.num_rows());
3915
3916        let original_data = ranged_pages
3917            .read_record_batch(0, ranged_pages.num_rows() as u64)
3918            .await
3919            .unwrap();
3920        let remapped_data = remapped_pages
3921            .read_record_batch(0, remapped_pages.num_rows() as u64)
3922            .await
3923            .unwrap();
3924
3925        assert_eq!(original_data, remapped_data);
3926    }
3927
3928    #[tokio::test]
3929    async fn test_update_ranged_index() {
3930        // Setup stores for both indexes
3931        let old_tmpdir = TempObjDir::default();
3932        let old_store = Arc::new(LanceIndexStore::new(
3933            Arc::new(ObjectStore::local()),
3934            old_tmpdir.clone(),
3935            Arc::new(LanceCache::no_cache()),
3936        ));
3937
3938        let new_tmpdir = TempObjDir::default();
3939        let new_store = Arc::new(LanceIndexStore::new(
3940            Arc::new(ObjectStore::local()),
3941            new_tmpdir.clone(),
3942            Arc::new(LanceCache::no_cache()),
3943        ));
3944
3945        // Create range 1 index, intentionally make it not divisible by DEFAULT_BTREE_BATCH_SIZE
3946        let range1_gen = gen_batch()
3947            .col("value", array::step::<Int32Type>())
3948            .col("_rowid", array::step::<UInt64Type>())
3949            .into_df_stream(
3950                RowCount::from(DEFAULT_BTREE_BATCH_SIZE / 2),
3951                BatchCount::from(5),
3952            );
3953        let range1_data_source = Box::pin(RecordBatchStreamAdapter::new(
3954            range1_gen.schema(),
3955            range1_gen,
3956        ));
3957
3958        train_btree_index(
3959            range1_data_source,
3960            old_store.as_ref(),
3961            DEFAULT_BTREE_BATCH_SIZE,
3962            None,
3963            Option::from(1u32),
3964        )
3965        .await
3966        .unwrap();
3967
3968        // Create range 2 index, also intentionally make it not divisible by DEFAULT_BTREE_BATCH_SIZE
3969        let start_val = (DEFAULT_BTREE_BATCH_SIZE * 2 + DEFAULT_BTREE_BATCH_SIZE / 2) as i32;
3970        let end_val = (4 * DEFAULT_BTREE_BATCH_SIZE) as i32;
3971        let values_second_half: Vec<i32> = (start_val..end_val).collect();
3972        let row_ids_second_half: Vec<u64> = (start_val as u64..end_val as u64).collect();
3973        let range2_gen = gen_batch()
3974            .col("value", array::cycle::<Int32Type>(values_second_half))
3975            .col("_rowid", array::cycle::<UInt64Type>(row_ids_second_half))
3976            .into_df_stream(
3977                RowCount::from(DEFAULT_BTREE_BATCH_SIZE / 2),
3978                BatchCount::from(3),
3979            );
3980        let range2_data_source = Box::pin(RecordBatchStreamAdapter::new(
3981            range2_gen.schema(),
3982            range2_gen,
3983        ));
3984
3985        train_btree_index(
3986            range2_data_source,
3987            old_store.as_ref(),
3988            DEFAULT_BTREE_BATCH_SIZE,
3989            None,
3990            Option::from(2u32),
3991        )
3992        .await
3993        .unwrap();
3994
3995        // Merge the fragment files
3996        let part_page_files = vec![
3997            part_page_data_file_path(1 << 32),
3998            part_page_data_file_path(2 << 32),
3999        ];
4000
4001        let part_lookup_files = vec![
4002            part_lookup_file_path(1 << 32),
4003            part_lookup_file_path(2 << 32),
4004        ];
4005
4006        super::merge_metadata_files(
4007            old_store.as_ref(),
4008            &part_page_files,
4009            &part_lookup_files,
4010            Option::from(1usize),
4011        )
4012        .await
4013        .unwrap();
4014
4015        // create some update data
4016        let start_val = (DEFAULT_BTREE_BATCH_SIZE * 2) as i32;
4017        let end_val = (DEFAULT_BTREE_BATCH_SIZE * 3) as i32;
4018        let row_id_delta = (DEFAULT_BTREE_BATCH_SIZE * 3) as i32;
4019        let values: Vec<i32> = (start_val..end_val).collect();
4020        let row_ids: Vec<u64> =
4021            ((start_val + row_id_delta) as u64..(end_val + row_id_delta) as u64).collect();
4022        let update_data = gen_batch()
4023            .col("value", array::cycle::<Int32Type>(values))
4024            .col("_rowid", array::cycle::<UInt64Type>(row_ids))
4025            .into_df_stream(
4026                RowCount::from(DEFAULT_BTREE_BATCH_SIZE / 2),
4027                BatchCount::from(2),
4028            );
4029        let update_data_source = Box::pin(RecordBatchStreamAdapter::new(
4030            update_data.schema(),
4031            update_data,
4032        ));
4033
4034        let ranged_index = BTreeIndex::load(old_store.clone(), None, &LanceCache::no_cache())
4035            .await
4036            .unwrap();
4037
4038        // update the ranged index
4039        ranged_index
4040            .update(update_data_source, new_store.as_ref(), None)
4041            .await
4042            .expect("Error in updating ranged index");
4043
4044        let updated_index = BTreeIndex::load(new_store.clone(), None, &LanceCache::no_cache())
4045            .await
4046            .unwrap();
4047
4048        assert!(
4049            updated_index.ranges_to_files.is_none(),
4050            "Updated ranged-btree-index should fall back to non-ranged"
4051        );
4052
4053        let updated_value = (DEFAULT_BTREE_BATCH_SIZE * 2 + (DEFAULT_BTREE_BATCH_SIZE / 2)) as i32;
4054        let updated_query = SargableQuery::Equals(ScalarValue::Int32(Some(updated_value)));
4055
4056        let query_result = updated_index
4057            .search(&updated_query, &NoOpMetricsCollector)
4058            .await
4059            .unwrap();
4060        match query_result {
4061            SearchResult::Exact(row_id_map) => {
4062                assert!(
4063                    row_id_map.selected(updated_value as u64),
4064                    "Updated index should contain original rowids."
4065                );
4066                assert!(
4067                    row_id_map.selected((updated_value + row_id_delta) as u64),
4068                    "Updated index should contain new rowids"
4069                );
4070            }
4071            _ => {
4072                panic!("Btree search result should always be Exact.");
4073            }
4074        }
4075    }
4076
4077    /// Rust equivalent of Python test `test_btree_remap_big_deletions`
4078    ///
4079    /// This test verifies that btree index remapping works correctly when a large
4080    /// portion of the data is deleted. The Python test:
4081    /// 1. Writes 15K rows in 3 fragments (values 0-14999)
4082    /// 2. Creates a btree index (will have multiple pages)
4083    /// 3. Deletes rows where a > 1000 AND a < 10000 (deletes values 1001-9999)
4084    /// 4. Runs compaction (materializes deletions via remap)
4085    /// 5. Verifies the index still works for remaining values
4086    #[tokio::test]
4087    async fn test_btree_remap_big_deletions() {
4088        let tmpdir = TempObjDir::default();
4089        let test_store = Arc::new(LanceIndexStore::new(
4090            Arc::new(ObjectStore::local()),
4091            tmpdir.clone(),
4092            Arc::new(LanceCache::no_cache()),
4093        ));
4094
4095        // Generate 15000 rows with values 0-14999 and row_ids 0-14999
4096        // Using a smaller batch size to ensure we get multiple pages
4097        let batch_size = 4096;
4098        let total_rows = 15000;
4099
4100        let stream = gen_batch()
4101            .col("value", array::step::<Int32Type>())
4102            .col("_rowid", array::step::<UInt64Type>())
4103            .into_df_stream(RowCount::from(total_rows), BatchCount::from(1));
4104
4105        train_btree_index(stream, test_store.as_ref(), batch_size, None, None)
4106            .await
4107            .unwrap();
4108
4109        let index = BTreeIndex::load(test_store.clone(), None, &LanceCache::no_cache())
4110            .await
4111            .unwrap();
4112
4113        // Create a mapping that simulates deleting rows where value > 1000 AND value < 10000
4114        // Since values match row_ids in our test data:
4115        // - Rows 0-1000 (values 0-1000) are kept with same row_ids
4116        // - Rows 1001-9999 (values 1001-9999) are deleted (mapped to None)
4117        // - Rows 10000-14999 (values 10000-14999) are remapped to new row_ids 1001-5999
4118        let mut mapping: HashMap<u64, Option<u64>> = HashMap::new();
4119
4120        // Mark deleted rows (values 1001-9999)
4121        for old_id in 1001..10000 {
4122            mapping.insert(old_id, None);
4123        }
4124
4125        let mut new_id_counter = 100_000;
4126
4127        // Remap all other rows
4128        for old_id in (0..1000).chain(10000..15000) {
4129            let new_id = new_id_counter;
4130            new_id_counter += 1;
4131            mapping.insert(old_id, Some(new_id));
4132        }
4133
4134        let remap_dir = TempObjDir::default();
4135        let remap_store = Arc::new(LanceIndexStore::new(
4136            Arc::new(ObjectStore::local()),
4137            remap_dir.clone(),
4138            Arc::new(LanceCache::no_cache()),
4139        ));
4140
4141        // Remap the index with our deletion mapping
4142        index.remap(&mapping, remap_store.as_ref()).await.unwrap();
4143
4144        let remapped_index = BTreeIndex::load(remap_store.clone(), None, &LanceCache::no_cache())
4145            .await
4146            .unwrap();
4147
4148        // Verify values that should exist (values 0-1000 and 10000-14999)
4149        // These correspond to: original values 0-1000 at row_ids 0-1000
4150        // and original values 10000-14999 at new row_ids 1001-5999
4151        let should_exist = vec![0, 500, 1000, 10000, 13000, 14000, 14999];
4152        for value in should_exist {
4153            let query = SargableQuery::Equals(ScalarValue::Int32(Some(value)));
4154            let result = remapped_index
4155                .search(&query, &NoOpMetricsCollector)
4156                .await
4157                .unwrap();
4158            match result {
4159                SearchResult::Exact(row_id_map) => {
4160                    assert!(
4161                        !row_id_map.is_empty(),
4162                        "Value {} should exist in remapped index but was not found",
4163                        value
4164                    );
4165                }
4166                _ => {
4167                    panic!("Btree search result should always be Exact.");
4168                }
4169            }
4170        }
4171
4172        // Verify values that should NOT exist (values 1001-9999 were deleted)
4173        let should_not_exist = vec![1001, 5000, 8000, 9999];
4174        for value in should_not_exist {
4175            let query = SargableQuery::Equals(ScalarValue::Int32(Some(value)));
4176            let result = remapped_index
4177                .search(&query, &NoOpMetricsCollector)
4178                .await
4179                .unwrap();
4180            match result {
4181                SearchResult::Exact(row_id_map) => {
4182                    assert!(
4183                        row_id_map.is_empty(),
4184                        "Value {} should NOT exist in remapped index but was found",
4185                        value
4186                    );
4187                }
4188                _ => {
4189                    panic!("Btree search result should always be Exact.");
4190                }
4191            }
4192        }
4193    }
4194
4195    /// Regression test: BTree search must track null row IDs for non-IsNull
4196    /// queries, even when no pages match the queried value.
4197    ///
4198    /// Without this, `NOT(x = val)` when `val` is absent from the data would
4199    /// produce an empty null set, causing NULL rows to incorrectly pass.
4200    #[tokio::test]
4201    async fn test_search_tracks_nulls_for_absent_value() {
4202        use arrow_array::{Int32Array, UInt64Array};
4203
4204        let tmpdir = TempObjDir::default();
4205        let test_store = Arc::new(LanceIndexStore::new(
4206            Arc::new(ObjectStore::local()),
4207            tmpdir.clone(),
4208            Arc::new(LanceCache::no_cache()),
4209        ));
4210
4211        // Create data with 80% nulls so that training produces separate
4212        // all-null pages (which are not in the BTree map). Non-null values
4213        // are all in [100, 5099], so value 0 never appears.
4214        let num_rows = 5000u64;
4215        let values: Int32Array = (0..num_rows)
4216            .map(|i| {
4217                if i % 5 != 0 {
4218                    None // 80% null
4219                } else {
4220                    Some(100 + i as i32) // non-null values in [100, 5099]
4221                }
4222            })
4223            .collect();
4224        let row_ids = UInt64Array::from_iter_values(0..num_rows);
4225        let data = arrow_array::RecordBatch::try_from_iter(vec![
4226            ("value", Arc::new(values) as arrow_array::ArrayRef),
4227            ("_rowid", Arc::new(row_ids) as arrow_array::ArrayRef),
4228        ])
4229        .unwrap();
4230
4231        let schema = data.schema();
4232        let stream: SendableRecordBatchStream = Box::pin(RecordBatchStreamAdapter::new(
4233            schema,
4234            stream::iter(vec![Ok(data)]),
4235        ));
4236        train_btree_index(stream, test_store.as_ref(), num_rows, None, None)
4237            .await
4238            .unwrap();
4239
4240        let index = BTreeIndex::load(test_store.clone(), None, &LanceCache::no_cache())
4241            .await
4242            .unwrap();
4243
4244        // Verify we have all-null pages (the bug depends on this)
4245        assert!(
4246            !index.page_lookup.all_null_pages.is_empty(),
4247            "Test setup requires all-null pages; got null_pages={}, all_null_pages={}",
4248            index.page_lookup.null_pages.len(),
4249            index.page_lookup.all_null_pages.len(),
4250        );
4251
4252        let metrics = NoOpMetricsCollector;
4253
4254        // Search for Equals(0) — value 0 doesn't exist in any page
4255        let result = index
4256            .search(
4257                &SargableQuery::Equals(ScalarValue::Int32(Some(0))),
4258                &metrics,
4259            )
4260            .await
4261            .unwrap();
4262
4263        match result {
4264            SearchResult::Exact(set) => {
4265                // No rows should be TRUE (value 0 doesn't exist)
4266                assert!(set.true_rows().is_empty(), "No rows should match Equals(0)");
4267                // NULL rows MUST be tracked as null
4268                assert!(
4269                    !set.null_rows().is_empty(),
4270                    "Null rows must be tracked even when no pages match the value"
4271                );
4272            }
4273            _ => panic!("BTree search should return Exact"),
4274        }
4275
4276        // Also verify Range query tracks nulls when no values match
4277        let result = index
4278            .search(
4279                &SargableQuery::Range(
4280                    std::ops::Bound::Unbounded,
4281                    std::ops::Bound::Excluded(ScalarValue::Int32(Some(50))),
4282                ),
4283                &metrics,
4284            )
4285            .await
4286            .unwrap();
4287
4288        match result {
4289            SearchResult::Exact(set) => {
4290                assert!(set.true_rows().is_empty(), "No rows should be < 50");
4291                assert!(
4292                    !set.null_rows().is_empty(),
4293                    "Null rows must be tracked for range queries too"
4294                );
4295            }
4296            _ => panic!("BTree search should return Exact"),
4297        }
4298    }
4299}