1use lance_core::utils::row_addr_remap::RowAddrRemap;
5use std::{
6 any::Any,
7 cmp::Ordering,
8 collections::{HashMap, HashSet},
9 fmt::{Debug, Display},
10 ops::Bound,
11 sync::Arc,
12};
13
14use super::{
15 AnyQuery, BuiltinIndexType, IndexFile, IndexReader, IndexStore, IndexWriter, MetricsCollector,
16 OldIndexDataFilter, SargableQuery, ScalarIndex, ScalarIndexParams, SearchResult,
17 compute_next_prefix,
18};
19use crate::cache_pb::{BTreeIndexHeader, RangeToFile};
20use crate::{Index, IndexType};
21use crate::{metrics::NoOpMetricsCollector, scalar::registry::TrainingCriteria};
22use crate::{pbold, scalar::btree::flat::FlatIndex};
23use crate::{
24 progress::{IndexBuildProgress, noop_progress},
25 scalar::{
26 CreatedIndex, RowIdRemapper, UpdateCriteria,
27 expression::{SargableQueryParser, ScalarQueryParser},
28 registry::{
29 BasicTrainer, ScalarIndexLoad, ScalarIndexPlugin, TrainingOrdering, TrainingRequest,
30 VALUE_COLUMN_NAME, single_flight_open,
31 },
32 },
33};
34use arrow_arith::numeric::add;
35use arrow_array::{
36 Array, ArrayAccessor, ArrowNativeTypeOp, PrimitiveArray, RecordBatch, UInt32Array,
37 cast::AsArray,
38 new_empty_array,
39 types::{
40 ArrowPrimitiveType, Decimal128Type, Decimal256Type, Float16Type, Float32Type, Float64Type,
41 Int8Type, Int16Type, Int32Type, Int64Type, UInt8Type, UInt16Type, UInt32Type, UInt64Type,
42 },
43};
44use arrow_ord::ord::make_comparator;
45use arrow_schema::{DataType, Field, IntervalUnit, Schema, SortOptions};
46use async_trait::async_trait;
47use datafusion::physical_plan::{
48 ExecutionPlan, SendableRecordBatchStream,
49 sorts::sort_preserving_merge::SortPreservingMergeExec, stream::RecordBatchStreamAdapter,
50 union::UnionExec,
51};
52use datafusion_common::{DFSchema, DataFusionError, ScalarValue};
53use datafusion_expr::execution_props::ExecutionProps;
54use datafusion_physical_expr::{
55 PhysicalExpr, PhysicalSortExpr, create_physical_expr, expressions::Column,
56};
57use futures::{
58 FutureExt, Stream, StreamExt, TryFutureExt, TryStreamExt,
59 future::BoxFuture,
60 stream::{self},
61};
62use lance_core::deepsize::DeepSizeOf;
63use lance_core::{
64 Error, ROW_ID, Result,
65 cache::{
66 CacheCodec, CacheCodecImpl, CacheEntryReader, CacheEntryWriter, CacheKey, LanceCache,
67 WeakLanceCache,
68 },
69 error::LanceOptionExt,
70 utils::{
71 tokio::get_num_compute_intensive_cpus,
72 tracing::{IO_TYPE_LOAD_SCALAR_PART, TRACE_IO_EVENTS},
73 },
74};
75use lance_datafusion::{
76 chunker::chunk_concat_stream,
77 exec::{LanceExecutionOptions, OneShotExec, execute_plan},
78};
79use lance_select::{NullableRowAddrSet, RowSetOps};
80use log::{debug, warn};
81use object_store::Error as ObjectStoreError;
82use rangemap::RangeInclusiveMap;
83use roaring::RoaringBitmap;
84use serde::{Deserialize, Serialize, Serializer};
85use tracing::{info, instrument};
86
87mod flat;
88
89pub const BTREE_LOOKUP_NAME: &str = "page_lookup.lance";
90const BTREE_PAGES_NAME: &str = "page_data.lance";
91pub const DEFAULT_BTREE_BATCH_SIZE: u64 = 4096;
92const BATCH_SIZE_META_KEY: &str = "batch_size";
93const DEFAULT_RANGE_PARTITIONED: bool = false;
94const RANGE_PARTITIONED_META_KEY: &str = "range_partitioned";
95const PAGE_NUM_PER_RANGE_PARTITION_META_KEY: &str = "page_num_per_range_partition";
96const BTREE_INDEX_VERSION: u32 = 0;
97pub(crate) const BTREE_VALUES_COLUMN: &str = "values";
98pub(crate) const BTREE_IDS_COLUMN: &str = "ids";
99
100#[derive(Clone, Debug)]
102pub struct OrderableScalarValue(pub ScalarValue);
103
104impl DeepSizeOf for OrderableScalarValue {
105 fn deep_size_of_children(&self, _context: &mut lance_core::deepsize::Context) -> usize {
106 self.0.size() - std::mem::size_of::<ScalarValue>()
108 }
109}
110
111impl Display for OrderableScalarValue {
112 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
113 std::fmt::Display::fmt(&self.0, f)
114 }
115}
116
117impl PartialEq for OrderableScalarValue {
118 fn eq(&self, other: &Self) -> bool {
119 self.0.eq(&other.0)
120 }
121}
122
123impl Eq for OrderableScalarValue {}
124
125impl PartialOrd for OrderableScalarValue {
126 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
127 Some(self.cmp(other))
128 }
129}
130
131impl Ord for OrderableScalarValue {
137 fn cmp(&self, other: &Self) -> Ordering {
138 use ScalarValue::*;
139 match (&self.0, &other.0) {
143 (Decimal32(v1, p1, s1), Decimal32(v2, p2, s2)) => {
144 if p1.eq(p2) && s1.eq(s2) {
145 v1.cmp(v2)
146 } else {
147 panic!("Attempt to compare decimals with unequal precision / scale")
149 }
150 }
151 (Decimal32(v1, _, _), Null) => {
152 if v1.is_none() {
153 Ordering::Equal
154 } else {
155 Ordering::Greater
156 }
157 }
158 (Decimal32(_, _, _), _) => panic!("Attempt to compare decimal with non-decimal"),
159 (Decimal64(v1, p1, s1), Decimal64(v2, p2, s2)) => {
160 if p1.eq(p2) && s1.eq(s2) {
161 v1.cmp(v2)
162 } else {
163 panic!("Attempt to compare decimals with unequal precision / scale")
165 }
166 }
167 (Decimal64(v1, _, _), Null) => {
168 if v1.is_none() {
169 Ordering::Equal
170 } else {
171 Ordering::Greater
172 }
173 }
174 (Decimal64(_, _, _), _) => panic!("Attempt to compare decimal with non-decimal"),
175 (Decimal128(v1, p1, s1), Decimal128(v2, p2, s2)) => {
176 if p1.eq(p2) && s1.eq(s2) {
177 v1.cmp(v2)
178 } else {
179 panic!("Attempt to compare decimals with unequal precision / scale")
181 }
182 }
183 (Decimal128(v1, _, _), Null) => {
184 if v1.is_none() {
185 Ordering::Equal
186 } else {
187 Ordering::Greater
188 }
189 }
190 (Decimal128(_, _, _), _) => panic!("Attempt to compare decimal with non-decimal"),
191 (Decimal256(v1, p1, s1), Decimal256(v2, p2, s2)) => {
192 if p1.eq(p2) && s1.eq(s2) {
193 v1.cmp(v2)
194 } else {
195 panic!("Attempt to compare decimals with unequal precision / scale")
197 }
198 }
199 (Decimal256(v1, _, _), Null) => {
200 if v1.is_none() {
201 Ordering::Equal
202 } else {
203 Ordering::Greater
204 }
205 }
206 (Decimal256(_, _, _), _) => panic!("Attempt to compare decimal with non-decimal"),
207
208 (Boolean(v1), Boolean(v2)) => v1.cmp(v2),
209 (Boolean(v1), Null) => {
210 if v1.is_none() {
211 Ordering::Equal
212 } else {
213 Ordering::Greater
214 }
215 }
216 (Boolean(_), _) => panic!("Attempt to compare boolean with non-boolean"),
217 (Float32(v1), Float32(v2)) => match (v1, v2) {
218 (Some(f1), Some(f2)) => f1.total_cmp(f2),
219 (None, Some(_)) => Ordering::Less,
220 (Some(_), None) => Ordering::Greater,
221 (None, None) => Ordering::Equal,
222 },
223 (Float32(v1), Null) => {
224 if v1.is_none() {
225 Ordering::Equal
226 } else {
227 Ordering::Greater
228 }
229 }
230 (Float32(_), _) => panic!("Attempt to compare f32 with non-f32"),
231 (Float64(v1), Float64(v2)) => match (v1, v2) {
232 (Some(f1), Some(f2)) => f1.total_cmp(f2),
233 (None, Some(_)) => Ordering::Less,
234 (Some(_), None) => Ordering::Greater,
235 (None, None) => Ordering::Equal,
236 },
237 (Float64(v1), Null) => {
238 if v1.is_none() {
239 Ordering::Equal
240 } else {
241 Ordering::Greater
242 }
243 }
244 (Float64(_), _) => panic!("Attempt to compare f64 with non-f64"),
245 (Float16(v1), Float16(v2)) => match (v1, v2) {
246 (Some(f1), Some(f2)) => f1.total_cmp(f2),
247 (None, Some(_)) => Ordering::Less,
248 (Some(_), None) => Ordering::Greater,
249 (None, None) => Ordering::Equal,
250 },
251 (Float16(v1), Null) => {
252 if v1.is_none() {
253 Ordering::Equal
254 } else {
255 Ordering::Greater
256 }
257 }
258 (Float16(_), _) => panic!("Attempt to compare f16 with non-f16"),
259 (Int8(v1), Int8(v2)) => v1.cmp(v2),
260 (Int8(v1), Null) => {
261 if v1.is_none() {
262 Ordering::Equal
263 } else {
264 Ordering::Greater
265 }
266 }
267 (Int8(_), _) => panic!("Attempt to compare Int8 with non-Int8"),
268 (Int16(v1), Int16(v2)) => v1.cmp(v2),
269 (Int16(v1), Null) => {
270 if v1.is_none() {
271 Ordering::Equal
272 } else {
273 Ordering::Greater
274 }
275 }
276 (Int16(_), _) => panic!("Attempt to compare Int16 with non-Int16"),
277 (Int32(v1), Int32(v2)) => v1.cmp(v2),
278 (Int32(v1), Null) => {
279 if v1.is_none() {
280 Ordering::Equal
281 } else {
282 Ordering::Greater
283 }
284 }
285 (Int32(_), _) => panic!("Attempt to compare Int32 with non-Int32"),
286 (Int64(v1), Int64(v2)) => v1.cmp(v2),
287 (Int64(v1), Null) => {
288 if v1.is_none() {
289 Ordering::Equal
290 } else {
291 Ordering::Greater
292 }
293 }
294 (Int64(_), _) => panic!("Attempt to compare Int64 with non-Int64"),
295 (UInt8(v1), UInt8(v2)) => v1.cmp(v2),
296 (UInt8(v1), Null) => {
297 if v1.is_none() {
298 Ordering::Equal
299 } else {
300 Ordering::Greater
301 }
302 }
303 (UInt8(_), _) => panic!("Attempt to compare UInt8 with non-UInt8"),
304 (UInt16(v1), UInt16(v2)) => v1.cmp(v2),
305 (UInt16(v1), Null) => {
306 if v1.is_none() {
307 Ordering::Equal
308 } else {
309 Ordering::Greater
310 }
311 }
312 (UInt16(_), _) => panic!("Attempt to compare UInt16 with non-UInt16"),
313 (UInt32(v1), UInt32(v2)) => v1.cmp(v2),
314 (UInt32(v1), Null) => {
315 if v1.is_none() {
316 Ordering::Equal
317 } else {
318 Ordering::Greater
319 }
320 }
321 (UInt32(_), _) => panic!("Attempt to compare UInt32 with non-UInt32"),
322 (UInt64(v1), UInt64(v2)) => v1.cmp(v2),
323 (UInt64(v1), Null) => {
324 if v1.is_none() {
325 Ordering::Equal
326 } else {
327 Ordering::Greater
328 }
329 }
330 (UInt64(_), _) => panic!("Attempt to compare UInt64 with non-UInt64"),
331 (Utf8(v1) | Utf8View(v1) | LargeUtf8(v1), Utf8(v2) | Utf8View(v2) | LargeUtf8(v2)) => {
332 v1.cmp(v2)
333 }
334 (Utf8(v1) | Utf8View(v1) | LargeUtf8(v1), Null) => {
335 if v1.is_none() {
336 Ordering::Equal
337 } else {
338 Ordering::Greater
339 }
340 }
341 (Utf8(_) | Utf8View(_) | LargeUtf8(_), _) => {
342 panic!("Attempt to compare Utf8 with non-Utf8")
343 }
344 (
345 Binary(v1) | LargeBinary(v1) | BinaryView(v1),
346 Binary(v2) | LargeBinary(v2) | BinaryView(v2),
347 ) => v1.cmp(v2),
348 (Binary(v1) | LargeBinary(v1) | BinaryView(v1), Null) => {
349 if v1.is_none() {
350 Ordering::Equal
351 } else {
352 Ordering::Greater
353 }
354 }
355 (Binary(_) | LargeBinary(_) | BinaryView(_), _) => {
356 panic!("Attempt to compare Binary with non-Binary")
357 }
358 (FixedSizeBinary(_, v1), FixedSizeBinary(_, v2)) => v1.cmp(v2),
359 (FixedSizeBinary(_, v1), Null) => {
360 if v1.is_none() {
361 Ordering::Equal
362 } else {
363 Ordering::Greater
364 }
365 }
366 (FixedSizeBinary(_, _), _) => {
367 panic!("Attempt to compare FixedSizeBinary with non-FixedSizeBinary")
368 }
369 (FixedSizeList(left), FixedSizeList(right)) => {
370 if left.eq(right) {
371 todo!()
372 } else {
373 panic!(
374 "Attempt to compare fixed size list elements with different widths/fields"
375 )
376 }
377 }
378 (FixedSizeList(left), Null) => {
379 if left.is_null(0) {
380 Ordering::Equal
381 } else {
382 Ordering::Greater
383 }
384 }
385 (FixedSizeList(_), _) => {
386 panic!("Attempt to compare FixedSizeList with non-FixedSizeList")
387 }
388 (List(_), List(_)) => todo!(),
389 (List(left), Null) => {
390 if left.is_null(0) {
391 Ordering::Equal
392 } else {
393 Ordering::Greater
394 }
395 }
396 (List(_), _) => {
397 panic!("Attempt to compare List with non-List")
398 }
399 (LargeList(_), _) => todo!(),
400 (ListView(_), _) => todo!(),
401 (LargeListView(_), _) => todo!(),
402 (Map(_), Map(_)) => todo!(),
403 (Map(left), Null) => {
404 if left.is_null(0) {
405 Ordering::Equal
406 } else {
407 Ordering::Greater
408 }
409 }
410 (Map(_), _) => {
411 panic!("Attempt to compare Map with non-Map")
412 }
413 (Date32(v1), Date32(v2)) => v1.cmp(v2),
414 (Date32(v1), Null) => {
415 if v1.is_none() {
416 Ordering::Equal
417 } else {
418 Ordering::Greater
419 }
420 }
421 (Date32(_), _) => panic!("Attempt to compare Date32 with non-Date32"),
422 (Date64(v1), Date64(v2)) => v1.cmp(v2),
423 (Date64(v1), Null) => {
424 if v1.is_none() {
425 Ordering::Equal
426 } else {
427 Ordering::Greater
428 }
429 }
430 (Date64(_), _) => panic!("Attempt to compare Date64 with non-Date64"),
431 (Time32Second(v1), Time32Second(v2)) => v1.cmp(v2),
432 (Time32Second(v1), Null) => {
433 if v1.is_none() {
434 Ordering::Equal
435 } else {
436 Ordering::Greater
437 }
438 }
439 (Time32Second(_), _) => panic!("Attempt to compare Time32Second with non-Time32Second"),
440 (Time32Millisecond(v1), Time32Millisecond(v2)) => v1.cmp(v2),
441 (Time32Millisecond(v1), Null) => {
442 if v1.is_none() {
443 Ordering::Equal
444 } else {
445 Ordering::Greater
446 }
447 }
448 (Time32Millisecond(_), _) => {
449 panic!("Attempt to compare Time32Millisecond with non-Time32Millisecond")
450 }
451 (Time64Microsecond(v1), Time64Microsecond(v2)) => v1.cmp(v2),
452 (Time64Microsecond(v1), Null) => {
453 if v1.is_none() {
454 Ordering::Equal
455 } else {
456 Ordering::Greater
457 }
458 }
459 (Time64Microsecond(_), _) => {
460 panic!("Attempt to compare Time64Microsecond with non-Time64Microsecond")
461 }
462 (Time64Nanosecond(v1), Time64Nanosecond(v2)) => v1.cmp(v2),
463 (Time64Nanosecond(v1), Null) => {
464 if v1.is_none() {
465 Ordering::Equal
466 } else {
467 Ordering::Greater
468 }
469 }
470 (Time64Nanosecond(_), _) => {
471 panic!("Attempt to compare Time64Nanosecond with non-Time64Nanosecond")
472 }
473 (TimestampSecond(v1, _), TimestampSecond(v2, _)) => v1.cmp(v2),
474 (TimestampSecond(v1, _), Null) => {
475 if v1.is_none() {
476 Ordering::Equal
477 } else {
478 Ordering::Greater
479 }
480 }
481 (TimestampSecond(_, _), _) => {
482 panic!("Attempt to compare TimestampSecond with non-TimestampSecond")
483 }
484 (TimestampMillisecond(v1, _), TimestampMillisecond(v2, _)) => v1.cmp(v2),
485 (TimestampMillisecond(v1, _), Null) => {
486 if v1.is_none() {
487 Ordering::Equal
488 } else {
489 Ordering::Greater
490 }
491 }
492 (TimestampMillisecond(_, _), _) => {
493 panic!("Attempt to compare TimestampMillisecond with non-TimestampMillisecond")
494 }
495 (TimestampMicrosecond(v1, _), TimestampMicrosecond(v2, _)) => v1.cmp(v2),
496 (TimestampMicrosecond(v1, _), Null) => {
497 if v1.is_none() {
498 Ordering::Equal
499 } else {
500 Ordering::Greater
501 }
502 }
503 (TimestampMicrosecond(_, _), _) => {
504 panic!("Attempt to compare TimestampMicrosecond with non-TimestampMicrosecond")
505 }
506 (TimestampNanosecond(v1, _), TimestampNanosecond(v2, _)) => v1.cmp(v2),
507 (TimestampNanosecond(v1, _), Null) => {
508 if v1.is_none() {
509 Ordering::Equal
510 } else {
511 Ordering::Greater
512 }
513 }
514 (TimestampNanosecond(_, _), _) => {
515 panic!("Attempt to compare TimestampNanosecond with non-TimestampNanosecond")
516 }
517 (IntervalYearMonth(v1), IntervalYearMonth(v2)) => v1.cmp(v2),
518 (IntervalYearMonth(v1), Null) => {
519 if v1.is_none() {
520 Ordering::Equal
521 } else {
522 Ordering::Greater
523 }
524 }
525 (IntervalYearMonth(_), _) => {
526 panic!("Attempt to compare IntervalYearMonth with non-IntervalYearMonth")
527 }
528 (IntervalDayTime(v1), IntervalDayTime(v2)) => v1.cmp(v2),
529 (IntervalDayTime(v1), Null) => {
530 if v1.is_none() {
531 Ordering::Equal
532 } else {
533 Ordering::Greater
534 }
535 }
536 (IntervalDayTime(_), _) => {
537 panic!("Attempt to compare IntervalDayTime with non-IntervalDayTime")
538 }
539 (IntervalMonthDayNano(v1), IntervalMonthDayNano(v2)) => v1.cmp(v2),
540 (IntervalMonthDayNano(v1), Null) => {
541 if v1.is_none() {
542 Ordering::Equal
543 } else {
544 Ordering::Greater
545 }
546 }
547 (IntervalMonthDayNano(_), _) => {
548 panic!("Attempt to compare IntervalMonthDayNano with non-IntervalMonthDayNano")
549 }
550 (DurationSecond(v1), DurationSecond(v2)) => v1.cmp(v2),
551 (DurationSecond(v1), Null) => {
552 if v1.is_none() {
553 Ordering::Equal
554 } else {
555 Ordering::Greater
556 }
557 }
558 (DurationSecond(_), _) => {
559 panic!("Attempt to compare DurationSecond with non-DurationSecond")
560 }
561 (DurationMillisecond(v1), DurationMillisecond(v2)) => v1.cmp(v2),
562 (DurationMillisecond(v1), Null) => {
563 if v1.is_none() {
564 Ordering::Equal
565 } else {
566 Ordering::Greater
567 }
568 }
569 (DurationMillisecond(_), _) => {
570 panic!("Attempt to compare DurationMillisecond with non-DurationMillisecond")
571 }
572 (DurationMicrosecond(v1), DurationMicrosecond(v2)) => v1.cmp(v2),
573 (DurationMicrosecond(v1), Null) => {
574 if v1.is_none() {
575 Ordering::Equal
576 } else {
577 Ordering::Greater
578 }
579 }
580 (DurationMicrosecond(_), _) => {
581 panic!("Attempt to compare DurationMicrosecond with non-DurationMicrosecond")
582 }
583 (DurationNanosecond(v1), DurationNanosecond(v2)) => v1.cmp(v2),
584 (DurationNanosecond(v1), Null) => {
585 if v1.is_none() {
586 Ordering::Equal
587 } else {
588 Ordering::Greater
589 }
590 }
591 (DurationNanosecond(_), _) => {
592 panic!("Attempt to compare DurationNanosecond with non-DurationNanosecond")
593 }
594 (Struct(_arr), Struct(_arr2)) => todo!(),
595 (Struct(arr), Null) => {
596 if arr.is_empty() {
597 Ordering::Equal
598 } else {
599 Ordering::Greater
600 }
601 }
602 (Struct(_arr), _) => panic!("Attempt to compare Struct with non-Struct"),
603 (Dictionary(_k1, v1), Dictionary(_k2, v2)) => Self(*v1.clone()).cmp(&Self(*v2.clone())),
604 (Dictionary(_, v1), Null) => Self(*v1.clone()).cmp(&Self(ScalarValue::Null)),
605 (Dictionary(_, _), _) => panic!("Attempt to compare Dictionary with non-Dictionary"),
606 (Union(_, _, _), _) => todo!("Support for union scalars"),
608 (RunEndEncoded(_, _, _), _) => {
609 todo!("Support for run-end encoded scalars")
610 }
611 (Null, Null) => Ordering::Equal,
612 (Null, _) => todo!(),
613 }
614 }
615}
616
617fn partition_point(lo: usize, hi: usize, mut pred: impl FnMut(usize) -> bool) -> usize {
622 let mut lo = lo;
623 let mut hi = hi;
624 while lo < hi {
625 let mid = lo + (hi - lo) / 2;
626 if pred(mid) {
627 lo = mid + 1;
628 } else {
629 hi = mid;
630 }
631 }
632 lo
633}
634
635fn accessor_cmp<'a, T, L, R>(left: L, right: R) -> impl Fn(usize, usize) -> Ordering + 'a
642where
643 T: Ord,
644 L: ArrayAccessor<Item = T> + 'a,
645 R: ArrayAccessor<Item = T> + 'a,
646{
647 move |i, j| match (left.is_null(i), right.is_null(j)) {
648 (true, true) => Ordering::Equal,
649 (true, false) => Ordering::Less,
650 (false, true) => Ordering::Greater,
651 (false, false) => left.value(i).cmp(&right.value(j)),
652 }
653}
654
655fn reinterpret_primitive<K: ArrowPrimitiveType>(arr: &dyn Array) -> Result<PrimitiveArray<K>> {
661 if let Some(arr) = arr.as_primitive_opt::<K>() {
662 return Ok(arr.clone());
663 }
664 let data = arr
665 .to_data()
666 .into_builder()
667 .data_type(K::DATA_TYPE)
668 .build()
669 .map_err(|e| {
670 Error::internal(format!(
671 "failed to reinterpret {} as {}: {e}",
672 arr.data_type(),
673 K::DATA_TYPE
674 ))
675 })?;
676 Ok(PrimitiveArray::<K>::from(data))
677}
678
679fn primitive_cmp<'a, T>(
683 left: &'a PrimitiveArray<T>,
684 right: &'a PrimitiveArray<T>,
685) -> impl Fn(usize, usize) -> Ordering + 'a
686where
687 T: ArrowPrimitiveType,
688{
689 move |i, j| match (left.is_null(i), right.is_null(j)) {
690 (true, true) => Ordering::Equal,
691 (true, false) => Ordering::Less,
692 (false, true) => Ordering::Greater,
693 (false, false) => left.value(i).compare(right.value(j)),
694 }
695}
696
697#[derive(Debug, PartialEq, DeepSizeOf)]
712pub struct BTreeLookup {
713 batch: RecordBatch,
715 null_pages: Vec<u32>,
717 all_null_pages: Vec<u32>,
719 search_start: usize,
722}
723
724#[derive(Debug, Copy, Clone, PartialEq, Eq)]
725enum Matches {
726 Some(u32),
727 All(u32),
728}
729
730impl Matches {
731 fn page_id(&self) -> u32 {
732 match self {
733 Self::Some(page_id) => *page_id,
734 Self::All(page_id) => *page_id,
735 }
736 }
737}
738
739impl BTreeLookup {
740 fn try_new(batch: RecordBatch) -> Result<Self> {
743 let mut null_pages = Vec::new();
744 let mut all_null_pages = Vec::new();
745 let mut search_start = batch.num_rows();
746
747 if batch.num_rows() > 0 {
748 let maxs = batch.column(1);
749 let null_counts = batch
750 .column(2)
751 .as_any()
752 .downcast_ref::<UInt32Array>()
753 .ok_or_else(|| Error::internal("BTree lookup null_count column must be UInt32"))?;
754 let page_numbers = batch
755 .column(3)
756 .as_any()
757 .downcast_ref::<UInt32Array>()
758 .ok_or_else(|| Error::internal("BTree lookup page_idx column must be UInt32"))?;
759
760 for idx in 0..batch.num_rows() {
761 let page_number = page_numbers.values()[idx];
762 if maxs.is_null(idx) {
764 all_null_pages.push(page_number);
765 continue;
766 }
767 if search_start == batch.num_rows() {
768 search_start = idx;
769 }
770 if null_counts.values()[idx] > 0 {
771 null_pages.push(page_number);
772 }
773 }
774 } else {
775 search_start = 0;
776 }
777
778 Ok(Self {
779 batch,
780 null_pages,
781 all_null_pages,
782 search_start,
783 })
784 }
785
786 fn page_numbers(&self) -> Result<&UInt32Array> {
787 self.batch
788 .column(3)
789 .as_any()
790 .downcast_ref::<UInt32Array>()
791 .ok_or_else(|| Error::internal("BTree lookup page_idx column must be UInt32"))
792 }
793
794 fn pages_eq(&self, query: &OrderableScalarValue) -> Result<Vec<Matches>> {
796 if query.0.is_null() {
797 Ok(self.pages_null())
798 } else {
799 let query_arr = query.0.to_array_of_size(1)?;
800 let pages = self.candidate_pages_for_values(query_arr.as_ref())?;
801 Ok(pages.into_iter().map(Matches::Some).collect())
802 }
803 }
804
805 fn pages_in(
807 &self,
808 values: impl IntoIterator<Item = OrderableScalarValue>,
809 ) -> Result<Vec<Matches>> {
810 let values = values.into_iter();
815 let mut has_null = false;
816 let mut non_null = Vec::with_capacity(values.size_hint().0);
817 for val in values {
818 if val.0.is_null() {
819 has_null = true;
820 } else {
821 non_null.push(val.0);
822 }
823 }
824
825 let mut all_pages = if non_null.is_empty() {
828 Vec::new()
829 } else {
830 let query_arr = ScalarValue::iter_to_array(non_null)?;
831 self.candidate_pages_for_values(query_arr.as_ref())?
832 };
833 if has_null {
834 all_pages.extend(self.pages_null().into_iter().map(|m| m.page_id()));
835 }
836 all_pages.sort_unstable();
837 all_pages.dedup();
838 Ok(all_pages.into_iter().map(Matches::Some).collect())
839 }
840
841 fn candidate_pages_for_values(&self, query: &dyn Array) -> Result<Vec<u32>> {
849 let num_rows = self.batch.num_rows();
850 if self.search_start >= num_rows || query.is_empty() {
851 return Ok(vec![]);
852 }
853
854 let mins = self.batch.column(0).as_ref();
855 let maxs = self.batch.column(1).as_ref();
856 let page_ids = self.page_numbers()?.values();
857
858 use DataType::*;
867 match query.data_type() {
868 Int8 => self.scan_native::<Int8Type>(mins, maxs, query, page_ids),
869 Int16 => self.scan_native::<Int16Type>(mins, maxs, query, page_ids),
870 Int32 | Date32 | Time32(_) | Decimal32(_, _) | Interval(IntervalUnit::YearMonth) => {
872 self.scan_native::<Int32Type>(mins, maxs, query, page_ids)
873 }
874 Int64 | Date64 | Time64(_) | Timestamp(_, _) | Duration(_) | Decimal64(_, _) => {
876 self.scan_native::<Int64Type>(mins, maxs, query, page_ids)
877 }
878 UInt8 => self.scan_native::<UInt8Type>(mins, maxs, query, page_ids),
879 UInt16 => self.scan_native::<UInt16Type>(mins, maxs, query, page_ids),
880 UInt32 => self.scan_native::<UInt32Type>(mins, maxs, query, page_ids),
881 UInt64 => self.scan_native::<UInt64Type>(mins, maxs, query, page_ids),
882 Float16 => self.scan_native::<Float16Type>(mins, maxs, query, page_ids),
883 Float32 => self.scan_native::<Float32Type>(mins, maxs, query, page_ids),
884 Float64 => self.scan_native::<Float64Type>(mins, maxs, query, page_ids),
885 Decimal128(_, _) => self.scan_native::<Decimal128Type>(mins, maxs, query, page_ids),
886 Decimal256(_, _) => self.scan_native::<Decimal256Type>(mins, maxs, query, page_ids),
887 Utf8 => Ok(self.scan_accessor(
888 mins.as_string::<i32>(),
889 maxs.as_string::<i32>(),
890 query.as_string::<i32>(),
891 page_ids,
892 )),
893 LargeUtf8 => Ok(self.scan_accessor(
894 mins.as_string::<i64>(),
895 maxs.as_string::<i64>(),
896 query.as_string::<i64>(),
897 page_ids,
898 )),
899 Binary => Ok(self.scan_accessor(
900 mins.as_binary::<i32>(),
901 maxs.as_binary::<i32>(),
902 query.as_binary::<i32>(),
903 page_ids,
904 )),
905 LargeBinary => Ok(self.scan_accessor(
906 mins.as_binary::<i64>(),
907 maxs.as_binary::<i64>(),
908 query.as_binary::<i64>(),
909 page_ids,
910 )),
911 FixedSizeBinary(_) => Ok(self.scan_accessor(
912 mins.as_fixed_size_binary(),
913 maxs.as_fixed_size_binary(),
914 query.as_fixed_size_binary(),
915 page_ids,
916 )),
917 _ => self.scan_fallback(mins, maxs, query, page_ids),
918 }
919 }
920
921 fn scan_native<K: ArrowPrimitiveType>(
925 &self,
926 mins: &dyn Array,
927 maxs: &dyn Array,
928 query: &dyn Array,
929 page_ids: &[u32],
930 ) -> Result<Vec<u32>> {
931 let mins = reinterpret_primitive::<K>(mins)?;
932 let maxs = reinterpret_primitive::<K>(maxs)?;
933 let query = reinterpret_primitive::<K>(query)?;
934 Ok(self.scan_equality_pages(
935 query.len(),
936 page_ids,
937 |idx| maxs.is_null(idx),
938 primitive_cmp(&mins, &query),
939 primitive_cmp(&maxs, &query),
940 primitive_cmp(&mins, &mins),
941 ))
942 }
943
944 fn scan_accessor<T, A>(&self, mins: A, maxs: A, query: A, page_ids: &[u32]) -> Vec<u32>
948 where
949 T: Ord,
950 A: ArrayAccessor<Item = T> + Copy,
951 {
952 self.scan_equality_pages(
953 query.len(),
954 page_ids,
955 |idx| maxs.is_null(idx),
956 accessor_cmp(mins, query),
957 accessor_cmp(maxs, query),
958 accessor_cmp(mins, mins),
959 )
960 }
961
962 fn scan_fallback(
965 &self,
966 mins: &dyn Array,
967 maxs: &dyn Array,
968 query: &dyn Array,
969 page_ids: &[u32],
970 ) -> Result<Vec<u32>> {
971 let opts = SortOptions {
974 descending: false,
975 nulls_first: true,
976 };
977 let cmp_min = make_comparator(mins, query, opts)?;
978 let cmp_max = make_comparator(maxs, query, opts)?;
979 let cmp_min_min = make_comparator(mins, mins, opts)?;
980 Ok(self.scan_equality_pages(
981 query.len(),
982 page_ids,
983 |idx| maxs.is_null(idx),
984 cmp_min,
985 cmp_max,
986 cmp_min_min,
987 ))
988 }
989
990 fn scan_equality_pages(
1000 &self,
1001 num_query: usize,
1002 page_ids: &[u32],
1003 max_is_null: impl Fn(usize) -> bool,
1004 cmp_min: impl Fn(usize, usize) -> Ordering,
1005 cmp_max: impl Fn(usize, usize) -> Ordering,
1006 cmp_min_min: impl Fn(usize, usize) -> Ordering,
1007 ) -> Vec<u32> {
1008 let num_rows = self.batch.num_rows();
1009 let mut pages = Vec::with_capacity(num_query);
1012 for j in 0..num_query {
1013 let p = partition_point(0, num_rows, |i| cmp_min(i, j) == Ordering::Less);
1017 let start = if p == 0 {
1018 self.search_start
1019 } else {
1020 let anchor = p - 1;
1021 partition_point(0, p, |i| cmp_min_min(i, anchor) == Ordering::Less)
1022 }
1023 .max(self.search_start);
1024
1025 let end = partition_point(start, num_rows, |i| cmp_min(i, j) != Ordering::Greater);
1027
1028 let bulk_start = p.max(start);
1037 for (offset, &page_id) in page_ids[start..bulk_start].iter().enumerate() {
1038 let idx = start + offset;
1039 if max_is_null(idx) {
1041 continue;
1042 }
1043 if cmp_max(idx, j) != Ordering::Less {
1045 pages.push(page_id);
1046 }
1047 }
1048 pages.extend_from_slice(&page_ids[bulk_start..end]);
1049 }
1050
1051 pages.sort_unstable();
1052 pages.dedup();
1053 pages
1054 }
1055
1056 fn pages_between(
1058 &self,
1059 range: (Bound<&OrderableScalarValue>, Bound<&OrderableScalarValue>),
1060 ) -> Result<Vec<Matches>> {
1061 let num_rows = self.batch.num_rows();
1062 if self.search_start >= num_rows {
1064 return Ok(vec![]);
1065 }
1066
1067 let mins = self.batch.column(0).as_ref();
1068 let maxs = self.batch.column(1).as_ref();
1069 let page_numbers = self.page_numbers()?;
1070
1071 let opts = SortOptions {
1075 descending: false,
1076 nulls_first: true,
1077 };
1078 let lower_arr = match range.0 {
1081 Bound::Unbounded => None,
1082 Bound::Included(v) | Bound::Excluded(v) => Some(v.0.to_array_of_size(1)?),
1083 };
1084 let upper_arr = match range.1 {
1085 Bound::Unbounded => None,
1086 Bound::Included(v) | Bound::Excluded(v) => Some(v.0.to_array_of_size(1)?),
1087 };
1088
1089 let start = match &lower_arr {
1093 None => self.search_start,
1094 Some(lower) => {
1095 let cmp = make_comparator(mins, lower.as_ref(), opts)?;
1096 let p = partition_point(0, num_rows, |i| cmp(i, 0) == Ordering::Less);
1098 if p == 0 {
1099 self.search_start
1100 } else {
1101 let straddle = mins.slice(p - 1, 1);
1103 let cmp = make_comparator(mins, straddle.as_ref(), opts)?;
1104 partition_point(0, p, |i| cmp(i, 0) == Ordering::Less)
1105 }
1106 }
1107 }
1108 .max(self.search_start);
1109
1110 let end = match &upper_arr {
1114 None => num_rows,
1115 Some(upper) => {
1116 let cmp = make_comparator(mins, upper.as_ref(), opts)?;
1117 partition_point(start, num_rows, |i| cmp(i, 0) != Ordering::Greater)
1118 }
1119 };
1120
1121 if start >= end {
1122 return Ok(vec![]);
1123 }
1124
1125 let cmp_max_lower = lower_arr
1127 .as_ref()
1128 .map(|l| make_comparator(maxs, l.as_ref(), opts))
1129 .transpose()?;
1130 let cmp_min_lower = lower_arr
1131 .as_ref()
1132 .map(|l| make_comparator(mins, l.as_ref(), opts))
1133 .transpose()?;
1134 let cmp_max_upper = upper_arr
1135 .as_ref()
1136 .map(|u| make_comparator(maxs, u.as_ref(), opts))
1137 .transpose()?;
1138
1139 let mut matches = Vec::new();
1140 for idx in start..end {
1141 if maxs.is_null(idx) {
1143 continue;
1144 }
1145
1146 let lower_ok = match (range.0, &cmp_max_lower) {
1148 (Bound::Unbounded, _) => true,
1149 (Bound::Included(_), Some(cmp)) => cmp(idx, 0) != Ordering::Less, (Bound::Excluded(_), Some(cmp)) => cmp(idx, 0) == Ordering::Greater, _ => unreachable!("lower bound and its comparator are constructed together"),
1152 };
1153 if !lower_ok {
1154 continue;
1155 }
1156
1157 let page_number = page_numbers.values()[idx];
1158
1159 if mins.is_null(idx) {
1162 matches.push(Matches::Some(page_number));
1163 continue;
1164 }
1165
1166 let lower_full = match (range.0, &cmp_min_lower) {
1168 (Bound::Unbounded, _) => true,
1169 (Bound::Included(_), Some(cmp)) => cmp(idx, 0) != Ordering::Less, (Bound::Excluded(_), Some(cmp)) => cmp(idx, 0) == Ordering::Greater, _ => unreachable!("lower bound and its comparator are constructed together"),
1172 };
1173 let upper_full = match (range.1, &cmp_max_upper) {
1174 (Bound::Unbounded, _) => true,
1175 (Bound::Included(_), Some(cmp)) => cmp(idx, 0) != Ordering::Greater, (Bound::Excluded(_), Some(cmp)) => cmp(idx, 0) == Ordering::Less, _ => unreachable!("upper bound and its comparator are constructed together"),
1178 };
1179 if lower_full && upper_full {
1180 matches.push(Matches::All(page_number));
1181 } else {
1182 matches.push(Matches::Some(page_number));
1183 }
1184 }
1185
1186 Ok(matches)
1187 }
1188
1189 fn pages_null(&self) -> Vec<Matches> {
1190 self.null_pages
1191 .iter()
1192 .copied()
1193 .map(Matches::Some)
1194 .chain(self.all_null_pages.iter().copied().map(Matches::All))
1195 .collect()
1196 }
1197}
1198
1199#[derive(Clone)]
1202struct LazyIndexReader {
1203 index_reader: Arc<tokio::sync::Mutex<Option<Arc<dyn IndexReader>>>>,
1204 store: Arc<dyn IndexStore>,
1205 ranges_to_files: Option<Arc<RangeInclusiveMap<u32, (String, u32)>>>,
1206}
1207
1208impl LazyIndexReader {
1209 fn new(
1210 store: Arc<dyn IndexStore>,
1211 ranges_to_files: Option<Arc<RangeInclusiveMap<u32, (String, u32)>>>,
1212 ) -> Self {
1213 Self {
1214 index_reader: Arc::new(tokio::sync::Mutex::new(None)),
1215 store,
1216 ranges_to_files,
1217 }
1218 }
1219
1220 async fn get(&self) -> Result<Arc<dyn IndexReader>> {
1221 let mut reader = self.index_reader.lock().await;
1222 if reader.is_none() {
1223 let index_reader = if let Some(ranges_to_files) = &self.ranges_to_files {
1224 Arc::new(LazyRangedIndexReader::new(
1225 self.store.clone(),
1226 ranges_to_files.clone(),
1227 ))
1228 } else {
1229 self.store.open_index_file(BTREE_PAGES_NAME).await?
1230 };
1231 *reader = Some(index_reader);
1232 }
1233 Ok(reader.as_ref().unwrap().clone())
1234 }
1235}
1236
1237struct LazyRangedIndexReader {
1239 #[allow(clippy::type_complexity)]
1240 readers:
1241 Arc<tokio::sync::Mutex<HashMap<String, Arc<tokio::sync::OnceCell<Arc<dyn IndexReader>>>>>>,
1242 store: Arc<dyn IndexStore>,
1243 ranges_to_files: Arc<RangeInclusiveMap<u32, (String, u32)>>,
1244}
1245
1246impl LazyRangedIndexReader {
1247 fn new(
1248 store: Arc<dyn IndexStore>,
1249 ranges_to_files: Arc<RangeInclusiveMap<u32, (String, u32)>>,
1250 ) -> Self {
1251 Self {
1252 readers: Arc::new(tokio::sync::Mutex::new(HashMap::new())),
1253 store,
1254 ranges_to_files,
1255 }
1256 }
1257
1258 async fn get_reader(&self, file_name: &str) -> Result<Arc<dyn IndexReader>> {
1259 let reader_cell = {
1260 let mut guard = self.readers.lock().await;
1261 guard
1262 .entry(file_name.to_string())
1263 .or_insert_with(|| Arc::new(tokio::sync::OnceCell::new()))
1264 .clone()
1265 };
1266 let reader = reader_cell
1267 .get_or_try_init(|| async { self.store.open_index_file(file_name).await })
1268 .await?;
1269 Ok(reader.clone())
1270 }
1271
1272 async fn get_reader_and_local_page_idx(
1273 &self,
1274 page_idx: u32,
1275 ) -> Result<(Arc<dyn IndexReader>, u32)> {
1276 let (page_file_name, offset) = self.ranges_to_files.get(&page_idx).ok_or_else(|| {
1277 Error::internal(format!(
1278 "Unexpected page index, index {} is out of range.",
1279 page_idx
1280 ))
1281 })?;
1282 let reader = self.get_reader(page_file_name).await?;
1283 Ok((reader.clone(), page_idx - *offset))
1284 }
1285}
1286
1287#[async_trait]
1288impl IndexReader for LazyRangedIndexReader {
1289 async fn read_record_batch(&self, n: u64, batch_size: u64) -> Result<RecordBatch> {
1290 let (reader, local_page_idx) = self.get_reader_and_local_page_idx(n as u32).await?;
1291 reader
1292 .read_record_batch(local_page_idx as u64, batch_size)
1293 .await
1294 }
1295
1296 async fn read_range(
1297 &self,
1298 _range: std::ops::Range<usize>,
1299 _projection: Option<&[&str]>,
1300 ) -> Result<RecordBatch> {
1301 unimplemented!("Read range is not implemented for lazy page file reader.");
1302 }
1303
1304 async fn num_batches(&self, batch_size: u64) -> u32 {
1305 let mut total_batches = 0;
1306 for (_, (file_name, _)) in self.ranges_to_files.iter() {
1307 let reader = self
1308 .get_reader(file_name)
1309 .await
1310 .unwrap_or_else(|_| panic!("Cannot open page file {}.", file_name));
1311 total_batches += reader.as_ref().num_batches(batch_size).await;
1312 }
1313 total_batches
1314 }
1315
1316 fn num_rows(&self) -> usize {
1317 unimplemented!("only async functions are available for lazy page index reader.");
1318 }
1319
1320 fn schema(&self) -> &lance_core::datatypes::Schema {
1321 unimplemented!("only async functions are available for lazy page index reader.");
1322 }
1323}
1324
1325#[derive(Debug, Clone, DeepSizeOf)]
1340pub struct CachedScalarIndex(Arc<dyn ScalarIndex>);
1341
1342impl CachedScalarIndex {
1343 pub fn new(index: Arc<dyn ScalarIndex>) -> Self {
1344 Self(index)
1345 }
1346
1347 pub fn into_inner(self) -> Arc<dyn ScalarIndex> {
1348 self.0
1349 }
1350}
1351
1352#[derive(Debug, Clone)]
1353pub struct BTreePageKey {
1354 pub page_number: u32,
1355}
1356
1357impl CacheKey for BTreePageKey {
1358 type ValueType = FlatIndex;
1359
1360 fn key(&self) -> std::borrow::Cow<'_, str> {
1361 format!("page-{}", self.page_number).into()
1362 }
1363
1364 fn type_name() -> &'static str {
1365 "BTreePage"
1366 }
1367
1368 fn codec() -> Option<CacheCodec> {
1369 Some(CacheCodec::from_impl::<FlatIndex>())
1371 }
1372}
1373
1374#[derive(Debug, Clone)]
1382struct BTreeIndexState {
1383 lookup_batch: RecordBatch,
1384 batch_size: u64,
1385 ranges_to_files: Option<Arc<RangeInclusiveMap<u32, (String, u32)>>>,
1386}
1387
1388impl DeepSizeOf for BTreeIndexState {
1389 fn deep_size_of_children(&self, context: &mut lance_core::deepsize::Context) -> usize {
1390 self.lookup_batch.deep_size_of_children(context)
1393 }
1394}
1395
1396impl BTreeIndexState {
1397 fn from_index(index: &dyn ScalarIndex) -> Result<Self> {
1398 let btree = index.as_any().downcast_ref::<BTreeIndex>().ok_or_else(|| {
1399 Error::internal("BTreeIndexState::from_index called with a non-BTree index")
1400 })?;
1401 Ok(Self {
1402 lookup_batch: btree.page_lookup.batch.clone(),
1403 batch_size: btree.batch_size,
1404 ranges_to_files: btree.ranges_to_files.clone(),
1405 })
1406 }
1407
1408 fn reconstruct(
1409 &self,
1410 store: Arc<dyn IndexStore>,
1411 index_cache: &LanceCache,
1412 frag_reuse_index: Option<Arc<dyn RowIdRemapper>>,
1413 ) -> Result<Arc<dyn ScalarIndex>> {
1414 let index = BTreeIndex::try_from_serialized(
1415 self.lookup_batch.clone(),
1416 store,
1417 index_cache,
1418 self.batch_size,
1419 self.ranges_to_files.clone(),
1420 frag_reuse_index,
1421 )?;
1422 Ok(Arc::new(index) as Arc<dyn ScalarIndex>)
1423 }
1424}
1425
1426impl CacheCodecImpl for BTreeIndexState {
1427 const TYPE_ID: &'static str = "lance.scalar.BTreeIndexState";
1428 const CURRENT_VERSION: u32 = 1;
1429
1430 fn serialize(&self, w: &mut CacheEntryWriter<'_>) -> Result<()> {
1436 let ranges_to_files = match &self.ranges_to_files {
1437 None => Vec::new(),
1438 Some(ranges) => ranges
1439 .iter()
1440 .map(|(range, (path, page_offset))| RangeToFile {
1441 start: *range.start(),
1442 end: *range.end(),
1443 page_offset: *page_offset,
1444 path: path.clone(),
1445 })
1446 .collect(),
1447 };
1448 let header = BTreeIndexHeader {
1449 batch_size: self.batch_size,
1450 has_ranges_to_files: self.ranges_to_files.is_some(),
1451 ranges_to_files,
1452 };
1453 w.write_header(&header)?;
1454 w.write_ipc(&self.lookup_batch)?;
1455 Ok(())
1456 }
1457
1458 fn deserialize(r: &mut CacheEntryReader<'_>) -> Result<Self> {
1459 let header: BTreeIndexHeader = r.read_header()?;
1460 let ranges_to_files = if header.has_ranges_to_files {
1461 let map: RangeInclusiveMap<u32, (String, u32)> = header
1462 .ranges_to_files
1463 .into_iter()
1464 .map(|entry| (entry.start..=entry.end, (entry.path, entry.page_offset)))
1465 .collect();
1466 Some(Arc::new(map))
1467 } else {
1468 None
1469 };
1470 let lookup_batch = r.read_ipc()?;
1471 Ok(Self {
1472 lookup_batch,
1473 batch_size: header.batch_size,
1474 ranges_to_files,
1475 })
1476 }
1477}
1478
1479struct BTreeIndexStateKey;
1482
1483impl CacheKey for BTreeIndexStateKey {
1484 type ValueType = BTreeIndexState;
1485
1486 fn key(&self) -> std::borrow::Cow<'_, str> {
1487 "state".into()
1488 }
1489
1490 fn type_name() -> &'static str {
1491 "BTreeIndexState"
1492 }
1493
1494 fn codec() -> Option<CacheCodec> {
1495 Some(CacheCodec::from_impl::<BTreeIndexState>())
1496 }
1497}
1498
1499#[derive(Clone, Debug)]
1502pub struct BTreeIndex {
1503 page_lookup: Arc<BTreeLookup>,
1504 index_cache: WeakLanceCache,
1505 store: Arc<dyn IndexStore>,
1506 data_type: DataType,
1507 batch_size: u64,
1508
1509 ranges_to_files: Option<Arc<RangeInclusiveMap<u32, (String, u32)>>>,
1538 frag_reuse_index: Option<Arc<dyn RowIdRemapper>>,
1539}
1540
1541impl DeepSizeOf for BTreeIndex {
1542 fn deep_size_of_children(&self, context: &mut lance_core::deepsize::Context) -> usize {
1543 self.page_lookup.deep_size_of_children(context) + self.store.deep_size_of_children(context)
1547 }
1548}
1549
1550impl BTreeIndex {
1551 #[allow(clippy::too_many_arguments)]
1552 fn new(
1553 page_lookup: Arc<BTreeLookup>,
1554 store: Arc<dyn IndexStore>,
1555 data_type: DataType,
1556 index_cache: WeakLanceCache,
1557 batch_size: u64,
1558 ranges_to_files: Option<Arc<RangeInclusiveMap<u32, (String, u32)>>>,
1559 frag_reuse_index: Option<Arc<dyn RowIdRemapper>>,
1560 ) -> Self {
1561 Self {
1562 page_lookup,
1563 store,
1564 data_type,
1565 index_cache,
1566 batch_size,
1567 ranges_to_files,
1568 frag_reuse_index,
1569 }
1570 }
1571
1572 pub async fn contains_keys(
1585 &self,
1586 keys: &[ScalarValue],
1587 metrics: &dyn MetricsCollector,
1588 ) -> Result<Vec<bool>> {
1589 let mut by_page: HashMap<u32, Vec<(usize, OrderableScalarValue)>> = HashMap::new();
1592 for (idx, key) in keys.iter().enumerate() {
1593 if key.is_null() {
1594 continue;
1595 }
1596 let ov = OrderableScalarValue(key.clone());
1597 for matches in self.page_lookup.pages_eq(&ov)? {
1598 by_page
1599 .entry(matches.page_id())
1600 .or_default()
1601 .push((idx, ov.clone()));
1602 }
1603 }
1604
1605 let index_reader = LazyIndexReader::new(self.store.clone(), self.ranges_to_files.clone());
1606 let page_tasks = by_page.into_iter().map(|(page_number, entries)| {
1607 let index_reader = index_reader.clone();
1608 async move {
1609 let page = self.lookup_page(page_number, index_reader, metrics).await?;
1610 let needles: Vec<OrderableScalarValue> =
1611 entries.iter().map(|(_, ov)| ov.clone()).collect();
1612 let present = page.contains_values(&needles)?;
1613 Result::Ok((entries, present))
1614 }
1615 });
1616
1617 let mut result = vec![false; keys.len()];
1618 let page_results: Vec<_> = stream::iter(page_tasks)
1619 .buffer_unordered(get_num_compute_intensive_cpus())
1620 .try_collect()
1621 .await?;
1622 for (entries, present) in page_results {
1623 for (idx, ov) in entries {
1624 if present.contains(&ov) {
1625 result[idx] = true;
1626 }
1627 }
1628 }
1629 Ok(result)
1630 }
1631
1632 async fn lookup_page(
1633 &self,
1634 page_number: u32,
1635 index_reader: LazyIndexReader,
1636 metrics: &dyn MetricsCollector,
1637 ) -> Result<Arc<FlatIndex>> {
1638 self.index_cache
1639 .get_or_insert_with_key(BTreePageKey { page_number }, move || async move {
1640 self.read_page(page_number, index_reader, metrics).await
1641 })
1642 .await
1643 }
1644
1645 #[instrument(level = "debug", skip_all)]
1646 async fn read_page(
1647 &self,
1648 page_number: u32,
1649 index_reader: LazyIndexReader,
1650 metrics: &dyn MetricsCollector,
1651 ) -> Result<FlatIndex> {
1652 metrics.record_part_load();
1653 info!(target: TRACE_IO_EVENTS, r#type=IO_TYPE_LOAD_SCALAR_PART, index_type="btree", part_id=page_number);
1654 let index_reader = index_reader.get().await?;
1655 let mut serialized_page = index_reader
1656 .read_record_batch(page_number as u64, self.batch_size)
1657 .await?;
1658 if let Some(frag_reuse_index_ref) = self.frag_reuse_index.as_ref() {
1659 serialized_page =
1660 frag_reuse_index_ref.remap_row_ids_record_batch(serialized_page, 1)?;
1661 }
1662 FlatIndex::try_new(serialized_page)
1663 }
1664
1665 fn compile_predicate(&self, query: &SargableQuery) -> Result<Arc<dyn PhysicalExpr>> {
1669 let schema = Arc::new(Schema::new(vec![
1670 Field::new(BTREE_VALUES_COLUMN, self.data_type.clone(), true),
1671 Field::new(BTREE_IDS_COLUMN, DataType::UInt64, false),
1672 ]));
1673 let df_schema = DFSchema::try_from(schema)?;
1674 Ok(create_physical_expr(
1675 &query.to_expr(BTREE_VALUES_COLUMN.to_string()),
1676 &df_schema,
1677 &ExecutionProps::default(),
1678 )?)
1679 }
1680
1681 async fn search_page(
1682 &self,
1683 query: &SargableQuery,
1684 matches: Matches,
1685 index_reader: LazyIndexReader,
1686 prebuilt: Option<&Arc<dyn PhysicalExpr>>,
1687 metrics: &dyn MetricsCollector,
1688 ) -> Result<NullableRowAddrSet> {
1689 let subindex = self
1690 .lookup_page(matches.page_id(), index_reader, metrics)
1691 .await?;
1692
1693 match matches {
1694 Matches::Some(_) => match prebuilt {
1697 Some(expr) => subindex.search_prebuilt(expr, metrics),
1698 None => subindex.search(query, metrics),
1699 },
1700 Matches::All(_) => Ok(match query {
1701 SargableQuery::IsNull() => subindex.all_ignore_nulls(),
1703 _ => subindex.all(),
1704 }),
1705 }
1706 }
1707
1708 #[instrument(level = "debug", skip_all)]
1709 fn try_from_serialized(
1710 data: RecordBatch,
1711 store: Arc<dyn IndexStore>,
1712 index_cache: &LanceCache,
1713 batch_size: u64,
1714 ranges_to_files: Option<Arc<RangeInclusiveMap<u32, (String, u32)>>>,
1715 frag_reuse_index: Option<Arc<dyn RowIdRemapper>>,
1716 ) -> Result<Self> {
1717 let data_type = data.column(0).data_type().clone();
1718 let page_lookup = Arc::new(BTreeLookup::try_new(data)?);
1719
1720 Ok(Self::new(
1721 page_lookup,
1722 store,
1723 data_type,
1724 WeakLanceCache::from(index_cache),
1725 batch_size,
1726 ranges_to_files,
1727 frag_reuse_index,
1728 ))
1729 }
1730
1731 async fn load(
1732 store: Arc<dyn IndexStore>,
1733 frag_reuse_index: Option<Arc<dyn RowIdRemapper>>,
1734 index_cache: &LanceCache,
1735 ) -> Result<Arc<Self>> {
1736 let (page_lookup_file, standalone_partition_page_file) =
1737 match store.open_index_file(BTREE_LOOKUP_NAME).await {
1738 Ok(page_lookup_file) => (page_lookup_file, None),
1739 Err(original_err) if is_missing_lookup_error(&original_err) => {
1740 let files = store.list_files_with_sizes().await?;
1741 let Some((lookup_file, page_file)) = find_single_partition_files(&files)?
1742 else {
1743 return Err(original_err);
1744 };
1745 (
1746 store.open_index_file(lookup_file).await?,
1747 Some(page_file.to_string()),
1748 )
1749 }
1750 Err(other_err) => return Err(other_err),
1751 };
1752 let num_rows_in_lookup = page_lookup_file.num_rows();
1753 let serialized_lookup = page_lookup_file
1754 .read_range(0..num_rows_in_lookup, None)
1755 .await?;
1756 let file_schema = page_lookup_file.schema();
1757 let batch_size = file_schema
1758 .metadata
1759 .get(BATCH_SIZE_META_KEY)
1760 .map(|bs| bs.parse().unwrap_or(DEFAULT_BTREE_BATCH_SIZE))
1761 .unwrap_or(DEFAULT_BTREE_BATCH_SIZE);
1762
1763 let range_partitioned = file_schema
1764 .metadata
1765 .get(RANGE_PARTITIONED_META_KEY)
1766 .map(|bs| bs.parse().unwrap_or(DEFAULT_RANGE_PARTITIONED))
1767 .unwrap_or(DEFAULT_RANGE_PARTITIONED);
1768 let ranges_to_files = if let Some(page_file_name) = standalone_partition_page_file {
1772 let page_numbers = serialized_lookup
1773 .column(3)
1774 .as_any()
1775 .downcast_ref::<UInt32Array>()
1776 .unwrap();
1777 let max_page_number = page_numbers.values().iter().copied().max().unwrap_or(0);
1778 let mut range_map = RangeInclusiveMap::new();
1779 range_map.insert(0..=max_page_number, (page_file_name, 0));
1780 Some(Arc::new(range_map))
1781 } else if range_partitioned {
1782 let part_sizes_str = file_schema
1783 .metadata
1784 .get(PAGE_NUM_PER_RANGE_PARTITION_META_KEY)
1785 .expect("Range-partitioned Btree lookup file must have page-number-per-range-file metadata!");
1786 let part_sizes_vec: Vec<(u64, u32)> = serde_json::from_str(part_sizes_str)?;
1787 let mut offset: u32 = 0;
1788
1789 let range_map = part_sizes_vec
1790 .into_iter()
1791 .map(|(id, size)| {
1792 let range = offset..=(offset + size - 1);
1793 let file_with_size = (part_page_data_file_path(id), offset);
1794 offset += size;
1795 (range, file_with_size)
1796 })
1797 .collect();
1798
1799 Some(Arc::new(range_map))
1800 } else {
1801 None
1802 };
1803
1804 Ok(Arc::new(Self::try_from_serialized(
1805 serialized_lookup,
1806 store,
1807 index_cache,
1808 batch_size,
1809 ranges_to_files,
1810 frag_reuse_index,
1811 )?))
1812 }
1813
1814 fn train_schema(&self) -> Schema {
1816 let value_field = Field::new(VALUE_COLUMN_NAME, self.data_type.clone(), true);
1817 let row_id_field = Field::new(ROW_ID, DataType::UInt64, false);
1818 Schema::new(vec![value_field, row_id_field])
1819 }
1820
1821 async fn data_stream(&self) -> Result<SendableRecordBatchStream> {
1823 let lazy_reader = LazyIndexReader::new(self.store.clone(), self.ranges_to_files.clone());
1824 let reader = lazy_reader.get().await?;
1825 let new_schema = Arc::new(self.train_schema());
1826 let new_schema_clone = new_schema.clone();
1827 let reader_stream = IndexReaderStream::new(reader, self.batch_size).await;
1828 let batches = reader_stream
1829 .map(|fut| fut.map_err(DataFusionError::from))
1830 .buffered(self.store.io_parallelism())
1831 .map_ok(move |batch| {
1832 RecordBatch::try_new(
1833 new_schema.clone(),
1834 vec![batch.column(0).clone(), batch.column(1).clone()],
1835 )
1836 .unwrap()
1837 })
1838 .boxed();
1839 Ok(Box::pin(RecordBatchStreamAdapter::new(
1840 new_schema_clone,
1841 batches,
1842 )))
1843 }
1844
1845 pub async fn merge_segments(
1848 segments: &[Arc<Self>],
1849 new_data: SendableRecordBatchStream,
1850 dest_store: &dyn IndexStore,
1851 old_data_filters: &[Option<OldIndexDataFilter>],
1852 ) -> Result<CreatedIndex> {
1853 let Some(first) = segments.first() else {
1854 return Err(Error::invalid_input(
1855 "cannot merge BTree index without at least one source segment".to_string(),
1856 ));
1857 };
1858
1859 if old_data_filters.len() != segments.len() {
1860 return Err(Error::invalid_input(format!(
1861 "BTree merge: expected one old-data filter per source segment \
1862 (segments={}, filters={})",
1863 segments.len(),
1864 old_data_filters.len()
1865 )));
1866 }
1867
1868 for segment in segments.iter().skip(1) {
1869 if segment.data_type != first.data_type {
1870 return Err(Error::index(format!(
1871 "cannot merge BTree segments with different value types ({:?} vs {:?})",
1872 first.data_type, segment.data_type
1873 )));
1874 }
1875 }
1876
1877 let new_schema = new_data.schema();
1878 let value_column_index = new_schema.index_of(VALUE_COLUMN_NAME)?;
1879 let new_value_type = new_schema.field(value_column_index).data_type();
1880 if new_value_type != &first.data_type {
1881 return Err(Error::invalid_input(format!(
1882 "BTree merge: new_data value column type {:?} does not match \
1883 segment value type {:?}",
1884 new_value_type, first.data_type
1885 )));
1886 }
1887
1888 let mut inputs: Vec<Arc<dyn ExecutionPlan>> = Vec::with_capacity(segments.len() + 1);
1889 for (segment, old_data_filter) in segments.iter().zip(old_data_filters) {
1890 if filter_keeps_nothing(old_data_filter) {
1891 continue;
1892 }
1893 let stream = segment.data_stream().await?;
1894 let stream = match segment.frag_reuse_index.clone() {
1895 Some(frag_reuse_index) => remap_row_ids(stream, frag_reuse_index),
1896 None => stream,
1897 };
1898 let stream = match old_data_filter.clone() {
1899 Some(filter) => filter_row_ids(stream, filter),
1900 None => stream,
1901 };
1902 inputs.push(Arc::new(OneShotExec::new(stream)));
1903 }
1904 inputs.push(Arc::new(OneShotExec::new(new_data)));
1905
1906 let sort_expr = PhysicalSortExpr {
1907 expr: Arc::new(Column::new(VALUE_COLUMN_NAME, value_column_index)),
1908 options: SortOptions {
1909 descending: false,
1910 nulls_first: true,
1911 },
1912 };
1913 let unioned = UnionExec::try_new(inputs)?;
1916 let ordered = Arc::new(SortPreservingMergeExec::new([sort_expr].into(), unioned));
1917 let unchunked = execute_plan(
1918 ordered,
1919 LanceExecutionOptions {
1920 use_spilling: true,
1921 ..Default::default()
1922 },
1923 )?;
1924 let merged_stream = chunk_concat_stream(unchunked, first.batch_size as usize);
1925
1926 let files =
1927 train_btree_index(merged_stream, dest_store, first.batch_size, None, None).await?;
1928
1929 Ok(CreatedIndex {
1930 index_details: prost_types::Any::from_msg(&pbold::BTreeIndexDetails::default())
1931 .unwrap(),
1932 index_version: BTREE_INDEX_VERSION,
1933 files,
1934 })
1935 }
1936}
1937
1938fn filter_row_ids(
1941 stream: SendableRecordBatchStream,
1942 old_data_filter: OldIndexDataFilter,
1943) -> SendableRecordBatchStream {
1944 let schema = stream.schema();
1945 let filtered = stream.map(move |batch_result| {
1946 let batch = batch_result?;
1947 let row_ids = batch[ROW_ID]
1948 .as_any()
1949 .downcast_ref::<arrow_array::UInt64Array>()
1950 .ok_or_else(|| Error::internal("expected UInt64Array for row_id column"))?;
1951 let mask = old_data_filter.filter_row_ids(row_ids);
1952 Ok(arrow_select::filter::filter_record_batch(&batch, &mask)?)
1953 });
1954 Box::pin(RecordBatchStreamAdapter::new(schema, filtered))
1955}
1956
1957fn filter_keeps_nothing(filter: &Option<OldIndexDataFilter>) -> bool {
1960 match filter {
1961 Some(OldIndexDataFilter::Fragments { to_keep, .. }) => to_keep.is_empty(),
1962 Some(OldIndexDataFilter::RowIds(valid)) => valid.is_empty(),
1963 None => false,
1964 }
1965}
1966
1967fn remap_row_ids(
1968 stream: SendableRecordBatchStream,
1969 frag_reuse_index: Arc<dyn RowIdRemapper>,
1970) -> SendableRecordBatchStream {
1971 let schema = stream.schema();
1972 let remapped = stream.map(move |batch_result| {
1973 let batch = batch_result?;
1974 Ok(frag_reuse_index.remap_row_ids_record_batch(batch, 1)?)
1975 });
1976 Box::pin(RecordBatchStreamAdapter::new(schema, remapped))
1977}
1978
1979fn wrap_bound(bound: &Bound<ScalarValue>) -> Bound<OrderableScalarValue> {
1980 match bound {
1981 Bound::Unbounded => Bound::Unbounded,
1982 Bound::Included(val) => Bound::Included(OrderableScalarValue(val.clone())),
1983 Bound::Excluded(val) => Bound::Excluded(OrderableScalarValue(val.clone())),
1984 }
1985}
1986
1987fn serialize_with_display<T: Display, S: Serializer>(
1988 value: &Option<T>,
1989 serializer: S,
1990) -> std::result::Result<S::Ok, S::Error> {
1991 if let Some(value) = value {
1992 serializer.collect_str(value)
1993 } else {
1994 serializer.collect_str("N/A")
1995 }
1996}
1997
1998#[derive(Serialize)]
1999struct BTreeStatistics {
2000 #[serde(serialize_with = "serialize_with_display")]
2001 min: Option<OrderableScalarValue>,
2002 #[serde(serialize_with = "serialize_with_display")]
2003 max: Option<OrderableScalarValue>,
2004 num_pages: u32,
2005}
2006
2007#[async_trait]
2008impl Index for BTreeIndex {
2009 fn as_any(&self) -> &dyn Any {
2010 self
2011 }
2012
2013 fn as_index(self: Arc<Self>) -> Arc<dyn Index> {
2014 self
2015 }
2016
2017 async fn prewarm(&self) -> Result<()> {
2018 let index_reader = LazyIndexReader::new(self.store.clone(), self.ranges_to_files.clone());
2019 let reader = index_reader.get().await?;
2020 let num_pages = reader.num_batches(self.batch_size).await;
2021 let mut pages = stream::iter(0..num_pages)
2022 .map(|page_idx| {
2023 let index_reader = index_reader.clone();
2024 async move {
2025 let page = self
2026 .read_page(page_idx, index_reader, &NoOpMetricsCollector)
2027 .await?;
2028 Result::Ok((page_idx, page))
2029 }
2030 })
2031 .buffer_unordered(get_num_compute_intensive_cpus());
2032
2033 while let Some((page_idx, page)) = pages.try_next().await? {
2034 let inserted = self
2035 .index_cache
2036 .insert_with_key(
2037 &BTreePageKey {
2038 page_number: page_idx,
2039 },
2040 Arc::new(page),
2041 )
2042 .await;
2043
2044 if !inserted {
2045 return Err(Error::internal(
2046 "Failed to prewarm index: cache is no longer available".to_string(),
2047 ));
2048 }
2049 }
2050
2051 Ok(())
2052 }
2053
2054 fn index_type(&self) -> IndexType {
2055 IndexType::BTree
2056 }
2057
2058 fn statistics(&self) -> Result<serde_json::Value> {
2059 let lookup = &self.page_lookup;
2060 let batch = &lookup.batch;
2061 let num_rows = batch.num_rows();
2062 let (min, max) = if lookup.search_start >= num_rows {
2066 (None, None)
2067 } else {
2068 let min = OrderableScalarValue(ScalarValue::try_from_array(
2069 batch.column(0),
2070 lookup.search_start,
2071 )?);
2072 let max =
2073 OrderableScalarValue(ScalarValue::try_from_array(batch.column(1), num_rows - 1)?);
2074 (Some(min), Some(max))
2075 };
2076 serde_json::to_value(&BTreeStatistics {
2077 num_pages: num_rows as u32,
2078 min,
2079 max,
2080 })
2081 .map_err(|err| err.into())
2082 }
2083
2084 async fn calculate_included_frags(&self) -> Result<RoaringBitmap> {
2085 let mut frag_ids = RoaringBitmap::default();
2086
2087 let lazy_reader = LazyIndexReader::new(self.store.clone(), self.ranges_to_files.clone());
2088 let sub_index_reader = lazy_reader.get().await?;
2089 let mut reader_stream = IndexReaderStream::new(sub_index_reader, self.batch_size)
2090 .await
2091 .buffered(self.store.io_parallelism());
2092 while let Some(serialized) = reader_stream.try_next().await? {
2093 let page = FlatIndex::try_new(serialized)?;
2094 frag_ids |= page.calculate_included_frags()?;
2095 }
2096
2097 Ok(frag_ids)
2098 }
2099}
2100
2101#[async_trait]
2102impl ScalarIndex for BTreeIndex {
2103 async fn search(
2104 &self,
2105 query: &dyn AnyQuery,
2106 metrics: &dyn MetricsCollector,
2107 ) -> Result<SearchResult> {
2108 let query = query.as_any().downcast_ref::<SargableQuery>().unwrap();
2109 let mut pages = match query {
2110 SargableQuery::Equals(val) => self
2111 .page_lookup
2112 .pages_eq(&OrderableScalarValue(val.clone())),
2113 SargableQuery::Range(start, end) => self
2114 .page_lookup
2115 .pages_between((wrap_bound(start).as_ref(), wrap_bound(end).as_ref())),
2116 SargableQuery::IsIn(values) => self
2117 .page_lookup
2118 .pages_in(values.iter().map(|val| OrderableScalarValue(val.clone()))),
2119 SargableQuery::FullTextSearch(_) => {
2120 return Err(Error::invalid_input(
2121 "full text search is not supported for BTree index, build a inverted index for it",
2122 ));
2123 }
2124 SargableQuery::IsNull() => Ok(self.page_lookup.pages_null()),
2125 SargableQuery::LikePrefix(prefix) => {
2126 match prefix {
2128 ScalarValue::Utf8(Some(s)) => {
2129 let start = Bound::Included(OrderableScalarValue(prefix.clone()));
2130 let end = match compute_next_prefix(s) {
2131 Some(next) => {
2132 Bound::Excluded(OrderableScalarValue(ScalarValue::Utf8(Some(next))))
2133 }
2134 None => Bound::Unbounded,
2135 };
2136 self.page_lookup
2137 .pages_between((start.as_ref(), end.as_ref()))
2138 }
2139 ScalarValue::LargeUtf8(Some(s)) => {
2140 let start = Bound::Included(OrderableScalarValue(prefix.clone()));
2141 let end = match compute_next_prefix(s) {
2142 Some(next) => Bound::Excluded(OrderableScalarValue(
2143 ScalarValue::LargeUtf8(Some(next)),
2144 )),
2145 None => Bound::Unbounded,
2146 };
2147 self.page_lookup
2148 .pages_between((start.as_ref(), end.as_ref()))
2149 }
2150 _ => {
2151 self.page_lookup
2154 .pages_between((Bound::Unbounded, Bound::Unbounded))
2155 }
2156 }
2157 }
2158 }?;
2159
2160 if !matches!(query, SargableQuery::IsNull()) {
2175 let existing: HashSet<u32> = pages.iter().map(|m| m.page_id()).collect();
2176 for &page_id in self
2177 .page_lookup
2178 .null_pages
2179 .iter()
2180 .chain(self.page_lookup.all_null_pages.iter())
2181 {
2182 if !existing.contains(&page_id) {
2183 pages.push(Matches::Some(page_id));
2184 }
2185 }
2186 }
2187
2188 let prebuilt = match query {
2192 SargableQuery::IsIn(_) => Some(self.compile_predicate(query)?),
2193 _ => None,
2194 };
2195
2196 let lazy_index_reader =
2197 LazyIndexReader::new(self.store.clone(), self.ranges_to_files.clone());
2198 let page_tasks = pages
2199 .into_iter()
2200 .map(|page_index| {
2201 self.search_page(
2202 query,
2203 page_index,
2204 lazy_index_reader.clone(),
2205 prebuilt.as_ref(),
2206 metrics,
2207 )
2208 .boxed()
2209 })
2210 .collect::<Vec<_>>();
2211 debug!("Searching {} btree pages", page_tasks.len());
2212
2213 let results: Vec<NullableRowAddrSet> = stream::iter(page_tasks)
2215 .buffered(get_num_compute_intensive_cpus())
2218 .try_collect()
2219 .await?;
2220
2221 let selection = NullableRowAddrSet::union_all(&results);
2223
2224 Ok(SearchResult::Exact(selection))
2225 }
2226
2227 fn can_remap(&self) -> bool {
2228 true
2229 }
2230
2231 async fn remap(
2232 &self,
2233 mapping: &RowAddrRemap,
2234 dest_store: &dyn IndexStore,
2235 ) -> Result<CreatedIndex> {
2236 let part_page_files: Vec<(Option<u32>, &str)> =
2241 if let Some(ranges_to_files) = &self.ranges_to_files {
2242 ranges_to_files
2244 .iter()
2245 .enumerate()
2246 .map(|(part_id, (_, (path, _)))| (Some(part_id as u32), path.as_str()))
2247 .collect()
2248 } else {
2249 vec![(None, BTREE_PAGES_NAME)]
2251 };
2252
2253 let mapping = Arc::new(mapping.clone());
2254 let train_schema = Arc::new(self.train_schema());
2255 let mut remapped_files = Vec::new();
2256
2257 for (part_id, page_file) in part_page_files {
2259 let sub_index_reader = self.store.open_index_file(page_file).await?;
2261 let mapping = mapping.clone();
2262
2263 let train_schema_clone = train_schema.clone();
2264 let train_schema = train_schema.clone();
2265
2266 let remapped_stream = IndexReaderStream::new(sub_index_reader, self.batch_size)
2267 .await
2268 .buffered(self.store.io_parallelism())
2269 .map_err(DataFusionError::from)
2270 .and_then(move |batch| {
2271 let remapped =
2273 FlatIndex::remap_batch(batch, &mapping).map_err(DataFusionError::from);
2274 let with_train_schema = remapped.and_then(|batch| {
2275 RecordBatch::try_new(train_schema.clone(), batch.columns().to_vec())
2276 .map_err(DataFusionError::from)
2277 });
2278 std::future::ready(with_train_schema)
2279 });
2280
2281 let remapped_stream = Box::pin(RecordBatchStreamAdapter::new(
2282 train_schema_clone,
2283 remapped_stream,
2284 ));
2285
2286 let mut files =
2287 train_btree_index(remapped_stream, dest_store, self.batch_size, None, part_id)
2288 .await?;
2289 remapped_files.append(&mut files);
2290 }
2291
2292 if let Some(ranges_to_files) = &self.ranges_to_files {
2293 let num_parts = ranges_to_files.len();
2294 let page_files = (0..num_parts)
2296 .map(|part_id| part_page_data_file_path((part_id as u64) << 32))
2297 .collect::<Vec<_>>();
2298 let lookup_files = (0..num_parts)
2299 .map(|part_id| part_lookup_file_path((part_id as u64) << 32))
2300 .collect::<Vec<_>>();
2301 let merged_files = merge_metadata_files(
2302 dest_store,
2303 &page_files,
2304 &lookup_files,
2305 None,
2306 noop_progress(),
2307 )
2308 .await?;
2309 remapped_files.retain(|file| file.path.ends_with("_page_data.lance"));
2310 remapped_files.extend(merged_files);
2311 }
2312
2313 Ok(CreatedIndex {
2314 index_details: prost_types::Any::from_msg(&pbold::BTreeIndexDetails::default())
2315 .unwrap(),
2316 index_version: BTREE_INDEX_VERSION,
2317 files: remapped_files,
2318 })
2319 }
2320
2321 async fn update(
2322 &self,
2323 new_data: SendableRecordBatchStream,
2324 dest_store: &dyn IndexStore,
2325 old_data_filter: Option<OldIndexDataFilter>,
2326 ) -> Result<CreatedIndex> {
2327 Self::merge_segments(
2330 &[Arc::new(self.clone())],
2331 new_data,
2332 dest_store,
2333 &[old_data_filter],
2334 )
2335 .await
2336 }
2337
2338 fn update_criteria(&self) -> UpdateCriteria {
2339 UpdateCriteria::only_new_data(TrainingCriteria::new(TrainingOrdering::Values).with_row_id())
2340 }
2341
2342 fn derive_index_params(&self) -> Result<ScalarIndexParams> {
2343 let params = serde_json::to_value(BTreeParameters {
2344 zone_size: Some(self.batch_size),
2345 range_id: None,
2346 })?;
2347 Ok(ScalarIndexParams::for_builtin(BuiltinIndexType::BTree).with_params(¶ms))
2348 }
2349}
2350
2351struct BatchStats {
2352 min: ScalarValue,
2353 max: ScalarValue,
2354 null_count: u32,
2355}
2356
2357fn analyze_batch(batch: &RecordBatch) -> Result<BatchStats> {
2358 let values = batch.column_by_name(VALUE_COLUMN_NAME).expect_ok()?;
2359 if values.is_empty() {
2360 return Err(Error::internal(
2361 "received an empty batch in btree training".to_string(),
2362 ));
2363 }
2364 let min = ScalarValue::try_from_array(&values, 0)
2365 .map_err(|e| Error::internal(format!("failed to get min value from batch: {}", e)))?;
2366 let max = ScalarValue::try_from_array(&values, values.len() - 1)
2367 .map_err(|e| Error::internal(format!("failed to get max value from batch: {}", e)))?;
2368
2369 Ok(BatchStats {
2370 min,
2371 max,
2372 null_count: values.null_count() as u32,
2373 })
2374}
2375
2376#[async_trait]
2378pub trait BTreeSubIndex: Debug + Send + Sync + DeepSizeOf {
2379 async fn train(&self, batch: RecordBatch) -> Result<RecordBatch>;
2381
2382 async fn load_subindex(&self, serialized: RecordBatch) -> Result<Arc<dyn ScalarIndex>>;
2384
2385 async fn retrieve_data(&self, serialized: RecordBatch) -> Result<RecordBatch>;
2392
2393 fn schema(&self) -> &Arc<Schema>;
2395
2396 async fn remap_subindex(
2398 &self,
2399 serialized: RecordBatch,
2400 mapping: &RowAddrRemap,
2401 ) -> Result<RecordBatch>;
2402}
2403
2404struct EncodedBatch {
2405 stats: BatchStats,
2406 page_number: u32,
2407}
2408
2409async fn train_btree_page(
2410 batch: RecordBatch,
2411 batch_idx: u32,
2412 writer: &mut dyn IndexWriter,
2413 schema: Arc<Schema>,
2414) -> Result<EncodedBatch> {
2415 let stats = analyze_batch(&batch)?;
2416
2417 let trained = RecordBatch::try_new(
2419 schema.clone(),
2420 vec![
2421 batch.column_by_name(VALUE_COLUMN_NAME).expect_ok()?.clone(),
2422 batch.column_by_name(ROW_ID).expect_ok()?.clone(),
2423 ],
2424 )?;
2425
2426 writer.write_record_batch(trained).await?;
2427 Ok(EncodedBatch {
2428 stats,
2429 page_number: batch_idx,
2430 })
2431}
2432
2433fn btree_stats_as_batch(stats: Vec<EncodedBatch>, value_type: &DataType) -> Result<RecordBatch> {
2434 let mins = if stats.is_empty() {
2435 new_empty_array(value_type)
2436 } else {
2437 ScalarValue::iter_to_array(stats.iter().map(|stat| stat.stats.min.clone()))?
2438 };
2439 let maxs = if stats.is_empty() {
2440 new_empty_array(value_type)
2441 } else {
2442 ScalarValue::iter_to_array(stats.iter().map(|stat| stat.stats.max.clone()))?
2443 };
2444 let null_counts = UInt32Array::from_iter_values(stats.iter().map(|stat| stat.stats.null_count));
2445 let page_numbers = UInt32Array::from_iter_values(stats.iter().map(|stat| stat.page_number));
2446
2447 let schema = Arc::new(Schema::new(vec![
2448 Field::new("min", mins.data_type().clone(), true),
2450 Field::new("max", maxs.data_type().clone(), true),
2451 Field::new("null_count", null_counts.data_type().clone(), false),
2452 Field::new("page_idx", page_numbers.data_type().clone(), false),
2453 ]));
2454
2455 let columns = vec![
2456 mins,
2457 maxs,
2458 Arc::new(null_counts) as Arc<dyn Array>,
2459 Arc::new(page_numbers) as Arc<dyn Array>,
2460 ];
2461
2462 Ok(RecordBatch::try_new(schema, columns)?)
2463}
2464
2465pub async fn train_btree_index(
2467 batches_source: SendableRecordBatchStream,
2468 index_store: &dyn IndexStore,
2469 batch_size: u64,
2470 fragment_ids: Option<Vec<u32>>,
2471 range_id: Option<u32>,
2472) -> Result<Vec<IndexFile>> {
2473 let partition_id = fragment_ids
2478 .as_ref()
2479 .and_then(|frag_ids| frag_ids.first())
2483 .map(|&first_frag_id| (first_frag_id as u64) << 32)
2484 .or_else(|| range_id.map(|id| (id as u64) << 32));
2488
2489 let flat_schema = Arc::new(Schema::new(vec![
2490 Field::new(
2491 BTREE_VALUES_COLUMN,
2492 batches_source.schema().field(0).data_type().clone(),
2493 true,
2494 ),
2495 Field::new(BTREE_IDS_COLUMN, DataType::UInt64, false),
2496 ]));
2497
2498 let mut sub_index_file = match partition_id {
2499 None => {
2500 index_store
2501 .new_index_file(BTREE_PAGES_NAME, flat_schema.clone())
2502 .await?
2503 }
2504 Some(partition_id) => {
2505 index_store
2506 .new_index_file(
2507 part_page_data_file_path(partition_id).as_str(),
2508 flat_schema.clone(),
2509 )
2510 .await?
2511 }
2512 };
2513
2514 let mut encoded_batches = Vec::new();
2515 let mut batch_idx = 0;
2516
2517 let value_type = batches_source
2518 .schema()
2519 .field_with_name(VALUE_COLUMN_NAME)?
2520 .data_type()
2521 .clone();
2522
2523 let mut batches_source = chunk_concat_stream(batches_source, batch_size as usize);
2524
2525 while let Some(batch) = batches_source.try_next().await? {
2526 encoded_batches.push(
2527 train_btree_page(
2528 batch,
2529 batch_idx,
2530 sub_index_file.as_mut(),
2531 flat_schema.clone(),
2532 )
2533 .await?,
2534 );
2535 batch_idx += 1;
2536 }
2537 let pages_file = sub_index_file.finish().await?;
2538 let record_batch = btree_stats_as_batch(encoded_batches, &value_type)?;
2539 let mut file_schema = record_batch.schema().as_ref().clone();
2540 file_schema
2541 .metadata
2542 .insert(BATCH_SIZE_META_KEY.to_string(), batch_size.to_string());
2543 file_schema.metadata.insert(
2544 RANGE_PARTITIONED_META_KEY.to_string(),
2545 range_id.is_some().to_string(),
2546 );
2547 let mut btree_index_file = match partition_id {
2548 None => {
2549 index_store
2550 .new_index_file(BTREE_LOOKUP_NAME, Arc::new(file_schema))
2551 .await?
2552 }
2553 Some(partition_id) => {
2554 index_store
2555 .new_index_file(
2556 part_lookup_file_path(partition_id).as_str(),
2557 Arc::new(file_schema),
2558 )
2559 .await?
2560 }
2561 };
2562 btree_index_file.write_record_batch(record_batch).await?;
2563 let lookup_file = btree_index_file.finish().await?;
2564 Ok(vec![pages_file, lookup_file])
2565}
2566
2567fn find_single_partition_files(files: &[super::IndexFile]) -> Result<Option<(&str, &str)>> {
2568 let lookup_files = files
2569 .iter()
2570 .filter_map(|file| {
2571 (file.path.starts_with("part_") && file.path.ends_with("_page_lookup.lance"))
2572 .then_some(file.path.as_str())
2573 })
2574 .collect::<Vec<_>>();
2575 let page_files = files
2576 .iter()
2577 .filter_map(|file| {
2578 (file.path.starts_with("part_") && file.path.ends_with("_page_data.lance"))
2579 .then_some(file.path.as_str())
2580 })
2581 .collect::<Vec<_>>();
2582
2583 if lookup_files.len() != 1 || page_files.len() != 1 {
2584 return Ok(None);
2585 }
2586
2587 let lookup_partition_id = extract_partition_id(lookup_files[0])?;
2588 let page_partition_id = extract_partition_id(page_files[0])?;
2589 if lookup_partition_id != page_partition_id {
2590 return Ok(None);
2591 }
2592
2593 Ok(Some((lookup_files[0], page_files[0])))
2594}
2595
2596fn is_missing_lookup_error(err: &Error) -> bool {
2597 matches!(err, Error::NotFound { .. })
2598 || matches!(
2599 err,
2600 Error::IO { source, .. }
2601 if source
2602 .downcast_ref::<ObjectStoreError>()
2603 .map(|os_err| matches!(os_err, ObjectStoreError::NotFound { .. }))
2604 .unwrap_or(false)
2605 )
2606}
2607
2608async fn merge_metadata_files(
2615 store: &dyn IndexStore,
2616 part_page_files: &[String],
2617 part_lookup_files: &[String],
2618 batch_readhead: Option<usize>,
2619 progress: Arc<dyn IndexBuildProgress>,
2620) -> Result<Vec<IndexFile>> {
2621 if part_lookup_files.is_empty() || part_page_files.is_empty() {
2622 return Err(Error::internal(
2623 "No partition files provided for merging".to_string(),
2624 ));
2625 }
2626
2627 if part_lookup_files.len() != part_page_files.len() {
2629 return Err(Error::internal(format!(
2630 "Number of partition lookup files ({}) does not match number of partition page files ({})",
2631 part_lookup_files.len(),
2632 part_page_files.len()
2633 )));
2634 }
2635 let mut page_files_map = HashMap::new();
2636 for page_file in part_page_files {
2637 let partition_id = extract_partition_id(page_file)?;
2638 page_files_map.insert(partition_id, page_file);
2639 }
2640
2641 for lookup_file in part_lookup_files {
2643 let partition_id = extract_partition_id(lookup_file)?;
2644 if !page_files_map.contains_key(&partition_id) {
2645 return Err(Error::internal(format!(
2646 "No corresponding page file found for lookup file: {} (partition_id: {})",
2647 lookup_file, partition_id
2648 )));
2649 }
2650 }
2651
2652 let first_lookup_reader = store.open_index_file(&part_lookup_files[0]).await?;
2654 let batch_size = first_lookup_reader
2655 .schema()
2656 .metadata
2657 .get(BATCH_SIZE_META_KEY)
2658 .map(|bs| bs.parse().unwrap_or(DEFAULT_BTREE_BATCH_SIZE))
2659 .unwrap_or(DEFAULT_BTREE_BATCH_SIZE);
2660 let range_partitioned = first_lookup_reader
2661 .schema()
2662 .metadata
2663 .get(RANGE_PARTITIONED_META_KEY)
2664 .map(|bs| bs.parse().unwrap_or(DEFAULT_RANGE_PARTITIONED))
2665 .unwrap_or(DEFAULT_RANGE_PARTITIONED);
2666
2667 let value_type = first_lookup_reader
2669 .schema()
2670 .fields
2671 .first()
2672 .unwrap()
2673 .data_type();
2674
2675 let mut metadata = HashMap::new();
2676 metadata.insert(BATCH_SIZE_META_KEY.to_string(), batch_size.to_string());
2677 let lookup_schema = Arc::new(Schema::new(vec![
2678 Field::new("min", value_type.clone(), true),
2679 Field::new("max", value_type.clone(), true),
2680 Field::new("null_count", DataType::UInt32, false),
2681 Field::new("page_idx", DataType::UInt32, false),
2682 ]));
2683
2684 if range_partitioned {
2686 merge_range_partitioned_lookups(
2687 store,
2688 part_lookup_files,
2689 lookup_schema,
2690 metadata,
2691 batch_size,
2692 batch_readhead,
2693 progress,
2694 )
2695 .await
2696 .map(|file| vec![file])
2697 } else {
2698 merge_pages_and_lookups(
2699 store,
2700 part_page_files,
2701 part_lookup_files,
2702 &page_files_map,
2703 lookup_schema,
2704 metadata,
2705 batch_size,
2706 batch_readhead,
2707 progress,
2708 )
2709 .await
2710 }
2711}
2712
2713async fn merge_range_partitioned_lookups(
2743 store: &dyn IndexStore,
2744 part_lookup_files: &[String],
2745 lookup_schema: Arc<Schema>,
2746 mut metadata: HashMap<String, String>,
2747 batch_size: u64,
2748 batch_readhead: Option<usize>,
2749 progress: Arc<dyn IndexBuildProgress>,
2750) -> Result<IndexFile> {
2751 let sorted_part_lookup_files = sort_files_by_partition_id(part_lookup_files)?;
2752 let mut lookup_file = store
2753 .new_index_file(BTREE_LOOKUP_NAME, lookup_schema)
2754 .await?;
2755
2756 let mut pages_per_file: Vec<(u64, u32)> = Vec::with_capacity(sorted_part_lookup_files.len());
2758 let mut num_pages_written = 0u32;
2759
2760 progress
2761 .stage_start(
2762 "merge_lookups",
2763 Some(sorted_part_lookup_files.len() as u64),
2764 "files",
2765 )
2766 .await?;
2767
2768 for (idx, (part_id, part_lookup_file)) in sorted_part_lookup_files.into_iter().enumerate() {
2769 let lookup_reader = store.open_index_file(&part_lookup_file).await?;
2770 let reader_stream = IndexReaderStream::new(lookup_reader.clone(), batch_size).await;
2771 let mut stream = reader_stream.buffered(batch_readhead.unwrap_or(1)).boxed();
2772 while let Some(batch) = stream.next().await {
2773 let original_batch = batch?;
2774 let modified_batch = add_offset_to_page_idx(&original_batch, num_pages_written)?;
2775 lookup_file.write_record_batch(modified_batch).await?;
2776 }
2777 pages_per_file.push((part_id, lookup_reader.num_rows() as u32));
2778 num_pages_written += lookup_reader.num_rows() as u32;
2779 progress
2780 .stage_progress("merge_lookups", idx as u64 + 1)
2781 .await?;
2782 }
2783
2784 metadata.insert(RANGE_PARTITIONED_META_KEY.to_string(), "true".to_string());
2785 metadata.insert(
2786 PAGE_NUM_PER_RANGE_PARTITION_META_KEY.to_string(),
2787 serde_json::to_string(&pages_per_file)?,
2788 );
2789
2790 let lookup_file = lookup_file.finish_with_metadata(metadata).await?;
2791 progress.stage_complete("merge_lookups").await?;
2792
2793 cleanup_partition_files(store, part_lookup_files, &[]).await;
2795 Ok(lookup_file)
2796}
2797
2798#[allow(clippy::too_many_arguments)]
2804async fn merge_pages_and_lookups(
2805 store: &dyn IndexStore,
2806 part_page_files: &[String],
2807 part_lookup_files: &[String],
2808 page_files_map: &HashMap<u64, &String>,
2809 lookup_schema: Arc<Schema>,
2810 metadata: HashMap<String, String>,
2811 batch_size: u64,
2812 batch_readhead: Option<usize>,
2813 progress: Arc<dyn IndexBuildProgress>,
2814) -> Result<Vec<IndexFile>> {
2815 let partition_id = extract_partition_id(part_lookup_files[0].as_str())?;
2817 let page_file = page_files_map.get(&partition_id).unwrap();
2818 let page_reader = store.open_index_file(page_file).await?;
2819 let page_schema = page_reader.schema().clone();
2820
2821 let arrow_schema = Arc::new(Schema::from(&page_schema));
2822 let mut page_file = store
2823 .new_index_file(BTREE_PAGES_NAME, arrow_schema.clone())
2824 .await?;
2825 progress.stage_start("merge_pages", None, "pages").await?;
2826 let lookup_entries = merge_pages(
2827 part_lookup_files,
2828 page_files_map,
2829 store,
2830 batch_size,
2831 &mut page_file,
2832 arrow_schema.clone(),
2833 batch_readhead,
2834 progress.clone(),
2835 )
2836 .await?;
2837 let page_file = page_file.finish().await?;
2838 progress.stage_complete("merge_pages").await?;
2839
2840 let lookup_batch = RecordBatch::try_new(
2841 lookup_schema.clone(),
2842 vec![
2843 ScalarValue::iter_to_array(lookup_entries.iter().map(|(min, _, _, _)| min.clone()))?,
2844 ScalarValue::iter_to_array(lookup_entries.iter().map(|(_, max, _, _)| max.clone()))?,
2845 Arc::new(UInt32Array::from_iter_values(
2846 lookup_entries
2847 .iter()
2848 .map(|(_, _, null_count, _)| *null_count),
2849 )),
2850 Arc::new(UInt32Array::from_iter_values(
2851 lookup_entries.iter().map(|(_, _, _, page_idx)| *page_idx),
2852 )),
2853 ],
2854 )?;
2855 let mut lookup_file = store
2856 .new_index_file(BTREE_LOOKUP_NAME, lookup_schema)
2857 .await?;
2858 progress
2859 .stage_start("write_lookup_file", Some(1), "files")
2860 .await?;
2861 lookup_file.write_record_batch(lookup_batch).await?;
2862 let lookup_file = lookup_file.finish_with_metadata(metadata).await?;
2863 progress.stage_progress("write_lookup_file", 1).await?;
2864 progress.stage_complete("write_lookup_file").await?;
2865
2866 cleanup_partition_files(store, part_lookup_files, part_page_files).await;
2869
2870 Ok(vec![page_file, lookup_file])
2871}
2872
2873fn add_offset_to_page_idx(batch: &RecordBatch, offset: u32) -> Result<RecordBatch> {
2875 let (page_idx_pos, _) = batch.schema().column_with_name("page_idx").ok_or_else(|| {
2876 Error::internal("Column 'page_idx' not found in RecordBatch schema".to_string())
2877 })?;
2878 let page_idx_array = batch
2879 .column(page_idx_pos)
2880 .as_any()
2881 .downcast_ref::<UInt32Array>()
2882 .ok_or_else(|| {
2883 Error::internal("Failed to downcast 'page_idx' column to UInt32Array".to_string())
2884 })?;
2885 let offset_array = UInt32Array::from(vec![offset; page_idx_array.len()]);
2886 let new_page_idx_array_ref = add(page_idx_array, &offset_array)?;
2887 let mut new_columns = batch.columns().to_vec();
2888 new_columns[page_idx_pos] = new_page_idx_array_ref;
2889 let new_batch = RecordBatch::try_new(batch.schema(), new_columns)?;
2890 Ok(new_batch)
2891}
2892
2893#[allow(clippy::too_many_arguments)]
2896async fn merge_pages(
2897 part_lookup_files: &[String],
2898 page_files_map: &HashMap<u64, &String>,
2899 store: &dyn IndexStore,
2900 batch_size: u64,
2901 page_file: &mut Box<dyn IndexWriter>,
2902 arrow_schema: Arc<Schema>,
2903 batch_readhead: Option<usize>,
2904 progress: Arc<dyn IndexBuildProgress>,
2905) -> Result<Vec<(ScalarValue, ScalarValue, u32, u32)>> {
2906 let mut lookup_entries = Vec::new();
2907 let mut page_idx = 0u32;
2908
2909 debug!(
2910 "Starting SortPreservingMerge with {} partitions",
2911 part_lookup_files.len()
2912 );
2913
2914 let value_field = arrow_schema.field(0).clone().with_name(VALUE_COLUMN_NAME);
2915 let row_id_field = arrow_schema.field(1).clone().with_name(ROW_ID);
2916 let stream_schema = Arc::new(Schema::new(vec![value_field, row_id_field]));
2917
2918 let mut inputs: Vec<Arc<dyn ExecutionPlan>> = Vec::new();
2920 for lookup_file in part_lookup_files {
2921 let partition_id = extract_partition_id(lookup_file)?;
2922 let page_file_name = (*page_files_map.get(&partition_id).ok_or_else(|| {
2923 Error::internal(format!(
2924 "Page file not found for partition ID: {}",
2925 partition_id
2926 ))
2927 })?)
2928 .clone();
2929
2930 let reader = store.open_index_file(&page_file_name).await?;
2931
2932 let reader_stream = IndexReaderStream::new(reader, batch_size).await;
2933
2934 let stream = reader_stream
2935 .map(|fut| fut.map_err(DataFusionError::from))
2936 .buffered(batch_readhead.unwrap_or(1))
2937 .boxed();
2938
2939 let sendable_stream =
2940 Box::pin(RecordBatchStreamAdapter::new(stream_schema.clone(), stream));
2941 inputs.push(Arc::new(OneShotExec::new(sendable_stream)));
2942 }
2943
2944 let union_inputs = UnionExec::try_new(inputs)?;
2946
2947 let value_column_index = stream_schema.index_of(VALUE_COLUMN_NAME)?;
2949 let sort_expr = PhysicalSortExpr {
2950 expr: Arc::new(Column::new(VALUE_COLUMN_NAME, value_column_index)),
2951 options: SortOptions {
2952 descending: false,
2953 nulls_first: true,
2954 },
2955 };
2956
2957 let merge_exec = Arc::new(SortPreservingMergeExec::new(
2958 [sort_expr].into(),
2959 union_inputs,
2960 ));
2961
2962 let unchunked = execute_plan(
2963 merge_exec,
2964 LanceExecutionOptions {
2965 use_spilling: false,
2966 ..Default::default()
2967 },
2968 )?;
2969
2970 let mut chunked_stream = chunk_concat_stream(unchunked, batch_size as usize);
2972
2973 while let Some(batch) = chunked_stream.try_next().await? {
2975 let writer_batch = RecordBatch::try_new(
2976 arrow_schema.clone(),
2977 vec![batch.column(0).clone(), batch.column(1).clone()],
2978 )?;
2979
2980 page_file.write_record_batch(writer_batch).await?;
2981
2982 let min_val = ScalarValue::try_from_array(batch.column(0), 0)?;
2983 let max_val = ScalarValue::try_from_array(batch.column(0), batch.num_rows() - 1)?;
2984 let null_count = batch.column(0).null_count() as u32;
2985
2986 lookup_entries.push((min_val, max_val, null_count, page_idx));
2987 page_idx += 1;
2988 progress
2989 .stage_progress("merge_pages", page_idx as u64)
2990 .await?;
2991 }
2992
2993 Ok(lookup_entries)
2994}
2995
2996fn sort_files_by_partition_id(part_files: &[String]) -> Result<Vec<(u64, String)>> {
2998 let mut files_with_ids: Vec<(u64, &String)> = part_files
2999 .iter()
3000 .map(|file| extract_partition_id(file).map(|id| (id, file)))
3001 .collect::<Result<Vec<_>>>()?;
3002
3003 files_with_ids.sort_unstable_by_key(|k| k.0);
3004
3005 let sorted_files = files_with_ids
3006 .into_iter()
3007 .map(|(id, file)| (id, file.clone()))
3008 .collect();
3009
3010 Ok(sorted_files)
3011}
3012
3013fn extract_partition_id(filename: &str) -> Result<u64> {
3016 if !filename.starts_with("part_") {
3017 return Err(Error::internal(format!(
3018 "Invalid partition file name format: {}",
3019 filename
3020 )));
3021 }
3022
3023 let parts: Vec<&str> = filename.split('_').collect();
3024 if parts.len() < 3 {
3025 return Err(Error::internal(format!(
3026 "Invalid partition file name format: {}",
3027 filename
3028 )));
3029 }
3030
3031 parts[1].parse::<u64>().map_err(|_| {
3032 Error::internal(format!(
3033 "Failed to parse partition ID from filename: {}",
3034 filename
3035 ))
3036 })
3037}
3038
3039async fn cleanup_partition_files(
3044 store: &dyn IndexStore,
3045 part_lookup_files: &[String],
3046 part_page_files: &[String],
3047) {
3048 for file_name in part_lookup_files {
3050 cleanup_single_file(
3051 store,
3052 file_name,
3053 "part_",
3054 "_page_lookup.lance",
3055 "partition lookup",
3056 )
3057 .await;
3058 }
3059
3060 for file_name in part_page_files {
3062 cleanup_single_file(
3063 store,
3064 file_name,
3065 "part_",
3066 "_page_data.lance",
3067 "partition page",
3068 )
3069 .await;
3070 }
3071}
3072
3073async fn cleanup_single_file(
3077 store: &dyn IndexStore,
3078 file_name: &str,
3079 expected_prefix: &str,
3080 expected_suffix: &str,
3081 file_type: &str,
3082) {
3083 if file_name.starts_with(expected_prefix) && file_name.ends_with(expected_suffix) {
3084 match store.delete_index_file(file_name).await {
3085 Ok(()) => {
3086 debug!("Successfully deleted {} file: {}", file_type, file_name);
3087 }
3088 Err(e) => {
3089 warn!(
3090 "Failed to delete {} file '{}': {}. \
3091 This does not affect the merge operation, but may leave \
3092 partition files that should be cleaned up manually.",
3093 file_type, file_name, e
3094 );
3095 }
3096 }
3097 } else {
3098 warn!(
3100 "Skipping deletion of file '{}' as it does not match the expected \
3101 {} file pattern ({}*{})",
3102 file_name, file_type, expected_prefix, expected_suffix
3103 );
3104 }
3105}
3106
3107pub(crate) fn part_page_data_file_path(partition_id: u64) -> String {
3108 format!("part_{}_{}", partition_id, BTREE_PAGES_NAME)
3109}
3110
3111pub(crate) fn part_lookup_file_path(partition_id: u64) -> String {
3112 format!("part_{}_{}", partition_id, BTREE_LOOKUP_NAME)
3113}
3114
3115struct IndexReaderStream {
3119 reader: Arc<dyn IndexReader>,
3120 batch_size: u64,
3121 num_batches: u32,
3122 batch_idx: u32,
3123}
3124
3125impl IndexReaderStream {
3126 async fn new(reader: Arc<dyn IndexReader>, batch_size: u64) -> Self {
3127 let num_batches = reader.num_batches(batch_size).await;
3128 Self {
3129 reader,
3130 batch_size,
3131 num_batches,
3132 batch_idx: 0,
3133 }
3134 }
3135}
3136
3137impl Stream for IndexReaderStream {
3138 type Item = BoxFuture<'static, Result<RecordBatch>>;
3139
3140 fn poll_next(
3141 self: std::pin::Pin<&mut Self>,
3142 _cx: &mut std::task::Context<'_>,
3143 ) -> std::task::Poll<Option<Self::Item>> {
3144 let this = self.get_mut();
3145 if this.batch_idx >= this.num_batches {
3146 return std::task::Poll::Ready(None);
3147 }
3148 let batch_num = this.batch_idx;
3149 this.batch_idx += 1;
3150 let reader_copy = this.reader.clone();
3151 let batch_size = this.batch_size;
3152 let read_task = async move {
3153 reader_copy
3154 .read_record_batch(batch_num as u64, batch_size)
3155 .await
3156 }
3157 .boxed();
3158 std::task::Poll::Ready(Some(read_task))
3159 }
3160}
3161
3162#[derive(Debug, Serialize, Deserialize)]
3164pub struct BTreeParameters {
3165 pub zone_size: Option<u64>,
3167
3168 pub range_id: Option<u32>,
3182}
3183
3184struct BTreeTrainingRequest {
3185 parameters: BTreeParameters,
3186 criteria: TrainingCriteria,
3187}
3188
3189impl BTreeTrainingRequest {
3190 pub fn new(parameters: BTreeParameters) -> Self {
3191 Self {
3192 parameters,
3193 criteria: TrainingCriteria::new(TrainingOrdering::Values).with_row_id(),
3195 }
3196 }
3197}
3198
3199impl TrainingRequest for BTreeTrainingRequest {
3200 fn as_any(&self) -> &dyn std::any::Any {
3201 self
3202 }
3203
3204 fn criteria(&self) -> &TrainingCriteria {
3205 &self.criteria
3206 }
3207}
3208
3209#[derive(Debug, Default)]
3210pub struct BTreeIndexPlugin;
3211
3212#[async_trait]
3213impl BasicTrainer for BTreeIndexPlugin {
3214 fn new_training_request(
3215 &self,
3216 params: &str,
3217 field: &Field,
3218 ) -> Result<Box<dyn TrainingRequest>> {
3219 if field.data_type().is_nested() {
3220 return Err(Error::invalid_input_source(
3221 "A btree index can only be created on a non-nested field.".into(),
3222 ));
3223 }
3224
3225 let params = serde_json::from_str::<BTreeParameters>(params)?;
3226 Ok(Box::new(BTreeTrainingRequest::new(params)))
3227 }
3228
3229 async fn train_index(
3230 &self,
3231 data: SendableRecordBatchStream,
3232 index_store: &dyn IndexStore,
3233 request: Box<dyn TrainingRequest>,
3234 _fragment_ids: Option<Vec<u32>>,
3235 _progress: Arc<dyn crate::progress::IndexBuildProgress>,
3236 ) -> Result<CreatedIndex> {
3237 let request = request
3238 .as_any()
3239 .downcast_ref::<BTreeTrainingRequest>()
3240 .unwrap();
3241 if request.parameters.range_id.is_some() {
3242 warn!(
3248 "BTree `range_id` is deprecated and now ignored; a pre-sorted data \
3249 stream is still supported, but `range_id` no longer needs to be passed. \
3250 Use the segmented-index APIs instead (build per-fragment segments, then \
3251 commit_existing_index_segments(...) / merge_existing_index_segments(...)). \
3252 The `range_id` field will be removed in a future release."
3253 );
3254 }
3255 let files = train_btree_index(
3256 data,
3257 index_store,
3258 request
3259 .parameters
3260 .zone_size
3261 .unwrap_or(DEFAULT_BTREE_BATCH_SIZE),
3262 None,
3263 None,
3264 )
3265 .await?;
3266 Ok(CreatedIndex {
3267 index_details: prost_types::Any::from_msg(&pbold::BTreeIndexDetails::default())
3268 .unwrap(),
3269 index_version: BTREE_INDEX_VERSION,
3270 files,
3271 })
3272 }
3273}
3274
3275#[async_trait]
3276impl ScalarIndexPlugin for BTreeIndexPlugin {
3277 fn basic_trainer(&self) -> Option<&dyn BasicTrainer> {
3278 Some(self)
3279 }
3280
3281 fn name(&self) -> &str {
3282 "BTree"
3283 }
3284
3285 fn provides_exact_answer(&self) -> bool {
3286 true
3287 }
3288
3289 fn version(&self) -> u32 {
3290 BTREE_INDEX_VERSION
3291 }
3292
3293 fn new_query_parser(
3294 &self,
3295 index_name: String,
3296 _index_details: &prost_types::Any,
3297 ) -> Option<Box<dyn ScalarQueryParser>> {
3298 Some(Box::new(SargableQueryParser::new(
3299 index_name,
3300 self.name().to_string(),
3301 false,
3302 )))
3303 }
3304
3305 async fn load_index(
3306 &self,
3307 index_store: Arc<dyn IndexStore>,
3308 _index_details: &prost_types::Any,
3309 frag_reuse_index: Option<Arc<dyn RowIdRemapper>>,
3310 cache: &LanceCache,
3311 ) -> Result<Arc<dyn ScalarIndex>> {
3312 Ok(BTreeIndex::load(index_store, frag_reuse_index, cache).await? as Arc<dyn ScalarIndex>)
3313 }
3314
3315 async fn get_from_cache(
3316 &self,
3317 index_store: Arc<dyn IndexStore>,
3318 frag_reuse_index: Option<Arc<dyn RowIdRemapper>>,
3319 cache: &LanceCache,
3320 ) -> Result<Option<Arc<dyn ScalarIndex>>> {
3321 let Some(state) = cache.get_with_key(&BTreeIndexStateKey).await else {
3322 return Ok(None);
3323 };
3324 Ok(Some(state.reconstruct(
3325 index_store,
3326 cache,
3327 frag_reuse_index,
3328 )?))
3329 }
3330
3331 async fn put_in_cache(&self, cache: &LanceCache, index: Arc<dyn ScalarIndex>) -> Result<()> {
3332 let state = BTreeIndexState::from_index(index.as_ref())?;
3333 cache
3334 .insert_with_key(&BTreeIndexStateKey, Arc::new(state))
3335 .await;
3336 Ok(())
3337 }
3338
3339 async fn get_or_insert_in_cache(
3340 &self,
3341 index_store: Arc<dyn IndexStore>,
3342 frag_reuse_index: Option<Arc<dyn RowIdRemapper>>,
3343 cache: &LanceCache,
3344 load: ScalarIndexLoad<'_>,
3345 ) -> Result<Arc<dyn ScalarIndex>> {
3346 single_flight_open(
3347 cache,
3348 BTreeIndexStateKey,
3349 load,
3350 BTreeIndexState::from_index,
3351 move |state| state.reconstruct(index_store, cache, frag_reuse_index),
3352 )
3353 .await
3354 }
3355}
3356
3357#[cfg(test)]
3358mod tests {
3359 use lance_core::utils::row_addr_remap::RowAddrRemap;
3360 use std::sync::atomic::Ordering;
3361 use std::{collections::HashMap, sync::Arc};
3362
3363 use arrow::datatypes::{Float32Type, Float64Type, Int32Type, UInt64Type};
3364 use arrow_array::{FixedSizeListArray, record_batch};
3365 use datafusion::{
3366 execution::{SendableRecordBatchStream, TaskContext},
3367 physical_plan::{ExecutionPlan, sorts::sort::SortExec, stream::RecordBatchStreamAdapter},
3368 };
3369 use datafusion_common::{DataFusionError, ScalarValue};
3370 use datafusion_physical_expr::{PhysicalSortExpr, expressions::col};
3371 use futures::TryStreamExt;
3372 use futures::stream;
3373 use lance_core::cache::LanceCache;
3374 use lance_core::deepsize::DeepSizeOf;
3375 use lance_core::utils::tempfile::TempObjDir;
3376 use lance_datafusion::{chunker::break_stream, datagen::DatafusionDatagenExt};
3377 use lance_datagen::{ArrayGeneratorExt, BatchCount, RowCount, array, gen_batch};
3378 use lance_io::object_store::ObjectStore;
3379 use lance_select::{RowAddrTreeMap, RowSetOps};
3380 use object_store::path::Path;
3381
3382 use crate::metrics::LocalMetricsCollector;
3383 use crate::progress::{IndexBuildProgress, noop_progress};
3384 use crate::{
3385 metrics::NoOpMetricsCollector,
3386 scalar::{
3387 IndexStore, OldIndexDataFilter, SargableQuery, ScalarIndex, SearchResult,
3388 btree::{BTREE_PAGES_NAME, BTreeIndex},
3389 lance_format::LanceIndexStore,
3390 },
3391 };
3392
3393 use super::{
3394 BTreeIndexPlugin, BTreeIndexState, BTreeLookup, BTreePageKey, DEFAULT_BTREE_BATCH_SIZE,
3395 Matches, OrderableScalarValue, part_lookup_file_path, part_page_data_file_path,
3396 train_btree_index,
3397 };
3398 use crate::scalar::registry::ScalarIndexPlugin;
3399 use arrow_array::RecordBatch;
3400 use lance_core::cache::{CacheCodecImpl, CacheEntryReader, CacheEntryWriter, CacheKey};
3401
3402 fn serialize_state(state: &BTreeIndexState) -> Vec<u8> {
3404 let mut buf = Vec::new();
3405 state
3406 .serialize(&mut CacheEntryWriter::new(&mut buf))
3407 .unwrap();
3408 buf
3409 }
3410
3411 fn deserialize_state(buf: Vec<u8>) -> lance_core::Result<BTreeIndexState> {
3413 let data = bytes::Bytes::from(buf);
3414 let mut reader = CacheEntryReader::new(&data, 0, BTreeIndexState::CURRENT_VERSION);
3415 BTreeIndexState::deserialize(&mut reader)
3416 }
3417 use rangemap::RangeInclusiveMap;
3418
3419 lance_testing::define_stage_event_progress!(
3420 RecordingProgress,
3421 IndexBuildProgress,
3422 lance_core::Result<()>
3423 );
3424 #[test]
3425 fn test_scalar_value_size() {
3426 let size_of_i32 = OrderableScalarValue(ScalarValue::Int32(Some(0))).deep_size_of();
3427 let size_of_many_i32 = OrderableScalarValue(ScalarValue::FixedSizeList(Arc::new(
3428 FixedSizeListArray::from_iter_primitive::<Int32Type, _, _>(
3429 vec![Some(vec![Some(0); 128])],
3430 128,
3431 ),
3432 )))
3433 .deep_size_of();
3434
3435 assert!(size_of_i32 > 4);
3437 assert!(size_of_many_i32 > 128 * 4);
3438 }
3439
3440 #[test]
3441 fn test_orderable_dictionary_cmp() {
3442 use arrow_schema::DataType;
3443 use std::cmp::Ordering;
3444
3445 let dict = |s: &str, key: DataType| {
3446 OrderableScalarValue(ScalarValue::Dictionary(
3447 Box::new(key),
3448 Box::new(ScalarValue::Utf8(Some(s.to_string()))),
3449 ))
3450 };
3451
3452 assert_eq!(
3456 dict("a", DataType::Int16).cmp(&dict("b", DataType::Int16)),
3457 Ordering::Less
3458 );
3459 assert_eq!(
3460 dict("b", DataType::Int32).cmp(&dict("b", DataType::Int16)),
3461 Ordering::Equal
3462 );
3463
3464 assert_eq!(
3466 dict("a", DataType::Int16).cmp(&OrderableScalarValue(ScalarValue::Null)),
3467 Ordering::Greater
3468 );
3469 }
3470
3471 #[tokio::test]
3472 async fn test_null_ids() {
3473 let tmpdir = TempObjDir::default();
3474 let test_store = Arc::new(LanceIndexStore::new(
3475 Arc::new(ObjectStore::local()),
3476 tmpdir.clone(),
3477 Arc::new(LanceCache::no_cache()),
3478 ));
3479
3480 let stream = gen_batch()
3482 .col(
3483 "value",
3484 array::rand::<Float32Type>().with_nulls(&[true, false, false, false, false]),
3485 )
3486 .col("_rowid", array::step::<UInt64Type>())
3487 .into_df_stream(RowCount::from(5000), BatchCount::from(10));
3488
3489 train_btree_index(stream, test_store.as_ref(), 5000, None, None)
3490 .await
3491 .unwrap();
3492
3493 let index = BTreeIndex::load(test_store.clone(), None, &LanceCache::no_cache())
3494 .await
3495 .unwrap();
3496
3497 assert_eq!(index.page_lookup.null_pages.len(), 10);
3498
3499 let remap_dir = TempObjDir::default();
3500 let remap_store = Arc::new(LanceIndexStore::new(
3501 Arc::new(ObjectStore::local()),
3502 remap_dir.clone(),
3503 Arc::new(LanceCache::no_cache()),
3504 ));
3505
3506 index
3508 .remap(&RowAddrRemap::empty(), remap_store.as_ref())
3509 .await
3510 .unwrap();
3511
3512 let remap_index = BTreeIndex::load(remap_store.clone(), None, &LanceCache::no_cache())
3513 .await
3514 .unwrap();
3515
3516 assert_eq!(remap_index.page_lookup, index.page_lookup);
3517
3518 let original_pages = test_store.open_index_file(BTREE_PAGES_NAME).await.unwrap();
3519 let remapped_pages = remap_store.open_index_file(BTREE_PAGES_NAME).await.unwrap();
3520
3521 assert_eq!(original_pages.num_rows(), remapped_pages.num_rows());
3522
3523 let original_data = original_pages
3524 .read_record_batch(0, original_pages.num_rows() as u64)
3525 .await
3526 .unwrap();
3527 let remapped_data = remapped_pages
3528 .read_record_batch(0, remapped_pages.num_rows() as u64)
3529 .await
3530 .unwrap();
3531
3532 assert_eq!(original_data, remapped_data);
3533 }
3534
3535 #[tokio::test]
3536 async fn test_nan_ordering() {
3537 let tmpdir = TempObjDir::default();
3538 let test_store = Arc::new(LanceIndexStore::new(
3539 Arc::new(ObjectStore::local()),
3540 tmpdir.clone(),
3541 Arc::new(LanceCache::no_cache()),
3542 ));
3543
3544 let values = vec![
3545 0.0,
3546 1.0,
3547 2.0,
3548 3.0,
3549 f64::NAN,
3550 f64::NEG_INFINITY,
3551 f64::INFINITY,
3552 ];
3553
3554 let data = gen_batch()
3558 .col("value", array::cycle::<Float64Type>(values.clone()))
3559 .col("_rowid", array::step::<UInt64Type>())
3560 .into_df_exec(RowCount::from(10), BatchCount::from(100));
3561 let schema = data.schema();
3562 let sort_expr = PhysicalSortExpr::new_default(col("value", schema.as_ref()).unwrap());
3563 let plan = Arc::new(SortExec::new([sort_expr].into(), data));
3564 let stream = plan.execute(0, Arc::new(TaskContext::default())).unwrap();
3565 let stream = break_stream(stream, 64);
3566 let stream = stream.map_err(DataFusionError::from);
3567 let stream =
3568 Box::pin(RecordBatchStreamAdapter::new(schema, stream)) as SendableRecordBatchStream;
3569
3570 train_btree_index(stream, test_store.as_ref(), 64, None, None)
3571 .await
3572 .unwrap();
3573
3574 let index = BTreeIndex::load(test_store, None, &LanceCache::no_cache())
3575 .await
3576 .unwrap();
3577
3578 for (idx, value) in values.into_iter().enumerate() {
3579 let query = SargableQuery::Equals(ScalarValue::Float64(Some(value)));
3580 let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
3581 assert_eq!(
3582 result,
3583 SearchResult::exact(RowAddrTreeMap::from_iter(((idx as u64)..1000).step_by(7)))
3584 );
3585 }
3586 }
3587
3588 #[tokio::test]
3589 async fn test_contains_keys_matches_search() {
3590 let tmpdir = TempObjDir::default();
3591 let test_store = Arc::new(LanceIndexStore::new(
3592 Arc::new(ObjectStore::local()),
3593 tmpdir.clone(),
3594 Arc::new(LanceCache::no_cache()),
3595 ));
3596
3597 let data = gen_batch()
3600 .col("value", array::step::<Int32Type>())
3601 .col("_rowid", array::step::<UInt64Type>())
3602 .into_df_exec(RowCount::from(100), BatchCount::from(10));
3603 let schema = data.schema();
3604 let sort_expr = PhysicalSortExpr::new_default(col("value", schema.as_ref()).unwrap());
3605 let plan = Arc::new(SortExec::new([sort_expr].into(), data));
3606 let stream = plan.execute(0, Arc::new(TaskContext::default())).unwrap();
3607 let stream = break_stream(stream, 64);
3608 let stream = stream.map_err(DataFusionError::from);
3609 let stream =
3610 Box::pin(RecordBatchStreamAdapter::new(schema, stream)) as SendableRecordBatchStream;
3611
3612 train_btree_index(stream, test_store.as_ref(), 64, None, None)
3613 .await
3614 .unwrap();
3615 let index = BTreeIndex::load(test_store, None, &LanceCache::no_cache())
3616 .await
3617 .unwrap();
3618
3619 let keys: Vec<i32> = vec![0, 999, 500, 1, 998, -1, 1000, 1500, 250, 251, 7, 64, 63, 65];
3622 let scalar_keys: Vec<ScalarValue> =
3623 keys.iter().map(|k| ScalarValue::Int32(Some(*k))).collect();
3624
3625 let batched = index
3626 .contains_keys(&scalar_keys, &NoOpMetricsCollector)
3627 .await
3628 .unwrap();
3629
3630 let mut oracle = Vec::with_capacity(keys.len());
3632 for k in &scalar_keys {
3633 let result = index
3634 .search(&SargableQuery::Equals(k.clone()), &NoOpMetricsCollector)
3635 .await
3636 .unwrap();
3637 oracle.push(!result.row_addrs().is_empty());
3638 }
3639 assert_eq!(
3640 batched, oracle,
3641 "contains_keys must agree with per-key Equals search; keys={keys:?}"
3642 );
3643
3644 let expected: Vec<bool> = keys.iter().map(|k| (0..1000).contains(k)).collect();
3646 assert_eq!(batched, expected);
3647
3648 assert!(
3650 index
3651 .contains_keys(&[], &NoOpMetricsCollector)
3652 .await
3653 .unwrap()
3654 .is_empty()
3655 );
3656
3657 let with_null = vec![ScalarValue::Int32(Some(5)), ScalarValue::Int32(None)];
3659 assert_eq!(
3660 index
3661 .contains_keys(&with_null, &NoOpMetricsCollector)
3662 .await
3663 .unwrap(),
3664 vec![true, false]
3665 );
3666 }
3667
3668 #[tokio::test]
3669 async fn test_page_cache() {
3670 let tmpdir = TempObjDir::default();
3671 let test_store = Arc::new(LanceIndexStore::new(
3672 Arc::new(ObjectStore::local()),
3673 tmpdir.clone(),
3674 Arc::new(LanceCache::no_cache()),
3675 ));
3676
3677 let data = gen_batch()
3678 .col("value", array::step::<Float32Type>())
3679 .col("_rowid", array::step::<UInt64Type>())
3680 .into_df_exec(RowCount::from(1000), BatchCount::from(10));
3681 let schema = data.schema();
3682 let sort_expr = PhysicalSortExpr::new_default(col("value", schema.as_ref()).unwrap());
3683 let plan = Arc::new(SortExec::new([sort_expr].into(), data));
3684 let stream = plan.execute(0, Arc::new(TaskContext::default())).unwrap();
3685 let stream = break_stream(stream, 64);
3686 let stream = stream.map_err(DataFusionError::from);
3687 let stream =
3688 Box::pin(RecordBatchStreamAdapter::new(schema, stream)) as SendableRecordBatchStream;
3689
3690 train_btree_index(stream, test_store.as_ref(), 64, None, None)
3691 .await
3692 .unwrap();
3693
3694 let cache = Arc::new(LanceCache::with_capacity(100 * 1024 * 1024));
3695 let index = BTreeIndex::load(test_store, None, cache.as_ref())
3696 .await
3697 .unwrap();
3698
3699 let query = SargableQuery::Equals(ScalarValue::Float32(Some(0.0)));
3700 let metrics = LocalMetricsCollector::default();
3701 let query1 = index.search(&query, &metrics);
3702 let query2 = index.search(&query, &metrics);
3703 tokio::join!(query1, query2).0.unwrap();
3704 assert_eq!(metrics.parts_loaded.load(Ordering::Relaxed), 1);
3705 }
3706
3707 #[tokio::test]
3708 async fn test_like_prefix_search() {
3709 use arrow::datatypes::DataType;
3710 use arrow_array::StringArray;
3711
3712 let tmpdir = TempObjDir::default();
3713 let test_store = Arc::new(LanceIndexStore::new(
3714 Arc::new(ObjectStore::local()),
3715 tmpdir.clone(),
3716 Arc::new(LanceCache::no_cache()),
3717 ));
3718
3719 let values = vec![
3721 "apple",
3722 "app",
3723 "application",
3724 "banana",
3725 "band",
3726 "test_ns$table1",
3727 "test_ns$table2",
3728 "test_ns2$table1",
3729 "test",
3730 "testing",
3731 ];
3732 let row_ids: Vec<u64> = (0..values.len() as u64).collect();
3733
3734 let schema = Arc::new(arrow::datatypes::Schema::new(vec![
3735 arrow::datatypes::Field::new("value", DataType::Utf8, false),
3736 arrow::datatypes::Field::new("_rowid", DataType::UInt64, false),
3737 ]));
3738
3739 let batch = arrow::record_batch::RecordBatch::try_new(
3740 schema.clone(),
3741 vec![
3742 Arc::new(StringArray::from(values.clone())),
3743 Arc::new(arrow_array::UInt64Array::from(row_ids)),
3744 ],
3745 )
3746 .unwrap();
3747
3748 let stream: SendableRecordBatchStream = Box::pin(RecordBatchStreamAdapter::new(
3749 schema,
3750 stream::once(async { Ok(batch) }),
3751 ));
3752
3753 train_btree_index(stream, test_store.as_ref(), 100, None, None)
3754 .await
3755 .unwrap();
3756
3757 let index = BTreeIndex::load(test_store, None, &LanceCache::no_cache())
3758 .await
3759 .unwrap();
3760
3761 let query = SargableQuery::LikePrefix(ScalarValue::Utf8(Some("app".to_string())));
3763 let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
3764
3765 match &result {
3766 SearchResult::Exact(row_ids) => {
3767 let ids: Vec<u64> = row_ids
3768 .true_rows()
3769 .row_addrs()
3770 .unwrap()
3771 .map(u64::from)
3772 .collect();
3773 assert!(ids.contains(&0), "Should contain row 0 (apple)");
3774 assert!(ids.contains(&1), "Should contain row 1 (app)");
3775 assert!(ids.contains(&2), "Should contain row 2 (application)");
3776 assert!(!ids.contains(&3), "Should not contain row 3 (banana)");
3777 }
3778 _ => panic!("Expected Exact result"),
3779 }
3780
3781 let query = SargableQuery::LikePrefix(ScalarValue::Utf8(Some("test_ns$".to_string())));
3783 let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
3784
3785 match &result {
3786 SearchResult::Exact(row_ids) => {
3787 let ids: Vec<u64> = row_ids
3788 .true_rows()
3789 .row_addrs()
3790 .unwrap()
3791 .map(u64::from)
3792 .collect();
3793 assert!(ids.contains(&5), "Should contain row 5 (test_ns$table1)");
3794 assert!(ids.contains(&6), "Should contain row 6 (test_ns$table2)");
3795 assert!(
3796 !ids.contains(&7),
3797 "Should not contain row 7 (test_ns2$table1)"
3798 );
3799 }
3800 _ => panic!("Expected Exact result"),
3801 }
3802
3803 let query = SargableQuery::LikePrefix(ScalarValue::Utf8(Some("test".to_string())));
3805 let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
3806
3807 match &result {
3808 SearchResult::Exact(row_ids) => {
3809 let ids: Vec<u64> = row_ids
3810 .true_rows()
3811 .row_addrs()
3812 .unwrap()
3813 .map(u64::from)
3814 .collect();
3815 assert!(
3816 ids.contains(&5),
3817 "Should contain row 5 (test_ns$table1): {:?}",
3818 ids
3819 );
3820 assert!(
3821 ids.contains(&6),
3822 "Should contain row 6 (test_ns$table2): {:?}",
3823 ids
3824 );
3825 assert!(
3826 ids.contains(&7),
3827 "Should contain row 7 (test_ns2$table1): {:?}",
3828 ids
3829 );
3830 assert!(ids.contains(&8), "Should contain row 8 (test): {:?}", ids);
3831 assert!(
3832 ids.contains(&9),
3833 "Should contain row 9 (testing): {:?}",
3834 ids
3835 );
3836 }
3837 _ => panic!("Expected Exact result"),
3838 }
3839 }
3840
3841 #[tokio::test]
3842 async fn test_like_prefix_search_large_utf8() {
3843 use arrow::datatypes::DataType;
3844 use arrow_array::LargeStringArray;
3845
3846 let tmpdir = TempObjDir::default();
3847 let test_store = Arc::new(LanceIndexStore::new(
3848 Arc::new(ObjectStore::local()),
3849 tmpdir.clone(),
3850 Arc::new(LanceCache::no_cache()),
3851 ));
3852
3853 let values = vec!["apple", "app", "application", "banana"];
3854 let row_ids: Vec<u64> = (0..values.len() as u64).collect();
3855
3856 let schema = Arc::new(arrow::datatypes::Schema::new(vec![
3857 arrow::datatypes::Field::new("value", DataType::LargeUtf8, false),
3858 arrow::datatypes::Field::new("_rowid", DataType::UInt64, false),
3859 ]));
3860
3861 let batch = arrow::record_batch::RecordBatch::try_new(
3862 schema.clone(),
3863 vec![
3864 Arc::new(LargeStringArray::from(values)),
3865 Arc::new(arrow_array::UInt64Array::from(row_ids)),
3866 ],
3867 )
3868 .unwrap();
3869
3870 let stream: SendableRecordBatchStream = Box::pin(RecordBatchStreamAdapter::new(
3871 schema,
3872 stream::once(async { Ok(batch) }),
3873 ));
3874
3875 train_btree_index(stream, test_store.as_ref(), 100, None, None)
3876 .await
3877 .unwrap();
3878
3879 let index = BTreeIndex::load(test_store, None, &LanceCache::no_cache())
3880 .await
3881 .unwrap();
3882
3883 let query = SargableQuery::LikePrefix(ScalarValue::LargeUtf8(Some("app".to_string())));
3885 let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
3886
3887 match &result {
3888 SearchResult::Exact(row_ids) => {
3889 let ids: Vec<u64> = row_ids
3890 .true_rows()
3891 .row_addrs()
3892 .unwrap()
3893 .map(u64::from)
3894 .collect();
3895 assert!(ids.contains(&0), "Should contain row 0 (apple)");
3896 assert!(ids.contains(&1), "Should contain row 1 (app)");
3897 assert!(ids.contains(&2), "Should contain row 2 (application)");
3898 assert!(!ids.contains(&3), "Should not contain row 3 (banana)");
3899 }
3900 _ => panic!("Expected Exact result"),
3901 }
3902 }
3903
3904 #[tokio::test]
3905 async fn test_fragment_btree_index_consistency() {
3906 let full_tmpdir = TempObjDir::default();
3908 let full_store = Arc::new(LanceIndexStore::new(
3909 Arc::new(ObjectStore::local()),
3910 full_tmpdir.clone(),
3911 Arc::new(LanceCache::no_cache()),
3912 ));
3913
3914 let fragment_tmpdir = TempObjDir::default();
3915 let fragment_store = Arc::new(LanceIndexStore::new(
3916 Arc::new(ObjectStore::local()),
3917 fragment_tmpdir.clone(),
3918 Arc::new(LanceCache::no_cache()),
3919 ));
3920
3921 let total_count = 2 * DEFAULT_BTREE_BATCH_SIZE;
3924 let full_data_gen = gen_batch()
3925 .col("value", array::step::<Int32Type>())
3926 .col("_rowid", array::step::<UInt64Type>())
3927 .into_df_stream(RowCount::from(total_count / 2), BatchCount::from(2));
3928 let full_data_source = Box::pin(RecordBatchStreamAdapter::new(
3929 full_data_gen.schema(),
3930 full_data_gen,
3931 ));
3932
3933 train_btree_index(
3934 full_data_source,
3935 full_store.as_ref(),
3936 DEFAULT_BTREE_BATCH_SIZE,
3937 None,
3938 None,
3939 )
3940 .await
3941 .unwrap();
3942
3943 let half_count = DEFAULT_BTREE_BATCH_SIZE;
3946 let fragment1_gen = gen_batch()
3947 .col("value", array::step::<Int32Type>())
3948 .col("_rowid", array::step::<UInt64Type>())
3949 .into_df_stream(RowCount::from(half_count), BatchCount::from(1));
3950 let fragment1_data_source = Box::pin(RecordBatchStreamAdapter::new(
3951 fragment1_gen.schema(),
3952 fragment1_gen,
3953 ));
3954
3955 train_btree_index(
3956 fragment1_data_source,
3957 fragment_store.as_ref(),
3958 DEFAULT_BTREE_BATCH_SIZE,
3959 Some(vec![1]), None,
3961 )
3962 .await
3963 .unwrap();
3964
3965 let start_val = DEFAULT_BTREE_BATCH_SIZE as i32;
3967 let end_val = (2 * DEFAULT_BTREE_BATCH_SIZE) as i32;
3968 let values_second_half: Vec<i32> = (start_val..end_val).collect();
3969 let row_ids_second_half: Vec<u64> = (start_val as u64..end_val as u64).collect();
3970 let fragment2_gen = gen_batch()
3971 .col("value", array::cycle::<Int32Type>(values_second_half))
3972 .col("_rowid", array::cycle::<UInt64Type>(row_ids_second_half))
3973 .into_df_stream(RowCount::from(half_count), BatchCount::from(1));
3974 let fragment2_data_source = Box::pin(RecordBatchStreamAdapter::new(
3975 fragment2_gen.schema(),
3976 fragment2_gen,
3977 ));
3978
3979 train_btree_index(
3980 fragment2_data_source,
3981 fragment_store.as_ref(),
3982 DEFAULT_BTREE_BATCH_SIZE,
3983 Some(vec![2]), None,
3985 )
3986 .await
3987 .unwrap();
3988
3989 let part_page_files = vec![
3991 part_page_data_file_path(1 << 32),
3992 part_page_data_file_path(2 << 32),
3993 ];
3994
3995 let part_lookup_files = vec![
3996 part_lookup_file_path(1 << 32),
3997 part_lookup_file_path(2 << 32),
3998 ];
3999
4000 let progress = Arc::new(RecordingProgress::default());
4001 super::merge_metadata_files(
4002 fragment_store.as_ref(),
4003 &part_page_files,
4004 &part_lookup_files,
4005 Option::from(1usize),
4006 progress.clone(),
4007 )
4008 .await
4009 .unwrap();
4010
4011 let tags = progress
4012 .recorded_events()
4013 .iter()
4014 .map(|(kind, stage, _)| format!("{kind}:{stage}"))
4015 .collect::<Vec<_>>();
4016 let merge_start = tags
4017 .iter()
4018 .position(|e| e == "start:merge_pages")
4019 .expect("missing merge_pages start");
4020 let merge_complete = tags
4021 .iter()
4022 .position(|e| e == "complete:merge_pages")
4023 .expect("missing merge_pages complete");
4024 let lookup_start = tags
4025 .iter()
4026 .position(|e| e == "start:write_lookup_file")
4027 .expect("missing write_lookup_file start");
4028 let lookup_complete = tags
4029 .iter()
4030 .position(|e| e == "complete:write_lookup_file")
4031 .expect("missing write_lookup_file complete");
4032 assert!(merge_start < merge_complete);
4033 assert!(merge_complete < lookup_start);
4034 assert!(lookup_start < lookup_complete);
4035 assert!(
4036 tags.iter().any(|e| e == "progress:merge_pages"),
4037 "expected merge_pages progress callbacks"
4038 );
4039 assert!(
4040 tags.iter().any(|e| e == "progress:write_lookup_file"),
4041 "expected write_lookup_file progress callbacks"
4042 );
4043
4044 let full_index = BTreeIndex::load(full_store.clone(), None, &LanceCache::no_cache())
4046 .await
4047 .unwrap();
4048
4049 let merged_index = BTreeIndex::load(fragment_store.clone(), None, &LanceCache::no_cache())
4050 .await
4051 .unwrap();
4052
4053 let query_0 = SargableQuery::Equals(ScalarValue::Int32(Some(0)));
4057 let full_result_0 = full_index
4058 .search(&query_0, &NoOpMetricsCollector)
4059 .await
4060 .unwrap();
4061 let merged_result_0 = merged_index
4062 .search(&query_0, &NoOpMetricsCollector)
4063 .await
4064 .unwrap();
4065 assert_eq!(full_result_0, merged_result_0, "Query for value 0 failed");
4066
4067 let mid_first_batch = (DEFAULT_BTREE_BATCH_SIZE / 2) as i32;
4069 let query_mid_first = SargableQuery::Equals(ScalarValue::Int32(Some(mid_first_batch)));
4070 let full_result_mid_first = full_index
4071 .search(&query_mid_first, &NoOpMetricsCollector)
4072 .await
4073 .unwrap();
4074 let merged_result_mid_first = merged_index
4075 .search(&query_mid_first, &NoOpMetricsCollector)
4076 .await
4077 .unwrap();
4078 assert_eq!(
4079 full_result_mid_first, merged_result_mid_first,
4080 "Query for value {} failed",
4081 mid_first_batch
4082 );
4083
4084 let first_second_batch = DEFAULT_BTREE_BATCH_SIZE as i32;
4086 let query_first_second =
4087 SargableQuery::Equals(ScalarValue::Int32(Some(first_second_batch)));
4088 let full_result_first_second = full_index
4089 .search(&query_first_second, &NoOpMetricsCollector)
4090 .await
4091 .unwrap();
4092 let merged_result_first_second = merged_index
4093 .search(&query_first_second, &NoOpMetricsCollector)
4094 .await
4095 .unwrap();
4096 assert_eq!(
4097 full_result_first_second, merged_result_first_second,
4098 "Query for value {} failed",
4099 first_second_batch
4100 );
4101
4102 let mid_second_batch = (DEFAULT_BTREE_BATCH_SIZE + DEFAULT_BTREE_BATCH_SIZE / 2) as i32;
4104 let query_mid_second = SargableQuery::Equals(ScalarValue::Int32(Some(mid_second_batch)));
4105
4106 let full_result_mid_second = full_index
4107 .search(&query_mid_second, &NoOpMetricsCollector)
4108 .await
4109 .unwrap();
4110 let merged_result_mid_second = merged_index
4111 .search(&query_mid_second, &NoOpMetricsCollector)
4112 .await
4113 .unwrap();
4114 assert_eq!(
4115 full_result_mid_second, merged_result_mid_second,
4116 "Query for value {} failed",
4117 mid_second_batch
4118 );
4119 }
4120
4121 #[tokio::test]
4122 async fn test_fragment_btree_index_boundary_queries() {
4123 let full_tmpdir = TempObjDir::default();
4125 let full_store = Arc::new(LanceIndexStore::new(
4126 Arc::new(ObjectStore::local()),
4127 full_tmpdir.clone(),
4128 Arc::new(LanceCache::no_cache()),
4129 ));
4130
4131 let fragment_tmpdir = TempObjDir::default();
4132 let fragment_store = Arc::new(LanceIndexStore::new(
4133 Arc::new(ObjectStore::local()),
4134 fragment_tmpdir.clone(),
4135 Arc::new(LanceCache::no_cache()),
4136 ));
4137
4138 let total_count = 3 * DEFAULT_BTREE_BATCH_SIZE;
4140
4141 let full_data_gen = gen_batch()
4143 .col("value", array::step::<Int32Type>())
4144 .col("_rowid", array::step::<UInt64Type>())
4145 .into_df_stream(RowCount::from(total_count / 3), BatchCount::from(3));
4146 let full_data_source = Box::pin(RecordBatchStreamAdapter::new(
4147 full_data_gen.schema(),
4148 full_data_gen,
4149 ));
4150
4151 train_btree_index(
4152 full_data_source,
4153 full_store.as_ref(),
4154 DEFAULT_BTREE_BATCH_SIZE,
4155 None,
4156 None,
4157 )
4158 .await
4159 .unwrap();
4160
4161 let fragment_size = DEFAULT_BTREE_BATCH_SIZE;
4164 let fragment1_gen = gen_batch()
4165 .col("value", array::step::<Int32Type>())
4166 .col("_rowid", array::step::<UInt64Type>())
4167 .into_df_stream(RowCount::from(fragment_size), BatchCount::from(1));
4168 let fragment1_data_source = Box::pin(RecordBatchStreamAdapter::new(
4169 fragment1_gen.schema(),
4170 fragment1_gen,
4171 ));
4172
4173 train_btree_index(
4174 fragment1_data_source,
4175 fragment_store.as_ref(),
4176 DEFAULT_BTREE_BATCH_SIZE,
4177 Some(vec![1]),
4178 None,
4179 )
4180 .await
4181 .unwrap();
4182
4183 let start_val2 = DEFAULT_BTREE_BATCH_SIZE as i32;
4185 let end_val2 = (2 * DEFAULT_BTREE_BATCH_SIZE) as i32;
4186 let values_fragment2: Vec<i32> = (start_val2..end_val2).collect();
4187 let row_ids_fragment2: Vec<u64> = (start_val2 as u64..end_val2 as u64).collect();
4188 let fragment2_gen = gen_batch()
4189 .col("value", array::cycle::<Int32Type>(values_fragment2))
4190 .col("_rowid", array::cycle::<UInt64Type>(row_ids_fragment2))
4191 .into_df_stream(RowCount::from(fragment_size), BatchCount::from(1));
4192 let fragment2_data_source = Box::pin(RecordBatchStreamAdapter::new(
4193 fragment2_gen.schema(),
4194 fragment2_gen,
4195 ));
4196
4197 train_btree_index(
4198 fragment2_data_source,
4199 fragment_store.as_ref(),
4200 DEFAULT_BTREE_BATCH_SIZE,
4201 Some(vec![2]),
4202 None,
4203 )
4204 .await
4205 .unwrap();
4206
4207 let start_val3 = (2 * DEFAULT_BTREE_BATCH_SIZE) as i32;
4209 let end_val3 = (3 * DEFAULT_BTREE_BATCH_SIZE) as i32;
4210 let values_fragment3: Vec<i32> = (start_val3..end_val3).collect();
4211 let row_ids_fragment3: Vec<u64> = (start_val3 as u64..end_val3 as u64).collect();
4212 let fragment3_gen = gen_batch()
4213 .col("value", array::cycle::<Int32Type>(values_fragment3))
4214 .col("_rowid", array::cycle::<UInt64Type>(row_ids_fragment3))
4215 .into_df_stream(RowCount::from(fragment_size), BatchCount::from(1));
4216 let fragment3_data_source = Box::pin(RecordBatchStreamAdapter::new(
4217 fragment3_gen.schema(),
4218 fragment3_gen,
4219 ));
4220
4221 train_btree_index(
4222 fragment3_data_source,
4223 fragment_store.as_ref(),
4224 DEFAULT_BTREE_BATCH_SIZE,
4225 Some(vec![3]),
4226 None,
4227 )
4228 .await
4229 .unwrap();
4230
4231 let part_page_files = vec![
4233 part_page_data_file_path(1 << 32),
4234 part_page_data_file_path(2 << 32),
4235 part_page_data_file_path(3 << 32),
4236 ];
4237
4238 let part_lookup_files = vec![
4239 part_lookup_file_path(1 << 32),
4240 part_lookup_file_path(2 << 32),
4241 part_lookup_file_path(3 << 32),
4242 ];
4243
4244 super::merge_metadata_files(
4245 fragment_store.as_ref(),
4246 &part_page_files,
4247 &part_lookup_files,
4248 Option::from(1usize),
4249 noop_progress(),
4250 )
4251 .await
4252 .unwrap();
4253
4254 let full_index = BTreeIndex::load(full_store.clone(), None, &LanceCache::no_cache())
4256 .await
4257 .unwrap();
4258
4259 let merged_index = BTreeIndex::load(fragment_store.clone(), None, &LanceCache::no_cache())
4260 .await
4261 .unwrap();
4262
4263 let query_min = SargableQuery::Equals(ScalarValue::Int32(Some(0)));
4267 let full_result_min = full_index
4268 .search(&query_min, &NoOpMetricsCollector)
4269 .await
4270 .unwrap();
4271 let merged_result_min = merged_index
4272 .search(&query_min, &NoOpMetricsCollector)
4273 .await
4274 .unwrap();
4275 assert_eq!(
4276 full_result_min, merged_result_min,
4277 "Query for minimum value 0 failed"
4278 );
4279
4280 let max_val = (3 * DEFAULT_BTREE_BATCH_SIZE - 1) as i32;
4282 let query_max = SargableQuery::Equals(ScalarValue::Int32(Some(max_val)));
4283 let full_result_max = full_index
4284 .search(&query_max, &NoOpMetricsCollector)
4285 .await
4286 .unwrap();
4287 let merged_result_max = merged_index
4288 .search(&query_max, &NoOpMetricsCollector)
4289 .await
4290 .unwrap();
4291 assert_eq!(
4292 full_result_max, merged_result_max,
4293 "Query for maximum value {} failed",
4294 max_val
4295 );
4296
4297 let fragment1_last = (DEFAULT_BTREE_BATCH_SIZE - 1) as i32;
4299 let query_frag1_last = SargableQuery::Equals(ScalarValue::Int32(Some(fragment1_last)));
4300 let full_result_frag1_last = full_index
4301 .search(&query_frag1_last, &NoOpMetricsCollector)
4302 .await
4303 .unwrap();
4304 let merged_result_frag1_last = merged_index
4305 .search(&query_frag1_last, &NoOpMetricsCollector)
4306 .await
4307 .unwrap();
4308 assert_eq!(
4309 full_result_frag1_last, merged_result_frag1_last,
4310 "Query for fragment 1 last value {} failed",
4311 fragment1_last
4312 );
4313
4314 let fragment2_first = DEFAULT_BTREE_BATCH_SIZE as i32;
4316 let query_frag2_first = SargableQuery::Equals(ScalarValue::Int32(Some(fragment2_first)));
4317 let full_result_frag2_first = full_index
4318 .search(&query_frag2_first, &NoOpMetricsCollector)
4319 .await
4320 .unwrap();
4321 let merged_result_frag2_first = merged_index
4322 .search(&query_frag2_first, &NoOpMetricsCollector)
4323 .await
4324 .unwrap();
4325 assert_eq!(
4326 full_result_frag2_first, merged_result_frag2_first,
4327 "Query for fragment 2 first value {} failed",
4328 fragment2_first
4329 );
4330
4331 let fragment2_last = (2 * DEFAULT_BTREE_BATCH_SIZE - 1) as i32;
4333 let query_frag2_last = SargableQuery::Equals(ScalarValue::Int32(Some(fragment2_last)));
4334 let full_result_frag2_last = full_index
4335 .search(&query_frag2_last, &NoOpMetricsCollector)
4336 .await
4337 .unwrap();
4338 let merged_result_frag2_last = merged_index
4339 .search(&query_frag2_last, &NoOpMetricsCollector)
4340 .await
4341 .unwrap();
4342 assert_eq!(
4343 full_result_frag2_last, merged_result_frag2_last,
4344 "Query for fragment 2 last value {} failed",
4345 fragment2_last
4346 );
4347
4348 let fragment3_first = (2 * DEFAULT_BTREE_BATCH_SIZE) as i32;
4350 let query_frag3_first = SargableQuery::Equals(ScalarValue::Int32(Some(fragment3_first)));
4351 let full_result_frag3_first = full_index
4352 .search(&query_frag3_first, &NoOpMetricsCollector)
4353 .await
4354 .unwrap();
4355 let merged_result_frag3_first = merged_index
4356 .search(&query_frag3_first, &NoOpMetricsCollector)
4357 .await
4358 .unwrap();
4359 assert_eq!(
4360 full_result_frag3_first, merged_result_frag3_first,
4361 "Query for fragment 3 first value {} failed",
4362 fragment3_first
4363 );
4364
4365 let query_below_min = SargableQuery::Equals(ScalarValue::Int32(Some(-1)));
4369 let full_result_below = full_index
4370 .search(&query_below_min, &NoOpMetricsCollector)
4371 .await
4372 .unwrap();
4373 let merged_result_below = merged_index
4374 .search(&query_below_min, &NoOpMetricsCollector)
4375 .await
4376 .unwrap();
4377 assert_eq!(
4378 full_result_below, merged_result_below,
4379 "Query for value below minimum (-1) failed"
4380 );
4381
4382 let query_above_max = SargableQuery::Equals(ScalarValue::Int32(Some(max_val + 1)));
4384 let full_result_above = full_index
4385 .search(&query_above_max, &NoOpMetricsCollector)
4386 .await
4387 .unwrap();
4388 let merged_result_above = merged_index
4389 .search(&query_above_max, &NoOpMetricsCollector)
4390 .await
4391 .unwrap();
4392 assert_eq!(
4393 full_result_above,
4394 merged_result_above,
4395 "Query for value above maximum ({}) failed",
4396 max_val + 1
4397 );
4398
4399 let range_start = (DEFAULT_BTREE_BATCH_SIZE - 100) as i32;
4403 let range_end = (DEFAULT_BTREE_BATCH_SIZE + 100) as i32;
4404 let query_cross_frag = SargableQuery::Range(
4405 std::collections::Bound::Included(ScalarValue::Int32(Some(range_start))),
4406 std::collections::Bound::Excluded(ScalarValue::Int32(Some(range_end))),
4407 );
4408 let full_result_cross = full_index
4409 .search(&query_cross_frag, &NoOpMetricsCollector)
4410 .await
4411 .unwrap();
4412 let merged_result_cross = merged_index
4413 .search(&query_cross_frag, &NoOpMetricsCollector)
4414 .await
4415 .unwrap();
4416 assert_eq!(
4417 full_result_cross, merged_result_cross,
4418 "Cross-fragment range query [{}, {}] failed",
4419 range_start, range_end
4420 );
4421
4422 let single_frag_start = 100i32;
4424 let single_frag_end = 200i32;
4425 let query_single_frag = SargableQuery::Range(
4426 std::collections::Bound::Included(ScalarValue::Int32(Some(single_frag_start))),
4427 std::collections::Bound::Excluded(ScalarValue::Int32(Some(single_frag_end))),
4428 );
4429 let full_result_single = full_index
4430 .search(&query_single_frag, &NoOpMetricsCollector)
4431 .await
4432 .unwrap();
4433 let merged_result_single = merged_index
4434 .search(&query_single_frag, &NoOpMetricsCollector)
4435 .await
4436 .unwrap();
4437 assert_eq!(
4438 full_result_single, merged_result_single,
4439 "Single fragment range query [{}, {}] failed",
4440 single_frag_start, single_frag_end
4441 );
4442
4443 let large_range_start = 100i32;
4445 let large_range_end = (3 * DEFAULT_BTREE_BATCH_SIZE - 100) as i32;
4446 let query_large_range = SargableQuery::Range(
4447 std::collections::Bound::Included(ScalarValue::Int32(Some(large_range_start))),
4448 std::collections::Bound::Excluded(ScalarValue::Int32(Some(large_range_end))),
4449 );
4450 let full_result_large = full_index
4451 .search(&query_large_range, &NoOpMetricsCollector)
4452 .await
4453 .unwrap();
4454 let merged_result_large = merged_index
4455 .search(&query_large_range, &NoOpMetricsCollector)
4456 .await
4457 .unwrap();
4458 assert_eq!(
4459 full_result_large, merged_result_large,
4460 "Large range query [{}, {}] failed",
4461 large_range_start, large_range_end
4462 );
4463
4464 let lt_val = (DEFAULT_BTREE_BATCH_SIZE / 2) as i32;
4468 let query_lt = SargableQuery::Range(
4469 std::collections::Bound::Included(ScalarValue::Int32(Some(0))),
4470 std::collections::Bound::Excluded(ScalarValue::Int32(Some(lt_val))),
4471 );
4472 let full_result_lt = full_index
4473 .search(&query_lt, &NoOpMetricsCollector)
4474 .await
4475 .unwrap();
4476 let merged_result_lt = merged_index
4477 .search(&query_lt, &NoOpMetricsCollector)
4478 .await
4479 .unwrap();
4480 assert_eq!(
4481 full_result_lt, merged_result_lt,
4482 "Less than query (<{}) failed",
4483 lt_val
4484 );
4485
4486 let gt_val = (2 * DEFAULT_BTREE_BATCH_SIZE) as i32;
4488 let max_range_val = (3 * DEFAULT_BTREE_BATCH_SIZE) as i32;
4489 let query_gt = SargableQuery::Range(
4490 std::collections::Bound::Excluded(ScalarValue::Int32(Some(gt_val))),
4491 std::collections::Bound::Excluded(ScalarValue::Int32(Some(max_range_val))),
4492 );
4493 let full_result_gt = full_index
4494 .search(&query_gt, &NoOpMetricsCollector)
4495 .await
4496 .unwrap();
4497 let merged_result_gt = merged_index
4498 .search(&query_gt, &NoOpMetricsCollector)
4499 .await
4500 .unwrap();
4501 assert_eq!(
4502 full_result_gt, merged_result_gt,
4503 "Greater than query (>{}) failed",
4504 gt_val
4505 );
4506
4507 let lte_val = (DEFAULT_BTREE_BATCH_SIZE - 1) as i32;
4509 let query_lte = SargableQuery::Range(
4510 std::collections::Bound::Included(ScalarValue::Int32(Some(0))),
4511 std::collections::Bound::Included(ScalarValue::Int32(Some(lte_val))),
4512 );
4513 let full_result_lte = full_index
4514 .search(&query_lte, &NoOpMetricsCollector)
4515 .await
4516 .unwrap();
4517 let merged_result_lte = merged_index
4518 .search(&query_lte, &NoOpMetricsCollector)
4519 .await
4520 .unwrap();
4521 assert_eq!(
4522 full_result_lte, merged_result_lte,
4523 "Less than or equal query (<={}) failed",
4524 lte_val
4525 );
4526
4527 let gte_val = (2 * DEFAULT_BTREE_BATCH_SIZE) as i32;
4529 let query_gte = SargableQuery::Range(
4530 std::collections::Bound::Included(ScalarValue::Int32(Some(gte_val))),
4531 std::collections::Bound::Excluded(ScalarValue::Int32(Some(max_range_val))),
4532 );
4533 let full_result_gte = full_index
4534 .search(&query_gte, &NoOpMetricsCollector)
4535 .await
4536 .unwrap();
4537 let merged_result_gte = merged_index
4538 .search(&query_gte, &NoOpMetricsCollector)
4539 .await
4540 .unwrap();
4541 assert_eq!(
4542 full_result_gte, merged_result_gte,
4543 "Greater than or equal query (>={}) failed",
4544 gte_val
4545 );
4546 }
4547
4548 #[test]
4549 fn test_extract_partition_id() {
4550 assert_eq!(
4552 super::extract_partition_id("part_123_page_data.lance").unwrap(),
4553 123
4554 );
4555 assert_eq!(
4556 super::extract_partition_id("part_456_page_lookup.lance").unwrap(),
4557 456
4558 );
4559 assert_eq!(
4560 super::extract_partition_id("part_4294967296_page_data.lance").unwrap(),
4561 4294967296
4562 );
4563
4564 assert!(super::extract_partition_id("invalid_filename.lance").is_err());
4566 assert!(super::extract_partition_id("part_abc_page_data.lance").is_err());
4567 assert!(super::extract_partition_id("part_123").is_err());
4568 assert!(super::extract_partition_id("part_").is_err());
4569 }
4570
4571 #[tokio::test]
4572 async fn test_cleanup_partition_files() {
4573 let tmpdir = TempObjDir::default();
4575 let test_store: Arc<dyn crate::scalar::IndexStore> = Arc::new(LanceIndexStore::new(
4576 Arc::new(ObjectStore::local()),
4577 tmpdir.clone(),
4578 Arc::new(LanceCache::no_cache()),
4579 ));
4580
4581 let lookup_files = vec![
4583 "part_123_page_lookup.lance".to_string(),
4584 "invalid_lookup_file.lance".to_string(),
4585 "part_456_page_lookup.lance".to_string(),
4586 ];
4587
4588 let page_files = vec![
4589 "part_123_page_data.lance".to_string(),
4590 "invalid_page_file.lance".to_string(),
4591 "part_456_page_data.lance".to_string(),
4592 ];
4593
4594 super::cleanup_partition_files(test_store.as_ref(), &lookup_files, &page_files).await;
4597 }
4598
4599 #[tokio::test]
4600 async fn test_btree_null_handling_in_queries() {
4601 let store = Arc::new(LanceIndexStore::new(
4602 Arc::new(ObjectStore::memory()),
4603 Path::default(),
4604 Arc::new(LanceCache::no_cache()),
4605 ));
4606
4607 let batch = record_batch!(
4610 ("value", Int32, [None, Some(0), Some(5)]),
4611 ("_rowid", UInt64, [0, 1, 2])
4612 )
4613 .unwrap();
4614 let stream = stream::once(futures::future::ok(batch.clone()));
4615 let stream = Box::pin(RecordBatchStreamAdapter::new(batch.schema(), stream));
4616
4617 super::train_btree_index(stream, store.as_ref(), 256, None, None)
4619 .await
4620 .unwrap();
4621
4622 let cache = LanceCache::with_capacity(1024 * 1024);
4623 let index = super::BTreeIndex::load(store.clone(), None, &cache)
4624 .await
4625 .unwrap();
4626
4627 let query = SargableQuery::Equals(ScalarValue::Int32(Some(5)));
4629 let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
4630
4631 match result {
4632 SearchResult::Exact(row_ids) => {
4633 let actual_rows: Vec<u64> = row_ids
4634 .true_rows()
4635 .row_addrs()
4636 .unwrap()
4637 .map(u64::from)
4638 .collect();
4639 assert_eq!(actual_rows, vec![2], "Should find row 2 where value == 5");
4640
4641 let null_row_ids = row_ids.null_rows();
4643 assert!(!null_row_ids.is_empty(), "null_row_ids should be non-empty");
4644 let null_rows: Vec<u64> =
4645 null_row_ids.row_addrs().unwrap().map(u64::from).collect();
4646 assert_eq!(null_rows, vec![0], "Should report row 0 as null");
4647 }
4648 _ => panic!("Expected Exact search result"),
4649 }
4650
4651 let query = SargableQuery::Range(
4653 std::ops::Bound::Included(ScalarValue::Int32(Some(0))),
4654 std::ops::Bound::Included(ScalarValue::Int32(Some(3))),
4655 );
4656 let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
4657
4658 match result {
4659 SearchResult::Exact(row_ids) => {
4660 let actual_rows: Vec<u64> = row_ids
4661 .true_rows()
4662 .row_addrs()
4663 .unwrap()
4664 .map(u64::from)
4665 .collect();
4666 assert_eq!(actual_rows, vec![1], "Should find row 1 where value == 0");
4667
4668 let null_row_ids = row_ids.null_rows();
4670 assert!(!null_row_ids.is_empty(), "null_row_ids should be non-empty");
4671 let null_rows: Vec<u64> =
4672 null_row_ids.row_addrs().unwrap().map(u64::from).collect();
4673 assert_eq!(null_rows, vec![0], "Should report row 0 as null");
4674 }
4675 _ => panic!("Expected Exact search result"),
4676 }
4677
4678 let query = SargableQuery::IsIn(vec![
4680 ScalarValue::Int32(Some(0)),
4681 ScalarValue::Int32(Some(5)),
4682 ]);
4683 let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
4684
4685 match result {
4686 SearchResult::Exact(row_ids) => {
4687 let mut actual_rows: Vec<u64> = row_ids
4688 .true_rows()
4689 .row_addrs()
4690 .unwrap()
4691 .map(u64::from)
4692 .collect();
4693 actual_rows.sort();
4694 assert_eq!(
4695 actual_rows,
4696 vec![1, 2],
4697 "Should find rows 1 and 2 where value in [0, 5]"
4698 );
4699
4700 let null_row_ids = row_ids.null_rows();
4702 assert!(!null_row_ids.is_empty(), "null_row_ids should be non-empty");
4703 let null_rows: Vec<u64> =
4704 null_row_ids.row_addrs().unwrap().map(u64::from).collect();
4705 assert_eq!(null_rows, vec![0], "Should report row 0 as null");
4706 }
4707 _ => panic!("Expected Exact search result"),
4708 }
4709 }
4710
4711 #[tokio::test]
4712 async fn test_range_btree_index_consistency() {
4713 let full_tmpdir = TempObjDir::default();
4715 let full_store = Arc::new(LanceIndexStore::new(
4716 Arc::new(ObjectStore::local()),
4717 full_tmpdir.clone(),
4718 Arc::new(LanceCache::no_cache()),
4719 ));
4720
4721 let range_tmpdir = TempObjDir::default();
4722 let range_store = Arc::new(LanceIndexStore::new(
4723 Arc::new(ObjectStore::local()),
4724 range_tmpdir.clone(),
4725 Arc::new(LanceCache::no_cache()),
4726 ));
4727
4728 let total_count = 4 * DEFAULT_BTREE_BATCH_SIZE;
4731 let full_data_gen = gen_batch()
4732 .col("value", array::step::<Int32Type>())
4733 .col("_rowid", array::step::<UInt64Type>())
4734 .into_df_stream(RowCount::from(total_count / 4), BatchCount::from(4));
4735 let full_data_source = Box::pin(RecordBatchStreamAdapter::new(
4736 full_data_gen.schema(),
4737 full_data_gen,
4738 ));
4739
4740 train_btree_index(
4741 full_data_source,
4742 full_store.as_ref(),
4743 DEFAULT_BTREE_BATCH_SIZE,
4744 None,
4745 None,
4746 )
4747 .await
4748 .unwrap();
4749
4750 let range1_gen = gen_batch()
4753 .col("value", array::step::<Int32Type>())
4754 .col("_rowid", array::step::<UInt64Type>())
4755 .into_df_stream(
4756 RowCount::from(DEFAULT_BTREE_BATCH_SIZE / 2),
4757 BatchCount::from(5),
4758 );
4759 let range1_data_source = Box::pin(RecordBatchStreamAdapter::new(
4760 range1_gen.schema(),
4761 range1_gen,
4762 ));
4763
4764 train_btree_index(
4765 range1_data_source,
4766 range_store.as_ref(),
4767 DEFAULT_BTREE_BATCH_SIZE,
4768 None,
4769 Option::from(0u32),
4770 )
4771 .await
4772 .unwrap();
4773
4774 let start_val = (DEFAULT_BTREE_BATCH_SIZE * 2 + DEFAULT_BTREE_BATCH_SIZE / 2) as i32;
4776 let end_val = (4 * DEFAULT_BTREE_BATCH_SIZE) as i32;
4777 let values_second_half: Vec<i32> = (start_val..end_val).collect();
4778 let row_ids_second_half: Vec<u64> = (start_val as u64..end_val as u64).collect();
4779 let range2_gen = gen_batch()
4780 .col("value", array::cycle::<Int32Type>(values_second_half))
4781 .col("_rowid", array::cycle::<UInt64Type>(row_ids_second_half))
4782 .into_df_stream(
4783 RowCount::from(DEFAULT_BTREE_BATCH_SIZE / 2),
4784 BatchCount::from(3),
4785 );
4786 let range2_data_source = Box::pin(RecordBatchStreamAdapter::new(
4787 range2_gen.schema(),
4788 range2_gen,
4789 ));
4790
4791 train_btree_index(
4792 range2_data_source,
4793 range_store.as_ref(),
4794 DEFAULT_BTREE_BATCH_SIZE,
4795 None,
4796 Option::from(1u32),
4797 )
4798 .await
4799 .unwrap();
4800
4801 let part_page_files = vec![
4803 part_page_data_file_path(0 << 32),
4804 part_page_data_file_path(1 << 32),
4805 ];
4806
4807 let part_lookup_files = vec![
4808 part_lookup_file_path(0 << 32),
4809 part_lookup_file_path(1 << 32),
4810 ];
4811
4812 super::merge_metadata_files(
4813 range_store.as_ref(),
4814 &part_page_files,
4815 &part_lookup_files,
4816 Option::from(1usize),
4817 noop_progress(),
4818 )
4819 .await
4820 .unwrap();
4821
4822 let full_index = BTreeIndex::load(full_store.clone(), None, &LanceCache::no_cache())
4823 .await
4824 .unwrap();
4825
4826 let ranged_index = BTreeIndex::load(range_store.clone(), None, &LanceCache::no_cache())
4827 .await
4828 .unwrap();
4829
4830 let query_0 = SargableQuery::Equals(ScalarValue::Int32(Some(0)));
4834 let full_result_0 = full_index
4835 .search(&query_0, &NoOpMetricsCollector)
4836 .await
4837 .unwrap();
4838 let ranged_result_0 = ranged_index
4839 .search(&query_0, &NoOpMetricsCollector)
4840 .await
4841 .unwrap();
4842 assert_eq!(full_result_0, ranged_result_0, "Query for value 0 failed");
4843
4844 let mid_first_batch = (DEFAULT_BTREE_BATCH_SIZE / 2) as i32;
4846 let query_mid_first = SargableQuery::Equals(ScalarValue::Int32(Some(mid_first_batch)));
4847 let full_result_mid_first = full_index
4848 .search(&query_mid_first, &NoOpMetricsCollector)
4849 .await
4850 .unwrap();
4851 let ranged_result_mid_first = ranged_index
4852 .search(&query_mid_first, &NoOpMetricsCollector)
4853 .await
4854 .unwrap();
4855 assert_eq!(
4856 full_result_mid_first, ranged_result_mid_first,
4857 "Query for value {} failed",
4858 mid_first_batch
4859 );
4860
4861 let mid_last_batch = (DEFAULT_BTREE_BATCH_SIZE * 3 + (DEFAULT_BTREE_BATCH_SIZE / 2)) as i32;
4863 let query_mid_last = SargableQuery::Equals(ScalarValue::Int32(Some(mid_last_batch)));
4864 let full_result_mid_last = full_index
4865 .search(&query_mid_last, &NoOpMetricsCollector)
4866 .await
4867 .unwrap();
4868 let ranged_result_mid_last = ranged_index
4869 .search(&query_mid_last, &NoOpMetricsCollector)
4870 .await
4871 .unwrap();
4872 assert_eq!(
4873 full_result_mid_last, ranged_result_mid_last,
4874 "Query for value {} failed",
4875 mid_last_batch
4876 );
4877
4878 let max_val = (4 * DEFAULT_BTREE_BATCH_SIZE - 1) as i32;
4880 let query_max = SargableQuery::Equals(ScalarValue::Int32(Some(max_val)));
4881 let full_result_max = full_index
4882 .search(&query_max, &NoOpMetricsCollector)
4883 .await
4884 .unwrap();
4885 let ranged_result_max = ranged_index
4886 .search(&query_max, &NoOpMetricsCollector)
4887 .await
4888 .unwrap();
4889 assert_eq!(
4890 full_result_max, ranged_result_max,
4891 "Query for maximum value {} failed",
4892 max_val
4893 );
4894
4895 let second_first_val = (DEFAULT_BTREE_BATCH_SIZE * 2 + DEFAULT_BTREE_BATCH_SIZE / 2) as i32;
4897 let query_second_first = SargableQuery::Equals(ScalarValue::Int32(Some(second_first_val)));
4898 let full_result_second_first = full_index
4899 .search(&query_second_first, &NoOpMetricsCollector)
4900 .await
4901 .unwrap();
4902 let ranged_result_second_first = ranged_index
4903 .search(&query_second_first, &NoOpMetricsCollector)
4904 .await
4905 .unwrap();
4906 assert_eq!(
4907 full_result_second_first, ranged_result_second_first,
4908 "Query for first value of the second page file {} failed",
4909 second_first_val
4910 );
4911
4912 let query_below_min = SargableQuery::Equals(ScalarValue::Int32(Some(-1)));
4914 let full_result_below = full_index
4915 .search(&query_below_min, &NoOpMetricsCollector)
4916 .await
4917 .unwrap();
4918 let ranged_result_below = ranged_index
4919 .search(&query_below_min, &NoOpMetricsCollector)
4920 .await
4921 .unwrap();
4922 assert_eq!(
4923 full_result_below, ranged_result_below,
4924 "Query for value below minimum (-1) failed"
4925 );
4926
4927 let query_above_max = SargableQuery::Equals(ScalarValue::Int32(Some(max_val + 1)));
4929 let full_result_above = full_index
4930 .search(&query_above_max, &NoOpMetricsCollector)
4931 .await
4932 .unwrap();
4933 let ranged_result_above = ranged_index
4934 .search(&query_above_max, &NoOpMetricsCollector)
4935 .await
4936 .unwrap();
4937 assert_eq!(
4938 full_result_above,
4939 ranged_result_above,
4940 "Query for value above maximum ({}) failed",
4941 max_val + 1
4942 );
4943
4944 let range_start =
4948 (DEFAULT_BTREE_BATCH_SIZE * 2 + DEFAULT_BTREE_BATCH_SIZE / 2 - 100) as i32;
4949 let range_end = range_start + 200;
4950 let query_cross_range = SargableQuery::Range(
4951 std::collections::Bound::Included(ScalarValue::Int32(Some(range_start))),
4952 std::collections::Bound::Excluded(ScalarValue::Int32(Some(range_end))),
4953 );
4954 let full_result_cross = full_index
4955 .search(&query_cross_range, &NoOpMetricsCollector)
4956 .await
4957 .unwrap();
4958 let ranged_result_cross = ranged_index
4959 .search(&query_cross_range, &NoOpMetricsCollector)
4960 .await
4961 .unwrap();
4962 assert_eq!(
4963 full_result_cross, ranged_result_cross,
4964 "Cross-range range query [{}, {}] failed",
4965 range_start, range_end
4966 );
4967
4968 let single_range_start = (DEFAULT_BTREE_BATCH_SIZE * 4 - 300) as i32;
4970 let single_range_end = single_range_start + 200;
4971 let query_single_range = SargableQuery::Range(
4972 std::collections::Bound::Included(ScalarValue::Int32(Some(single_range_start))),
4973 std::collections::Bound::Excluded(ScalarValue::Int32(Some(single_range_end))),
4974 );
4975 let full_result_single = full_index
4976 .search(&query_single_range, &NoOpMetricsCollector)
4977 .await
4978 .unwrap();
4979 let ranged_result_single = ranged_index
4980 .search(&query_single_range, &NoOpMetricsCollector)
4981 .await
4982 .unwrap();
4983 assert_eq!(
4984 full_result_single, ranged_result_single,
4985 "Single range query [{}, {}] failed",
4986 single_range_start, single_range_end
4987 );
4988
4989 let large_range_start = 100_i32;
4991 let large_range_end = (DEFAULT_BTREE_BATCH_SIZE * 4 - 100) as i32;
4992 let query_large_range = SargableQuery::Range(
4993 std::collections::Bound::Included(ScalarValue::Int32(Some(large_range_start))),
4994 std::collections::Bound::Excluded(ScalarValue::Int32(Some(large_range_end))),
4995 );
4996 let full_result_single = full_index
4997 .search(&query_large_range, &NoOpMetricsCollector)
4998 .await
4999 .unwrap();
5000 let ranged_result_single = ranged_index
5001 .search(&query_large_range, &NoOpMetricsCollector)
5002 .await
5003 .unwrap();
5004 assert_eq!(
5005 full_result_single, ranged_result_single,
5006 "Single fragment range query [{}, {}] failed",
5007 large_range_start, large_range_end
5008 );
5009
5010 let remap_dir = TempObjDir::default();
5011 let remap_store = Arc::new(LanceIndexStore::new(
5012 Arc::new(ObjectStore::local()),
5013 remap_dir.clone(),
5014 Arc::new(LanceCache::no_cache()),
5015 ));
5016
5017 ranged_index
5019 .remap(&RowAddrRemap::empty(), remap_store.as_ref())
5020 .await
5021 .unwrap();
5022
5023 let remap_index = BTreeIndex::load(remap_store.clone(), None, &LanceCache::no_cache())
5024 .await
5025 .unwrap();
5026
5027 assert_eq!(remap_index.page_lookup, ranged_index.page_lookup);
5028
5029 let ranged_pages = range_store
5030 .open_index_file(part_page_data_file_path(1 << 32).as_str())
5031 .await
5032 .unwrap();
5033 let remapped_pages = remap_store
5034 .open_index_file(part_page_data_file_path(1 << 32).as_str())
5035 .await
5036 .unwrap();
5037
5038 assert_eq!(ranged_pages.num_rows(), remapped_pages.num_rows());
5039
5040 let original_data = ranged_pages
5041 .read_record_batch(0, ranged_pages.num_rows() as u64)
5042 .await
5043 .unwrap();
5044 let remapped_data = remapped_pages
5045 .read_record_batch(0, remapped_pages.num_rows() as u64)
5046 .await
5047 .unwrap();
5048
5049 assert_eq!(original_data, remapped_data);
5050 }
5051
5052 #[tokio::test]
5053 async fn test_update_ranged_index() {
5054 let old_tmpdir = TempObjDir::default();
5056 let old_store = Arc::new(LanceIndexStore::new(
5057 Arc::new(ObjectStore::local()),
5058 old_tmpdir.clone(),
5059 Arc::new(LanceCache::no_cache()),
5060 ));
5061
5062 let new_tmpdir = TempObjDir::default();
5063 let new_store = Arc::new(LanceIndexStore::new(
5064 Arc::new(ObjectStore::local()),
5065 new_tmpdir.clone(),
5066 Arc::new(LanceCache::no_cache()),
5067 ));
5068
5069 let range1_gen = gen_batch()
5071 .col("value", array::step::<Int32Type>())
5072 .col("_rowid", array::step::<UInt64Type>())
5073 .into_df_stream(
5074 RowCount::from(DEFAULT_BTREE_BATCH_SIZE / 2),
5075 BatchCount::from(5),
5076 );
5077 let range1_data_source = Box::pin(RecordBatchStreamAdapter::new(
5078 range1_gen.schema(),
5079 range1_gen,
5080 ));
5081
5082 train_btree_index(
5083 range1_data_source,
5084 old_store.as_ref(),
5085 DEFAULT_BTREE_BATCH_SIZE,
5086 None,
5087 Option::from(1u32),
5088 )
5089 .await
5090 .unwrap();
5091
5092 let start_val = (DEFAULT_BTREE_BATCH_SIZE * 2 + DEFAULT_BTREE_BATCH_SIZE / 2) as i32;
5094 let end_val = (4 * DEFAULT_BTREE_BATCH_SIZE) as i32;
5095 let values_second_half: Vec<i32> = (start_val..end_val).collect();
5096 let row_ids_second_half: Vec<u64> = (start_val as u64..end_val as u64).collect();
5097 let range2_gen = gen_batch()
5098 .col("value", array::cycle::<Int32Type>(values_second_half))
5099 .col("_rowid", array::cycle::<UInt64Type>(row_ids_second_half))
5100 .into_df_stream(
5101 RowCount::from(DEFAULT_BTREE_BATCH_SIZE / 2),
5102 BatchCount::from(3),
5103 );
5104 let range2_data_source = Box::pin(RecordBatchStreamAdapter::new(
5105 range2_gen.schema(),
5106 range2_gen,
5107 ));
5108
5109 train_btree_index(
5110 range2_data_source,
5111 old_store.as_ref(),
5112 DEFAULT_BTREE_BATCH_SIZE,
5113 None,
5114 Option::from(2u32),
5115 )
5116 .await
5117 .unwrap();
5118
5119 let part_page_files = vec![
5121 part_page_data_file_path(1 << 32),
5122 part_page_data_file_path(2 << 32),
5123 ];
5124
5125 let part_lookup_files = vec![
5126 part_lookup_file_path(1 << 32),
5127 part_lookup_file_path(2 << 32),
5128 ];
5129
5130 super::merge_metadata_files(
5131 old_store.as_ref(),
5132 &part_page_files,
5133 &part_lookup_files,
5134 Option::from(1usize),
5135 noop_progress(),
5136 )
5137 .await
5138 .unwrap();
5139
5140 let start_val = (DEFAULT_BTREE_BATCH_SIZE * 2) as i32;
5142 let end_val = (DEFAULT_BTREE_BATCH_SIZE * 3) as i32;
5143 let row_id_delta = (DEFAULT_BTREE_BATCH_SIZE * 3) as i32;
5144 let values: Vec<i32> = (start_val..end_val).collect();
5145 let row_ids: Vec<u64> =
5146 ((start_val + row_id_delta) as u64..(end_val + row_id_delta) as u64).collect();
5147 let update_data = gen_batch()
5148 .col("value", array::cycle::<Int32Type>(values))
5149 .col("_rowid", array::cycle::<UInt64Type>(row_ids))
5150 .into_df_stream(
5151 RowCount::from(DEFAULT_BTREE_BATCH_SIZE / 2),
5152 BatchCount::from(2),
5153 );
5154 let update_data_source = Box::pin(RecordBatchStreamAdapter::new(
5155 update_data.schema(),
5156 update_data,
5157 ));
5158
5159 let ranged_index = BTreeIndex::load(old_store.clone(), None, &LanceCache::no_cache())
5160 .await
5161 .unwrap();
5162
5163 ranged_index
5165 .update(update_data_source, new_store.as_ref(), None)
5166 .await
5167 .expect("Error in updating ranged index");
5168
5169 let updated_index = BTreeIndex::load(new_store.clone(), None, &LanceCache::no_cache())
5170 .await
5171 .unwrap();
5172
5173 assert!(
5174 updated_index.ranges_to_files.is_none(),
5175 "Updated ranged-btree-index should fall back to non-ranged"
5176 );
5177
5178 let updated_value = (DEFAULT_BTREE_BATCH_SIZE * 2 + (DEFAULT_BTREE_BATCH_SIZE / 2)) as i32;
5179 let updated_query = SargableQuery::Equals(ScalarValue::Int32(Some(updated_value)));
5180
5181 let query_result = updated_index
5182 .search(&updated_query, &NoOpMetricsCollector)
5183 .await
5184 .unwrap();
5185 match query_result {
5186 SearchResult::Exact(row_id_map) => {
5187 assert!(
5188 row_id_map.selected(updated_value as u64),
5189 "Updated index should contain original rowids."
5190 );
5191 assert!(
5192 row_id_map.selected((updated_value + row_id_delta) as u64),
5193 "Updated index should contain new rowids"
5194 );
5195 }
5196 _ => {
5197 panic!("Btree search result should always be Exact.");
5198 }
5199 }
5200 }
5201
5202 #[tokio::test]
5203 async fn test_update_with_exact_row_id_filter() {
5204 let old_tmpdir = TempObjDir::default();
5205 let old_store = Arc::new(LanceIndexStore::new(
5206 Arc::new(ObjectStore::local()),
5207 old_tmpdir.clone(),
5208 Arc::new(LanceCache::no_cache()),
5209 ));
5210
5211 let new_tmpdir = TempObjDir::default();
5212 let new_store = Arc::new(LanceIndexStore::new(
5213 Arc::new(ObjectStore::local()),
5214 new_tmpdir.clone(),
5215 Arc::new(LanceCache::no_cache()),
5216 ));
5217
5218 let old_data = gen_batch()
5219 .col("value", array::step::<Int32Type>())
5220 .col("_rowid", array::step::<UInt64Type>())
5221 .into_df_stream(RowCount::from(512), BatchCount::from(2));
5222 let old_data_source = Box::pin(RecordBatchStreamAdapter::new(old_data.schema(), old_data));
5223 train_btree_index(
5224 old_data_source,
5225 old_store.as_ref(),
5226 DEFAULT_BTREE_BATCH_SIZE,
5227 None,
5228 None,
5229 )
5230 .await
5231 .unwrap();
5232
5233 let index = BTreeIndex::load(old_store.clone(), None, &LanceCache::no_cache())
5234 .await
5235 .unwrap();
5236
5237 let new_data = gen_batch()
5238 .col("value", array::step_custom::<Int32Type>(2000, 1))
5239 .col("_rowid", array::step_custom::<UInt64Type>(2000, 1))
5240 .into_df_stream(RowCount::from(100), BatchCount::from(1));
5241 let new_data_source = Box::pin(RecordBatchStreamAdapter::new(new_data.schema(), new_data));
5242
5243 let mut retained_old_rows = RowAddrTreeMap::new();
5244 retained_old_rows.insert_range(0..64);
5245 retained_old_rows.insert_range(300..364);
5246
5247 index
5248 .update(
5249 new_data_source,
5250 new_store.as_ref(),
5251 Some(OldIndexDataFilter::RowIds(retained_old_rows)),
5252 )
5253 .await
5254 .unwrap();
5255
5256 let updated_index = BTreeIndex::load(new_store.clone(), None, &LanceCache::no_cache())
5257 .await
5258 .unwrap();
5259
5260 let present = |value: i32| {
5261 let updated_index = updated_index.clone();
5262 async move {
5263 let query = SargableQuery::Equals(ScalarValue::Int32(Some(value)));
5264 match updated_index
5265 .search(&query, &NoOpMetricsCollector)
5266 .await
5267 .unwrap()
5268 {
5269 SearchResult::Exact(row_id_map) => row_id_map.selected(value as u64),
5270 _ => unreachable!("Btree search result should always be Exact"),
5271 }
5272 }
5273 };
5274
5275 assert!(present(12).await);
5276 assert!(present(320).await);
5277 assert!(!present(120).await);
5278 assert!(!present(420).await);
5279 assert!(present(2005).await);
5280 }
5281
5282 #[tokio::test]
5292 async fn test_btree_remap_big_deletions() {
5293 let tmpdir = TempObjDir::default();
5294 let test_store = Arc::new(LanceIndexStore::new(
5295 Arc::new(ObjectStore::local()),
5296 tmpdir.clone(),
5297 Arc::new(LanceCache::no_cache()),
5298 ));
5299
5300 let batch_size = 4096;
5303 let total_rows = 15000;
5304
5305 let stream = gen_batch()
5306 .col("value", array::step::<Int32Type>())
5307 .col("_rowid", array::step::<UInt64Type>())
5308 .into_df_stream(RowCount::from(total_rows), BatchCount::from(1));
5309
5310 train_btree_index(stream, test_store.as_ref(), batch_size, None, None)
5311 .await
5312 .unwrap();
5313
5314 let index = BTreeIndex::load(test_store.clone(), None, &LanceCache::no_cache())
5315 .await
5316 .unwrap();
5317
5318 let mut mapping: HashMap<u64, Option<u64>> = HashMap::new();
5324
5325 for old_id in 1001..10000 {
5327 mapping.insert(old_id, None);
5328 }
5329
5330 for (new_id, old_id) in (100_000..).zip((0..1000).chain(10000..15000)) {
5332 mapping.insert(old_id, Some(new_id));
5333 }
5334
5335 let remap_dir = TempObjDir::default();
5336 let remap_store = Arc::new(LanceIndexStore::new(
5337 Arc::new(ObjectStore::local()),
5338 remap_dir.clone(),
5339 Arc::new(LanceCache::no_cache()),
5340 ));
5341
5342 index
5344 .remap(&RowAddrRemap::direct(mapping), remap_store.as_ref())
5345 .await
5346 .unwrap();
5347
5348 let remapped_index = BTreeIndex::load(remap_store.clone(), None, &LanceCache::no_cache())
5349 .await
5350 .unwrap();
5351
5352 let should_exist = vec![0, 500, 1000, 10000, 13000, 14000, 14999];
5356 for value in should_exist {
5357 let query = SargableQuery::Equals(ScalarValue::Int32(Some(value)));
5358 let result = remapped_index
5359 .search(&query, &NoOpMetricsCollector)
5360 .await
5361 .unwrap();
5362 match result {
5363 SearchResult::Exact(row_id_map) => {
5364 assert!(
5365 !row_id_map.is_empty(),
5366 "Value {} should exist in remapped index but was not found",
5367 value
5368 );
5369 }
5370 _ => {
5371 panic!("Btree search result should always be Exact.");
5372 }
5373 }
5374 }
5375
5376 let should_not_exist = vec![1001, 5000, 8000, 9999];
5378 for value in should_not_exist {
5379 let query = SargableQuery::Equals(ScalarValue::Int32(Some(value)));
5380 let result = remapped_index
5381 .search(&query, &NoOpMetricsCollector)
5382 .await
5383 .unwrap();
5384 match result {
5385 SearchResult::Exact(row_id_map) => {
5386 assert!(
5387 row_id_map.is_empty(),
5388 "Value {} should NOT exist in remapped index but was found",
5389 value
5390 );
5391 }
5392 _ => {
5393 panic!("Btree search result should always be Exact.");
5394 }
5395 }
5396 }
5397 }
5398
5399 #[tokio::test]
5405 async fn test_search_tracks_nulls_for_absent_value() {
5406 use arrow_array::{Int32Array, UInt64Array};
5407
5408 let tmpdir = TempObjDir::default();
5409 let test_store = Arc::new(LanceIndexStore::new(
5410 Arc::new(ObjectStore::local()),
5411 tmpdir.clone(),
5412 Arc::new(LanceCache::no_cache()),
5413 ));
5414
5415 let num_rows = 5000u64;
5419 let values: Int32Array = (0..num_rows)
5420 .map(|i| {
5421 if i % 5 != 0 {
5422 None } else {
5424 Some(100 + i as i32) }
5426 })
5427 .collect();
5428 let row_ids = UInt64Array::from_iter_values(0..num_rows);
5429 let data = arrow_array::RecordBatch::try_from_iter(vec![
5430 ("value", Arc::new(values) as arrow_array::ArrayRef),
5431 ("_rowid", Arc::new(row_ids) as arrow_array::ArrayRef),
5432 ])
5433 .unwrap();
5434
5435 let schema = data.schema();
5436 let stream: SendableRecordBatchStream = Box::pin(RecordBatchStreamAdapter::new(
5437 schema,
5438 stream::iter(vec![Ok(data)]),
5439 ));
5440 train_btree_index(stream, test_store.as_ref(), num_rows, None, None)
5441 .await
5442 .unwrap();
5443
5444 let index = BTreeIndex::load(test_store.clone(), None, &LanceCache::no_cache())
5445 .await
5446 .unwrap();
5447
5448 assert!(
5450 !index.page_lookup.all_null_pages.is_empty(),
5451 "Test setup requires all-null pages; got null_pages={}, all_null_pages={}",
5452 index.page_lookup.null_pages.len(),
5453 index.page_lookup.all_null_pages.len(),
5454 );
5455
5456 let metrics = NoOpMetricsCollector;
5457
5458 let result = index
5460 .search(
5461 &SargableQuery::Equals(ScalarValue::Int32(Some(0))),
5462 &metrics,
5463 )
5464 .await
5465 .unwrap();
5466
5467 match result {
5468 SearchResult::Exact(set) => {
5469 assert!(set.true_rows().is_empty(), "No rows should match Equals(0)");
5471 assert!(
5473 !set.null_rows().is_empty(),
5474 "Null rows must be tracked even when no pages match the value"
5475 );
5476 }
5477 _ => panic!("BTree search should return Exact"),
5478 }
5479
5480 let result = index
5482 .search(
5483 &SargableQuery::Range(
5484 std::ops::Bound::Unbounded,
5485 std::ops::Bound::Excluded(ScalarValue::Int32(Some(50))),
5486 ),
5487 &metrics,
5488 )
5489 .await
5490 .unwrap();
5491
5492 match result {
5493 SearchResult::Exact(set) => {
5494 assert!(set.true_rows().is_empty(), "No rows should be < 50");
5495 assert!(
5496 !set.null_rows().is_empty(),
5497 "Null rows must be tracked for range queries too"
5498 );
5499 }
5500 _ => panic!("BTree search should return Exact"),
5501 }
5502 }
5503
5504 fn sample_lookup_batch() -> RecordBatch {
5505 record_batch!(
5506 ("min", Int32, [Some(0), Some(10), Some(20)]),
5507 ("max", Int32, [Some(9), Some(19), Some(29)]),
5508 ("null_count", UInt32, [0, 2, 0]),
5509 ("page_idx", UInt32, [0, 1, 2])
5510 )
5511 .unwrap()
5512 }
5513
5514 fn osv(v: i32) -> OrderableScalarValue {
5515 OrderableScalarValue(ScalarValue::Int32(Some(v)))
5516 }
5517
5518 #[test]
5522 fn test_btree_lookup_pages_between() {
5523 let batch = record_batch!(
5526 ("min", Int32, [None, Some(10), Some(20), Some(20), Some(40)]),
5527 (
5528 "max",
5529 Int32,
5530 [Some(5), Some(20), Some(20), Some(30), Some(50)]
5531 ),
5532 ("null_count", UInt32, [2, 0, 0, 0, 0]),
5533 ("page_idx", UInt32, [0, 1, 2, 3, 4])
5534 )
5535 .unwrap();
5536 let lookup = BTreeLookup::try_new(batch).unwrap();
5537 assert_eq!(lookup.null_pages, vec![0]);
5538 assert!(lookup.all_null_pages.is_empty());
5539 assert_eq!(lookup.search_start, 0);
5540
5541 let between = |lo: i32, hi: i32| {
5542 let mut m = lookup
5543 .pages_between((
5544 std::ops::Bound::Included(&osv(lo)),
5545 std::ops::Bound::Included(&osv(hi)),
5546 ))
5547 .unwrap();
5548 m.sort_by_key(|m| m.page_id());
5549 m
5550 };
5551
5552 assert_eq!(lookup.pages_eq(&osv(15)).unwrap(), vec![Matches::Some(1)]);
5554 assert_eq!(
5555 lookup.pages_eq(&osv(20)).unwrap(),
5556 vec![Matches::Some(1), Matches::Some(2), Matches::Some(3)]
5557 );
5558 assert!(lookup.pages_eq(&osv(35)).unwrap().is_empty());
5559
5560 assert_eq!(
5563 between(20, 25),
5564 vec![Matches::Some(1), Matches::All(2), Matches::Some(3)]
5565 );
5566
5567 assert_eq!(between(0, 5), vec![Matches::Some(0)]);
5570
5571 assert_eq!(
5573 lookup
5574 .pages_between((
5575 std::ops::Bound::Included(&osv(40)),
5576 std::ops::Bound::Unbounded
5577 ))
5578 .unwrap(),
5579 vec![Matches::All(4)]
5580 );
5581
5582 assert!(between(31, 39).is_empty());
5584 assert!(
5585 lookup
5586 .pages_between((
5587 std::ops::Bound::Included(&osv(25)),
5588 std::ops::Bound::Included(&osv(15))
5589 ))
5590 .unwrap()
5591 .is_empty()
5592 );
5593 }
5594
5595 #[test]
5599 fn test_btree_lookup_pages_eq_bytes() {
5600 use arrow_array::{
5601 ArrayRef, BinaryArray, FixedSizeBinaryArray, LargeBinaryArray, LargeStringArray,
5602 UInt32Array,
5603 };
5604 use arrow_schema::{DataType, Field, Schema};
5605
5606 fn be(v: u16) -> [u8; 2] {
5610 v.to_be_bytes()
5611 }
5612 let mins = [None, Some(10u16), Some(20), Some(20), Some(40)];
5613 let maxs = [Some(5u16), Some(20), Some(20), Some(30), Some(50)];
5614 let null_count = UInt32Array::from(vec![2u32, 0, 0, 0, 0]);
5615 let page_idx = UInt32Array::from(vec![0u32, 1, 2, 3, 4]);
5616
5617 let assert_byte_lookup =
5618 |min_arr: ArrayRef, max_arr: ArrayRef, sv: &dyn Fn(u16) -> ScalarValue| {
5619 let batch = RecordBatch::try_new(
5620 Arc::new(Schema::new(vec![
5621 Field::new("min", min_arr.data_type().clone(), true),
5622 Field::new("max", max_arr.data_type().clone(), true),
5623 Field::new("null_count", DataType::UInt32, false),
5624 Field::new("page_idx", DataType::UInt32, false),
5625 ])),
5626 vec![
5627 min_arr,
5628 max_arr,
5629 Arc::new(null_count.clone()),
5630 Arc::new(page_idx.clone()),
5631 ],
5632 )
5633 .unwrap();
5634 let lookup = BTreeLookup::try_new(batch).unwrap();
5635
5636 let eq = |v: u16| {
5637 let mut p: Vec<u32> = lookup
5638 .pages_eq(&OrderableScalarValue(sv(v)))
5639 .unwrap()
5640 .into_iter()
5641 .map(|m| m.page_id())
5642 .collect();
5643 p.sort_unstable();
5644 p
5645 };
5646 assert_eq!(eq(15), vec![1]); assert_eq!(eq(20), vec![1, 2, 3]); assert!(eq(35).is_empty()); assert_eq!(eq(5), vec![0]); let mut in_pages: Vec<u32> = lookup
5653 .pages_in([5u16, 15].into_iter().map(|v| OrderableScalarValue(sv(v))))
5654 .unwrap()
5655 .into_iter()
5656 .map(|m| m.page_id())
5657 .collect();
5658 in_pages.sort_unstable();
5659 assert_eq!(in_pages, vec![0, 1]);
5660 };
5661
5662 let fsb = |arr: &[Option<u16>]| -> ArrayRef {
5663 Arc::new(
5664 FixedSizeBinaryArray::try_from_sparse_iter_with_size(
5665 arr.iter().copied().map(|o| o.map(be)),
5666 2,
5667 )
5668 .unwrap(),
5669 )
5670 };
5671 assert_byte_lookup(fsb(&mins), fsb(&maxs), &|v| {
5672 ScalarValue::FixedSizeBinary(2, Some(be(v).to_vec()))
5673 });
5674
5675 let bin = |arr: &[Option<u16>]| -> ArrayRef {
5676 Arc::new(BinaryArray::from_iter(
5677 arr.iter().copied().map(|o| o.map(|v| be(v).to_vec())),
5678 ))
5679 };
5680 assert_byte_lookup(bin(&mins), bin(&maxs), &|v| {
5681 ScalarValue::Binary(Some(be(v).to_vec()))
5682 });
5683
5684 let lbin = |arr: &[Option<u16>]| -> ArrayRef {
5685 Arc::new(LargeBinaryArray::from_iter(
5686 arr.iter().copied().map(|o| o.map(|v| be(v).to_vec())),
5687 ))
5688 };
5689 assert_byte_lookup(lbin(&mins), lbin(&maxs), &|v| {
5690 ScalarValue::LargeBinary(Some(be(v).to_vec()))
5691 });
5692
5693 let lstr = |arr: &[Option<u16>]| -> ArrayRef {
5696 Arc::new(LargeStringArray::from_iter(
5697 arr.iter().copied().map(|o| o.map(|v| format!("{v:02}"))),
5698 ))
5699 };
5700 assert_byte_lookup(lstr(&mins), lstr(&maxs), &|v| {
5701 ScalarValue::LargeUtf8(Some(format!("{v:02}")))
5702 });
5703 }
5704
5705 #[test]
5709 fn test_btree_lookup_pages_eq_temporal() {
5710 use arrow_array::{ArrayRef, Date32Array, TimestampMicrosecondArray, UInt32Array};
5711 use arrow_schema::{DataType, Field, Schema};
5712
5713 let null_count = UInt32Array::from(vec![2u32, 0, 0, 0, 0]);
5714 let page_idx = UInt32Array::from(vec![0u32, 1, 2, 3, 4]);
5715
5716 let assert_lookup =
5717 |min_arr: ArrayRef, max_arr: ArrayRef, sv: &dyn Fn(i64) -> ScalarValue| {
5718 let batch = RecordBatch::try_new(
5719 Arc::new(Schema::new(vec![
5720 Field::new("min", min_arr.data_type().clone(), true),
5721 Field::new("max", max_arr.data_type().clone(), true),
5722 Field::new("null_count", DataType::UInt32, false),
5723 Field::new("page_idx", DataType::UInt32, false),
5724 ])),
5725 vec![
5726 min_arr,
5727 max_arr,
5728 Arc::new(null_count.clone()),
5729 Arc::new(page_idx.clone()),
5730 ],
5731 )
5732 .unwrap();
5733 let lookup = BTreeLookup::try_new(batch).unwrap();
5734 let eq = |v: i64| {
5735 let mut p: Vec<u32> = lookup
5736 .pages_eq(&OrderableScalarValue(sv(v)))
5737 .unwrap()
5738 .into_iter()
5739 .map(|m| m.page_id())
5740 .collect();
5741 p.sort_unstable();
5742 p
5743 };
5744 assert_eq!(eq(15), vec![1]); assert_eq!(eq(20), vec![1, 2, 3]); assert!(eq(35).is_empty()); assert_eq!(eq(5), vec![0]); };
5749
5750 assert_lookup(
5752 Arc::new(TimestampMicrosecondArray::from(vec![
5753 None,
5754 Some(10),
5755 Some(20),
5756 Some(20),
5757 Some(40),
5758 ])),
5759 Arc::new(TimestampMicrosecondArray::from(vec![
5760 Some(5),
5761 Some(20),
5762 Some(20),
5763 Some(30),
5764 Some(50),
5765 ])),
5766 &|v| ScalarValue::TimestampMicrosecond(Some(v), None),
5767 );
5768
5769 assert_lookup(
5771 Arc::new(Date32Array::from(vec![
5772 None,
5773 Some(10),
5774 Some(20),
5775 Some(20),
5776 Some(40),
5777 ])),
5778 Arc::new(Date32Array::from(vec![
5779 Some(5),
5780 Some(20),
5781 Some(20),
5782 Some(30),
5783 Some(50),
5784 ])),
5785 &|v| ScalarValue::Date32(Some(v as i32)),
5786 );
5787 }
5788
5789 #[test]
5795 fn test_btree_lookup_pages_eq_numeric_widths() {
5796 use arrow::datatypes::i256;
5797 use arrow_array::{
5798 ArrayRef, Decimal128Array, Decimal256Array, Float16Array, Int8Array, Int16Array,
5799 UInt8Array, UInt16Array, UInt32Array, UInt64Array,
5800 };
5801 use arrow_schema::{DataType, Field, Schema};
5802 use half::f16;
5803
5804 let null_count = UInt32Array::from(vec![2u32, 0, 0, 0, 0]);
5805 let page_idx = UInt32Array::from(vec![0u32, 1, 2, 3, 4]);
5806 let assert_lookup =
5807 |min_arr: ArrayRef, max_arr: ArrayRef, sv: &dyn Fn(i64) -> ScalarValue| {
5808 let batch = RecordBatch::try_new(
5809 Arc::new(Schema::new(vec![
5810 Field::new("min", min_arr.data_type().clone(), true),
5811 Field::new("max", max_arr.data_type().clone(), true),
5812 Field::new("null_count", DataType::UInt32, false),
5813 Field::new("page_idx", DataType::UInt32, false),
5814 ])),
5815 vec![
5816 min_arr,
5817 max_arr,
5818 Arc::new(null_count.clone()),
5819 Arc::new(page_idx.clone()),
5820 ],
5821 )
5822 .unwrap();
5823 let lookup = BTreeLookup::try_new(batch).unwrap();
5824 let eq = |v: i64| {
5825 let mut p: Vec<u32> = lookup
5826 .pages_eq(&OrderableScalarValue(sv(v)))
5827 .unwrap()
5828 .into_iter()
5829 .map(|m| m.page_id())
5830 .collect();
5831 p.sort_unstable();
5832 p
5833 };
5834 assert_eq!(eq(15), vec![1]); assert_eq!(eq(20), vec![1, 2, 3]); assert!(eq(35).is_empty()); assert_eq!(eq(5), vec![0]); };
5839
5840 assert_lookup(
5841 Arc::new(Int8Array::from(vec![
5842 None,
5843 Some(10),
5844 Some(20),
5845 Some(20),
5846 Some(40),
5847 ])),
5848 Arc::new(Int8Array::from(vec![
5849 Some(5),
5850 Some(20),
5851 Some(20),
5852 Some(30),
5853 Some(50),
5854 ])),
5855 &|v| ScalarValue::Int8(Some(v as i8)),
5856 );
5857 assert_lookup(
5858 Arc::new(Int16Array::from(vec![
5859 None,
5860 Some(10),
5861 Some(20),
5862 Some(20),
5863 Some(40),
5864 ])),
5865 Arc::new(Int16Array::from(vec![
5866 Some(5),
5867 Some(20),
5868 Some(20),
5869 Some(30),
5870 Some(50),
5871 ])),
5872 &|v| ScalarValue::Int16(Some(v as i16)),
5873 );
5874 assert_lookup(
5875 Arc::new(UInt8Array::from(vec![
5876 None,
5877 Some(10),
5878 Some(20),
5879 Some(20),
5880 Some(40),
5881 ])),
5882 Arc::new(UInt8Array::from(vec![
5883 Some(5),
5884 Some(20),
5885 Some(20),
5886 Some(30),
5887 Some(50),
5888 ])),
5889 &|v| ScalarValue::UInt8(Some(v as u8)),
5890 );
5891 assert_lookup(
5892 Arc::new(UInt16Array::from(vec![
5893 None,
5894 Some(10),
5895 Some(20),
5896 Some(20),
5897 Some(40),
5898 ])),
5899 Arc::new(UInt16Array::from(vec![
5900 Some(5),
5901 Some(20),
5902 Some(20),
5903 Some(30),
5904 Some(50),
5905 ])),
5906 &|v| ScalarValue::UInt16(Some(v as u16)),
5907 );
5908 assert_lookup(
5909 Arc::new(UInt32Array::from(vec![
5910 None,
5911 Some(10),
5912 Some(20),
5913 Some(20),
5914 Some(40),
5915 ])),
5916 Arc::new(UInt32Array::from(vec![
5917 Some(5),
5918 Some(20),
5919 Some(20),
5920 Some(30),
5921 Some(50),
5922 ])),
5923 &|v| ScalarValue::UInt32(Some(v as u32)),
5924 );
5925 assert_lookup(
5926 Arc::new(UInt64Array::from(vec![
5927 None,
5928 Some(10),
5929 Some(20),
5930 Some(20),
5931 Some(40),
5932 ])),
5933 Arc::new(UInt64Array::from(vec![
5934 Some(5),
5935 Some(20),
5936 Some(20),
5937 Some(30),
5938 Some(50),
5939 ])),
5940 &|v| ScalarValue::UInt64(Some(v as u64)),
5941 );
5942
5943 let f = |v: f64| f16::from_f64(v);
5944 assert_lookup(
5945 Arc::new(Float16Array::from(vec![
5946 None,
5947 Some(f(10.0)),
5948 Some(f(20.0)),
5949 Some(f(20.0)),
5950 Some(f(40.0)),
5951 ])),
5952 Arc::new(Float16Array::from(vec![
5953 Some(f(5.0)),
5954 Some(f(20.0)),
5955 Some(f(20.0)),
5956 Some(f(30.0)),
5957 Some(f(50.0)),
5958 ])),
5959 &|v| ScalarValue::Float16(Some(f(v as f64))),
5960 );
5961
5962 let dec128 = |vals: Vec<Option<i128>>| -> ArrayRef {
5965 Arc::new(
5966 Decimal128Array::from(vals)
5967 .with_precision_and_scale(18, 0)
5968 .unwrap(),
5969 )
5970 };
5971 assert_lookup(
5972 dec128(vec![None, Some(10), Some(20), Some(20), Some(40)]),
5973 dec128(vec![Some(5), Some(20), Some(20), Some(30), Some(50)]),
5974 &|v| ScalarValue::Decimal128(Some(v as i128), 18, 0),
5975 );
5976
5977 let dec256 = |vals: Vec<Option<i128>>| -> ArrayRef {
5979 Arc::new(
5980 Decimal256Array::from(
5981 vals.into_iter()
5982 .map(|o| o.map(i256::from_i128))
5983 .collect::<Vec<_>>(),
5984 )
5985 .with_precision_and_scale(40, 0)
5986 .unwrap(),
5987 )
5988 };
5989 assert_lookup(
5990 dec256(vec![None, Some(10), Some(20), Some(20), Some(40)]),
5991 dec256(vec![Some(5), Some(20), Some(20), Some(30), Some(50)]),
5992 &|v| ScalarValue::Decimal256(Some(i256::from_i128(v as i128)), 40, 0),
5993 );
5994 }
5995
5996 #[test]
6000 fn test_btree_lookup_pages_null() {
6001 let batch = record_batch!(
6004 ("min", Int32, [None, None, Some(10), Some(20), Some(40)]),
6005 ("max", Int32, [None, Some(5), Some(20), Some(30), Some(50)]),
6006 ("null_count", UInt32, [3, 2, 1, 0, 0]),
6007 ("page_idx", UInt32, [0, 1, 2, 3, 4])
6008 )
6009 .unwrap();
6010 let lookup = BTreeLookup::try_new(batch).unwrap();
6011 assert_eq!(lookup.all_null_pages, vec![0]);
6012 assert_eq!(lookup.null_pages, vec![1, 2]);
6013
6014 assert_eq!(
6017 lookup
6018 .pages_eq(&OrderableScalarValue(ScalarValue::Int32(None)))
6019 .unwrap(),
6020 vec![Matches::Some(1), Matches::Some(2), Matches::All(0)]
6021 );
6022
6023 let in_ids = |vals: Vec<Option<i32>>| {
6024 let mut p: Vec<u32> = lookup
6025 .pages_in(
6026 vals.into_iter()
6027 .map(|v| OrderableScalarValue(ScalarValue::Int32(v))),
6028 )
6029 .unwrap()
6030 .into_iter()
6031 .map(|m| m.page_id())
6032 .collect();
6033 p.sort_unstable();
6034 p
6035 };
6036 assert_eq!(in_ids(vec![Some(45)]), vec![4]);
6038 assert_eq!(in_ids(vec![Some(45), None]), vec![0, 1, 2, 4]);
6040 assert_eq!(in_ids(vec![None]), vec![0, 1, 2]);
6042 }
6043
6044 #[test]
6047 fn test_btree_lookup_empty_batch() {
6048 use arrow_schema::{DataType, Field, Schema};
6049
6050 let schema = Arc::new(Schema::new(vec![
6051 Field::new("min", DataType::Int32, true),
6052 Field::new("max", DataType::Int32, true),
6053 Field::new("null_count", DataType::UInt32, false),
6054 Field::new("page_idx", DataType::UInt32, false),
6055 ]));
6056 let lookup = BTreeLookup::try_new(RecordBatch::new_empty(schema)).unwrap();
6057 assert_eq!(lookup.search_start, 0);
6058 assert!(lookup.null_pages.is_empty());
6059 assert!(lookup.all_null_pages.is_empty());
6060
6061 assert!(lookup.pages_eq(&osv(5)).unwrap().is_empty());
6062 assert!(lookup.pages_in([osv(5)]).unwrap().is_empty());
6063 assert!(
6064 lookup
6065 .pages_between((
6066 std::ops::Bound::Included(&osv(0)),
6067 std::ops::Bound::Included(&osv(100)),
6068 ))
6069 .unwrap()
6070 .is_empty()
6071 );
6072 assert!(lookup.pages_null().is_empty());
6073 }
6074
6075 #[test]
6081 fn test_btree_lookup_skips_all_null_page_in_scan_window() {
6082 let batch = record_batch!(
6084 ("min", Int32, [None, None, Some(10), Some(20), Some(40)]),
6085 ("max", Int32, [Some(5), None, Some(20), Some(30), Some(50)]),
6086 ("null_count", UInt32, [2, 3, 0, 0, 0]),
6087 ("page_idx", UInt32, [0, 1, 2, 3, 4])
6088 )
6089 .unwrap();
6090 let lookup = BTreeLookup::try_new(batch).unwrap();
6091 assert_eq!(lookup.search_start, 0); assert_eq!(lookup.all_null_pages, vec![1]);
6093 assert_eq!(lookup.null_pages, vec![0]);
6094
6095 assert_eq!(
6098 lookup
6099 .pages_eq(&osv(5))
6100 .unwrap()
6101 .into_iter()
6102 .map(|m| m.page_id())
6103 .collect::<Vec<_>>(),
6104 vec![0]
6105 );
6106
6107 let mut between = lookup
6110 .pages_between((
6111 std::ops::Bound::Included(&osv(0)),
6112 std::ops::Bound::Included(&osv(100)),
6113 ))
6114 .unwrap();
6115 between.sort_by_key(|m| m.page_id());
6116 assert_eq!(
6117 between,
6118 vec![
6119 Matches::Some(0),
6120 Matches::All(2),
6121 Matches::All(3),
6122 Matches::All(4),
6123 ]
6124 );
6125 }
6126
6127 fn assert_state_roundtrips(state: &BTreeIndexState) {
6128 let restored = deserialize_state(serialize_state(state)).unwrap();
6129 assert_eq!(restored.lookup_batch, state.lookup_batch);
6130 assert_eq!(restored.batch_size, state.batch_size);
6131 assert_eq!(restored.ranges_to_files, state.ranges_to_files);
6132 }
6133
6134 #[test]
6135 fn test_btree_page_key_codec() {
6136 assert!(BTreePageKey::codec().is_some());
6138 }
6139
6140 #[test]
6141 fn test_btree_index_state_roundtrip() {
6142 assert_state_roundtrips(&BTreeIndexState {
6144 lookup_batch: sample_lookup_batch(),
6145 batch_size: DEFAULT_BTREE_BATCH_SIZE,
6146 ranges_to_files: None,
6147 });
6148
6149 let ranges: RangeInclusiveMap<u32, (String, u32)> = [
6151 (0..=99, ("part_0_page_file.lance".to_string(), 0)),
6152 (100..=199, ("part_1_page_file.lance".to_string(), 100)),
6153 ]
6154 .into_iter()
6155 .collect();
6156 assert_state_roundtrips(&BTreeIndexState {
6157 lookup_batch: sample_lookup_batch(),
6158 batch_size: 8192,
6159 ranges_to_files: Some(Arc::new(ranges)),
6160 });
6161
6162 assert_state_roundtrips(&BTreeIndexState {
6164 lookup_batch: RecordBatch::new_empty(sample_lookup_batch().schema()),
6165 batch_size: DEFAULT_BTREE_BATCH_SIZE,
6166 ranges_to_files: None,
6167 });
6168 }
6169
6170 #[tokio::test]
6171 async fn test_btree_index_state_reconstruct_and_plugin_cache() {
6172 let tmpdir = TempObjDir::default();
6173 let test_store = Arc::new(LanceIndexStore::new(
6174 Arc::new(ObjectStore::local()),
6175 tmpdir.clone(),
6176 Arc::new(LanceCache::no_cache()),
6177 ));
6178
6179 let stream = gen_batch()
6180 .col("value", array::step::<Int32Type>())
6181 .col("_rowid", array::step::<UInt64Type>())
6182 .into_df_stream(RowCount::from(1000), BatchCount::from(5));
6183 train_btree_index(stream, test_store.as_ref(), 1000, None, None)
6184 .await
6185 .unwrap();
6186
6187 let index = BTreeIndex::load(test_store.clone(), None, &LanceCache::no_cache())
6188 .await
6189 .unwrap();
6190
6191 let state = BTreeIndexState {
6193 lookup_batch: index.page_lookup.batch.clone(),
6194 batch_size: index.batch_size,
6195 ranges_to_files: index.ranges_to_files.clone(),
6196 };
6197 let restored = deserialize_state(serialize_state(&state)).unwrap();
6198 let reconstructed = restored
6199 .reconstruct(test_store.clone(), &LanceCache::no_cache(), None)
6200 .unwrap();
6201 assert_eq!(
6202 reconstructed
6203 .as_any()
6204 .downcast_ref::<BTreeIndex>()
6205 .unwrap()
6206 .page_lookup,
6207 index.page_lookup
6208 );
6209
6210 let cache = LanceCache::with_capacity(64 * 1024 * 1024);
6212 let plugin = BTreeIndexPlugin;
6213 plugin.put_in_cache(&cache, index.clone()).await.unwrap();
6214 let from_cache = plugin
6215 .get_from_cache(test_store.clone(), None, &cache)
6216 .await
6217 .unwrap()
6218 .expect("index should be served from the cache");
6219
6220 let query = SargableQuery::Range(
6222 std::ops::Bound::Included(ScalarValue::Int32(Some(100))),
6223 std::ops::Bound::Excluded(ScalarValue::Int32(Some(200))),
6224 );
6225 let expected = index.search(&query, &NoOpMetricsCollector).await.unwrap();
6226 let actual = from_cache
6227 .search(&query, &NoOpMetricsCollector)
6228 .await
6229 .unwrap();
6230 assert_eq!(expected, actual);
6231 }
6232
6233 #[test]
6236 fn test_btree_index_state_lookup_is_zero_copy() {
6237 use lance_core::cache::CacheCodec;
6238 const ALIGN: usize = 64;
6239
6240 let ranges: RangeInclusiveMap<u32, (String, u32)> =
6241 [(0..=99, ("part_0_page_file.lance".to_string(), 0))]
6242 .into_iter()
6243 .collect();
6244 let state = BTreeIndexState {
6245 lookup_batch: sample_lookup_batch(),
6246 batch_size: 8192,
6247 ranges_to_files: Some(Arc::new(ranges)),
6248 };
6249
6250 let codec = CacheCodec::from_impl::<BTreeIndexState>();
6251 let any: Arc<dyn std::any::Any + Send + Sync> = Arc::new(state);
6252 let mut buf = Vec::new();
6253 codec.serialize(&any, &mut buf).unwrap();
6254
6255 let mut v = vec![0u8; buf.len() + ALIGN];
6256 let pad = (ALIGN - (v.as_ptr() as usize % ALIGN)) % ALIGN;
6257 v[pad..pad + buf.len()].copy_from_slice(&buf);
6258 let data = bytes::Bytes::from(v).slice(pad..pad + buf.len());
6259
6260 let restored = codec.deserialize(&data).hit().unwrap();
6261 let restored = restored.downcast::<BTreeIndexState>().unwrap();
6262
6263 let base = data.as_ptr() as usize;
6264 let end = base + data.len();
6265 for col in restored.lookup_batch.columns() {
6266 for buffer in col.to_data().buffers() {
6267 let ptr = buffer.as_ptr() as usize;
6268 assert!(
6269 ptr >= base && ptr < end,
6270 "lookup batch buffer was realigned out of the input — misaligned IPC section",
6271 );
6272 }
6273 }
6274 }
6275
6276 #[test]
6277 fn test_btree_index_state_rejects_truncated_header() {
6278 let mut buf = Vec::new();
6281 buf.extend_from_slice(&100u32.to_le_bytes()); buf.extend_from_slice(&[0u8; 4]); assert!(deserialize_state(buf).is_err());
6284 }
6285
6286 #[tokio::test]
6287 async fn test_btree_index_state_reconstruct_applies_frag_reuse_index() {
6288 use crate::frag_reuse::{FragReuseIndex, FragReuseIndexDetails, FragReuseIndexHandle};
6289 use std::collections::HashMap;
6290 use uuid::Uuid;
6291
6292 let tmpdir = TempObjDir::default();
6293 let test_store = Arc::new(LanceIndexStore::new(
6294 Arc::new(ObjectStore::local()),
6295 tmpdir.clone(),
6296 Arc::new(LanceCache::no_cache()),
6297 ));
6298
6299 let stream = gen_batch()
6301 .col("value", array::step::<Int32Type>())
6302 .col("_rowid", array::step::<UInt64Type>())
6303 .into_df_stream(RowCount::from(1000), BatchCount::from(1));
6304 train_btree_index(stream, test_store.as_ref(), 1000, None, None)
6305 .await
6306 .unwrap();
6307
6308 let index = BTreeIndex::load(test_store.clone(), None, &LanceCache::no_cache())
6309 .await
6310 .unwrap();
6311 let state = BTreeIndexState {
6312 lookup_batch: index.page_lookup.batch.clone(),
6313 batch_size: index.batch_size,
6314 ranges_to_files: index.ranges_to_files.clone(),
6315 };
6316
6317 let frag_reuse_index: Arc<dyn crate::scalar::RowIdRemapper> =
6321 Arc::new(FragReuseIndexHandle(Arc::new(FragReuseIndex::new(
6322 Uuid::new_v4(),
6323 vec![HashMap::from([(0u64, Some(5000u64))])],
6324 FragReuseIndexDetails { versions: vec![] },
6325 ))));
6326 let reconstructed = state
6327 .reconstruct(
6328 test_store.clone(),
6329 &LanceCache::no_cache(),
6330 Some(frag_reuse_index),
6331 )
6332 .unwrap();
6333
6334 let result = reconstructed
6335 .search(
6336 &SargableQuery::Equals(ScalarValue::Int32(Some(0))),
6337 &NoOpMetricsCollector,
6338 )
6339 .await
6340 .unwrap();
6341 let row_ids: Vec<u64> = match &result {
6342 SearchResult::Exact(set) => set
6343 .true_rows()
6344 .row_addrs()
6345 .unwrap()
6346 .map(u64::from)
6347 .collect(),
6348 other => panic!("expected Exact, got {other:?}"),
6349 };
6350 assert_eq!(
6351 row_ids,
6352 vec![5000],
6353 "frag_reuse_index remap was not applied"
6354 );
6355 }
6356
6357 #[tokio::test]
6358 async fn test_btree_index_state_range_partitioned_plugin_cache_roundtrip() {
6359 let tmpdir = TempObjDir::default();
6363 let store = Arc::new(LanceIndexStore::new(
6364 Arc::new(ObjectStore::local()),
6365 tmpdir.clone(),
6366 Arc::new(LanceCache::no_cache()),
6367 ));
6368
6369 let half = DEFAULT_BTREE_BATCH_SIZE;
6370 let total = (2 * half) as i32;
6371
6372 let part0 = gen_batch()
6374 .col("value", array::step::<Int32Type>())
6375 .col("_rowid", array::step::<UInt64Type>())
6376 .into_df_stream(RowCount::from(half), BatchCount::from(1));
6377 train_btree_index(part0, store.as_ref(), half, None, Some(0u32))
6378 .await
6379 .unwrap();
6380
6381 let values: Vec<i32> = (half as i32..total).collect();
6383 let row_ids: Vec<u64> = (half..total as u64).collect();
6384 let part1 = gen_batch()
6385 .col("value", array::cycle::<Int32Type>(values))
6386 .col("_rowid", array::cycle::<UInt64Type>(row_ids))
6387 .into_df_stream(RowCount::from(half), BatchCount::from(1));
6388 train_btree_index(part1, store.as_ref(), half, None, Some(1u32))
6389 .await
6390 .unwrap();
6391
6392 super::merge_metadata_files(
6393 store.as_ref(),
6394 &[
6395 part_page_data_file_path(0 << 32),
6396 part_page_data_file_path(1 << 32),
6397 ],
6398 &[
6399 part_lookup_file_path(0 << 32),
6400 part_lookup_file_path(1 << 32),
6401 ],
6402 Some(1usize),
6403 noop_progress(),
6404 )
6405 .await
6406 .unwrap();
6407
6408 let index = BTreeIndex::load(store.clone(), None, &LanceCache::no_cache())
6409 .await
6410 .unwrap();
6411 assert!(
6412 index.ranges_to_files.is_some(),
6413 "test setup should produce a range-partitioned index",
6414 );
6415
6416 let cache = LanceCache::with_capacity(64 * 1024 * 1024);
6417 let plugin = BTreeIndexPlugin;
6418 plugin.put_in_cache(&cache, index.clone()).await.unwrap();
6419 let from_cache = plugin
6420 .get_from_cache(store.clone(), None, &cache)
6421 .await
6422 .unwrap()
6423 .expect("index should be served from the cache");
6424
6425 for value in [0i32, total - 1] {
6427 let query = SargableQuery::Equals(ScalarValue::Int32(Some(value)));
6428 let expected = index.search(&query, &NoOpMetricsCollector).await.unwrap();
6429 let actual = from_cache
6430 .search(&query, &NoOpMetricsCollector)
6431 .await
6432 .unwrap();
6433 assert_eq!(expected, actual, "value {value}");
6434 }
6435 }
6436}