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, CacheKeySchema,
67 KeyBuilder, LanceCache, 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 schema() -> CacheKeySchema {
1369 CacheKeySchema::new("lance.scalar.btree-page-key", 1)
1370 }
1371
1372 fn write_key(&self, builder: &mut KeyBuilder) {
1373 builder.write_u32(self.page_number);
1374 }
1375
1376 fn codec() -> Option<CacheCodec> {
1377 Some(CacheCodec::from_impl::<FlatIndex>())
1379 }
1380}
1381
1382#[derive(Debug, Clone)]
1390struct BTreeIndexState {
1391 lookup_batch: RecordBatch,
1392 batch_size: u64,
1393 ranges_to_files: Option<Arc<RangeInclusiveMap<u32, (String, u32)>>>,
1394}
1395
1396impl DeepSizeOf for BTreeIndexState {
1397 fn deep_size_of_children(&self, context: &mut lance_core::deepsize::Context) -> usize {
1398 self.lookup_batch.deep_size_of_children(context)
1401 }
1402}
1403
1404impl BTreeIndexState {
1405 fn from_index(index: &dyn ScalarIndex) -> Result<Self> {
1406 let btree = index.as_any().downcast_ref::<BTreeIndex>().ok_or_else(|| {
1407 Error::internal("BTreeIndexState::from_index called with a non-BTree index")
1408 })?;
1409 Ok(Self {
1410 lookup_batch: btree.page_lookup.batch.clone(),
1411 batch_size: btree.batch_size,
1412 ranges_to_files: btree.ranges_to_files.clone(),
1413 })
1414 }
1415
1416 fn reconstruct(
1417 &self,
1418 store: Arc<dyn IndexStore>,
1419 index_cache: &LanceCache,
1420 frag_reuse_index: Option<Arc<dyn RowIdRemapper>>,
1421 ) -> Result<Arc<dyn ScalarIndex>> {
1422 let index = BTreeIndex::try_from_serialized(
1423 self.lookup_batch.clone(),
1424 store,
1425 index_cache,
1426 self.batch_size,
1427 self.ranges_to_files.clone(),
1428 frag_reuse_index,
1429 )?;
1430 Ok(Arc::new(index) as Arc<dyn ScalarIndex>)
1431 }
1432}
1433
1434impl CacheCodecImpl for BTreeIndexState {
1435 const TYPE_ID: &'static str = "lance.scalar.BTreeIndexState";
1436 const CURRENT_VERSION: u32 = 1;
1437
1438 fn serialize(&self, w: &mut CacheEntryWriter<'_>) -> Result<()> {
1444 let ranges_to_files = match &self.ranges_to_files {
1445 None => Vec::new(),
1446 Some(ranges) => ranges
1447 .iter()
1448 .map(|(range, (path, page_offset))| RangeToFile {
1449 start: *range.start(),
1450 end: *range.end(),
1451 page_offset: *page_offset,
1452 path: path.clone(),
1453 })
1454 .collect(),
1455 };
1456 let header = BTreeIndexHeader {
1457 batch_size: self.batch_size,
1458 has_ranges_to_files: self.ranges_to_files.is_some(),
1459 ranges_to_files,
1460 };
1461 w.write_header(&header)?;
1462 w.write_ipc(&self.lookup_batch)?;
1463 Ok(())
1464 }
1465
1466 fn deserialize(r: &mut CacheEntryReader<'_>) -> Result<Self> {
1467 let header: BTreeIndexHeader = r.read_header()?;
1468 let ranges_to_files = if header.has_ranges_to_files {
1469 let map: RangeInclusiveMap<u32, (String, u32)> = header
1470 .ranges_to_files
1471 .into_iter()
1472 .map(|entry| (entry.start..=entry.end, (entry.path, entry.page_offset)))
1473 .collect();
1474 Some(Arc::new(map))
1475 } else {
1476 None
1477 };
1478 let lookup_batch = r.read_ipc()?;
1479 Ok(Self {
1480 lookup_batch,
1481 batch_size: header.batch_size,
1482 ranges_to_files,
1483 })
1484 }
1485}
1486
1487struct BTreeIndexStateKey;
1490
1491impl CacheKey for BTreeIndexStateKey {
1492 type ValueType = BTreeIndexState;
1493
1494 fn key(&self) -> std::borrow::Cow<'_, str> {
1495 "state".into()
1496 }
1497
1498 fn type_name() -> &'static str {
1499 "BTreeIndexState"
1500 }
1501
1502 fn schema() -> CacheKeySchema {
1503 CacheKeySchema::new("lance.scalar.btree-index-state-key", 1)
1504 }
1505
1506 fn write_key(&self, builder: &mut KeyBuilder) {
1507 builder.write_variant(0);
1508 }
1509
1510 fn codec() -> Option<CacheCodec> {
1511 Some(CacheCodec::from_impl::<BTreeIndexState>())
1512 }
1513}
1514
1515#[derive(Clone, Debug)]
1518pub struct BTreeIndex {
1519 page_lookup: Arc<BTreeLookup>,
1520 index_cache: WeakLanceCache,
1521 store: Arc<dyn IndexStore>,
1522 data_type: DataType,
1523 batch_size: u64,
1524
1525 ranges_to_files: Option<Arc<RangeInclusiveMap<u32, (String, u32)>>>,
1554 frag_reuse_index: Option<Arc<dyn RowIdRemapper>>,
1555}
1556
1557impl DeepSizeOf for BTreeIndex {
1558 fn deep_size_of_children(&self, context: &mut lance_core::deepsize::Context) -> usize {
1559 self.page_lookup.deep_size_of_children(context) + self.store.deep_size_of_children(context)
1563 }
1564}
1565
1566impl BTreeIndex {
1567 #[allow(clippy::too_many_arguments)]
1568 fn new(
1569 page_lookup: Arc<BTreeLookup>,
1570 store: Arc<dyn IndexStore>,
1571 data_type: DataType,
1572 index_cache: WeakLanceCache,
1573 batch_size: u64,
1574 ranges_to_files: Option<Arc<RangeInclusiveMap<u32, (String, u32)>>>,
1575 frag_reuse_index: Option<Arc<dyn RowIdRemapper>>,
1576 ) -> Self {
1577 Self {
1578 page_lookup,
1579 store,
1580 data_type,
1581 index_cache,
1582 batch_size,
1583 ranges_to_files,
1584 frag_reuse_index,
1585 }
1586 }
1587
1588 pub async fn contains_keys(
1601 &self,
1602 keys: &[ScalarValue],
1603 metrics: &dyn MetricsCollector,
1604 ) -> Result<Vec<bool>> {
1605 let mut by_page: HashMap<u32, Vec<(usize, OrderableScalarValue)>> = HashMap::new();
1608 for (idx, key) in keys.iter().enumerate() {
1609 if key.is_null() {
1610 continue;
1611 }
1612 let ov = OrderableScalarValue(key.clone());
1613 for matches in self.page_lookup.pages_eq(&ov)? {
1614 by_page
1615 .entry(matches.page_id())
1616 .or_default()
1617 .push((idx, ov.clone()));
1618 }
1619 }
1620
1621 let index_reader = LazyIndexReader::new(self.store.clone(), self.ranges_to_files.clone());
1622 let page_tasks = by_page.into_iter().map(|(page_number, entries)| {
1623 let index_reader = index_reader.clone();
1624 async move {
1625 let page = self.lookup_page(page_number, index_reader, metrics).await?;
1626 let needles: Vec<OrderableScalarValue> =
1627 entries.iter().map(|(_, ov)| ov.clone()).collect();
1628 let present = page.contains_values(&needles)?;
1629 Result::Ok((entries, present))
1630 }
1631 });
1632
1633 let mut result = vec![false; keys.len()];
1634 let page_results: Vec<_> = stream::iter(page_tasks)
1635 .buffer_unordered(get_num_compute_intensive_cpus())
1636 .try_collect()
1637 .await?;
1638 for (entries, present) in page_results {
1639 for (idx, ov) in entries {
1640 if present.contains(&ov) {
1641 result[idx] = true;
1642 }
1643 }
1644 }
1645 Ok(result)
1646 }
1647
1648 async fn lookup_page(
1649 &self,
1650 page_number: u32,
1651 index_reader: LazyIndexReader,
1652 metrics: &dyn MetricsCollector,
1653 ) -> Result<Arc<FlatIndex>> {
1654 let result = self
1655 .index_cache
1656 .get_or_insert_with_key_hit(BTreePageKey { page_number }, move || async move {
1657 self.read_page(page_number, index_reader, metrics).await
1658 })
1659 .await;
1660 match &result {
1661 Ok((_, true)) => metrics.record_index_cache_hit(),
1662 _ => metrics.record_index_cache_miss(),
1663 }
1664 result.map(|(page, _)| page)
1665 }
1666
1667 #[instrument(level = "debug", skip_all)]
1668 async fn read_page(
1669 &self,
1670 page_number: u32,
1671 index_reader: LazyIndexReader,
1672 metrics: &dyn MetricsCollector,
1673 ) -> Result<FlatIndex> {
1674 metrics.record_part_load();
1675 info!(target: TRACE_IO_EVENTS, r#type=IO_TYPE_LOAD_SCALAR_PART, index_type="btree", part_id=page_number);
1676 let index_reader = index_reader.get().await?;
1677 let mut serialized_page = index_reader
1678 .read_record_batch(page_number as u64, self.batch_size)
1679 .await?;
1680 if let Some(frag_reuse_index_ref) = self.frag_reuse_index.as_ref() {
1681 serialized_page =
1682 frag_reuse_index_ref.remap_row_ids_record_batch(serialized_page, 1)?;
1683 }
1684 FlatIndex::try_new(serialized_page)
1685 }
1686
1687 fn compile_predicate(&self, query: &SargableQuery) -> Result<Arc<dyn PhysicalExpr>> {
1691 let schema = Arc::new(Schema::new(vec![
1692 Field::new(BTREE_VALUES_COLUMN, self.data_type.clone(), true),
1693 Field::new(BTREE_IDS_COLUMN, DataType::UInt64, false),
1694 ]));
1695 let df_schema = DFSchema::try_from(schema)?;
1696 Ok(create_physical_expr(
1697 &query.to_expr(BTREE_VALUES_COLUMN.to_string()),
1698 &df_schema,
1699 &ExecutionProps::default(),
1700 )?)
1701 }
1702
1703 async fn search_page(
1704 &self,
1705 query: &SargableQuery,
1706 matches: Matches,
1707 index_reader: LazyIndexReader,
1708 prebuilt: Option<&Arc<dyn PhysicalExpr>>,
1709 metrics: &dyn MetricsCollector,
1710 ) -> Result<NullableRowAddrSet> {
1711 let subindex = self
1712 .lookup_page(matches.page_id(), index_reader, metrics)
1713 .await?;
1714
1715 match matches {
1716 Matches::Some(_) => match prebuilt {
1719 Some(expr) => subindex.search_prebuilt(expr, metrics),
1720 None => subindex.search(query, metrics),
1721 },
1722 Matches::All(_) => Ok(match query {
1723 SargableQuery::IsNull() => subindex.all_ignore_nulls(),
1725 _ => subindex.all(),
1726 }),
1727 }
1728 }
1729
1730 #[instrument(level = "debug", skip_all)]
1731 fn try_from_serialized(
1732 data: RecordBatch,
1733 store: Arc<dyn IndexStore>,
1734 index_cache: &LanceCache,
1735 batch_size: u64,
1736 ranges_to_files: Option<Arc<RangeInclusiveMap<u32, (String, u32)>>>,
1737 frag_reuse_index: Option<Arc<dyn RowIdRemapper>>,
1738 ) -> Result<Self> {
1739 let data_type = data.column(0).data_type().clone();
1740 let page_lookup = Arc::new(BTreeLookup::try_new(data)?);
1741
1742 Ok(Self::new(
1743 page_lookup,
1744 store,
1745 data_type,
1746 WeakLanceCache::from(index_cache),
1747 batch_size,
1748 ranges_to_files,
1749 frag_reuse_index,
1750 ))
1751 }
1752
1753 async fn load(
1754 store: Arc<dyn IndexStore>,
1755 frag_reuse_index: Option<Arc<dyn RowIdRemapper>>,
1756 index_cache: &LanceCache,
1757 ) -> Result<Arc<Self>> {
1758 let (page_lookup_file, standalone_partition_page_file) =
1759 match store.open_index_file(BTREE_LOOKUP_NAME).await {
1760 Ok(page_lookup_file) => (page_lookup_file, None),
1761 Err(original_err) if is_missing_lookup_error(&original_err) => {
1762 let files = store.list_files_with_sizes().await?;
1763 let Some((lookup_file, page_file)) = find_single_partition_files(&files)?
1764 else {
1765 return Err(original_err);
1766 };
1767 (
1768 store.open_index_file(lookup_file).await?,
1769 Some(page_file.to_string()),
1770 )
1771 }
1772 Err(other_err) => return Err(other_err),
1773 };
1774 let num_rows_in_lookup = page_lookup_file.num_rows();
1775 let serialized_lookup = page_lookup_file
1776 .read_range(0..num_rows_in_lookup, None)
1777 .await?;
1778 let file_schema = page_lookup_file.schema();
1779 let batch_size = file_schema
1780 .metadata
1781 .get(BATCH_SIZE_META_KEY)
1782 .map(|bs| bs.parse().unwrap_or(DEFAULT_BTREE_BATCH_SIZE))
1783 .unwrap_or(DEFAULT_BTREE_BATCH_SIZE);
1784
1785 let range_partitioned = file_schema
1786 .metadata
1787 .get(RANGE_PARTITIONED_META_KEY)
1788 .map(|bs| bs.parse().unwrap_or(DEFAULT_RANGE_PARTITIONED))
1789 .unwrap_or(DEFAULT_RANGE_PARTITIONED);
1790 let ranges_to_files = if let Some(page_file_name) = standalone_partition_page_file {
1794 let page_numbers = serialized_lookup
1795 .column(3)
1796 .as_any()
1797 .downcast_ref::<UInt32Array>()
1798 .unwrap();
1799 let max_page_number = page_numbers.values().iter().copied().max().unwrap_or(0);
1800 let mut range_map = RangeInclusiveMap::new();
1801 range_map.insert(0..=max_page_number, (page_file_name, 0));
1802 Some(Arc::new(range_map))
1803 } else if range_partitioned {
1804 let part_sizes_str = file_schema
1805 .metadata
1806 .get(PAGE_NUM_PER_RANGE_PARTITION_META_KEY)
1807 .expect("Range-partitioned Btree lookup file must have page-number-per-range-file metadata!");
1808 let part_sizes_vec: Vec<(u64, u32)> = serde_json::from_str(part_sizes_str)?;
1809 let mut offset: u32 = 0;
1810
1811 let range_map = part_sizes_vec
1812 .into_iter()
1813 .map(|(id, size)| {
1814 let range = offset..=(offset + size - 1);
1815 let file_with_size = (part_page_data_file_path(id), offset);
1816 offset += size;
1817 (range, file_with_size)
1818 })
1819 .collect();
1820
1821 Some(Arc::new(range_map))
1822 } else {
1823 None
1824 };
1825
1826 Ok(Arc::new(Self::try_from_serialized(
1827 serialized_lookup,
1828 store,
1829 index_cache,
1830 batch_size,
1831 ranges_to_files,
1832 frag_reuse_index,
1833 )?))
1834 }
1835
1836 fn train_schema(&self) -> Schema {
1838 let value_field = Field::new(VALUE_COLUMN_NAME, self.data_type.clone(), true);
1839 let row_id_field = Field::new(ROW_ID, DataType::UInt64, false);
1840 Schema::new(vec![value_field, row_id_field])
1841 }
1842
1843 async fn data_stream(&self) -> Result<SendableRecordBatchStream> {
1845 let lazy_reader = LazyIndexReader::new(self.store.clone(), self.ranges_to_files.clone());
1846 let reader = lazy_reader.get().await?;
1847 let new_schema = Arc::new(self.train_schema());
1848 let new_schema_clone = new_schema.clone();
1849 let reader_stream = IndexReaderStream::new(reader, self.batch_size).await;
1850 let batches = reader_stream
1851 .map(|fut| fut.map_err(DataFusionError::from))
1852 .buffered(self.store.io_parallelism())
1853 .map_ok(move |batch| {
1854 RecordBatch::try_new(
1855 new_schema.clone(),
1856 vec![batch.column(0).clone(), batch.column(1).clone()],
1857 )
1858 .unwrap()
1859 })
1860 .boxed();
1861 Ok(Box::pin(RecordBatchStreamAdapter::new(
1862 new_schema_clone,
1863 batches,
1864 )))
1865 }
1866
1867 pub async fn merge_segments(
1870 segments: &[Arc<Self>],
1871 new_data: SendableRecordBatchStream,
1872 dest_store: &dyn IndexStore,
1873 old_data_filters: &[Option<OldIndexDataFilter>],
1874 ) -> Result<CreatedIndex> {
1875 let Some(first) = segments.first() else {
1876 return Err(Error::invalid_input(
1877 "cannot merge BTree index without at least one source segment".to_string(),
1878 ));
1879 };
1880
1881 if old_data_filters.len() != segments.len() {
1882 return Err(Error::invalid_input(format!(
1883 "BTree merge: expected one old-data filter per source segment \
1884 (segments={}, filters={})",
1885 segments.len(),
1886 old_data_filters.len()
1887 )));
1888 }
1889
1890 for segment in segments.iter().skip(1) {
1891 if segment.data_type != first.data_type {
1892 return Err(Error::index(format!(
1893 "cannot merge BTree segments with different value types ({:?} vs {:?})",
1894 first.data_type, segment.data_type
1895 )));
1896 }
1897 }
1898
1899 let new_schema = new_data.schema();
1900 let value_column_index = new_schema.index_of(VALUE_COLUMN_NAME)?;
1901 let new_value_type = new_schema.field(value_column_index).data_type();
1902 if new_value_type != &first.data_type {
1903 return Err(Error::invalid_input(format!(
1904 "BTree merge: new_data value column type {:?} does not match \
1905 segment value type {:?}",
1906 new_value_type, first.data_type
1907 )));
1908 }
1909
1910 let mut inputs: Vec<Arc<dyn ExecutionPlan>> = Vec::with_capacity(segments.len() + 1);
1911 for (segment, old_data_filter) in segments.iter().zip(old_data_filters) {
1912 if filter_keeps_nothing(old_data_filter) {
1913 continue;
1914 }
1915 let stream = segment.data_stream().await?;
1916 let stream = match segment.frag_reuse_index.clone() {
1917 Some(frag_reuse_index) => remap_row_ids(stream, frag_reuse_index),
1918 None => stream,
1919 };
1920 let stream = match old_data_filter.clone() {
1921 Some(filter) => filter_row_ids(stream, filter),
1922 None => stream,
1923 };
1924 inputs.push(Arc::new(OneShotExec::new(stream)));
1925 }
1926 inputs.push(Arc::new(OneShotExec::new(new_data)));
1927
1928 let sort_expr = PhysicalSortExpr {
1929 expr: Arc::new(Column::new(VALUE_COLUMN_NAME, value_column_index)),
1930 options: SortOptions {
1931 descending: false,
1932 nulls_first: true,
1933 },
1934 };
1935 let unioned = UnionExec::try_new(inputs)?;
1938 let ordered = Arc::new(SortPreservingMergeExec::new([sort_expr].into(), unioned));
1939 let unchunked = execute_plan(
1940 ordered,
1941 LanceExecutionOptions {
1942 use_spilling: true,
1943 ..Default::default()
1944 },
1945 )?;
1946 let merged_stream = chunk_concat_stream(unchunked, first.batch_size as usize);
1947
1948 let files =
1949 train_btree_index(merged_stream, dest_store, first.batch_size, None, None).await?;
1950
1951 Ok(CreatedIndex {
1952 index_details: prost_types::Any::from_msg(&pbold::BTreeIndexDetails::default())
1953 .unwrap(),
1954 index_version: BTREE_INDEX_VERSION,
1955 files,
1956 })
1957 }
1958}
1959
1960fn filter_row_ids(
1963 stream: SendableRecordBatchStream,
1964 old_data_filter: OldIndexDataFilter,
1965) -> SendableRecordBatchStream {
1966 let schema = stream.schema();
1967 let filtered = stream.map(move |batch_result| {
1968 let batch = batch_result?;
1969 let row_ids = batch[ROW_ID]
1970 .as_any()
1971 .downcast_ref::<arrow_array::UInt64Array>()
1972 .ok_or_else(|| Error::internal("expected UInt64Array for row_id column"))?;
1973 let mask = old_data_filter.filter_row_ids(row_ids);
1974 Ok(arrow_select::filter::filter_record_batch(&batch, &mask)?)
1975 });
1976 Box::pin(RecordBatchStreamAdapter::new(schema, filtered))
1977}
1978
1979fn filter_keeps_nothing(filter: &Option<OldIndexDataFilter>) -> bool {
1982 match filter {
1983 Some(OldIndexDataFilter::Fragments { to_keep, .. }) => to_keep.is_empty(),
1984 Some(OldIndexDataFilter::RowIds(valid)) => valid.is_empty(),
1985 None => false,
1986 }
1987}
1988
1989fn remap_row_ids(
1990 stream: SendableRecordBatchStream,
1991 frag_reuse_index: Arc<dyn RowIdRemapper>,
1992) -> SendableRecordBatchStream {
1993 let schema = stream.schema();
1994 let remapped = stream.map(move |batch_result| {
1995 let batch = batch_result?;
1996 Ok(frag_reuse_index.remap_row_ids_record_batch(batch, 1)?)
1997 });
1998 Box::pin(RecordBatchStreamAdapter::new(schema, remapped))
1999}
2000
2001fn wrap_bound(bound: &Bound<ScalarValue>) -> Bound<OrderableScalarValue> {
2002 match bound {
2003 Bound::Unbounded => Bound::Unbounded,
2004 Bound::Included(val) => Bound::Included(OrderableScalarValue(val.clone())),
2005 Bound::Excluded(val) => Bound::Excluded(OrderableScalarValue(val.clone())),
2006 }
2007}
2008
2009fn serialize_with_display<T: Display, S: Serializer>(
2010 value: &Option<T>,
2011 serializer: S,
2012) -> std::result::Result<S::Ok, S::Error> {
2013 if let Some(value) = value {
2014 serializer.collect_str(value)
2015 } else {
2016 serializer.collect_str("N/A")
2017 }
2018}
2019
2020#[derive(Serialize)]
2021struct BTreeStatistics {
2022 #[serde(serialize_with = "serialize_with_display")]
2023 min: Option<OrderableScalarValue>,
2024 #[serde(serialize_with = "serialize_with_display")]
2025 max: Option<OrderableScalarValue>,
2026 num_pages: u32,
2027}
2028
2029#[async_trait]
2030impl Index for BTreeIndex {
2031 fn as_any(&self) -> &dyn Any {
2032 self
2033 }
2034
2035 fn as_index(self: Arc<Self>) -> Arc<dyn Index> {
2036 self
2037 }
2038
2039 async fn prewarm(&self) -> Result<()> {
2040 let index_reader = LazyIndexReader::new(self.store.clone(), self.ranges_to_files.clone());
2041 let reader = index_reader.get().await?;
2042 let num_pages = reader.num_batches(self.batch_size).await;
2043 let mut pages = stream::iter(0..num_pages)
2044 .map(|page_idx| {
2045 let index_reader = index_reader.clone();
2046 async move {
2047 let page = self
2048 .read_page(page_idx, index_reader, &NoOpMetricsCollector)
2049 .await?;
2050 Result::Ok((page_idx, page))
2051 }
2052 })
2053 .buffer_unordered(get_num_compute_intensive_cpus());
2054
2055 while let Some((page_idx, page)) = pages.try_next().await? {
2056 let inserted = self
2057 .index_cache
2058 .insert_with_key(
2059 &BTreePageKey {
2060 page_number: page_idx,
2061 },
2062 Arc::new(page),
2063 )
2064 .await;
2065
2066 if !inserted {
2067 return Err(Error::internal(
2068 "Failed to prewarm index: cache is no longer available".to_string(),
2069 ));
2070 }
2071 }
2072
2073 Ok(())
2074 }
2075
2076 fn index_type(&self) -> IndexType {
2077 IndexType::BTree
2078 }
2079
2080 fn statistics(&self) -> Result<serde_json::Value> {
2081 let lookup = &self.page_lookup;
2082 let batch = &lookup.batch;
2083 let num_rows = batch.num_rows();
2084 let (min, max) = if lookup.search_start >= num_rows {
2088 (None, None)
2089 } else {
2090 let min = OrderableScalarValue(ScalarValue::try_from_array(
2091 batch.column(0),
2092 lookup.search_start,
2093 )?);
2094 let max =
2095 OrderableScalarValue(ScalarValue::try_from_array(batch.column(1), num_rows - 1)?);
2096 (Some(min), Some(max))
2097 };
2098 serde_json::to_value(&BTreeStatistics {
2099 num_pages: num_rows as u32,
2100 min,
2101 max,
2102 })
2103 .map_err(|err| err.into())
2104 }
2105
2106 async fn calculate_included_frags(&self) -> Result<RoaringBitmap> {
2107 let mut frag_ids = RoaringBitmap::default();
2108
2109 let lazy_reader = LazyIndexReader::new(self.store.clone(), self.ranges_to_files.clone());
2110 let sub_index_reader = lazy_reader.get().await?;
2111 let mut reader_stream = IndexReaderStream::new(sub_index_reader, self.batch_size)
2112 .await
2113 .buffered(self.store.io_parallelism());
2114 while let Some(serialized) = reader_stream.try_next().await? {
2115 let page = FlatIndex::try_new(serialized)?;
2116 frag_ids |= page.calculate_included_frags()?;
2117 }
2118
2119 Ok(frag_ids)
2120 }
2121}
2122
2123#[async_trait]
2124impl ScalarIndex for BTreeIndex {
2125 async fn search(
2126 &self,
2127 query: &dyn AnyQuery,
2128 metrics: &dyn MetricsCollector,
2129 ) -> Result<SearchResult> {
2130 let query = query.as_any().downcast_ref::<SargableQuery>().unwrap();
2131 let mut pages = match query {
2132 SargableQuery::Equals(val) => self
2133 .page_lookup
2134 .pages_eq(&OrderableScalarValue(val.clone())),
2135 SargableQuery::Range(start, end) => self
2136 .page_lookup
2137 .pages_between((wrap_bound(start).as_ref(), wrap_bound(end).as_ref())),
2138 SargableQuery::IsIn(values) => self
2139 .page_lookup
2140 .pages_in(values.iter().map(|val| OrderableScalarValue(val.clone()))),
2141 SargableQuery::FullTextSearch(_) => {
2142 return Err(Error::invalid_input(
2143 "full text search is not supported for BTree index, build a inverted index for it",
2144 ));
2145 }
2146 SargableQuery::IsNull() => Ok(self.page_lookup.pages_null()),
2147 SargableQuery::LikePrefix(prefix) => {
2148 match prefix {
2150 ScalarValue::Utf8(Some(s)) => {
2151 let start = Bound::Included(OrderableScalarValue(prefix.clone()));
2152 let end = match compute_next_prefix(s) {
2153 Some(next) => {
2154 Bound::Excluded(OrderableScalarValue(ScalarValue::Utf8(Some(next))))
2155 }
2156 None => Bound::Unbounded,
2157 };
2158 self.page_lookup
2159 .pages_between((start.as_ref(), end.as_ref()))
2160 }
2161 ScalarValue::LargeUtf8(Some(s)) => {
2162 let start = Bound::Included(OrderableScalarValue(prefix.clone()));
2163 let end = match compute_next_prefix(s) {
2164 Some(next) => Bound::Excluded(OrderableScalarValue(
2165 ScalarValue::LargeUtf8(Some(next)),
2166 )),
2167 None => Bound::Unbounded,
2168 };
2169 self.page_lookup
2170 .pages_between((start.as_ref(), end.as_ref()))
2171 }
2172 _ => {
2173 self.page_lookup
2176 .pages_between((Bound::Unbounded, Bound::Unbounded))
2177 }
2178 }
2179 }
2180 }?;
2181
2182 if !matches!(query, SargableQuery::IsNull()) {
2197 let existing: HashSet<u32> = pages.iter().map(|m| m.page_id()).collect();
2198 for &page_id in self
2199 .page_lookup
2200 .null_pages
2201 .iter()
2202 .chain(self.page_lookup.all_null_pages.iter())
2203 {
2204 if !existing.contains(&page_id) {
2205 pages.push(Matches::Some(page_id));
2206 }
2207 }
2208 }
2209
2210 let prebuilt = match query {
2214 SargableQuery::IsIn(_) => Some(self.compile_predicate(query)?),
2215 _ => None,
2216 };
2217
2218 let lazy_index_reader =
2219 LazyIndexReader::new(self.store.clone(), self.ranges_to_files.clone());
2220 let page_tasks = pages
2221 .into_iter()
2222 .map(|page_index| {
2223 self.search_page(
2224 query,
2225 page_index,
2226 lazy_index_reader.clone(),
2227 prebuilt.as_ref(),
2228 metrics,
2229 )
2230 .boxed()
2231 })
2232 .collect::<Vec<_>>();
2233 debug!("Searching {} btree pages", page_tasks.len());
2234
2235 let results: Vec<NullableRowAddrSet> = stream::iter(page_tasks)
2237 .buffered(get_num_compute_intensive_cpus())
2240 .try_collect()
2241 .await?;
2242
2243 let selection = NullableRowAddrSet::union_all(&results);
2245
2246 Ok(SearchResult::Exact(selection))
2247 }
2248
2249 fn can_remap(&self) -> bool {
2250 true
2251 }
2252
2253 async fn remap(
2254 &self,
2255 mapping: &RowAddrRemap,
2256 dest_store: &dyn IndexStore,
2257 ) -> Result<CreatedIndex> {
2258 let part_page_files: Vec<(Option<u32>, &str)> =
2263 if let Some(ranges_to_files) = &self.ranges_to_files {
2264 ranges_to_files
2266 .iter()
2267 .enumerate()
2268 .map(|(part_id, (_, (path, _)))| (Some(part_id as u32), path.as_str()))
2269 .collect()
2270 } else {
2271 vec![(None, BTREE_PAGES_NAME)]
2273 };
2274
2275 let mapping = Arc::new(mapping.clone());
2276 let train_schema = Arc::new(self.train_schema());
2277 let mut remapped_files = Vec::new();
2278
2279 for (part_id, page_file) in part_page_files {
2281 let sub_index_reader = self.store.open_index_file(page_file).await?;
2283 let mapping = mapping.clone();
2284
2285 let train_schema_clone = train_schema.clone();
2286 let train_schema = train_schema.clone();
2287
2288 let remapped_stream = IndexReaderStream::new(sub_index_reader, self.batch_size)
2289 .await
2290 .buffered(self.store.io_parallelism())
2291 .map_err(DataFusionError::from)
2292 .and_then(move |batch| {
2293 let remapped =
2295 FlatIndex::remap_batch(batch, &mapping).map_err(DataFusionError::from);
2296 let with_train_schema = remapped.and_then(|batch| {
2297 RecordBatch::try_new(train_schema.clone(), batch.columns().to_vec())
2298 .map_err(DataFusionError::from)
2299 });
2300 std::future::ready(with_train_schema)
2301 });
2302
2303 let remapped_stream = Box::pin(RecordBatchStreamAdapter::new(
2304 train_schema_clone,
2305 remapped_stream,
2306 ));
2307
2308 let mut files =
2309 train_btree_index(remapped_stream, dest_store, self.batch_size, None, part_id)
2310 .await?;
2311 remapped_files.append(&mut files);
2312 }
2313
2314 if let Some(ranges_to_files) = &self.ranges_to_files {
2315 let num_parts = ranges_to_files.len();
2316 let page_files = (0..num_parts)
2318 .map(|part_id| part_page_data_file_path((part_id as u64) << 32))
2319 .collect::<Vec<_>>();
2320 let lookup_files = (0..num_parts)
2321 .map(|part_id| part_lookup_file_path((part_id as u64) << 32))
2322 .collect::<Vec<_>>();
2323 let merged_files = merge_metadata_files(
2324 dest_store,
2325 &page_files,
2326 &lookup_files,
2327 None,
2328 noop_progress(),
2329 )
2330 .await?;
2331 remapped_files.retain(|file| file.path.ends_with("_page_data.lance"));
2332 remapped_files.extend(merged_files);
2333 }
2334
2335 Ok(CreatedIndex {
2336 index_details: prost_types::Any::from_msg(&pbold::BTreeIndexDetails::default())
2337 .unwrap(),
2338 index_version: BTREE_INDEX_VERSION,
2339 files: remapped_files,
2340 })
2341 }
2342
2343 async fn update(
2344 &self,
2345 new_data: SendableRecordBatchStream,
2346 dest_store: &dyn IndexStore,
2347 old_data_filter: Option<OldIndexDataFilter>,
2348 ) -> Result<CreatedIndex> {
2349 Self::merge_segments(
2352 &[Arc::new(self.clone())],
2353 new_data,
2354 dest_store,
2355 &[old_data_filter],
2356 )
2357 .await
2358 }
2359
2360 fn update_criteria(&self) -> UpdateCriteria {
2361 UpdateCriteria::only_new_data(TrainingCriteria::new(TrainingOrdering::Values).with_row_id())
2362 }
2363
2364 fn derive_index_params(&self) -> Result<ScalarIndexParams> {
2365 let params = serde_json::to_value(BTreeParameters {
2366 zone_size: Some(self.batch_size),
2367 range_id: None,
2368 })?;
2369 Ok(ScalarIndexParams::for_builtin(BuiltinIndexType::BTree).with_params(¶ms))
2370 }
2371}
2372
2373struct BatchStats {
2374 min: ScalarValue,
2375 max: ScalarValue,
2376 null_count: u32,
2377}
2378
2379fn analyze_batch(batch: &RecordBatch) -> Result<BatchStats> {
2380 let values = batch.column_by_name(VALUE_COLUMN_NAME).expect_ok()?;
2381 if values.is_empty() {
2382 return Err(Error::internal(
2383 "received an empty batch in btree training".to_string(),
2384 ));
2385 }
2386 let min = ScalarValue::try_from_array(&values, 0)
2387 .map_err(|e| Error::internal(format!("failed to get min value from batch: {}", e)))?;
2388 let max = ScalarValue::try_from_array(&values, values.len() - 1)
2389 .map_err(|e| Error::internal(format!("failed to get max value from batch: {}", e)))?;
2390
2391 Ok(BatchStats {
2392 min,
2393 max,
2394 null_count: values.null_count() as u32,
2395 })
2396}
2397
2398#[async_trait]
2400pub trait BTreeSubIndex: Debug + Send + Sync + DeepSizeOf {
2401 async fn train(&self, batch: RecordBatch) -> Result<RecordBatch>;
2403
2404 async fn load_subindex(&self, serialized: RecordBatch) -> Result<Arc<dyn ScalarIndex>>;
2406
2407 async fn retrieve_data(&self, serialized: RecordBatch) -> Result<RecordBatch>;
2414
2415 fn schema(&self) -> &Arc<Schema>;
2417
2418 async fn remap_subindex(
2420 &self,
2421 serialized: RecordBatch,
2422 mapping: &RowAddrRemap,
2423 ) -> Result<RecordBatch>;
2424}
2425
2426struct EncodedBatch {
2427 stats: BatchStats,
2428 page_number: u32,
2429}
2430
2431async fn train_btree_page(
2432 batch: RecordBatch,
2433 batch_idx: u32,
2434 writer: &mut dyn IndexWriter,
2435 schema: Arc<Schema>,
2436) -> Result<EncodedBatch> {
2437 let stats = analyze_batch(&batch)?;
2438
2439 let trained = RecordBatch::try_new(
2441 schema.clone(),
2442 vec![
2443 batch.column_by_name(VALUE_COLUMN_NAME).expect_ok()?.clone(),
2444 batch.column_by_name(ROW_ID).expect_ok()?.clone(),
2445 ],
2446 )?;
2447
2448 writer.write_record_batch(trained).await?;
2449 Ok(EncodedBatch {
2450 stats,
2451 page_number: batch_idx,
2452 })
2453}
2454
2455fn btree_stats_as_batch(stats: Vec<EncodedBatch>, value_type: &DataType) -> Result<RecordBatch> {
2456 let mins = if stats.is_empty() {
2457 new_empty_array(value_type)
2458 } else {
2459 ScalarValue::iter_to_array(stats.iter().map(|stat| stat.stats.min.clone()))?
2460 };
2461 let maxs = if stats.is_empty() {
2462 new_empty_array(value_type)
2463 } else {
2464 ScalarValue::iter_to_array(stats.iter().map(|stat| stat.stats.max.clone()))?
2465 };
2466 let null_counts = UInt32Array::from_iter_values(stats.iter().map(|stat| stat.stats.null_count));
2467 let page_numbers = UInt32Array::from_iter_values(stats.iter().map(|stat| stat.page_number));
2468
2469 let schema = Arc::new(Schema::new(vec![
2470 Field::new("min", mins.data_type().clone(), true),
2472 Field::new("max", maxs.data_type().clone(), true),
2473 Field::new("null_count", null_counts.data_type().clone(), false),
2474 Field::new("page_idx", page_numbers.data_type().clone(), false),
2475 ]));
2476
2477 let columns = vec![
2478 mins,
2479 maxs,
2480 Arc::new(null_counts) as Arc<dyn Array>,
2481 Arc::new(page_numbers) as Arc<dyn Array>,
2482 ];
2483
2484 Ok(RecordBatch::try_new(schema, columns)?)
2485}
2486
2487pub async fn train_btree_index(
2489 batches_source: SendableRecordBatchStream,
2490 index_store: &dyn IndexStore,
2491 batch_size: u64,
2492 fragment_ids: Option<Vec<u32>>,
2493 range_id: Option<u32>,
2494) -> Result<Vec<IndexFile>> {
2495 let partition_id = fragment_ids
2500 .as_ref()
2501 .and_then(|frag_ids| frag_ids.first())
2505 .map(|&first_frag_id| (first_frag_id as u64) << 32)
2506 .or_else(|| range_id.map(|id| (id as u64) << 32));
2510
2511 let flat_schema = Arc::new(Schema::new(vec![
2512 Field::new(
2513 BTREE_VALUES_COLUMN,
2514 batches_source.schema().field(0).data_type().clone(),
2515 true,
2516 ),
2517 Field::new(BTREE_IDS_COLUMN, DataType::UInt64, false),
2518 ]));
2519
2520 let mut sub_index_file = match partition_id {
2521 None => {
2522 index_store
2523 .new_index_file(BTREE_PAGES_NAME, flat_schema.clone())
2524 .await?
2525 }
2526 Some(partition_id) => {
2527 index_store
2528 .new_index_file(
2529 part_page_data_file_path(partition_id).as_str(),
2530 flat_schema.clone(),
2531 )
2532 .await?
2533 }
2534 };
2535
2536 let mut encoded_batches = Vec::new();
2537 let mut batch_idx = 0;
2538
2539 let value_type = batches_source
2540 .schema()
2541 .field_with_name(VALUE_COLUMN_NAME)?
2542 .data_type()
2543 .clone();
2544
2545 let mut batches_source = chunk_concat_stream(batches_source, batch_size as usize);
2546
2547 while let Some(batch) = batches_source.try_next().await? {
2548 encoded_batches.push(
2549 train_btree_page(
2550 batch,
2551 batch_idx,
2552 sub_index_file.as_mut(),
2553 flat_schema.clone(),
2554 )
2555 .await?,
2556 );
2557 batch_idx += 1;
2558 }
2559 let pages_file = sub_index_file.finish().await?;
2560 let record_batch = btree_stats_as_batch(encoded_batches, &value_type)?;
2561 let mut file_schema = record_batch.schema().as_ref().clone();
2562 file_schema
2563 .metadata
2564 .insert(BATCH_SIZE_META_KEY.to_string(), batch_size.to_string());
2565 file_schema.metadata.insert(
2566 RANGE_PARTITIONED_META_KEY.to_string(),
2567 range_id.is_some().to_string(),
2568 );
2569 let mut btree_index_file = match partition_id {
2570 None => {
2571 index_store
2572 .new_index_file(BTREE_LOOKUP_NAME, Arc::new(file_schema))
2573 .await?
2574 }
2575 Some(partition_id) => {
2576 index_store
2577 .new_index_file(
2578 part_lookup_file_path(partition_id).as_str(),
2579 Arc::new(file_schema),
2580 )
2581 .await?
2582 }
2583 };
2584 btree_index_file.write_record_batch(record_batch).await?;
2585 let lookup_file = btree_index_file.finish().await?;
2586 Ok(vec![pages_file, lookup_file])
2587}
2588
2589fn find_single_partition_files(files: &[super::IndexFile]) -> Result<Option<(&str, &str)>> {
2590 let lookup_files = files
2591 .iter()
2592 .filter_map(|file| {
2593 (file.path.starts_with("part_") && file.path.ends_with("_page_lookup.lance"))
2594 .then_some(file.path.as_str())
2595 })
2596 .collect::<Vec<_>>();
2597 let page_files = files
2598 .iter()
2599 .filter_map(|file| {
2600 (file.path.starts_with("part_") && file.path.ends_with("_page_data.lance"))
2601 .then_some(file.path.as_str())
2602 })
2603 .collect::<Vec<_>>();
2604
2605 if lookup_files.len() != 1 || page_files.len() != 1 {
2606 return Ok(None);
2607 }
2608
2609 let lookup_partition_id = extract_partition_id(lookup_files[0])?;
2610 let page_partition_id = extract_partition_id(page_files[0])?;
2611 if lookup_partition_id != page_partition_id {
2612 return Ok(None);
2613 }
2614
2615 Ok(Some((lookup_files[0], page_files[0])))
2616}
2617
2618fn is_missing_lookup_error(err: &Error) -> bool {
2619 matches!(err, Error::NotFound { .. })
2620 || matches!(
2621 err,
2622 Error::IO { source, .. }
2623 if source
2624 .downcast_ref::<ObjectStoreError>()
2625 .map(|os_err| matches!(os_err, ObjectStoreError::NotFound { .. }))
2626 .unwrap_or(false)
2627 )
2628}
2629
2630async fn merge_metadata_files(
2637 store: &dyn IndexStore,
2638 part_page_files: &[String],
2639 part_lookup_files: &[String],
2640 batch_readhead: Option<usize>,
2641 progress: Arc<dyn IndexBuildProgress>,
2642) -> Result<Vec<IndexFile>> {
2643 if part_lookup_files.is_empty() || part_page_files.is_empty() {
2644 return Err(Error::internal(
2645 "No partition files provided for merging".to_string(),
2646 ));
2647 }
2648
2649 if part_lookup_files.len() != part_page_files.len() {
2651 return Err(Error::internal(format!(
2652 "Number of partition lookup files ({}) does not match number of partition page files ({})",
2653 part_lookup_files.len(),
2654 part_page_files.len()
2655 )));
2656 }
2657 let mut page_files_map = HashMap::new();
2658 for page_file in part_page_files {
2659 let partition_id = extract_partition_id(page_file)?;
2660 page_files_map.insert(partition_id, page_file);
2661 }
2662
2663 for lookup_file in part_lookup_files {
2665 let partition_id = extract_partition_id(lookup_file)?;
2666 if !page_files_map.contains_key(&partition_id) {
2667 return Err(Error::internal(format!(
2668 "No corresponding page file found for lookup file: {} (partition_id: {})",
2669 lookup_file, partition_id
2670 )));
2671 }
2672 }
2673
2674 let first_lookup_reader = store.open_index_file(&part_lookup_files[0]).await?;
2676 let batch_size = first_lookup_reader
2677 .schema()
2678 .metadata
2679 .get(BATCH_SIZE_META_KEY)
2680 .map(|bs| bs.parse().unwrap_or(DEFAULT_BTREE_BATCH_SIZE))
2681 .unwrap_or(DEFAULT_BTREE_BATCH_SIZE);
2682 let range_partitioned = first_lookup_reader
2683 .schema()
2684 .metadata
2685 .get(RANGE_PARTITIONED_META_KEY)
2686 .map(|bs| bs.parse().unwrap_or(DEFAULT_RANGE_PARTITIONED))
2687 .unwrap_or(DEFAULT_RANGE_PARTITIONED);
2688
2689 let value_type = first_lookup_reader
2691 .schema()
2692 .fields
2693 .first()
2694 .unwrap()
2695 .data_type();
2696
2697 let mut metadata = HashMap::new();
2698 metadata.insert(BATCH_SIZE_META_KEY.to_string(), batch_size.to_string());
2699 let lookup_schema = Arc::new(Schema::new(vec![
2700 Field::new("min", value_type.clone(), true),
2701 Field::new("max", value_type.clone(), true),
2702 Field::new("null_count", DataType::UInt32, false),
2703 Field::new("page_idx", DataType::UInt32, false),
2704 ]));
2705
2706 if range_partitioned {
2708 merge_range_partitioned_lookups(
2709 store,
2710 part_lookup_files,
2711 lookup_schema,
2712 metadata,
2713 batch_size,
2714 batch_readhead,
2715 progress,
2716 )
2717 .await
2718 .map(|file| vec![file])
2719 } else {
2720 merge_pages_and_lookups(
2721 store,
2722 part_page_files,
2723 part_lookup_files,
2724 &page_files_map,
2725 lookup_schema,
2726 metadata,
2727 batch_size,
2728 batch_readhead,
2729 progress,
2730 )
2731 .await
2732 }
2733}
2734
2735async fn merge_range_partitioned_lookups(
2765 store: &dyn IndexStore,
2766 part_lookup_files: &[String],
2767 lookup_schema: Arc<Schema>,
2768 mut metadata: HashMap<String, String>,
2769 batch_size: u64,
2770 batch_readhead: Option<usize>,
2771 progress: Arc<dyn IndexBuildProgress>,
2772) -> Result<IndexFile> {
2773 let sorted_part_lookup_files = sort_files_by_partition_id(part_lookup_files)?;
2774 let mut lookup_file = store
2775 .new_index_file(BTREE_LOOKUP_NAME, lookup_schema)
2776 .await?;
2777
2778 let mut pages_per_file: Vec<(u64, u32)> = Vec::with_capacity(sorted_part_lookup_files.len());
2780 let mut num_pages_written = 0u32;
2781
2782 progress
2783 .stage_start(
2784 "merge_lookups",
2785 Some(sorted_part_lookup_files.len() as u64),
2786 "files",
2787 )
2788 .await?;
2789
2790 for (idx, (part_id, part_lookup_file)) in sorted_part_lookup_files.into_iter().enumerate() {
2791 let lookup_reader = store.open_index_file(&part_lookup_file).await?;
2792 let reader_stream = IndexReaderStream::new(lookup_reader.clone(), batch_size).await;
2793 let mut stream = reader_stream.buffered(batch_readhead.unwrap_or(1)).boxed();
2794 while let Some(batch) = stream.next().await {
2795 let original_batch = batch?;
2796 let modified_batch = add_offset_to_page_idx(&original_batch, num_pages_written)?;
2797 lookup_file.write_record_batch(modified_batch).await?;
2798 }
2799 pages_per_file.push((part_id, lookup_reader.num_rows() as u32));
2800 num_pages_written += lookup_reader.num_rows() as u32;
2801 progress
2802 .stage_progress("merge_lookups", idx as u64 + 1)
2803 .await?;
2804 }
2805
2806 metadata.insert(RANGE_PARTITIONED_META_KEY.to_string(), "true".to_string());
2807 metadata.insert(
2808 PAGE_NUM_PER_RANGE_PARTITION_META_KEY.to_string(),
2809 serde_json::to_string(&pages_per_file)?,
2810 );
2811
2812 let lookup_file = lookup_file.finish_with_metadata(metadata).await?;
2813 progress.stage_complete("merge_lookups").await?;
2814
2815 cleanup_partition_files(store, part_lookup_files, &[]).await;
2817 Ok(lookup_file)
2818}
2819
2820#[allow(clippy::too_many_arguments)]
2826async fn merge_pages_and_lookups(
2827 store: &dyn IndexStore,
2828 part_page_files: &[String],
2829 part_lookup_files: &[String],
2830 page_files_map: &HashMap<u64, &String>,
2831 lookup_schema: Arc<Schema>,
2832 metadata: HashMap<String, String>,
2833 batch_size: u64,
2834 batch_readhead: Option<usize>,
2835 progress: Arc<dyn IndexBuildProgress>,
2836) -> Result<Vec<IndexFile>> {
2837 let partition_id = extract_partition_id(part_lookup_files[0].as_str())?;
2839 let page_file = page_files_map.get(&partition_id).unwrap();
2840 let page_reader = store.open_index_file(page_file).await?;
2841 let page_schema = page_reader.schema().clone();
2842
2843 let arrow_schema = Arc::new(Schema::from(&page_schema));
2844 let mut page_file = store
2845 .new_index_file(BTREE_PAGES_NAME, arrow_schema.clone())
2846 .await?;
2847 progress.stage_start("merge_pages", None, "pages").await?;
2848 let lookup_entries = merge_pages(
2849 part_lookup_files,
2850 page_files_map,
2851 store,
2852 batch_size,
2853 &mut page_file,
2854 arrow_schema.clone(),
2855 batch_readhead,
2856 progress.clone(),
2857 )
2858 .await?;
2859 let page_file = page_file.finish().await?;
2860 progress.stage_complete("merge_pages").await?;
2861
2862 let lookup_batch = RecordBatch::try_new(
2863 lookup_schema.clone(),
2864 vec![
2865 ScalarValue::iter_to_array(lookup_entries.iter().map(|(min, _, _, _)| min.clone()))?,
2866 ScalarValue::iter_to_array(lookup_entries.iter().map(|(_, max, _, _)| max.clone()))?,
2867 Arc::new(UInt32Array::from_iter_values(
2868 lookup_entries
2869 .iter()
2870 .map(|(_, _, null_count, _)| *null_count),
2871 )),
2872 Arc::new(UInt32Array::from_iter_values(
2873 lookup_entries.iter().map(|(_, _, _, page_idx)| *page_idx),
2874 )),
2875 ],
2876 )?;
2877 let mut lookup_file = store
2878 .new_index_file(BTREE_LOOKUP_NAME, lookup_schema)
2879 .await?;
2880 progress
2881 .stage_start("write_lookup_file", Some(1), "files")
2882 .await?;
2883 lookup_file.write_record_batch(lookup_batch).await?;
2884 let lookup_file = lookup_file.finish_with_metadata(metadata).await?;
2885 progress.stage_progress("write_lookup_file", 1).await?;
2886 progress.stage_complete("write_lookup_file").await?;
2887
2888 cleanup_partition_files(store, part_lookup_files, part_page_files).await;
2891
2892 Ok(vec![page_file, lookup_file])
2893}
2894
2895fn add_offset_to_page_idx(batch: &RecordBatch, offset: u32) -> Result<RecordBatch> {
2897 let (page_idx_pos, _) = batch.schema().column_with_name("page_idx").ok_or_else(|| {
2898 Error::internal("Column 'page_idx' not found in RecordBatch schema".to_string())
2899 })?;
2900 let page_idx_array = batch
2901 .column(page_idx_pos)
2902 .as_any()
2903 .downcast_ref::<UInt32Array>()
2904 .ok_or_else(|| {
2905 Error::internal("Failed to downcast 'page_idx' column to UInt32Array".to_string())
2906 })?;
2907 let offset_array = UInt32Array::from(vec![offset; page_idx_array.len()]);
2908 let new_page_idx_array_ref = add(page_idx_array, &offset_array)?;
2909 let mut new_columns = batch.columns().to_vec();
2910 new_columns[page_idx_pos] = new_page_idx_array_ref;
2911 let new_batch = RecordBatch::try_new(batch.schema(), new_columns)?;
2912 Ok(new_batch)
2913}
2914
2915#[allow(clippy::too_many_arguments)]
2918async fn merge_pages(
2919 part_lookup_files: &[String],
2920 page_files_map: &HashMap<u64, &String>,
2921 store: &dyn IndexStore,
2922 batch_size: u64,
2923 page_file: &mut Box<dyn IndexWriter>,
2924 arrow_schema: Arc<Schema>,
2925 batch_readhead: Option<usize>,
2926 progress: Arc<dyn IndexBuildProgress>,
2927) -> Result<Vec<(ScalarValue, ScalarValue, u32, u32)>> {
2928 let mut lookup_entries = Vec::new();
2929 let mut page_idx = 0u32;
2930
2931 debug!(
2932 "Starting SortPreservingMerge with {} partitions",
2933 part_lookup_files.len()
2934 );
2935
2936 let value_field = arrow_schema.field(0).clone().with_name(VALUE_COLUMN_NAME);
2937 let row_id_field = arrow_schema.field(1).clone().with_name(ROW_ID);
2938 let stream_schema = Arc::new(Schema::new(vec![value_field, row_id_field]));
2939
2940 let mut inputs: Vec<Arc<dyn ExecutionPlan>> = Vec::new();
2942 for lookup_file in part_lookup_files {
2943 let partition_id = extract_partition_id(lookup_file)?;
2944 let page_file_name = (*page_files_map.get(&partition_id).ok_or_else(|| {
2945 Error::internal(format!(
2946 "Page file not found for partition ID: {}",
2947 partition_id
2948 ))
2949 })?)
2950 .clone();
2951
2952 let reader = store.open_index_file(&page_file_name).await?;
2953
2954 let reader_stream = IndexReaderStream::new(reader, batch_size).await;
2955
2956 let stream = reader_stream
2957 .map(|fut| fut.map_err(DataFusionError::from))
2958 .buffered(batch_readhead.unwrap_or(1))
2959 .boxed();
2960
2961 let sendable_stream =
2962 Box::pin(RecordBatchStreamAdapter::new(stream_schema.clone(), stream));
2963 inputs.push(Arc::new(OneShotExec::new(sendable_stream)));
2964 }
2965
2966 let union_inputs = UnionExec::try_new(inputs)?;
2968
2969 let value_column_index = stream_schema.index_of(VALUE_COLUMN_NAME)?;
2971 let sort_expr = PhysicalSortExpr {
2972 expr: Arc::new(Column::new(VALUE_COLUMN_NAME, value_column_index)),
2973 options: SortOptions {
2974 descending: false,
2975 nulls_first: true,
2976 },
2977 };
2978
2979 let merge_exec = Arc::new(SortPreservingMergeExec::new(
2980 [sort_expr].into(),
2981 union_inputs,
2982 ));
2983
2984 let unchunked = execute_plan(
2985 merge_exec,
2986 LanceExecutionOptions {
2987 use_spilling: false,
2988 ..Default::default()
2989 },
2990 )?;
2991
2992 let mut chunked_stream = chunk_concat_stream(unchunked, batch_size as usize);
2994
2995 while let Some(batch) = chunked_stream.try_next().await? {
2997 let writer_batch = RecordBatch::try_new(
2998 arrow_schema.clone(),
2999 vec![batch.column(0).clone(), batch.column(1).clone()],
3000 )?;
3001
3002 page_file.write_record_batch(writer_batch).await?;
3003
3004 let min_val = ScalarValue::try_from_array(batch.column(0), 0)?;
3005 let max_val = ScalarValue::try_from_array(batch.column(0), batch.num_rows() - 1)?;
3006 let null_count = batch.column(0).null_count() as u32;
3007
3008 lookup_entries.push((min_val, max_val, null_count, page_idx));
3009 page_idx += 1;
3010 progress
3011 .stage_progress("merge_pages", page_idx as u64)
3012 .await?;
3013 }
3014
3015 Ok(lookup_entries)
3016}
3017
3018fn sort_files_by_partition_id(part_files: &[String]) -> Result<Vec<(u64, String)>> {
3020 let mut files_with_ids: Vec<(u64, &String)> = part_files
3021 .iter()
3022 .map(|file| extract_partition_id(file).map(|id| (id, file)))
3023 .collect::<Result<Vec<_>>>()?;
3024
3025 files_with_ids.sort_unstable_by_key(|k| k.0);
3026
3027 let sorted_files = files_with_ids
3028 .into_iter()
3029 .map(|(id, file)| (id, file.clone()))
3030 .collect();
3031
3032 Ok(sorted_files)
3033}
3034
3035fn extract_partition_id(filename: &str) -> Result<u64> {
3038 if !filename.starts_with("part_") {
3039 return Err(Error::internal(format!(
3040 "Invalid partition file name format: {}",
3041 filename
3042 )));
3043 }
3044
3045 let parts: Vec<&str> = filename.split('_').collect();
3046 if parts.len() < 3 {
3047 return Err(Error::internal(format!(
3048 "Invalid partition file name format: {}",
3049 filename
3050 )));
3051 }
3052
3053 parts[1].parse::<u64>().map_err(|_| {
3054 Error::internal(format!(
3055 "Failed to parse partition ID from filename: {}",
3056 filename
3057 ))
3058 })
3059}
3060
3061async fn cleanup_partition_files(
3066 store: &dyn IndexStore,
3067 part_lookup_files: &[String],
3068 part_page_files: &[String],
3069) {
3070 for file_name in part_lookup_files {
3072 cleanup_single_file(
3073 store,
3074 file_name,
3075 "part_",
3076 "_page_lookup.lance",
3077 "partition lookup",
3078 )
3079 .await;
3080 }
3081
3082 for file_name in part_page_files {
3084 cleanup_single_file(
3085 store,
3086 file_name,
3087 "part_",
3088 "_page_data.lance",
3089 "partition page",
3090 )
3091 .await;
3092 }
3093}
3094
3095async fn cleanup_single_file(
3099 store: &dyn IndexStore,
3100 file_name: &str,
3101 expected_prefix: &str,
3102 expected_suffix: &str,
3103 file_type: &str,
3104) {
3105 if file_name.starts_with(expected_prefix) && file_name.ends_with(expected_suffix) {
3106 match store.delete_index_file(file_name).await {
3107 Ok(()) => {
3108 debug!("Successfully deleted {} file: {}", file_type, file_name);
3109 }
3110 Err(e) => {
3111 warn!(
3112 "Failed to delete {} file '{}': {}. \
3113 This does not affect the merge operation, but may leave \
3114 partition files that should be cleaned up manually.",
3115 file_type, file_name, e
3116 );
3117 }
3118 }
3119 } else {
3120 warn!(
3122 "Skipping deletion of file '{}' as it does not match the expected \
3123 {} file pattern ({}*{})",
3124 file_name, file_type, expected_prefix, expected_suffix
3125 );
3126 }
3127}
3128
3129pub(crate) fn part_page_data_file_path(partition_id: u64) -> String {
3130 format!("part_{}_{}", partition_id, BTREE_PAGES_NAME)
3131}
3132
3133pub(crate) fn part_lookup_file_path(partition_id: u64) -> String {
3134 format!("part_{}_{}", partition_id, BTREE_LOOKUP_NAME)
3135}
3136
3137struct IndexReaderStream {
3141 reader: Arc<dyn IndexReader>,
3142 batch_size: u64,
3143 num_batches: u32,
3144 batch_idx: u32,
3145}
3146
3147impl IndexReaderStream {
3148 async fn new(reader: Arc<dyn IndexReader>, batch_size: u64) -> Self {
3149 let num_batches = reader.num_batches(batch_size).await;
3150 Self {
3151 reader,
3152 batch_size,
3153 num_batches,
3154 batch_idx: 0,
3155 }
3156 }
3157}
3158
3159impl Stream for IndexReaderStream {
3160 type Item = BoxFuture<'static, Result<RecordBatch>>;
3161
3162 fn poll_next(
3163 self: std::pin::Pin<&mut Self>,
3164 _cx: &mut std::task::Context<'_>,
3165 ) -> std::task::Poll<Option<Self::Item>> {
3166 let this = self.get_mut();
3167 if this.batch_idx >= this.num_batches {
3168 return std::task::Poll::Ready(None);
3169 }
3170 let batch_num = this.batch_idx;
3171 this.batch_idx += 1;
3172 let reader_copy = this.reader.clone();
3173 let batch_size = this.batch_size;
3174 let read_task = async move {
3175 reader_copy
3176 .read_record_batch(batch_num as u64, batch_size)
3177 .await
3178 }
3179 .boxed();
3180 std::task::Poll::Ready(Some(read_task))
3181 }
3182}
3183
3184#[derive(Debug, Serialize, Deserialize)]
3186pub struct BTreeParameters {
3187 pub zone_size: Option<u64>,
3189
3190 pub range_id: Option<u32>,
3204}
3205
3206struct BTreeTrainingRequest {
3207 parameters: BTreeParameters,
3208 criteria: TrainingCriteria,
3209}
3210
3211impl BTreeTrainingRequest {
3212 pub fn new(parameters: BTreeParameters) -> Self {
3213 Self {
3214 parameters,
3215 criteria: TrainingCriteria::new(TrainingOrdering::Values).with_row_id(),
3217 }
3218 }
3219}
3220
3221impl TrainingRequest for BTreeTrainingRequest {
3222 fn as_any(&self) -> &dyn std::any::Any {
3223 self
3224 }
3225
3226 fn criteria(&self) -> &TrainingCriteria {
3227 &self.criteria
3228 }
3229}
3230
3231#[derive(Debug, Default)]
3232pub struct BTreeIndexPlugin;
3233
3234#[async_trait]
3235impl BasicTrainer for BTreeIndexPlugin {
3236 fn new_training_request(
3237 &self,
3238 params: &str,
3239 field: &Field,
3240 ) -> Result<Box<dyn TrainingRequest>> {
3241 if field.data_type().is_nested() {
3242 return Err(Error::invalid_input_source(
3243 "A btree index can only be created on a non-nested field.".into(),
3244 ));
3245 }
3246
3247 let params = serde_json::from_str::<BTreeParameters>(params)?;
3248 Ok(Box::new(BTreeTrainingRequest::new(params)))
3249 }
3250
3251 async fn train_index(
3252 &self,
3253 data: SendableRecordBatchStream,
3254 index_store: &dyn IndexStore,
3255 request: Box<dyn TrainingRequest>,
3256 _fragment_ids: Option<Vec<u32>>,
3257 _progress: Arc<dyn crate::progress::IndexBuildProgress>,
3258 ) -> Result<CreatedIndex> {
3259 let request = request
3260 .as_any()
3261 .downcast_ref::<BTreeTrainingRequest>()
3262 .unwrap();
3263 if request.parameters.range_id.is_some() {
3264 warn!(
3270 "BTree `range_id` is deprecated and now ignored; a pre-sorted data \
3271 stream is still supported, but `range_id` no longer needs to be passed. \
3272 Use the segmented-index APIs instead (build per-fragment segments, then \
3273 commit_existing_index_segments(...) / merge_existing_index_segments(...)). \
3274 The `range_id` field will be removed in a future release."
3275 );
3276 }
3277 let files = train_btree_index(
3278 data,
3279 index_store,
3280 request
3281 .parameters
3282 .zone_size
3283 .unwrap_or(DEFAULT_BTREE_BATCH_SIZE),
3284 None,
3285 None,
3286 )
3287 .await?;
3288 Ok(CreatedIndex {
3289 index_details: prost_types::Any::from_msg(&pbold::BTreeIndexDetails::default())
3290 .unwrap(),
3291 index_version: BTREE_INDEX_VERSION,
3292 files,
3293 })
3294 }
3295}
3296
3297#[async_trait]
3298impl ScalarIndexPlugin for BTreeIndexPlugin {
3299 fn basic_trainer(&self) -> Option<&dyn BasicTrainer> {
3300 Some(self)
3301 }
3302
3303 fn name(&self) -> &str {
3304 "BTree"
3305 }
3306
3307 fn provides_exact_answer(&self) -> bool {
3308 true
3309 }
3310
3311 fn version(&self) -> u32 {
3312 BTREE_INDEX_VERSION
3313 }
3314
3315 fn new_query_parser(
3316 &self,
3317 index_name: String,
3318 _index_details: &prost_types::Any,
3319 ) -> Option<Box<dyn ScalarQueryParser>> {
3320 Some(Box::new(SargableQueryParser::new(
3321 index_name,
3322 self.name().to_string(),
3323 false,
3324 )))
3325 }
3326
3327 async fn load_index(
3328 &self,
3329 index_store: Arc<dyn IndexStore>,
3330 _index_details: &prost_types::Any,
3331 frag_reuse_index: Option<Arc<dyn RowIdRemapper>>,
3332 cache: &LanceCache,
3333 ) -> Result<Arc<dyn ScalarIndex>> {
3334 Ok(BTreeIndex::load(index_store, frag_reuse_index, cache).await? as Arc<dyn ScalarIndex>)
3335 }
3336
3337 async fn get_from_cache(
3338 &self,
3339 index_store: Arc<dyn IndexStore>,
3340 frag_reuse_index: Option<Arc<dyn RowIdRemapper>>,
3341 cache: &LanceCache,
3342 ) -> Result<Option<Arc<dyn ScalarIndex>>> {
3343 let Some(state) = cache.get_with_key(&BTreeIndexStateKey).await else {
3344 return Ok(None);
3345 };
3346 Ok(Some(state.reconstruct(
3347 index_store,
3348 cache,
3349 frag_reuse_index,
3350 )?))
3351 }
3352
3353 async fn put_in_cache(&self, cache: &LanceCache, index: Arc<dyn ScalarIndex>) -> Result<()> {
3354 let state = BTreeIndexState::from_index(index.as_ref())?;
3355 cache
3356 .insert_with_key(&BTreeIndexStateKey, Arc::new(state))
3357 .await;
3358 Ok(())
3359 }
3360
3361 async fn get_or_insert_in_cache(
3362 &self,
3363 index_store: Arc<dyn IndexStore>,
3364 frag_reuse_index: Option<Arc<dyn RowIdRemapper>>,
3365 cache: &LanceCache,
3366 load: ScalarIndexLoad<'_>,
3367 ) -> Result<Arc<dyn ScalarIndex>> {
3368 single_flight_open(
3369 cache,
3370 BTreeIndexStateKey,
3371 load,
3372 BTreeIndexState::from_index,
3373 move |state| state.reconstruct(index_store, cache, frag_reuse_index),
3374 )
3375 .await
3376 }
3377}
3378
3379#[cfg(test)]
3380mod tests {
3381 use lance_core::utils::row_addr_remap::RowAddrRemap;
3382 use std::sync::atomic::Ordering;
3383 use std::{collections::HashMap, sync::Arc};
3384
3385 use arrow::datatypes::{Float32Type, Float64Type, Int32Type, UInt64Type};
3386 use arrow_array::{FixedSizeListArray, record_batch};
3387 use datafusion::{
3388 execution::{SendableRecordBatchStream, TaskContext},
3389 physical_plan::{ExecutionPlan, sorts::sort::SortExec, stream::RecordBatchStreamAdapter},
3390 };
3391 use datafusion_common::{DataFusionError, ScalarValue};
3392 use datafusion_physical_expr::{PhysicalSortExpr, expressions::col};
3393 use futures::TryStreamExt;
3394 use futures::stream;
3395 use lance_core::cache::LanceCache;
3396 use lance_core::deepsize::DeepSizeOf;
3397 use lance_core::utils::tempfile::TempObjDir;
3398 use lance_datafusion::{chunker::break_stream, datagen::DatafusionDatagenExt};
3399 use lance_datagen::{ArrayGeneratorExt, BatchCount, RowCount, array, gen_batch};
3400 use lance_io::object_store::ObjectStore;
3401 use lance_select::{RowAddrTreeMap, RowSetOps};
3402 use object_store::path::Path;
3403
3404 use crate::metrics::LocalMetricsCollector;
3405 use crate::progress::{IndexBuildProgress, noop_progress};
3406 use crate::{
3407 metrics::NoOpMetricsCollector,
3408 scalar::{
3409 IndexStore, OldIndexDataFilter, SargableQuery, ScalarIndex, SearchResult,
3410 btree::{BTREE_PAGES_NAME, BTreeIndex},
3411 lance_format::LanceIndexStore,
3412 },
3413 };
3414
3415 use super::{
3416 BTreeIndexPlugin, BTreeIndexState, BTreeLookup, BTreePageKey, DEFAULT_BTREE_BATCH_SIZE,
3417 Matches, OrderableScalarValue, part_lookup_file_path, part_page_data_file_path,
3418 train_btree_index,
3419 };
3420 use crate::scalar::registry::ScalarIndexPlugin;
3421 use arrow_array::RecordBatch;
3422 use lance_core::cache::{CacheCodecImpl, CacheEntryReader, CacheEntryWriter, CacheKey};
3423
3424 fn serialize_state(state: &BTreeIndexState) -> Vec<u8> {
3426 let mut buf = Vec::new();
3427 state
3428 .serialize(&mut CacheEntryWriter::new(&mut buf))
3429 .unwrap();
3430 buf
3431 }
3432
3433 fn deserialize_state(buf: Vec<u8>) -> lance_core::Result<BTreeIndexState> {
3435 let data = bytes::Bytes::from(buf);
3436 let mut reader = CacheEntryReader::new(&data, 0, BTreeIndexState::CURRENT_VERSION);
3437 BTreeIndexState::deserialize(&mut reader)
3438 }
3439 use rangemap::RangeInclusiveMap;
3440
3441 lance_testing::define_stage_event_progress!(
3442 RecordingProgress,
3443 IndexBuildProgress,
3444 lance_core::Result<()>
3445 );
3446 #[test]
3447 fn test_scalar_value_size() {
3448 let size_of_i32 = OrderableScalarValue(ScalarValue::Int32(Some(0))).deep_size_of();
3449 let size_of_many_i32 = OrderableScalarValue(ScalarValue::FixedSizeList(Arc::new(
3450 FixedSizeListArray::from_iter_primitive::<Int32Type, _, _>(
3451 vec![Some(vec![Some(0); 128])],
3452 128,
3453 ),
3454 )))
3455 .deep_size_of();
3456
3457 assert!(size_of_i32 > 4);
3459 assert!(size_of_many_i32 > 128 * 4);
3460 }
3461
3462 #[test]
3463 fn test_orderable_dictionary_cmp() {
3464 use arrow_schema::DataType;
3465 use std::cmp::Ordering;
3466
3467 let dict = |s: &str, key: DataType| {
3468 OrderableScalarValue(ScalarValue::Dictionary(
3469 Box::new(key),
3470 Box::new(ScalarValue::Utf8(Some(s.to_string()))),
3471 ))
3472 };
3473
3474 assert_eq!(
3478 dict("a", DataType::Int16).cmp(&dict("b", DataType::Int16)),
3479 Ordering::Less
3480 );
3481 assert_eq!(
3482 dict("b", DataType::Int32).cmp(&dict("b", DataType::Int16)),
3483 Ordering::Equal
3484 );
3485
3486 assert_eq!(
3488 dict("a", DataType::Int16).cmp(&OrderableScalarValue(ScalarValue::Null)),
3489 Ordering::Greater
3490 );
3491 }
3492
3493 #[tokio::test]
3494 async fn test_null_ids() {
3495 let tmpdir = TempObjDir::default();
3496 let test_store = Arc::new(LanceIndexStore::new(
3497 Arc::new(ObjectStore::local()),
3498 tmpdir.clone(),
3499 Arc::new(LanceCache::no_cache()),
3500 ));
3501
3502 let stream = gen_batch()
3504 .col(
3505 "value",
3506 array::rand::<Float32Type>().with_nulls(&[true, false, false, false, false]),
3507 )
3508 .col("_rowid", array::step::<UInt64Type>())
3509 .into_df_stream(RowCount::from(5000), BatchCount::from(10));
3510
3511 train_btree_index(stream, test_store.as_ref(), 5000, None, None)
3512 .await
3513 .unwrap();
3514
3515 let index = BTreeIndex::load(test_store.clone(), None, &LanceCache::no_cache())
3516 .await
3517 .unwrap();
3518
3519 assert_eq!(index.page_lookup.null_pages.len(), 10);
3520
3521 let remap_dir = TempObjDir::default();
3522 let remap_store = Arc::new(LanceIndexStore::new(
3523 Arc::new(ObjectStore::local()),
3524 remap_dir.clone(),
3525 Arc::new(LanceCache::no_cache()),
3526 ));
3527
3528 index
3530 .remap(&RowAddrRemap::empty(), remap_store.as_ref())
3531 .await
3532 .unwrap();
3533
3534 let remap_index = BTreeIndex::load(remap_store.clone(), None, &LanceCache::no_cache())
3535 .await
3536 .unwrap();
3537
3538 assert_eq!(remap_index.page_lookup, index.page_lookup);
3539
3540 let original_pages = test_store.open_index_file(BTREE_PAGES_NAME).await.unwrap();
3541 let remapped_pages = remap_store.open_index_file(BTREE_PAGES_NAME).await.unwrap();
3542
3543 assert_eq!(original_pages.num_rows(), remapped_pages.num_rows());
3544
3545 let original_data = original_pages
3546 .read_record_batch(0, original_pages.num_rows() as u64)
3547 .await
3548 .unwrap();
3549 let remapped_data = remapped_pages
3550 .read_record_batch(0, remapped_pages.num_rows() as u64)
3551 .await
3552 .unwrap();
3553
3554 assert_eq!(original_data, remapped_data);
3555 }
3556
3557 #[tokio::test]
3558 async fn test_nan_ordering() {
3559 let tmpdir = TempObjDir::default();
3560 let test_store = Arc::new(LanceIndexStore::new(
3561 Arc::new(ObjectStore::local()),
3562 tmpdir.clone(),
3563 Arc::new(LanceCache::no_cache()),
3564 ));
3565
3566 let values = vec![
3567 0.0,
3568 1.0,
3569 2.0,
3570 3.0,
3571 f64::NAN,
3572 f64::NEG_INFINITY,
3573 f64::INFINITY,
3574 ];
3575
3576 let data = gen_batch()
3580 .col("value", array::cycle::<Float64Type>(values.clone()))
3581 .col("_rowid", array::step::<UInt64Type>())
3582 .into_df_exec(RowCount::from(10), BatchCount::from(100));
3583 let schema = data.schema();
3584 let sort_expr = PhysicalSortExpr::new_default(col("value", schema.as_ref()).unwrap());
3585 let plan = Arc::new(SortExec::new([sort_expr].into(), data));
3586 let stream = plan.execute(0, Arc::new(TaskContext::default())).unwrap();
3587 let stream = break_stream(stream, 64);
3588 let stream = stream.map_err(DataFusionError::from);
3589 let stream =
3590 Box::pin(RecordBatchStreamAdapter::new(schema, stream)) as SendableRecordBatchStream;
3591
3592 train_btree_index(stream, test_store.as_ref(), 64, None, None)
3593 .await
3594 .unwrap();
3595
3596 let index = BTreeIndex::load(test_store, None, &LanceCache::no_cache())
3597 .await
3598 .unwrap();
3599
3600 for (idx, value) in values.into_iter().enumerate() {
3601 let query = SargableQuery::Equals(ScalarValue::Float64(Some(value)));
3602 let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
3603 assert_eq!(
3604 result,
3605 SearchResult::exact(RowAddrTreeMap::from_iter(((idx as u64)..1000).step_by(7)))
3606 );
3607 }
3608 }
3609
3610 #[tokio::test]
3611 async fn test_contains_keys_matches_search() {
3612 let tmpdir = TempObjDir::default();
3613 let test_store = Arc::new(LanceIndexStore::new(
3614 Arc::new(ObjectStore::local()),
3615 tmpdir.clone(),
3616 Arc::new(LanceCache::no_cache()),
3617 ));
3618
3619 let data = gen_batch()
3622 .col("value", array::step::<Int32Type>())
3623 .col("_rowid", array::step::<UInt64Type>())
3624 .into_df_exec(RowCount::from(100), BatchCount::from(10));
3625 let schema = data.schema();
3626 let sort_expr = PhysicalSortExpr::new_default(col("value", schema.as_ref()).unwrap());
3627 let plan = Arc::new(SortExec::new([sort_expr].into(), data));
3628 let stream = plan.execute(0, Arc::new(TaskContext::default())).unwrap();
3629 let stream = break_stream(stream, 64);
3630 let stream = stream.map_err(DataFusionError::from);
3631 let stream =
3632 Box::pin(RecordBatchStreamAdapter::new(schema, stream)) as SendableRecordBatchStream;
3633
3634 train_btree_index(stream, test_store.as_ref(), 64, None, None)
3635 .await
3636 .unwrap();
3637 let index = BTreeIndex::load(test_store, None, &LanceCache::no_cache())
3638 .await
3639 .unwrap();
3640
3641 let keys: Vec<i32> = vec![0, 999, 500, 1, 998, -1, 1000, 1500, 250, 251, 7, 64, 63, 65];
3644 let scalar_keys: Vec<ScalarValue> =
3645 keys.iter().map(|k| ScalarValue::Int32(Some(*k))).collect();
3646
3647 let batched = index
3648 .contains_keys(&scalar_keys, &NoOpMetricsCollector)
3649 .await
3650 .unwrap();
3651
3652 let mut oracle = Vec::with_capacity(keys.len());
3654 for k in &scalar_keys {
3655 let result = index
3656 .search(&SargableQuery::Equals(k.clone()), &NoOpMetricsCollector)
3657 .await
3658 .unwrap();
3659 oracle.push(!result.row_addrs().is_empty());
3660 }
3661 assert_eq!(
3662 batched, oracle,
3663 "contains_keys must agree with per-key Equals search; keys={keys:?}"
3664 );
3665
3666 let expected: Vec<bool> = keys.iter().map(|k| (0..1000).contains(k)).collect();
3668 assert_eq!(batched, expected);
3669
3670 assert!(
3672 index
3673 .contains_keys(&[], &NoOpMetricsCollector)
3674 .await
3675 .unwrap()
3676 .is_empty()
3677 );
3678
3679 let with_null = vec![ScalarValue::Int32(Some(5)), ScalarValue::Int32(None)];
3681 assert_eq!(
3682 index
3683 .contains_keys(&with_null, &NoOpMetricsCollector)
3684 .await
3685 .unwrap(),
3686 vec![true, false]
3687 );
3688 }
3689
3690 #[tokio::test]
3691 async fn test_page_cache() {
3692 let tmpdir = TempObjDir::default();
3693 let test_store = Arc::new(LanceIndexStore::new(
3694 Arc::new(ObjectStore::local()),
3695 tmpdir.clone(),
3696 Arc::new(LanceCache::no_cache()),
3697 ));
3698
3699 let data = gen_batch()
3700 .col("value", array::step::<Float32Type>())
3701 .col("_rowid", array::step::<UInt64Type>())
3702 .into_df_exec(RowCount::from(1000), BatchCount::from(10));
3703 let schema = data.schema();
3704 let sort_expr = PhysicalSortExpr::new_default(col("value", schema.as_ref()).unwrap());
3705 let plan = Arc::new(SortExec::new([sort_expr].into(), data));
3706 let stream = plan.execute(0, Arc::new(TaskContext::default())).unwrap();
3707 let stream = break_stream(stream, 64);
3708 let stream = stream.map_err(DataFusionError::from);
3709 let stream =
3710 Box::pin(RecordBatchStreamAdapter::new(schema, stream)) as SendableRecordBatchStream;
3711
3712 train_btree_index(stream, test_store.as_ref(), 64, None, None)
3713 .await
3714 .unwrap();
3715
3716 let cache = Arc::new(LanceCache::with_capacity(100 * 1024 * 1024));
3717 let index = BTreeIndex::load(test_store, None, cache.as_ref())
3718 .await
3719 .unwrap();
3720
3721 let query = SargableQuery::Equals(ScalarValue::Float32(Some(0.0)));
3722 let metrics = LocalMetricsCollector::default();
3723 let query1 = index.search(&query, &metrics);
3724 let query2 = index.search(&query, &metrics);
3725 tokio::join!(query1, query2).0.unwrap();
3726 assert_eq!(metrics.parts_loaded.load(Ordering::Relaxed), 1);
3727 }
3728
3729 #[tokio::test]
3730 async fn test_page_cache_hit_miss_counts() {
3731 let tmpdir = TempObjDir::default();
3732 let test_store = Arc::new(LanceIndexStore::new(
3733 Arc::new(ObjectStore::local()),
3734 tmpdir.clone(),
3735 Arc::new(LanceCache::no_cache()),
3736 ));
3737
3738 let data = gen_batch()
3739 .col("value", array::step::<Float32Type>())
3740 .col("_rowid", array::step::<UInt64Type>())
3741 .into_df_exec(RowCount::from(1000), BatchCount::from(10));
3742 let schema = data.schema();
3743 let sort_expr = PhysicalSortExpr::new_default(col("value", schema.as_ref()).unwrap());
3744 let plan = Arc::new(SortExec::new([sort_expr].into(), data));
3745 let stream = plan.execute(0, Arc::new(TaskContext::default())).unwrap();
3746 let stream = break_stream(stream, 64);
3747 let stream = stream.map_err(DataFusionError::from);
3748 let stream =
3749 Box::pin(RecordBatchStreamAdapter::new(schema, stream)) as SendableRecordBatchStream;
3750
3751 train_btree_index(stream, test_store.as_ref(), 64, None, None)
3752 .await
3753 .unwrap();
3754
3755 let cache = Arc::new(LanceCache::with_capacity(100 * 1024 * 1024));
3756 let index = BTreeIndex::load(test_store, None, cache.as_ref())
3757 .await
3758 .unwrap();
3759
3760 let query = SargableQuery::Equals(ScalarValue::Float32(Some(0.0)));
3762 let cold = LocalMetricsCollector::default();
3763 index.search(&query, &cold).await.unwrap();
3764 assert_eq!(cold.index_cache_hits(), 0);
3765 assert_eq!(cold.index_cache_misses(), 1);
3766 assert_eq!(cold.parts_loaded.load(Ordering::Relaxed), 1);
3767
3768 let warm = LocalMetricsCollector::default();
3770 index.search(&query, &warm).await.unwrap();
3771 assert_eq!(warm.index_cache_hits(), 1);
3772 assert_eq!(warm.index_cache_misses(), 0);
3773 assert_eq!(warm.parts_loaded.load(Ordering::Relaxed), 0);
3774 }
3775
3776 #[tokio::test]
3777 async fn test_like_prefix_search() {
3778 use arrow::datatypes::DataType;
3779 use arrow_array::StringArray;
3780
3781 let tmpdir = TempObjDir::default();
3782 let test_store = Arc::new(LanceIndexStore::new(
3783 Arc::new(ObjectStore::local()),
3784 tmpdir.clone(),
3785 Arc::new(LanceCache::no_cache()),
3786 ));
3787
3788 let values = vec![
3790 "apple",
3791 "app",
3792 "application",
3793 "banana",
3794 "band",
3795 "test_ns$table1",
3796 "test_ns$table2",
3797 "test_ns2$table1",
3798 "test",
3799 "testing",
3800 ];
3801 let row_ids: Vec<u64> = (0..values.len() as u64).collect();
3802
3803 let schema = Arc::new(arrow::datatypes::Schema::new(vec![
3804 arrow::datatypes::Field::new("value", DataType::Utf8, false),
3805 arrow::datatypes::Field::new("_rowid", DataType::UInt64, false),
3806 ]));
3807
3808 let batch = arrow::record_batch::RecordBatch::try_new(
3809 schema.clone(),
3810 vec![
3811 Arc::new(StringArray::from(values.clone())),
3812 Arc::new(arrow_array::UInt64Array::from(row_ids)),
3813 ],
3814 )
3815 .unwrap();
3816
3817 let stream: SendableRecordBatchStream = Box::pin(RecordBatchStreamAdapter::new(
3818 schema,
3819 stream::once(async { Ok(batch) }),
3820 ));
3821
3822 train_btree_index(stream, test_store.as_ref(), 100, None, None)
3823 .await
3824 .unwrap();
3825
3826 let index = BTreeIndex::load(test_store, None, &LanceCache::no_cache())
3827 .await
3828 .unwrap();
3829
3830 let query = SargableQuery::LikePrefix(ScalarValue::Utf8(Some("app".to_string())));
3832 let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
3833
3834 match &result {
3835 SearchResult::Exact(row_ids) => {
3836 let ids: Vec<u64> = row_ids
3837 .true_rows()
3838 .row_addrs()
3839 .unwrap()
3840 .map(u64::from)
3841 .collect();
3842 assert!(ids.contains(&0), "Should contain row 0 (apple)");
3843 assert!(ids.contains(&1), "Should contain row 1 (app)");
3844 assert!(ids.contains(&2), "Should contain row 2 (application)");
3845 assert!(!ids.contains(&3), "Should not contain row 3 (banana)");
3846 }
3847 _ => panic!("Expected Exact result"),
3848 }
3849
3850 let query = SargableQuery::LikePrefix(ScalarValue::Utf8(Some("test_ns$".to_string())));
3852 let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
3853
3854 match &result {
3855 SearchResult::Exact(row_ids) => {
3856 let ids: Vec<u64> = row_ids
3857 .true_rows()
3858 .row_addrs()
3859 .unwrap()
3860 .map(u64::from)
3861 .collect();
3862 assert!(ids.contains(&5), "Should contain row 5 (test_ns$table1)");
3863 assert!(ids.contains(&6), "Should contain row 6 (test_ns$table2)");
3864 assert!(
3865 !ids.contains(&7),
3866 "Should not contain row 7 (test_ns2$table1)"
3867 );
3868 }
3869 _ => panic!("Expected Exact result"),
3870 }
3871
3872 let query = SargableQuery::LikePrefix(ScalarValue::Utf8(Some("test".to_string())));
3874 let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
3875
3876 match &result {
3877 SearchResult::Exact(row_ids) => {
3878 let ids: Vec<u64> = row_ids
3879 .true_rows()
3880 .row_addrs()
3881 .unwrap()
3882 .map(u64::from)
3883 .collect();
3884 assert!(
3885 ids.contains(&5),
3886 "Should contain row 5 (test_ns$table1): {:?}",
3887 ids
3888 );
3889 assert!(
3890 ids.contains(&6),
3891 "Should contain row 6 (test_ns$table2): {:?}",
3892 ids
3893 );
3894 assert!(
3895 ids.contains(&7),
3896 "Should contain row 7 (test_ns2$table1): {:?}",
3897 ids
3898 );
3899 assert!(ids.contains(&8), "Should contain row 8 (test): {:?}", ids);
3900 assert!(
3901 ids.contains(&9),
3902 "Should contain row 9 (testing): {:?}",
3903 ids
3904 );
3905 }
3906 _ => panic!("Expected Exact result"),
3907 }
3908 }
3909
3910 #[tokio::test]
3911 async fn test_like_prefix_search_large_utf8() {
3912 use arrow::datatypes::DataType;
3913 use arrow_array::LargeStringArray;
3914
3915 let tmpdir = TempObjDir::default();
3916 let test_store = Arc::new(LanceIndexStore::new(
3917 Arc::new(ObjectStore::local()),
3918 tmpdir.clone(),
3919 Arc::new(LanceCache::no_cache()),
3920 ));
3921
3922 let values = vec!["apple", "app", "application", "banana"];
3923 let row_ids: Vec<u64> = (0..values.len() as u64).collect();
3924
3925 let schema = Arc::new(arrow::datatypes::Schema::new(vec![
3926 arrow::datatypes::Field::new("value", DataType::LargeUtf8, false),
3927 arrow::datatypes::Field::new("_rowid", DataType::UInt64, false),
3928 ]));
3929
3930 let batch = arrow::record_batch::RecordBatch::try_new(
3931 schema.clone(),
3932 vec![
3933 Arc::new(LargeStringArray::from(values)),
3934 Arc::new(arrow_array::UInt64Array::from(row_ids)),
3935 ],
3936 )
3937 .unwrap();
3938
3939 let stream: SendableRecordBatchStream = Box::pin(RecordBatchStreamAdapter::new(
3940 schema,
3941 stream::once(async { Ok(batch) }),
3942 ));
3943
3944 train_btree_index(stream, test_store.as_ref(), 100, None, None)
3945 .await
3946 .unwrap();
3947
3948 let index = BTreeIndex::load(test_store, None, &LanceCache::no_cache())
3949 .await
3950 .unwrap();
3951
3952 let query = SargableQuery::LikePrefix(ScalarValue::LargeUtf8(Some("app".to_string())));
3954 let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
3955
3956 match &result {
3957 SearchResult::Exact(row_ids) => {
3958 let ids: Vec<u64> = row_ids
3959 .true_rows()
3960 .row_addrs()
3961 .unwrap()
3962 .map(u64::from)
3963 .collect();
3964 assert!(ids.contains(&0), "Should contain row 0 (apple)");
3965 assert!(ids.contains(&1), "Should contain row 1 (app)");
3966 assert!(ids.contains(&2), "Should contain row 2 (application)");
3967 assert!(!ids.contains(&3), "Should not contain row 3 (banana)");
3968 }
3969 _ => panic!("Expected Exact result"),
3970 }
3971 }
3972
3973 #[tokio::test]
3974 async fn test_fragment_btree_index_consistency() {
3975 let full_tmpdir = TempObjDir::default();
3977 let full_store = Arc::new(LanceIndexStore::new(
3978 Arc::new(ObjectStore::local()),
3979 full_tmpdir.clone(),
3980 Arc::new(LanceCache::no_cache()),
3981 ));
3982
3983 let fragment_tmpdir = TempObjDir::default();
3984 let fragment_store = Arc::new(LanceIndexStore::new(
3985 Arc::new(ObjectStore::local()),
3986 fragment_tmpdir.clone(),
3987 Arc::new(LanceCache::no_cache()),
3988 ));
3989
3990 let total_count = 2 * DEFAULT_BTREE_BATCH_SIZE;
3993 let full_data_gen = gen_batch()
3994 .col("value", array::step::<Int32Type>())
3995 .col("_rowid", array::step::<UInt64Type>())
3996 .into_df_stream(RowCount::from(total_count / 2), BatchCount::from(2));
3997 let full_data_source = Box::pin(RecordBatchStreamAdapter::new(
3998 full_data_gen.schema(),
3999 full_data_gen,
4000 ));
4001
4002 train_btree_index(
4003 full_data_source,
4004 full_store.as_ref(),
4005 DEFAULT_BTREE_BATCH_SIZE,
4006 None,
4007 None,
4008 )
4009 .await
4010 .unwrap();
4011
4012 let half_count = DEFAULT_BTREE_BATCH_SIZE;
4015 let fragment1_gen = gen_batch()
4016 .col("value", array::step::<Int32Type>())
4017 .col("_rowid", array::step::<UInt64Type>())
4018 .into_df_stream(RowCount::from(half_count), BatchCount::from(1));
4019 let fragment1_data_source = Box::pin(RecordBatchStreamAdapter::new(
4020 fragment1_gen.schema(),
4021 fragment1_gen,
4022 ));
4023
4024 train_btree_index(
4025 fragment1_data_source,
4026 fragment_store.as_ref(),
4027 DEFAULT_BTREE_BATCH_SIZE,
4028 Some(vec![1]), None,
4030 )
4031 .await
4032 .unwrap();
4033
4034 let start_val = DEFAULT_BTREE_BATCH_SIZE as i32;
4036 let end_val = (2 * DEFAULT_BTREE_BATCH_SIZE) as i32;
4037 let values_second_half: Vec<i32> = (start_val..end_val).collect();
4038 let row_ids_second_half: Vec<u64> = (start_val as u64..end_val as u64).collect();
4039 let fragment2_gen = gen_batch()
4040 .col("value", array::cycle::<Int32Type>(values_second_half))
4041 .col("_rowid", array::cycle::<UInt64Type>(row_ids_second_half))
4042 .into_df_stream(RowCount::from(half_count), BatchCount::from(1));
4043 let fragment2_data_source = Box::pin(RecordBatchStreamAdapter::new(
4044 fragment2_gen.schema(),
4045 fragment2_gen,
4046 ));
4047
4048 train_btree_index(
4049 fragment2_data_source,
4050 fragment_store.as_ref(),
4051 DEFAULT_BTREE_BATCH_SIZE,
4052 Some(vec![2]), None,
4054 )
4055 .await
4056 .unwrap();
4057
4058 let part_page_files = vec![
4060 part_page_data_file_path(1 << 32),
4061 part_page_data_file_path(2 << 32),
4062 ];
4063
4064 let part_lookup_files = vec![
4065 part_lookup_file_path(1 << 32),
4066 part_lookup_file_path(2 << 32),
4067 ];
4068
4069 let progress = Arc::new(RecordingProgress::default());
4070 super::merge_metadata_files(
4071 fragment_store.as_ref(),
4072 &part_page_files,
4073 &part_lookup_files,
4074 Option::from(1usize),
4075 progress.clone(),
4076 )
4077 .await
4078 .unwrap();
4079
4080 let tags = progress
4081 .recorded_events()
4082 .iter()
4083 .map(|(kind, stage, _)| format!("{kind}:{stage}"))
4084 .collect::<Vec<_>>();
4085 let merge_start = tags
4086 .iter()
4087 .position(|e| e == "start:merge_pages")
4088 .expect("missing merge_pages start");
4089 let merge_complete = tags
4090 .iter()
4091 .position(|e| e == "complete:merge_pages")
4092 .expect("missing merge_pages complete");
4093 let lookup_start = tags
4094 .iter()
4095 .position(|e| e == "start:write_lookup_file")
4096 .expect("missing write_lookup_file start");
4097 let lookup_complete = tags
4098 .iter()
4099 .position(|e| e == "complete:write_lookup_file")
4100 .expect("missing write_lookup_file complete");
4101 assert!(merge_start < merge_complete);
4102 assert!(merge_complete < lookup_start);
4103 assert!(lookup_start < lookup_complete);
4104 assert!(
4105 tags.iter().any(|e| e == "progress:merge_pages"),
4106 "expected merge_pages progress callbacks"
4107 );
4108 assert!(
4109 tags.iter().any(|e| e == "progress:write_lookup_file"),
4110 "expected write_lookup_file progress callbacks"
4111 );
4112
4113 let full_index = BTreeIndex::load(full_store.clone(), None, &LanceCache::no_cache())
4115 .await
4116 .unwrap();
4117
4118 let merged_index = BTreeIndex::load(fragment_store.clone(), None, &LanceCache::no_cache())
4119 .await
4120 .unwrap();
4121
4122 let query_0 = SargableQuery::Equals(ScalarValue::Int32(Some(0)));
4126 let full_result_0 = full_index
4127 .search(&query_0, &NoOpMetricsCollector)
4128 .await
4129 .unwrap();
4130 let merged_result_0 = merged_index
4131 .search(&query_0, &NoOpMetricsCollector)
4132 .await
4133 .unwrap();
4134 assert_eq!(full_result_0, merged_result_0, "Query for value 0 failed");
4135
4136 let mid_first_batch = (DEFAULT_BTREE_BATCH_SIZE / 2) as i32;
4138 let query_mid_first = SargableQuery::Equals(ScalarValue::Int32(Some(mid_first_batch)));
4139 let full_result_mid_first = full_index
4140 .search(&query_mid_first, &NoOpMetricsCollector)
4141 .await
4142 .unwrap();
4143 let merged_result_mid_first = merged_index
4144 .search(&query_mid_first, &NoOpMetricsCollector)
4145 .await
4146 .unwrap();
4147 assert_eq!(
4148 full_result_mid_first, merged_result_mid_first,
4149 "Query for value {} failed",
4150 mid_first_batch
4151 );
4152
4153 let first_second_batch = DEFAULT_BTREE_BATCH_SIZE as i32;
4155 let query_first_second =
4156 SargableQuery::Equals(ScalarValue::Int32(Some(first_second_batch)));
4157 let full_result_first_second = full_index
4158 .search(&query_first_second, &NoOpMetricsCollector)
4159 .await
4160 .unwrap();
4161 let merged_result_first_second = merged_index
4162 .search(&query_first_second, &NoOpMetricsCollector)
4163 .await
4164 .unwrap();
4165 assert_eq!(
4166 full_result_first_second, merged_result_first_second,
4167 "Query for value {} failed",
4168 first_second_batch
4169 );
4170
4171 let mid_second_batch = (DEFAULT_BTREE_BATCH_SIZE + DEFAULT_BTREE_BATCH_SIZE / 2) as i32;
4173 let query_mid_second = SargableQuery::Equals(ScalarValue::Int32(Some(mid_second_batch)));
4174
4175 let full_result_mid_second = full_index
4176 .search(&query_mid_second, &NoOpMetricsCollector)
4177 .await
4178 .unwrap();
4179 let merged_result_mid_second = merged_index
4180 .search(&query_mid_second, &NoOpMetricsCollector)
4181 .await
4182 .unwrap();
4183 assert_eq!(
4184 full_result_mid_second, merged_result_mid_second,
4185 "Query for value {} failed",
4186 mid_second_batch
4187 );
4188 }
4189
4190 #[tokio::test]
4191 async fn test_fragment_btree_index_boundary_queries() {
4192 let full_tmpdir = TempObjDir::default();
4194 let full_store = Arc::new(LanceIndexStore::new(
4195 Arc::new(ObjectStore::local()),
4196 full_tmpdir.clone(),
4197 Arc::new(LanceCache::no_cache()),
4198 ));
4199
4200 let fragment_tmpdir = TempObjDir::default();
4201 let fragment_store = Arc::new(LanceIndexStore::new(
4202 Arc::new(ObjectStore::local()),
4203 fragment_tmpdir.clone(),
4204 Arc::new(LanceCache::no_cache()),
4205 ));
4206
4207 let total_count = 3 * DEFAULT_BTREE_BATCH_SIZE;
4209
4210 let full_data_gen = gen_batch()
4212 .col("value", array::step::<Int32Type>())
4213 .col("_rowid", array::step::<UInt64Type>())
4214 .into_df_stream(RowCount::from(total_count / 3), BatchCount::from(3));
4215 let full_data_source = Box::pin(RecordBatchStreamAdapter::new(
4216 full_data_gen.schema(),
4217 full_data_gen,
4218 ));
4219
4220 train_btree_index(
4221 full_data_source,
4222 full_store.as_ref(),
4223 DEFAULT_BTREE_BATCH_SIZE,
4224 None,
4225 None,
4226 )
4227 .await
4228 .unwrap();
4229
4230 let fragment_size = DEFAULT_BTREE_BATCH_SIZE;
4233 let fragment1_gen = gen_batch()
4234 .col("value", array::step::<Int32Type>())
4235 .col("_rowid", array::step::<UInt64Type>())
4236 .into_df_stream(RowCount::from(fragment_size), BatchCount::from(1));
4237 let fragment1_data_source = Box::pin(RecordBatchStreamAdapter::new(
4238 fragment1_gen.schema(),
4239 fragment1_gen,
4240 ));
4241
4242 train_btree_index(
4243 fragment1_data_source,
4244 fragment_store.as_ref(),
4245 DEFAULT_BTREE_BATCH_SIZE,
4246 Some(vec![1]),
4247 None,
4248 )
4249 .await
4250 .unwrap();
4251
4252 let start_val2 = DEFAULT_BTREE_BATCH_SIZE as i32;
4254 let end_val2 = (2 * DEFAULT_BTREE_BATCH_SIZE) as i32;
4255 let values_fragment2: Vec<i32> = (start_val2..end_val2).collect();
4256 let row_ids_fragment2: Vec<u64> = (start_val2 as u64..end_val2 as u64).collect();
4257 let fragment2_gen = gen_batch()
4258 .col("value", array::cycle::<Int32Type>(values_fragment2))
4259 .col("_rowid", array::cycle::<UInt64Type>(row_ids_fragment2))
4260 .into_df_stream(RowCount::from(fragment_size), BatchCount::from(1));
4261 let fragment2_data_source = Box::pin(RecordBatchStreamAdapter::new(
4262 fragment2_gen.schema(),
4263 fragment2_gen,
4264 ));
4265
4266 train_btree_index(
4267 fragment2_data_source,
4268 fragment_store.as_ref(),
4269 DEFAULT_BTREE_BATCH_SIZE,
4270 Some(vec![2]),
4271 None,
4272 )
4273 .await
4274 .unwrap();
4275
4276 let start_val3 = (2 * DEFAULT_BTREE_BATCH_SIZE) as i32;
4278 let end_val3 = (3 * DEFAULT_BTREE_BATCH_SIZE) as i32;
4279 let values_fragment3: Vec<i32> = (start_val3..end_val3).collect();
4280 let row_ids_fragment3: Vec<u64> = (start_val3 as u64..end_val3 as u64).collect();
4281 let fragment3_gen = gen_batch()
4282 .col("value", array::cycle::<Int32Type>(values_fragment3))
4283 .col("_rowid", array::cycle::<UInt64Type>(row_ids_fragment3))
4284 .into_df_stream(RowCount::from(fragment_size), BatchCount::from(1));
4285 let fragment3_data_source = Box::pin(RecordBatchStreamAdapter::new(
4286 fragment3_gen.schema(),
4287 fragment3_gen,
4288 ));
4289
4290 train_btree_index(
4291 fragment3_data_source,
4292 fragment_store.as_ref(),
4293 DEFAULT_BTREE_BATCH_SIZE,
4294 Some(vec![3]),
4295 None,
4296 )
4297 .await
4298 .unwrap();
4299
4300 let part_page_files = vec![
4302 part_page_data_file_path(1 << 32),
4303 part_page_data_file_path(2 << 32),
4304 part_page_data_file_path(3 << 32),
4305 ];
4306
4307 let part_lookup_files = vec![
4308 part_lookup_file_path(1 << 32),
4309 part_lookup_file_path(2 << 32),
4310 part_lookup_file_path(3 << 32),
4311 ];
4312
4313 super::merge_metadata_files(
4314 fragment_store.as_ref(),
4315 &part_page_files,
4316 &part_lookup_files,
4317 Option::from(1usize),
4318 noop_progress(),
4319 )
4320 .await
4321 .unwrap();
4322
4323 let full_index = BTreeIndex::load(full_store.clone(), None, &LanceCache::no_cache())
4325 .await
4326 .unwrap();
4327
4328 let merged_index = BTreeIndex::load(fragment_store.clone(), None, &LanceCache::no_cache())
4329 .await
4330 .unwrap();
4331
4332 let query_min = SargableQuery::Equals(ScalarValue::Int32(Some(0)));
4336 let full_result_min = full_index
4337 .search(&query_min, &NoOpMetricsCollector)
4338 .await
4339 .unwrap();
4340 let merged_result_min = merged_index
4341 .search(&query_min, &NoOpMetricsCollector)
4342 .await
4343 .unwrap();
4344 assert_eq!(
4345 full_result_min, merged_result_min,
4346 "Query for minimum value 0 failed"
4347 );
4348
4349 let max_val = (3 * DEFAULT_BTREE_BATCH_SIZE - 1) as i32;
4351 let query_max = SargableQuery::Equals(ScalarValue::Int32(Some(max_val)));
4352 let full_result_max = full_index
4353 .search(&query_max, &NoOpMetricsCollector)
4354 .await
4355 .unwrap();
4356 let merged_result_max = merged_index
4357 .search(&query_max, &NoOpMetricsCollector)
4358 .await
4359 .unwrap();
4360 assert_eq!(
4361 full_result_max, merged_result_max,
4362 "Query for maximum value {} failed",
4363 max_val
4364 );
4365
4366 let fragment1_last = (DEFAULT_BTREE_BATCH_SIZE - 1) as i32;
4368 let query_frag1_last = SargableQuery::Equals(ScalarValue::Int32(Some(fragment1_last)));
4369 let full_result_frag1_last = full_index
4370 .search(&query_frag1_last, &NoOpMetricsCollector)
4371 .await
4372 .unwrap();
4373 let merged_result_frag1_last = merged_index
4374 .search(&query_frag1_last, &NoOpMetricsCollector)
4375 .await
4376 .unwrap();
4377 assert_eq!(
4378 full_result_frag1_last, merged_result_frag1_last,
4379 "Query for fragment 1 last value {} failed",
4380 fragment1_last
4381 );
4382
4383 let fragment2_first = DEFAULT_BTREE_BATCH_SIZE as i32;
4385 let query_frag2_first = SargableQuery::Equals(ScalarValue::Int32(Some(fragment2_first)));
4386 let full_result_frag2_first = full_index
4387 .search(&query_frag2_first, &NoOpMetricsCollector)
4388 .await
4389 .unwrap();
4390 let merged_result_frag2_first = merged_index
4391 .search(&query_frag2_first, &NoOpMetricsCollector)
4392 .await
4393 .unwrap();
4394 assert_eq!(
4395 full_result_frag2_first, merged_result_frag2_first,
4396 "Query for fragment 2 first value {} failed",
4397 fragment2_first
4398 );
4399
4400 let fragment2_last = (2 * DEFAULT_BTREE_BATCH_SIZE - 1) as i32;
4402 let query_frag2_last = SargableQuery::Equals(ScalarValue::Int32(Some(fragment2_last)));
4403 let full_result_frag2_last = full_index
4404 .search(&query_frag2_last, &NoOpMetricsCollector)
4405 .await
4406 .unwrap();
4407 let merged_result_frag2_last = merged_index
4408 .search(&query_frag2_last, &NoOpMetricsCollector)
4409 .await
4410 .unwrap();
4411 assert_eq!(
4412 full_result_frag2_last, merged_result_frag2_last,
4413 "Query for fragment 2 last value {} failed",
4414 fragment2_last
4415 );
4416
4417 let fragment3_first = (2 * DEFAULT_BTREE_BATCH_SIZE) as i32;
4419 let query_frag3_first = SargableQuery::Equals(ScalarValue::Int32(Some(fragment3_first)));
4420 let full_result_frag3_first = full_index
4421 .search(&query_frag3_first, &NoOpMetricsCollector)
4422 .await
4423 .unwrap();
4424 let merged_result_frag3_first = merged_index
4425 .search(&query_frag3_first, &NoOpMetricsCollector)
4426 .await
4427 .unwrap();
4428 assert_eq!(
4429 full_result_frag3_first, merged_result_frag3_first,
4430 "Query for fragment 3 first value {} failed",
4431 fragment3_first
4432 );
4433
4434 let query_below_min = SargableQuery::Equals(ScalarValue::Int32(Some(-1)));
4438 let full_result_below = full_index
4439 .search(&query_below_min, &NoOpMetricsCollector)
4440 .await
4441 .unwrap();
4442 let merged_result_below = merged_index
4443 .search(&query_below_min, &NoOpMetricsCollector)
4444 .await
4445 .unwrap();
4446 assert_eq!(
4447 full_result_below, merged_result_below,
4448 "Query for value below minimum (-1) failed"
4449 );
4450
4451 let query_above_max = SargableQuery::Equals(ScalarValue::Int32(Some(max_val + 1)));
4453 let full_result_above = full_index
4454 .search(&query_above_max, &NoOpMetricsCollector)
4455 .await
4456 .unwrap();
4457 let merged_result_above = merged_index
4458 .search(&query_above_max, &NoOpMetricsCollector)
4459 .await
4460 .unwrap();
4461 assert_eq!(
4462 full_result_above,
4463 merged_result_above,
4464 "Query for value above maximum ({}) failed",
4465 max_val + 1
4466 );
4467
4468 let range_start = (DEFAULT_BTREE_BATCH_SIZE - 100) as i32;
4472 let range_end = (DEFAULT_BTREE_BATCH_SIZE + 100) as i32;
4473 let query_cross_frag = SargableQuery::Range(
4474 std::collections::Bound::Included(ScalarValue::Int32(Some(range_start))),
4475 std::collections::Bound::Excluded(ScalarValue::Int32(Some(range_end))),
4476 );
4477 let full_result_cross = full_index
4478 .search(&query_cross_frag, &NoOpMetricsCollector)
4479 .await
4480 .unwrap();
4481 let merged_result_cross = merged_index
4482 .search(&query_cross_frag, &NoOpMetricsCollector)
4483 .await
4484 .unwrap();
4485 assert_eq!(
4486 full_result_cross, merged_result_cross,
4487 "Cross-fragment range query [{}, {}] failed",
4488 range_start, range_end
4489 );
4490
4491 let single_frag_start = 100i32;
4493 let single_frag_end = 200i32;
4494 let query_single_frag = SargableQuery::Range(
4495 std::collections::Bound::Included(ScalarValue::Int32(Some(single_frag_start))),
4496 std::collections::Bound::Excluded(ScalarValue::Int32(Some(single_frag_end))),
4497 );
4498 let full_result_single = full_index
4499 .search(&query_single_frag, &NoOpMetricsCollector)
4500 .await
4501 .unwrap();
4502 let merged_result_single = merged_index
4503 .search(&query_single_frag, &NoOpMetricsCollector)
4504 .await
4505 .unwrap();
4506 assert_eq!(
4507 full_result_single, merged_result_single,
4508 "Single fragment range query [{}, {}] failed",
4509 single_frag_start, single_frag_end
4510 );
4511
4512 let large_range_start = 100i32;
4514 let large_range_end = (3 * DEFAULT_BTREE_BATCH_SIZE - 100) as i32;
4515 let query_large_range = SargableQuery::Range(
4516 std::collections::Bound::Included(ScalarValue::Int32(Some(large_range_start))),
4517 std::collections::Bound::Excluded(ScalarValue::Int32(Some(large_range_end))),
4518 );
4519 let full_result_large = full_index
4520 .search(&query_large_range, &NoOpMetricsCollector)
4521 .await
4522 .unwrap();
4523 let merged_result_large = merged_index
4524 .search(&query_large_range, &NoOpMetricsCollector)
4525 .await
4526 .unwrap();
4527 assert_eq!(
4528 full_result_large, merged_result_large,
4529 "Large range query [{}, {}] failed",
4530 large_range_start, large_range_end
4531 );
4532
4533 let lt_val = (DEFAULT_BTREE_BATCH_SIZE / 2) as i32;
4537 let query_lt = SargableQuery::Range(
4538 std::collections::Bound::Included(ScalarValue::Int32(Some(0))),
4539 std::collections::Bound::Excluded(ScalarValue::Int32(Some(lt_val))),
4540 );
4541 let full_result_lt = full_index
4542 .search(&query_lt, &NoOpMetricsCollector)
4543 .await
4544 .unwrap();
4545 let merged_result_lt = merged_index
4546 .search(&query_lt, &NoOpMetricsCollector)
4547 .await
4548 .unwrap();
4549 assert_eq!(
4550 full_result_lt, merged_result_lt,
4551 "Less than query (<{}) failed",
4552 lt_val
4553 );
4554
4555 let gt_val = (2 * DEFAULT_BTREE_BATCH_SIZE) as i32;
4557 let max_range_val = (3 * DEFAULT_BTREE_BATCH_SIZE) as i32;
4558 let query_gt = SargableQuery::Range(
4559 std::collections::Bound::Excluded(ScalarValue::Int32(Some(gt_val))),
4560 std::collections::Bound::Excluded(ScalarValue::Int32(Some(max_range_val))),
4561 );
4562 let full_result_gt = full_index
4563 .search(&query_gt, &NoOpMetricsCollector)
4564 .await
4565 .unwrap();
4566 let merged_result_gt = merged_index
4567 .search(&query_gt, &NoOpMetricsCollector)
4568 .await
4569 .unwrap();
4570 assert_eq!(
4571 full_result_gt, merged_result_gt,
4572 "Greater than query (>{}) failed",
4573 gt_val
4574 );
4575
4576 let lte_val = (DEFAULT_BTREE_BATCH_SIZE - 1) as i32;
4578 let query_lte = SargableQuery::Range(
4579 std::collections::Bound::Included(ScalarValue::Int32(Some(0))),
4580 std::collections::Bound::Included(ScalarValue::Int32(Some(lte_val))),
4581 );
4582 let full_result_lte = full_index
4583 .search(&query_lte, &NoOpMetricsCollector)
4584 .await
4585 .unwrap();
4586 let merged_result_lte = merged_index
4587 .search(&query_lte, &NoOpMetricsCollector)
4588 .await
4589 .unwrap();
4590 assert_eq!(
4591 full_result_lte, merged_result_lte,
4592 "Less than or equal query (<={}) failed",
4593 lte_val
4594 );
4595
4596 let gte_val = (2 * DEFAULT_BTREE_BATCH_SIZE) as i32;
4598 let query_gte = SargableQuery::Range(
4599 std::collections::Bound::Included(ScalarValue::Int32(Some(gte_val))),
4600 std::collections::Bound::Excluded(ScalarValue::Int32(Some(max_range_val))),
4601 );
4602 let full_result_gte = full_index
4603 .search(&query_gte, &NoOpMetricsCollector)
4604 .await
4605 .unwrap();
4606 let merged_result_gte = merged_index
4607 .search(&query_gte, &NoOpMetricsCollector)
4608 .await
4609 .unwrap();
4610 assert_eq!(
4611 full_result_gte, merged_result_gte,
4612 "Greater than or equal query (>={}) failed",
4613 gte_val
4614 );
4615 }
4616
4617 #[test]
4618 fn test_extract_partition_id() {
4619 assert_eq!(
4621 super::extract_partition_id("part_123_page_data.lance").unwrap(),
4622 123
4623 );
4624 assert_eq!(
4625 super::extract_partition_id("part_456_page_lookup.lance").unwrap(),
4626 456
4627 );
4628 assert_eq!(
4629 super::extract_partition_id("part_4294967296_page_data.lance").unwrap(),
4630 4294967296
4631 );
4632
4633 assert!(super::extract_partition_id("invalid_filename.lance").is_err());
4635 assert!(super::extract_partition_id("part_abc_page_data.lance").is_err());
4636 assert!(super::extract_partition_id("part_123").is_err());
4637 assert!(super::extract_partition_id("part_").is_err());
4638 }
4639
4640 #[tokio::test]
4641 async fn test_cleanup_partition_files() {
4642 let tmpdir = TempObjDir::default();
4644 let test_store: Arc<dyn crate::scalar::IndexStore> = Arc::new(LanceIndexStore::new(
4645 Arc::new(ObjectStore::local()),
4646 tmpdir.clone(),
4647 Arc::new(LanceCache::no_cache()),
4648 ));
4649
4650 let lookup_files = vec![
4652 "part_123_page_lookup.lance".to_string(),
4653 "invalid_lookup_file.lance".to_string(),
4654 "part_456_page_lookup.lance".to_string(),
4655 ];
4656
4657 let page_files = vec![
4658 "part_123_page_data.lance".to_string(),
4659 "invalid_page_file.lance".to_string(),
4660 "part_456_page_data.lance".to_string(),
4661 ];
4662
4663 super::cleanup_partition_files(test_store.as_ref(), &lookup_files, &page_files).await;
4666 }
4667
4668 #[tokio::test]
4669 async fn test_btree_null_handling_in_queries() {
4670 let store = Arc::new(LanceIndexStore::new(
4671 Arc::new(ObjectStore::memory()),
4672 Path::default(),
4673 Arc::new(LanceCache::no_cache()),
4674 ));
4675
4676 let batch = record_batch!(
4679 ("value", Int32, [None, Some(0), Some(5)]),
4680 ("_rowid", UInt64, [0, 1, 2])
4681 )
4682 .unwrap();
4683 let stream = stream::once(futures::future::ok(batch.clone()));
4684 let stream = Box::pin(RecordBatchStreamAdapter::new(batch.schema(), stream));
4685
4686 super::train_btree_index(stream, store.as_ref(), 256, None, None)
4688 .await
4689 .unwrap();
4690
4691 let cache = LanceCache::with_capacity(1024 * 1024);
4692 let index = super::BTreeIndex::load(store.clone(), None, &cache)
4693 .await
4694 .unwrap();
4695
4696 let query = SargableQuery::Equals(ScalarValue::Int32(Some(5)));
4698 let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
4699
4700 match result {
4701 SearchResult::Exact(row_ids) => {
4702 let actual_rows: Vec<u64> = row_ids
4703 .true_rows()
4704 .row_addrs()
4705 .unwrap()
4706 .map(u64::from)
4707 .collect();
4708 assert_eq!(actual_rows, vec![2], "Should find row 2 where value == 5");
4709
4710 let null_row_ids = row_ids.null_rows();
4712 assert!(!null_row_ids.is_empty(), "null_row_ids should be non-empty");
4713 let null_rows: Vec<u64> =
4714 null_row_ids.row_addrs().unwrap().map(u64::from).collect();
4715 assert_eq!(null_rows, vec![0], "Should report row 0 as null");
4716 }
4717 _ => panic!("Expected Exact search result"),
4718 }
4719
4720 let query = SargableQuery::Range(
4722 std::ops::Bound::Included(ScalarValue::Int32(Some(0))),
4723 std::ops::Bound::Included(ScalarValue::Int32(Some(3))),
4724 );
4725 let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
4726
4727 match result {
4728 SearchResult::Exact(row_ids) => {
4729 let actual_rows: Vec<u64> = row_ids
4730 .true_rows()
4731 .row_addrs()
4732 .unwrap()
4733 .map(u64::from)
4734 .collect();
4735 assert_eq!(actual_rows, vec![1], "Should find row 1 where value == 0");
4736
4737 let null_row_ids = row_ids.null_rows();
4739 assert!(!null_row_ids.is_empty(), "null_row_ids should be non-empty");
4740 let null_rows: Vec<u64> =
4741 null_row_ids.row_addrs().unwrap().map(u64::from).collect();
4742 assert_eq!(null_rows, vec![0], "Should report row 0 as null");
4743 }
4744 _ => panic!("Expected Exact search result"),
4745 }
4746
4747 let query = SargableQuery::IsIn(vec![
4749 ScalarValue::Int32(Some(0)),
4750 ScalarValue::Int32(Some(5)),
4751 ]);
4752 let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
4753
4754 match result {
4755 SearchResult::Exact(row_ids) => {
4756 let mut actual_rows: Vec<u64> = row_ids
4757 .true_rows()
4758 .row_addrs()
4759 .unwrap()
4760 .map(u64::from)
4761 .collect();
4762 actual_rows.sort();
4763 assert_eq!(
4764 actual_rows,
4765 vec![1, 2],
4766 "Should find rows 1 and 2 where value in [0, 5]"
4767 );
4768
4769 let null_row_ids = row_ids.null_rows();
4771 assert!(!null_row_ids.is_empty(), "null_row_ids should be non-empty");
4772 let null_rows: Vec<u64> =
4773 null_row_ids.row_addrs().unwrap().map(u64::from).collect();
4774 assert_eq!(null_rows, vec![0], "Should report row 0 as null");
4775 }
4776 _ => panic!("Expected Exact search result"),
4777 }
4778 }
4779
4780 #[tokio::test]
4781 async fn test_range_btree_index_consistency() {
4782 let full_tmpdir = TempObjDir::default();
4784 let full_store = Arc::new(LanceIndexStore::new(
4785 Arc::new(ObjectStore::local()),
4786 full_tmpdir.clone(),
4787 Arc::new(LanceCache::no_cache()),
4788 ));
4789
4790 let range_tmpdir = TempObjDir::default();
4791 let range_store = Arc::new(LanceIndexStore::new(
4792 Arc::new(ObjectStore::local()),
4793 range_tmpdir.clone(),
4794 Arc::new(LanceCache::no_cache()),
4795 ));
4796
4797 let total_count = 4 * DEFAULT_BTREE_BATCH_SIZE;
4800 let full_data_gen = gen_batch()
4801 .col("value", array::step::<Int32Type>())
4802 .col("_rowid", array::step::<UInt64Type>())
4803 .into_df_stream(RowCount::from(total_count / 4), BatchCount::from(4));
4804 let full_data_source = Box::pin(RecordBatchStreamAdapter::new(
4805 full_data_gen.schema(),
4806 full_data_gen,
4807 ));
4808
4809 train_btree_index(
4810 full_data_source,
4811 full_store.as_ref(),
4812 DEFAULT_BTREE_BATCH_SIZE,
4813 None,
4814 None,
4815 )
4816 .await
4817 .unwrap();
4818
4819 let range1_gen = gen_batch()
4822 .col("value", array::step::<Int32Type>())
4823 .col("_rowid", array::step::<UInt64Type>())
4824 .into_df_stream(
4825 RowCount::from(DEFAULT_BTREE_BATCH_SIZE / 2),
4826 BatchCount::from(5),
4827 );
4828 let range1_data_source = Box::pin(RecordBatchStreamAdapter::new(
4829 range1_gen.schema(),
4830 range1_gen,
4831 ));
4832
4833 train_btree_index(
4834 range1_data_source,
4835 range_store.as_ref(),
4836 DEFAULT_BTREE_BATCH_SIZE,
4837 None,
4838 Option::from(0u32),
4839 )
4840 .await
4841 .unwrap();
4842
4843 let start_val = (DEFAULT_BTREE_BATCH_SIZE * 2 + DEFAULT_BTREE_BATCH_SIZE / 2) as i32;
4845 let end_val = (4 * DEFAULT_BTREE_BATCH_SIZE) as i32;
4846 let values_second_half: Vec<i32> = (start_val..end_val).collect();
4847 let row_ids_second_half: Vec<u64> = (start_val as u64..end_val as u64).collect();
4848 let range2_gen = gen_batch()
4849 .col("value", array::cycle::<Int32Type>(values_second_half))
4850 .col("_rowid", array::cycle::<UInt64Type>(row_ids_second_half))
4851 .into_df_stream(
4852 RowCount::from(DEFAULT_BTREE_BATCH_SIZE / 2),
4853 BatchCount::from(3),
4854 );
4855 let range2_data_source = Box::pin(RecordBatchStreamAdapter::new(
4856 range2_gen.schema(),
4857 range2_gen,
4858 ));
4859
4860 train_btree_index(
4861 range2_data_source,
4862 range_store.as_ref(),
4863 DEFAULT_BTREE_BATCH_SIZE,
4864 None,
4865 Option::from(1u32),
4866 )
4867 .await
4868 .unwrap();
4869
4870 let part_page_files = vec![
4872 part_page_data_file_path(0 << 32),
4873 part_page_data_file_path(1 << 32),
4874 ];
4875
4876 let part_lookup_files = vec![
4877 part_lookup_file_path(0 << 32),
4878 part_lookup_file_path(1 << 32),
4879 ];
4880
4881 super::merge_metadata_files(
4882 range_store.as_ref(),
4883 &part_page_files,
4884 &part_lookup_files,
4885 Option::from(1usize),
4886 noop_progress(),
4887 )
4888 .await
4889 .unwrap();
4890
4891 let full_index = BTreeIndex::load(full_store.clone(), None, &LanceCache::no_cache())
4892 .await
4893 .unwrap();
4894
4895 let ranged_index = BTreeIndex::load(range_store.clone(), None, &LanceCache::no_cache())
4896 .await
4897 .unwrap();
4898
4899 let query_0 = SargableQuery::Equals(ScalarValue::Int32(Some(0)));
4903 let full_result_0 = full_index
4904 .search(&query_0, &NoOpMetricsCollector)
4905 .await
4906 .unwrap();
4907 let ranged_result_0 = ranged_index
4908 .search(&query_0, &NoOpMetricsCollector)
4909 .await
4910 .unwrap();
4911 assert_eq!(full_result_0, ranged_result_0, "Query for value 0 failed");
4912
4913 let mid_first_batch = (DEFAULT_BTREE_BATCH_SIZE / 2) as i32;
4915 let query_mid_first = SargableQuery::Equals(ScalarValue::Int32(Some(mid_first_batch)));
4916 let full_result_mid_first = full_index
4917 .search(&query_mid_first, &NoOpMetricsCollector)
4918 .await
4919 .unwrap();
4920 let ranged_result_mid_first = ranged_index
4921 .search(&query_mid_first, &NoOpMetricsCollector)
4922 .await
4923 .unwrap();
4924 assert_eq!(
4925 full_result_mid_first, ranged_result_mid_first,
4926 "Query for value {} failed",
4927 mid_first_batch
4928 );
4929
4930 let mid_last_batch = (DEFAULT_BTREE_BATCH_SIZE * 3 + (DEFAULT_BTREE_BATCH_SIZE / 2)) as i32;
4932 let query_mid_last = SargableQuery::Equals(ScalarValue::Int32(Some(mid_last_batch)));
4933 let full_result_mid_last = full_index
4934 .search(&query_mid_last, &NoOpMetricsCollector)
4935 .await
4936 .unwrap();
4937 let ranged_result_mid_last = ranged_index
4938 .search(&query_mid_last, &NoOpMetricsCollector)
4939 .await
4940 .unwrap();
4941 assert_eq!(
4942 full_result_mid_last, ranged_result_mid_last,
4943 "Query for value {} failed",
4944 mid_last_batch
4945 );
4946
4947 let max_val = (4 * DEFAULT_BTREE_BATCH_SIZE - 1) as i32;
4949 let query_max = SargableQuery::Equals(ScalarValue::Int32(Some(max_val)));
4950 let full_result_max = full_index
4951 .search(&query_max, &NoOpMetricsCollector)
4952 .await
4953 .unwrap();
4954 let ranged_result_max = ranged_index
4955 .search(&query_max, &NoOpMetricsCollector)
4956 .await
4957 .unwrap();
4958 assert_eq!(
4959 full_result_max, ranged_result_max,
4960 "Query for maximum value {} failed",
4961 max_val
4962 );
4963
4964 let second_first_val = (DEFAULT_BTREE_BATCH_SIZE * 2 + DEFAULT_BTREE_BATCH_SIZE / 2) as i32;
4966 let query_second_first = SargableQuery::Equals(ScalarValue::Int32(Some(second_first_val)));
4967 let full_result_second_first = full_index
4968 .search(&query_second_first, &NoOpMetricsCollector)
4969 .await
4970 .unwrap();
4971 let ranged_result_second_first = ranged_index
4972 .search(&query_second_first, &NoOpMetricsCollector)
4973 .await
4974 .unwrap();
4975 assert_eq!(
4976 full_result_second_first, ranged_result_second_first,
4977 "Query for first value of the second page file {} failed",
4978 second_first_val
4979 );
4980
4981 let query_below_min = SargableQuery::Equals(ScalarValue::Int32(Some(-1)));
4983 let full_result_below = full_index
4984 .search(&query_below_min, &NoOpMetricsCollector)
4985 .await
4986 .unwrap();
4987 let ranged_result_below = ranged_index
4988 .search(&query_below_min, &NoOpMetricsCollector)
4989 .await
4990 .unwrap();
4991 assert_eq!(
4992 full_result_below, ranged_result_below,
4993 "Query for value below minimum (-1) failed"
4994 );
4995
4996 let query_above_max = SargableQuery::Equals(ScalarValue::Int32(Some(max_val + 1)));
4998 let full_result_above = full_index
4999 .search(&query_above_max, &NoOpMetricsCollector)
5000 .await
5001 .unwrap();
5002 let ranged_result_above = ranged_index
5003 .search(&query_above_max, &NoOpMetricsCollector)
5004 .await
5005 .unwrap();
5006 assert_eq!(
5007 full_result_above,
5008 ranged_result_above,
5009 "Query for value above maximum ({}) failed",
5010 max_val + 1
5011 );
5012
5013 let range_start =
5017 (DEFAULT_BTREE_BATCH_SIZE * 2 + DEFAULT_BTREE_BATCH_SIZE / 2 - 100) as i32;
5018 let range_end = range_start + 200;
5019 let query_cross_range = SargableQuery::Range(
5020 std::collections::Bound::Included(ScalarValue::Int32(Some(range_start))),
5021 std::collections::Bound::Excluded(ScalarValue::Int32(Some(range_end))),
5022 );
5023 let full_result_cross = full_index
5024 .search(&query_cross_range, &NoOpMetricsCollector)
5025 .await
5026 .unwrap();
5027 let ranged_result_cross = ranged_index
5028 .search(&query_cross_range, &NoOpMetricsCollector)
5029 .await
5030 .unwrap();
5031 assert_eq!(
5032 full_result_cross, ranged_result_cross,
5033 "Cross-range range query [{}, {}] failed",
5034 range_start, range_end
5035 );
5036
5037 let single_range_start = (DEFAULT_BTREE_BATCH_SIZE * 4 - 300) as i32;
5039 let single_range_end = single_range_start + 200;
5040 let query_single_range = SargableQuery::Range(
5041 std::collections::Bound::Included(ScalarValue::Int32(Some(single_range_start))),
5042 std::collections::Bound::Excluded(ScalarValue::Int32(Some(single_range_end))),
5043 );
5044 let full_result_single = full_index
5045 .search(&query_single_range, &NoOpMetricsCollector)
5046 .await
5047 .unwrap();
5048 let ranged_result_single = ranged_index
5049 .search(&query_single_range, &NoOpMetricsCollector)
5050 .await
5051 .unwrap();
5052 assert_eq!(
5053 full_result_single, ranged_result_single,
5054 "Single range query [{}, {}] failed",
5055 single_range_start, single_range_end
5056 );
5057
5058 let large_range_start = 100_i32;
5060 let large_range_end = (DEFAULT_BTREE_BATCH_SIZE * 4 - 100) as i32;
5061 let query_large_range = SargableQuery::Range(
5062 std::collections::Bound::Included(ScalarValue::Int32(Some(large_range_start))),
5063 std::collections::Bound::Excluded(ScalarValue::Int32(Some(large_range_end))),
5064 );
5065 let full_result_single = full_index
5066 .search(&query_large_range, &NoOpMetricsCollector)
5067 .await
5068 .unwrap();
5069 let ranged_result_single = ranged_index
5070 .search(&query_large_range, &NoOpMetricsCollector)
5071 .await
5072 .unwrap();
5073 assert_eq!(
5074 full_result_single, ranged_result_single,
5075 "Single fragment range query [{}, {}] failed",
5076 large_range_start, large_range_end
5077 );
5078
5079 let remap_dir = TempObjDir::default();
5080 let remap_store = Arc::new(LanceIndexStore::new(
5081 Arc::new(ObjectStore::local()),
5082 remap_dir.clone(),
5083 Arc::new(LanceCache::no_cache()),
5084 ));
5085
5086 ranged_index
5088 .remap(&RowAddrRemap::empty(), remap_store.as_ref())
5089 .await
5090 .unwrap();
5091
5092 let remap_index = BTreeIndex::load(remap_store.clone(), None, &LanceCache::no_cache())
5093 .await
5094 .unwrap();
5095
5096 assert_eq!(remap_index.page_lookup, ranged_index.page_lookup);
5097
5098 let ranged_pages = range_store
5099 .open_index_file(part_page_data_file_path(1 << 32).as_str())
5100 .await
5101 .unwrap();
5102 let remapped_pages = remap_store
5103 .open_index_file(part_page_data_file_path(1 << 32).as_str())
5104 .await
5105 .unwrap();
5106
5107 assert_eq!(ranged_pages.num_rows(), remapped_pages.num_rows());
5108
5109 let original_data = ranged_pages
5110 .read_record_batch(0, ranged_pages.num_rows() as u64)
5111 .await
5112 .unwrap();
5113 let remapped_data = remapped_pages
5114 .read_record_batch(0, remapped_pages.num_rows() as u64)
5115 .await
5116 .unwrap();
5117
5118 assert_eq!(original_data, remapped_data);
5119 }
5120
5121 #[tokio::test]
5122 async fn test_update_ranged_index() {
5123 let old_tmpdir = TempObjDir::default();
5125 let old_store = Arc::new(LanceIndexStore::new(
5126 Arc::new(ObjectStore::local()),
5127 old_tmpdir.clone(),
5128 Arc::new(LanceCache::no_cache()),
5129 ));
5130
5131 let new_tmpdir = TempObjDir::default();
5132 let new_store = Arc::new(LanceIndexStore::new(
5133 Arc::new(ObjectStore::local()),
5134 new_tmpdir.clone(),
5135 Arc::new(LanceCache::no_cache()),
5136 ));
5137
5138 let range1_gen = gen_batch()
5140 .col("value", array::step::<Int32Type>())
5141 .col("_rowid", array::step::<UInt64Type>())
5142 .into_df_stream(
5143 RowCount::from(DEFAULT_BTREE_BATCH_SIZE / 2),
5144 BatchCount::from(5),
5145 );
5146 let range1_data_source = Box::pin(RecordBatchStreamAdapter::new(
5147 range1_gen.schema(),
5148 range1_gen,
5149 ));
5150
5151 train_btree_index(
5152 range1_data_source,
5153 old_store.as_ref(),
5154 DEFAULT_BTREE_BATCH_SIZE,
5155 None,
5156 Option::from(1u32),
5157 )
5158 .await
5159 .unwrap();
5160
5161 let start_val = (DEFAULT_BTREE_BATCH_SIZE * 2 + DEFAULT_BTREE_BATCH_SIZE / 2) as i32;
5163 let end_val = (4 * DEFAULT_BTREE_BATCH_SIZE) as i32;
5164 let values_second_half: Vec<i32> = (start_val..end_val).collect();
5165 let row_ids_second_half: Vec<u64> = (start_val as u64..end_val as u64).collect();
5166 let range2_gen = gen_batch()
5167 .col("value", array::cycle::<Int32Type>(values_second_half))
5168 .col("_rowid", array::cycle::<UInt64Type>(row_ids_second_half))
5169 .into_df_stream(
5170 RowCount::from(DEFAULT_BTREE_BATCH_SIZE / 2),
5171 BatchCount::from(3),
5172 );
5173 let range2_data_source = Box::pin(RecordBatchStreamAdapter::new(
5174 range2_gen.schema(),
5175 range2_gen,
5176 ));
5177
5178 train_btree_index(
5179 range2_data_source,
5180 old_store.as_ref(),
5181 DEFAULT_BTREE_BATCH_SIZE,
5182 None,
5183 Option::from(2u32),
5184 )
5185 .await
5186 .unwrap();
5187
5188 let part_page_files = vec![
5190 part_page_data_file_path(1 << 32),
5191 part_page_data_file_path(2 << 32),
5192 ];
5193
5194 let part_lookup_files = vec![
5195 part_lookup_file_path(1 << 32),
5196 part_lookup_file_path(2 << 32),
5197 ];
5198
5199 super::merge_metadata_files(
5200 old_store.as_ref(),
5201 &part_page_files,
5202 &part_lookup_files,
5203 Option::from(1usize),
5204 noop_progress(),
5205 )
5206 .await
5207 .unwrap();
5208
5209 let start_val = (DEFAULT_BTREE_BATCH_SIZE * 2) as i32;
5211 let end_val = (DEFAULT_BTREE_BATCH_SIZE * 3) as i32;
5212 let row_id_delta = (DEFAULT_BTREE_BATCH_SIZE * 3) as i32;
5213 let values: Vec<i32> = (start_val..end_val).collect();
5214 let row_ids: Vec<u64> =
5215 ((start_val + row_id_delta) as u64..(end_val + row_id_delta) as u64).collect();
5216 let update_data = gen_batch()
5217 .col("value", array::cycle::<Int32Type>(values))
5218 .col("_rowid", array::cycle::<UInt64Type>(row_ids))
5219 .into_df_stream(
5220 RowCount::from(DEFAULT_BTREE_BATCH_SIZE / 2),
5221 BatchCount::from(2),
5222 );
5223 let update_data_source = Box::pin(RecordBatchStreamAdapter::new(
5224 update_data.schema(),
5225 update_data,
5226 ));
5227
5228 let ranged_index = BTreeIndex::load(old_store.clone(), None, &LanceCache::no_cache())
5229 .await
5230 .unwrap();
5231
5232 ranged_index
5234 .update(update_data_source, new_store.as_ref(), None)
5235 .await
5236 .expect("Error in updating ranged index");
5237
5238 let updated_index = BTreeIndex::load(new_store.clone(), None, &LanceCache::no_cache())
5239 .await
5240 .unwrap();
5241
5242 assert!(
5243 updated_index.ranges_to_files.is_none(),
5244 "Updated ranged-btree-index should fall back to non-ranged"
5245 );
5246
5247 let updated_value = (DEFAULT_BTREE_BATCH_SIZE * 2 + (DEFAULT_BTREE_BATCH_SIZE / 2)) as i32;
5248 let updated_query = SargableQuery::Equals(ScalarValue::Int32(Some(updated_value)));
5249
5250 let query_result = updated_index
5251 .search(&updated_query, &NoOpMetricsCollector)
5252 .await
5253 .unwrap();
5254 match query_result {
5255 SearchResult::Exact(row_id_map) => {
5256 assert!(
5257 row_id_map.selected(updated_value as u64),
5258 "Updated index should contain original rowids."
5259 );
5260 assert!(
5261 row_id_map.selected((updated_value + row_id_delta) as u64),
5262 "Updated index should contain new rowids"
5263 );
5264 }
5265 _ => {
5266 panic!("Btree search result should always be Exact.");
5267 }
5268 }
5269 }
5270
5271 #[tokio::test]
5272 async fn test_update_with_exact_row_id_filter() {
5273 let old_tmpdir = TempObjDir::default();
5274 let old_store = Arc::new(LanceIndexStore::new(
5275 Arc::new(ObjectStore::local()),
5276 old_tmpdir.clone(),
5277 Arc::new(LanceCache::no_cache()),
5278 ));
5279
5280 let new_tmpdir = TempObjDir::default();
5281 let new_store = Arc::new(LanceIndexStore::new(
5282 Arc::new(ObjectStore::local()),
5283 new_tmpdir.clone(),
5284 Arc::new(LanceCache::no_cache()),
5285 ));
5286
5287 let old_data = gen_batch()
5288 .col("value", array::step::<Int32Type>())
5289 .col("_rowid", array::step::<UInt64Type>())
5290 .into_df_stream(RowCount::from(512), BatchCount::from(2));
5291 let old_data_source = Box::pin(RecordBatchStreamAdapter::new(old_data.schema(), old_data));
5292 train_btree_index(
5293 old_data_source,
5294 old_store.as_ref(),
5295 DEFAULT_BTREE_BATCH_SIZE,
5296 None,
5297 None,
5298 )
5299 .await
5300 .unwrap();
5301
5302 let index = BTreeIndex::load(old_store.clone(), None, &LanceCache::no_cache())
5303 .await
5304 .unwrap();
5305
5306 let new_data = gen_batch()
5307 .col("value", array::step_custom::<Int32Type>(2000, 1))
5308 .col("_rowid", array::step_custom::<UInt64Type>(2000, 1))
5309 .into_df_stream(RowCount::from(100), BatchCount::from(1));
5310 let new_data_source = Box::pin(RecordBatchStreamAdapter::new(new_data.schema(), new_data));
5311
5312 let mut retained_old_rows = RowAddrTreeMap::new();
5313 retained_old_rows.insert_range(0..64);
5314 retained_old_rows.insert_range(300..364);
5315
5316 index
5317 .update(
5318 new_data_source,
5319 new_store.as_ref(),
5320 Some(OldIndexDataFilter::RowIds(retained_old_rows)),
5321 )
5322 .await
5323 .unwrap();
5324
5325 let updated_index = BTreeIndex::load(new_store.clone(), None, &LanceCache::no_cache())
5326 .await
5327 .unwrap();
5328
5329 let present = |value: i32| {
5330 let updated_index = updated_index.clone();
5331 async move {
5332 let query = SargableQuery::Equals(ScalarValue::Int32(Some(value)));
5333 match updated_index
5334 .search(&query, &NoOpMetricsCollector)
5335 .await
5336 .unwrap()
5337 {
5338 SearchResult::Exact(row_id_map) => row_id_map.selected(value as u64),
5339 _ => unreachable!("Btree search result should always be Exact"),
5340 }
5341 }
5342 };
5343
5344 assert!(present(12).await);
5345 assert!(present(320).await);
5346 assert!(!present(120).await);
5347 assert!(!present(420).await);
5348 assert!(present(2005).await);
5349 }
5350
5351 #[tokio::test]
5361 async fn test_btree_remap_big_deletions() {
5362 let tmpdir = TempObjDir::default();
5363 let test_store = Arc::new(LanceIndexStore::new(
5364 Arc::new(ObjectStore::local()),
5365 tmpdir.clone(),
5366 Arc::new(LanceCache::no_cache()),
5367 ));
5368
5369 let batch_size = 4096;
5372 let total_rows = 15000;
5373
5374 let stream = gen_batch()
5375 .col("value", array::step::<Int32Type>())
5376 .col("_rowid", array::step::<UInt64Type>())
5377 .into_df_stream(RowCount::from(total_rows), BatchCount::from(1));
5378
5379 train_btree_index(stream, test_store.as_ref(), batch_size, None, None)
5380 .await
5381 .unwrap();
5382
5383 let index = BTreeIndex::load(test_store.clone(), None, &LanceCache::no_cache())
5384 .await
5385 .unwrap();
5386
5387 let mut mapping: HashMap<u64, Option<u64>> = HashMap::new();
5393
5394 for old_id in 1001..10000 {
5396 mapping.insert(old_id, None);
5397 }
5398
5399 for (new_id, old_id) in (100_000..).zip((0..1000).chain(10000..15000)) {
5401 mapping.insert(old_id, Some(new_id));
5402 }
5403
5404 let remap_dir = TempObjDir::default();
5405 let remap_store = Arc::new(LanceIndexStore::new(
5406 Arc::new(ObjectStore::local()),
5407 remap_dir.clone(),
5408 Arc::new(LanceCache::no_cache()),
5409 ));
5410
5411 index
5413 .remap(&RowAddrRemap::direct(mapping), remap_store.as_ref())
5414 .await
5415 .unwrap();
5416
5417 let remapped_index = BTreeIndex::load(remap_store.clone(), None, &LanceCache::no_cache())
5418 .await
5419 .unwrap();
5420
5421 let should_exist = vec![0, 500, 1000, 10000, 13000, 14000, 14999];
5425 for value in should_exist {
5426 let query = SargableQuery::Equals(ScalarValue::Int32(Some(value)));
5427 let result = remapped_index
5428 .search(&query, &NoOpMetricsCollector)
5429 .await
5430 .unwrap();
5431 match result {
5432 SearchResult::Exact(row_id_map) => {
5433 assert!(
5434 !row_id_map.is_empty(),
5435 "Value {} should exist in remapped index but was not found",
5436 value
5437 );
5438 }
5439 _ => {
5440 panic!("Btree search result should always be Exact.");
5441 }
5442 }
5443 }
5444
5445 let should_not_exist = vec![1001, 5000, 8000, 9999];
5447 for value in should_not_exist {
5448 let query = SargableQuery::Equals(ScalarValue::Int32(Some(value)));
5449 let result = remapped_index
5450 .search(&query, &NoOpMetricsCollector)
5451 .await
5452 .unwrap();
5453 match result {
5454 SearchResult::Exact(row_id_map) => {
5455 assert!(
5456 row_id_map.is_empty(),
5457 "Value {} should NOT exist in remapped index but was found",
5458 value
5459 );
5460 }
5461 _ => {
5462 panic!("Btree search result should always be Exact.");
5463 }
5464 }
5465 }
5466 }
5467
5468 #[tokio::test]
5474 async fn test_search_tracks_nulls_for_absent_value() {
5475 use arrow_array::{Int32Array, UInt64Array};
5476
5477 let tmpdir = TempObjDir::default();
5478 let test_store = Arc::new(LanceIndexStore::new(
5479 Arc::new(ObjectStore::local()),
5480 tmpdir.clone(),
5481 Arc::new(LanceCache::no_cache()),
5482 ));
5483
5484 let num_rows = 5000u64;
5488 let values: Int32Array = (0..num_rows)
5489 .map(|i| {
5490 if i % 5 != 0 {
5491 None } else {
5493 Some(100 + i as i32) }
5495 })
5496 .collect();
5497 let row_ids = UInt64Array::from_iter_values(0..num_rows);
5498 let data = arrow_array::RecordBatch::try_from_iter(vec![
5499 ("value", Arc::new(values) as arrow_array::ArrayRef),
5500 ("_rowid", Arc::new(row_ids) as arrow_array::ArrayRef),
5501 ])
5502 .unwrap();
5503
5504 let schema = data.schema();
5505 let stream: SendableRecordBatchStream = Box::pin(RecordBatchStreamAdapter::new(
5506 schema,
5507 stream::iter(vec![Ok(data)]),
5508 ));
5509 train_btree_index(stream, test_store.as_ref(), num_rows, None, None)
5510 .await
5511 .unwrap();
5512
5513 let index = BTreeIndex::load(test_store.clone(), None, &LanceCache::no_cache())
5514 .await
5515 .unwrap();
5516
5517 assert!(
5519 !index.page_lookup.all_null_pages.is_empty(),
5520 "Test setup requires all-null pages; got null_pages={}, all_null_pages={}",
5521 index.page_lookup.null_pages.len(),
5522 index.page_lookup.all_null_pages.len(),
5523 );
5524
5525 let metrics = NoOpMetricsCollector;
5526
5527 let result = index
5529 .search(
5530 &SargableQuery::Equals(ScalarValue::Int32(Some(0))),
5531 &metrics,
5532 )
5533 .await
5534 .unwrap();
5535
5536 match result {
5537 SearchResult::Exact(set) => {
5538 assert!(set.true_rows().is_empty(), "No rows should match Equals(0)");
5540 assert!(
5542 !set.null_rows().is_empty(),
5543 "Null rows must be tracked even when no pages match the value"
5544 );
5545 }
5546 _ => panic!("BTree search should return Exact"),
5547 }
5548
5549 let result = index
5551 .search(
5552 &SargableQuery::Range(
5553 std::ops::Bound::Unbounded,
5554 std::ops::Bound::Excluded(ScalarValue::Int32(Some(50))),
5555 ),
5556 &metrics,
5557 )
5558 .await
5559 .unwrap();
5560
5561 match result {
5562 SearchResult::Exact(set) => {
5563 assert!(set.true_rows().is_empty(), "No rows should be < 50");
5564 assert!(
5565 !set.null_rows().is_empty(),
5566 "Null rows must be tracked for range queries too"
5567 );
5568 }
5569 _ => panic!("BTree search should return Exact"),
5570 }
5571 }
5572
5573 fn sample_lookup_batch() -> RecordBatch {
5574 record_batch!(
5575 ("min", Int32, [Some(0), Some(10), Some(20)]),
5576 ("max", Int32, [Some(9), Some(19), Some(29)]),
5577 ("null_count", UInt32, [0, 2, 0]),
5578 ("page_idx", UInt32, [0, 1, 2])
5579 )
5580 .unwrap()
5581 }
5582
5583 fn osv(v: i32) -> OrderableScalarValue {
5584 OrderableScalarValue(ScalarValue::Int32(Some(v)))
5585 }
5586
5587 #[test]
5591 fn test_btree_lookup_pages_between() {
5592 let batch = record_batch!(
5595 ("min", Int32, [None, Some(10), Some(20), Some(20), Some(40)]),
5596 (
5597 "max",
5598 Int32,
5599 [Some(5), Some(20), Some(20), Some(30), Some(50)]
5600 ),
5601 ("null_count", UInt32, [2, 0, 0, 0, 0]),
5602 ("page_idx", UInt32, [0, 1, 2, 3, 4])
5603 )
5604 .unwrap();
5605 let lookup = BTreeLookup::try_new(batch).unwrap();
5606 assert_eq!(lookup.null_pages, vec![0]);
5607 assert!(lookup.all_null_pages.is_empty());
5608 assert_eq!(lookup.search_start, 0);
5609
5610 let between = |lo: i32, hi: i32| {
5611 let mut m = lookup
5612 .pages_between((
5613 std::ops::Bound::Included(&osv(lo)),
5614 std::ops::Bound::Included(&osv(hi)),
5615 ))
5616 .unwrap();
5617 m.sort_by_key(|m| m.page_id());
5618 m
5619 };
5620
5621 assert_eq!(lookup.pages_eq(&osv(15)).unwrap(), vec![Matches::Some(1)]);
5623 assert_eq!(
5624 lookup.pages_eq(&osv(20)).unwrap(),
5625 vec![Matches::Some(1), Matches::Some(2), Matches::Some(3)]
5626 );
5627 assert!(lookup.pages_eq(&osv(35)).unwrap().is_empty());
5628
5629 assert_eq!(
5632 between(20, 25),
5633 vec![Matches::Some(1), Matches::All(2), Matches::Some(3)]
5634 );
5635
5636 assert_eq!(between(0, 5), vec![Matches::Some(0)]);
5639
5640 assert_eq!(
5642 lookup
5643 .pages_between((
5644 std::ops::Bound::Included(&osv(40)),
5645 std::ops::Bound::Unbounded
5646 ))
5647 .unwrap(),
5648 vec![Matches::All(4)]
5649 );
5650
5651 assert!(between(31, 39).is_empty());
5653 assert!(
5654 lookup
5655 .pages_between((
5656 std::ops::Bound::Included(&osv(25)),
5657 std::ops::Bound::Included(&osv(15))
5658 ))
5659 .unwrap()
5660 .is_empty()
5661 );
5662 }
5663
5664 #[test]
5668 fn test_btree_lookup_pages_eq_bytes() {
5669 use arrow_array::{
5670 ArrayRef, BinaryArray, FixedSizeBinaryArray, LargeBinaryArray, LargeStringArray,
5671 UInt32Array,
5672 };
5673 use arrow_schema::{DataType, Field, Schema};
5674
5675 fn be(v: u16) -> [u8; 2] {
5679 v.to_be_bytes()
5680 }
5681 let mins = [None, Some(10u16), Some(20), Some(20), Some(40)];
5682 let maxs = [Some(5u16), Some(20), Some(20), Some(30), Some(50)];
5683 let null_count = UInt32Array::from(vec![2u32, 0, 0, 0, 0]);
5684 let page_idx = UInt32Array::from(vec![0u32, 1, 2, 3, 4]);
5685
5686 let assert_byte_lookup =
5687 |min_arr: ArrayRef, max_arr: ArrayRef, sv: &dyn Fn(u16) -> ScalarValue| {
5688 let batch = RecordBatch::try_new(
5689 Arc::new(Schema::new(vec![
5690 Field::new("min", min_arr.data_type().clone(), true),
5691 Field::new("max", max_arr.data_type().clone(), true),
5692 Field::new("null_count", DataType::UInt32, false),
5693 Field::new("page_idx", DataType::UInt32, false),
5694 ])),
5695 vec![
5696 min_arr,
5697 max_arr,
5698 Arc::new(null_count.clone()),
5699 Arc::new(page_idx.clone()),
5700 ],
5701 )
5702 .unwrap();
5703 let lookup = BTreeLookup::try_new(batch).unwrap();
5704
5705 let eq = |v: u16| {
5706 let mut p: Vec<u32> = lookup
5707 .pages_eq(&OrderableScalarValue(sv(v)))
5708 .unwrap()
5709 .into_iter()
5710 .map(|m| m.page_id())
5711 .collect();
5712 p.sort_unstable();
5713 p
5714 };
5715 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
5722 .pages_in([5u16, 15].into_iter().map(|v| OrderableScalarValue(sv(v))))
5723 .unwrap()
5724 .into_iter()
5725 .map(|m| m.page_id())
5726 .collect();
5727 in_pages.sort_unstable();
5728 assert_eq!(in_pages, vec![0, 1]);
5729 };
5730
5731 let fsb = |arr: &[Option<u16>]| -> ArrayRef {
5732 Arc::new(
5733 FixedSizeBinaryArray::try_from_sparse_iter_with_size(
5734 arr.iter().copied().map(|o| o.map(be)),
5735 2,
5736 )
5737 .unwrap(),
5738 )
5739 };
5740 assert_byte_lookup(fsb(&mins), fsb(&maxs), &|v| {
5741 ScalarValue::FixedSizeBinary(2, Some(be(v).to_vec()))
5742 });
5743
5744 let bin = |arr: &[Option<u16>]| -> ArrayRef {
5745 Arc::new(BinaryArray::from_iter(
5746 arr.iter().copied().map(|o| o.map(|v| be(v).to_vec())),
5747 ))
5748 };
5749 assert_byte_lookup(bin(&mins), bin(&maxs), &|v| {
5750 ScalarValue::Binary(Some(be(v).to_vec()))
5751 });
5752
5753 let lbin = |arr: &[Option<u16>]| -> ArrayRef {
5754 Arc::new(LargeBinaryArray::from_iter(
5755 arr.iter().copied().map(|o| o.map(|v| be(v).to_vec())),
5756 ))
5757 };
5758 assert_byte_lookup(lbin(&mins), lbin(&maxs), &|v| {
5759 ScalarValue::LargeBinary(Some(be(v).to_vec()))
5760 });
5761
5762 let lstr = |arr: &[Option<u16>]| -> ArrayRef {
5765 Arc::new(LargeStringArray::from_iter(
5766 arr.iter().copied().map(|o| o.map(|v| format!("{v:02}"))),
5767 ))
5768 };
5769 assert_byte_lookup(lstr(&mins), lstr(&maxs), &|v| {
5770 ScalarValue::LargeUtf8(Some(format!("{v:02}")))
5771 });
5772 }
5773
5774 #[test]
5778 fn test_btree_lookup_pages_eq_temporal() {
5779 use arrow_array::{ArrayRef, Date32Array, TimestampMicrosecondArray, UInt32Array};
5780 use arrow_schema::{DataType, Field, Schema};
5781
5782 let null_count = UInt32Array::from(vec![2u32, 0, 0, 0, 0]);
5783 let page_idx = UInt32Array::from(vec![0u32, 1, 2, 3, 4]);
5784
5785 let assert_lookup =
5786 |min_arr: ArrayRef, max_arr: ArrayRef, sv: &dyn Fn(i64) -> ScalarValue| {
5787 let batch = RecordBatch::try_new(
5788 Arc::new(Schema::new(vec![
5789 Field::new("min", min_arr.data_type().clone(), true),
5790 Field::new("max", max_arr.data_type().clone(), true),
5791 Field::new("null_count", DataType::UInt32, false),
5792 Field::new("page_idx", DataType::UInt32, false),
5793 ])),
5794 vec![
5795 min_arr,
5796 max_arr,
5797 Arc::new(null_count.clone()),
5798 Arc::new(page_idx.clone()),
5799 ],
5800 )
5801 .unwrap();
5802 let lookup = BTreeLookup::try_new(batch).unwrap();
5803 let eq = |v: i64| {
5804 let mut p: Vec<u32> = lookup
5805 .pages_eq(&OrderableScalarValue(sv(v)))
5806 .unwrap()
5807 .into_iter()
5808 .map(|m| m.page_id())
5809 .collect();
5810 p.sort_unstable();
5811 p
5812 };
5813 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]); };
5818
5819 assert_lookup(
5821 Arc::new(TimestampMicrosecondArray::from(vec![
5822 None,
5823 Some(10),
5824 Some(20),
5825 Some(20),
5826 Some(40),
5827 ])),
5828 Arc::new(TimestampMicrosecondArray::from(vec![
5829 Some(5),
5830 Some(20),
5831 Some(20),
5832 Some(30),
5833 Some(50),
5834 ])),
5835 &|v| ScalarValue::TimestampMicrosecond(Some(v), None),
5836 );
5837
5838 assert_lookup(
5840 Arc::new(Date32Array::from(vec![
5841 None,
5842 Some(10),
5843 Some(20),
5844 Some(20),
5845 Some(40),
5846 ])),
5847 Arc::new(Date32Array::from(vec![
5848 Some(5),
5849 Some(20),
5850 Some(20),
5851 Some(30),
5852 Some(50),
5853 ])),
5854 &|v| ScalarValue::Date32(Some(v as i32)),
5855 );
5856 }
5857
5858 #[test]
5864 fn test_btree_lookup_pages_eq_numeric_widths() {
5865 use arrow::datatypes::i256;
5866 use arrow_array::{
5867 ArrayRef, Decimal128Array, Decimal256Array, Float16Array, Int8Array, Int16Array,
5868 UInt8Array, UInt16Array, UInt32Array, UInt64Array,
5869 };
5870 use arrow_schema::{DataType, Field, Schema};
5871 use half::f16;
5872
5873 let null_count = UInt32Array::from(vec![2u32, 0, 0, 0, 0]);
5874 let page_idx = UInt32Array::from(vec![0u32, 1, 2, 3, 4]);
5875 let assert_lookup =
5876 |min_arr: ArrayRef, max_arr: ArrayRef, sv: &dyn Fn(i64) -> ScalarValue| {
5877 let batch = RecordBatch::try_new(
5878 Arc::new(Schema::new(vec![
5879 Field::new("min", min_arr.data_type().clone(), true),
5880 Field::new("max", max_arr.data_type().clone(), true),
5881 Field::new("null_count", DataType::UInt32, false),
5882 Field::new("page_idx", DataType::UInt32, false),
5883 ])),
5884 vec![
5885 min_arr,
5886 max_arr,
5887 Arc::new(null_count.clone()),
5888 Arc::new(page_idx.clone()),
5889 ],
5890 )
5891 .unwrap();
5892 let lookup = BTreeLookup::try_new(batch).unwrap();
5893 let eq = |v: i64| {
5894 let mut p: Vec<u32> = lookup
5895 .pages_eq(&OrderableScalarValue(sv(v)))
5896 .unwrap()
5897 .into_iter()
5898 .map(|m| m.page_id())
5899 .collect();
5900 p.sort_unstable();
5901 p
5902 };
5903 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]); };
5908
5909 assert_lookup(
5910 Arc::new(Int8Array::from(vec![
5911 None,
5912 Some(10),
5913 Some(20),
5914 Some(20),
5915 Some(40),
5916 ])),
5917 Arc::new(Int8Array::from(vec![
5918 Some(5),
5919 Some(20),
5920 Some(20),
5921 Some(30),
5922 Some(50),
5923 ])),
5924 &|v| ScalarValue::Int8(Some(v as i8)),
5925 );
5926 assert_lookup(
5927 Arc::new(Int16Array::from(vec![
5928 None,
5929 Some(10),
5930 Some(20),
5931 Some(20),
5932 Some(40),
5933 ])),
5934 Arc::new(Int16Array::from(vec![
5935 Some(5),
5936 Some(20),
5937 Some(20),
5938 Some(30),
5939 Some(50),
5940 ])),
5941 &|v| ScalarValue::Int16(Some(v as i16)),
5942 );
5943 assert_lookup(
5944 Arc::new(UInt8Array::from(vec![
5945 None,
5946 Some(10),
5947 Some(20),
5948 Some(20),
5949 Some(40),
5950 ])),
5951 Arc::new(UInt8Array::from(vec![
5952 Some(5),
5953 Some(20),
5954 Some(20),
5955 Some(30),
5956 Some(50),
5957 ])),
5958 &|v| ScalarValue::UInt8(Some(v as u8)),
5959 );
5960 assert_lookup(
5961 Arc::new(UInt16Array::from(vec![
5962 None,
5963 Some(10),
5964 Some(20),
5965 Some(20),
5966 Some(40),
5967 ])),
5968 Arc::new(UInt16Array::from(vec![
5969 Some(5),
5970 Some(20),
5971 Some(20),
5972 Some(30),
5973 Some(50),
5974 ])),
5975 &|v| ScalarValue::UInt16(Some(v as u16)),
5976 );
5977 assert_lookup(
5978 Arc::new(UInt32Array::from(vec![
5979 None,
5980 Some(10),
5981 Some(20),
5982 Some(20),
5983 Some(40),
5984 ])),
5985 Arc::new(UInt32Array::from(vec![
5986 Some(5),
5987 Some(20),
5988 Some(20),
5989 Some(30),
5990 Some(50),
5991 ])),
5992 &|v| ScalarValue::UInt32(Some(v as u32)),
5993 );
5994 assert_lookup(
5995 Arc::new(UInt64Array::from(vec![
5996 None,
5997 Some(10),
5998 Some(20),
5999 Some(20),
6000 Some(40),
6001 ])),
6002 Arc::new(UInt64Array::from(vec![
6003 Some(5),
6004 Some(20),
6005 Some(20),
6006 Some(30),
6007 Some(50),
6008 ])),
6009 &|v| ScalarValue::UInt64(Some(v as u64)),
6010 );
6011
6012 let f = |v: f64| f16::from_f64(v);
6013 assert_lookup(
6014 Arc::new(Float16Array::from(vec![
6015 None,
6016 Some(f(10.0)),
6017 Some(f(20.0)),
6018 Some(f(20.0)),
6019 Some(f(40.0)),
6020 ])),
6021 Arc::new(Float16Array::from(vec![
6022 Some(f(5.0)),
6023 Some(f(20.0)),
6024 Some(f(20.0)),
6025 Some(f(30.0)),
6026 Some(f(50.0)),
6027 ])),
6028 &|v| ScalarValue::Float16(Some(f(v as f64))),
6029 );
6030
6031 let dec128 = |vals: Vec<Option<i128>>| -> ArrayRef {
6034 Arc::new(
6035 Decimal128Array::from(vals)
6036 .with_precision_and_scale(18, 0)
6037 .unwrap(),
6038 )
6039 };
6040 assert_lookup(
6041 dec128(vec![None, Some(10), Some(20), Some(20), Some(40)]),
6042 dec128(vec![Some(5), Some(20), Some(20), Some(30), Some(50)]),
6043 &|v| ScalarValue::Decimal128(Some(v as i128), 18, 0),
6044 );
6045
6046 let dec256 = |vals: Vec<Option<i128>>| -> ArrayRef {
6048 Arc::new(
6049 Decimal256Array::from(
6050 vals.into_iter()
6051 .map(|o| o.map(i256::from_i128))
6052 .collect::<Vec<_>>(),
6053 )
6054 .with_precision_and_scale(40, 0)
6055 .unwrap(),
6056 )
6057 };
6058 assert_lookup(
6059 dec256(vec![None, Some(10), Some(20), Some(20), Some(40)]),
6060 dec256(vec![Some(5), Some(20), Some(20), Some(30), Some(50)]),
6061 &|v| ScalarValue::Decimal256(Some(i256::from_i128(v as i128)), 40, 0),
6062 );
6063 }
6064
6065 #[test]
6069 fn test_btree_lookup_pages_null() {
6070 let batch = record_batch!(
6073 ("min", Int32, [None, None, Some(10), Some(20), Some(40)]),
6074 ("max", Int32, [None, Some(5), Some(20), Some(30), Some(50)]),
6075 ("null_count", UInt32, [3, 2, 1, 0, 0]),
6076 ("page_idx", UInt32, [0, 1, 2, 3, 4])
6077 )
6078 .unwrap();
6079 let lookup = BTreeLookup::try_new(batch).unwrap();
6080 assert_eq!(lookup.all_null_pages, vec![0]);
6081 assert_eq!(lookup.null_pages, vec![1, 2]);
6082
6083 assert_eq!(
6086 lookup
6087 .pages_eq(&OrderableScalarValue(ScalarValue::Int32(None)))
6088 .unwrap(),
6089 vec![Matches::Some(1), Matches::Some(2), Matches::All(0)]
6090 );
6091
6092 let in_ids = |vals: Vec<Option<i32>>| {
6093 let mut p: Vec<u32> = lookup
6094 .pages_in(
6095 vals.into_iter()
6096 .map(|v| OrderableScalarValue(ScalarValue::Int32(v))),
6097 )
6098 .unwrap()
6099 .into_iter()
6100 .map(|m| m.page_id())
6101 .collect();
6102 p.sort_unstable();
6103 p
6104 };
6105 assert_eq!(in_ids(vec![Some(45)]), vec![4]);
6107 assert_eq!(in_ids(vec![Some(45), None]), vec![0, 1, 2, 4]);
6109 assert_eq!(in_ids(vec![None]), vec![0, 1, 2]);
6111 }
6112
6113 #[test]
6116 fn test_btree_lookup_empty_batch() {
6117 use arrow_schema::{DataType, Field, Schema};
6118
6119 let schema = Arc::new(Schema::new(vec![
6120 Field::new("min", DataType::Int32, true),
6121 Field::new("max", DataType::Int32, true),
6122 Field::new("null_count", DataType::UInt32, false),
6123 Field::new("page_idx", DataType::UInt32, false),
6124 ]));
6125 let lookup = BTreeLookup::try_new(RecordBatch::new_empty(schema)).unwrap();
6126 assert_eq!(lookup.search_start, 0);
6127 assert!(lookup.null_pages.is_empty());
6128 assert!(lookup.all_null_pages.is_empty());
6129
6130 assert!(lookup.pages_eq(&osv(5)).unwrap().is_empty());
6131 assert!(lookup.pages_in([osv(5)]).unwrap().is_empty());
6132 assert!(
6133 lookup
6134 .pages_between((
6135 std::ops::Bound::Included(&osv(0)),
6136 std::ops::Bound::Included(&osv(100)),
6137 ))
6138 .unwrap()
6139 .is_empty()
6140 );
6141 assert!(lookup.pages_null().is_empty());
6142 }
6143
6144 #[test]
6150 fn test_btree_lookup_skips_all_null_page_in_scan_window() {
6151 let batch = record_batch!(
6153 ("min", Int32, [None, None, Some(10), Some(20), Some(40)]),
6154 ("max", Int32, [Some(5), None, Some(20), Some(30), Some(50)]),
6155 ("null_count", UInt32, [2, 3, 0, 0, 0]),
6156 ("page_idx", UInt32, [0, 1, 2, 3, 4])
6157 )
6158 .unwrap();
6159 let lookup = BTreeLookup::try_new(batch).unwrap();
6160 assert_eq!(lookup.search_start, 0); assert_eq!(lookup.all_null_pages, vec![1]);
6162 assert_eq!(lookup.null_pages, vec![0]);
6163
6164 assert_eq!(
6167 lookup
6168 .pages_eq(&osv(5))
6169 .unwrap()
6170 .into_iter()
6171 .map(|m| m.page_id())
6172 .collect::<Vec<_>>(),
6173 vec![0]
6174 );
6175
6176 let mut between = lookup
6179 .pages_between((
6180 std::ops::Bound::Included(&osv(0)),
6181 std::ops::Bound::Included(&osv(100)),
6182 ))
6183 .unwrap();
6184 between.sort_by_key(|m| m.page_id());
6185 assert_eq!(
6186 between,
6187 vec![
6188 Matches::Some(0),
6189 Matches::All(2),
6190 Matches::All(3),
6191 Matches::All(4),
6192 ]
6193 );
6194 }
6195
6196 fn assert_state_roundtrips(state: &BTreeIndexState) {
6197 let restored = deserialize_state(serialize_state(state)).unwrap();
6198 assert_eq!(restored.lookup_batch, state.lookup_batch);
6199 assert_eq!(restored.batch_size, state.batch_size);
6200 assert_eq!(restored.ranges_to_files, state.ranges_to_files);
6201 }
6202
6203 #[test]
6204 fn test_btree_page_key_codec() {
6205 assert!(BTreePageKey::codec().is_some());
6207 }
6208
6209 #[test]
6210 fn test_btree_index_state_roundtrip() {
6211 assert_state_roundtrips(&BTreeIndexState {
6213 lookup_batch: sample_lookup_batch(),
6214 batch_size: DEFAULT_BTREE_BATCH_SIZE,
6215 ranges_to_files: None,
6216 });
6217
6218 let ranges: RangeInclusiveMap<u32, (String, u32)> = [
6220 (0..=99, ("part_0_page_file.lance".to_string(), 0)),
6221 (100..=199, ("part_1_page_file.lance".to_string(), 100)),
6222 ]
6223 .into_iter()
6224 .collect();
6225 assert_state_roundtrips(&BTreeIndexState {
6226 lookup_batch: sample_lookup_batch(),
6227 batch_size: 8192,
6228 ranges_to_files: Some(Arc::new(ranges)),
6229 });
6230
6231 assert_state_roundtrips(&BTreeIndexState {
6233 lookup_batch: RecordBatch::new_empty(sample_lookup_batch().schema()),
6234 batch_size: DEFAULT_BTREE_BATCH_SIZE,
6235 ranges_to_files: None,
6236 });
6237 }
6238
6239 #[tokio::test]
6240 async fn test_btree_index_state_reconstruct_and_plugin_cache() {
6241 let tmpdir = TempObjDir::default();
6242 let test_store = Arc::new(LanceIndexStore::new(
6243 Arc::new(ObjectStore::local()),
6244 tmpdir.clone(),
6245 Arc::new(LanceCache::no_cache()),
6246 ));
6247
6248 let stream = gen_batch()
6249 .col("value", array::step::<Int32Type>())
6250 .col("_rowid", array::step::<UInt64Type>())
6251 .into_df_stream(RowCount::from(1000), BatchCount::from(5));
6252 train_btree_index(stream, test_store.as_ref(), 1000, None, None)
6253 .await
6254 .unwrap();
6255
6256 let index = BTreeIndex::load(test_store.clone(), None, &LanceCache::no_cache())
6257 .await
6258 .unwrap();
6259
6260 let state = BTreeIndexState {
6262 lookup_batch: index.page_lookup.batch.clone(),
6263 batch_size: index.batch_size,
6264 ranges_to_files: index.ranges_to_files.clone(),
6265 };
6266 let restored = deserialize_state(serialize_state(&state)).unwrap();
6267 let reconstructed = restored
6268 .reconstruct(test_store.clone(), &LanceCache::no_cache(), None)
6269 .unwrap();
6270 assert_eq!(
6271 reconstructed
6272 .as_any()
6273 .downcast_ref::<BTreeIndex>()
6274 .unwrap()
6275 .page_lookup,
6276 index.page_lookup
6277 );
6278
6279 let cache = LanceCache::with_capacity(64 * 1024 * 1024);
6281 let plugin = BTreeIndexPlugin;
6282 plugin.put_in_cache(&cache, index.clone()).await.unwrap();
6283 let from_cache = plugin
6284 .get_from_cache(test_store.clone(), None, &cache)
6285 .await
6286 .unwrap()
6287 .expect("index should be served from the cache");
6288
6289 let query = SargableQuery::Range(
6291 std::ops::Bound::Included(ScalarValue::Int32(Some(100))),
6292 std::ops::Bound::Excluded(ScalarValue::Int32(Some(200))),
6293 );
6294 let expected = index.search(&query, &NoOpMetricsCollector).await.unwrap();
6295 let actual = from_cache
6296 .search(&query, &NoOpMetricsCollector)
6297 .await
6298 .unwrap();
6299 assert_eq!(expected, actual);
6300 }
6301
6302 #[test]
6305 fn test_btree_index_state_lookup_is_zero_copy() {
6306 use lance_core::cache::CacheCodec;
6307 const ALIGN: usize = 64;
6308
6309 let ranges: RangeInclusiveMap<u32, (String, u32)> =
6310 [(0..=99, ("part_0_page_file.lance".to_string(), 0))]
6311 .into_iter()
6312 .collect();
6313 let state = BTreeIndexState {
6314 lookup_batch: sample_lookup_batch(),
6315 batch_size: 8192,
6316 ranges_to_files: Some(Arc::new(ranges)),
6317 };
6318
6319 let codec = CacheCodec::from_impl::<BTreeIndexState>();
6320 let any: Arc<dyn std::any::Any + Send + Sync> = Arc::new(state);
6321 let mut buf = Vec::new();
6322 codec.serialize(&any, &mut buf).unwrap();
6323
6324 let mut v = vec![0u8; buf.len() + ALIGN];
6325 let pad = (ALIGN - (v.as_ptr() as usize % ALIGN)) % ALIGN;
6326 v[pad..pad + buf.len()].copy_from_slice(&buf);
6327 let data = bytes::Bytes::from(v).slice(pad..pad + buf.len());
6328
6329 let restored = codec.deserialize(&data).hit().unwrap();
6330 let restored = restored.downcast::<BTreeIndexState>().unwrap();
6331
6332 let base = data.as_ptr() as usize;
6333 let end = base + data.len();
6334 for col in restored.lookup_batch.columns() {
6335 for buffer in col.to_data().buffers() {
6336 let ptr = buffer.as_ptr() as usize;
6337 assert!(
6338 ptr >= base && ptr < end,
6339 "lookup batch buffer was realigned out of the input — misaligned IPC section",
6340 );
6341 }
6342 }
6343 }
6344
6345 #[test]
6346 fn test_btree_index_state_rejects_truncated_header() {
6347 let mut buf = Vec::new();
6350 buf.extend_from_slice(&100u32.to_le_bytes()); buf.extend_from_slice(&[0u8; 4]); assert!(deserialize_state(buf).is_err());
6353 }
6354
6355 #[tokio::test]
6356 async fn test_btree_index_state_reconstruct_applies_frag_reuse_index() {
6357 use crate::frag_reuse::{FragReuseIndex, FragReuseIndexDetails, FragReuseIndexHandle};
6358 use std::collections::HashMap;
6359 use uuid::Uuid;
6360
6361 let tmpdir = TempObjDir::default();
6362 let test_store = Arc::new(LanceIndexStore::new(
6363 Arc::new(ObjectStore::local()),
6364 tmpdir.clone(),
6365 Arc::new(LanceCache::no_cache()),
6366 ));
6367
6368 let stream = gen_batch()
6370 .col("value", array::step::<Int32Type>())
6371 .col("_rowid", array::step::<UInt64Type>())
6372 .into_df_stream(RowCount::from(1000), BatchCount::from(1));
6373 train_btree_index(stream, test_store.as_ref(), 1000, None, None)
6374 .await
6375 .unwrap();
6376
6377 let index = BTreeIndex::load(test_store.clone(), None, &LanceCache::no_cache())
6378 .await
6379 .unwrap();
6380 let state = BTreeIndexState {
6381 lookup_batch: index.page_lookup.batch.clone(),
6382 batch_size: index.batch_size,
6383 ranges_to_files: index.ranges_to_files.clone(),
6384 };
6385
6386 let frag_reuse_index: Arc<dyn crate::scalar::RowIdRemapper> =
6390 Arc::new(FragReuseIndexHandle(Arc::new(FragReuseIndex::new(
6391 Uuid::new_v4(),
6392 vec![HashMap::from([(0u64, Some(5000u64))])],
6393 FragReuseIndexDetails { versions: vec![] },
6394 ))));
6395 let reconstructed = state
6396 .reconstruct(
6397 test_store.clone(),
6398 &LanceCache::no_cache(),
6399 Some(frag_reuse_index),
6400 )
6401 .unwrap();
6402
6403 let result = reconstructed
6404 .search(
6405 &SargableQuery::Equals(ScalarValue::Int32(Some(0))),
6406 &NoOpMetricsCollector,
6407 )
6408 .await
6409 .unwrap();
6410 let row_ids: Vec<u64> = match &result {
6411 SearchResult::Exact(set) => set
6412 .true_rows()
6413 .row_addrs()
6414 .unwrap()
6415 .map(u64::from)
6416 .collect(),
6417 other => panic!("expected Exact, got {other:?}"),
6418 };
6419 assert_eq!(
6420 row_ids,
6421 vec![5000],
6422 "frag_reuse_index remap was not applied"
6423 );
6424 }
6425
6426 #[tokio::test]
6427 async fn test_btree_index_state_range_partitioned_plugin_cache_roundtrip() {
6428 let tmpdir = TempObjDir::default();
6432 let store = Arc::new(LanceIndexStore::new(
6433 Arc::new(ObjectStore::local()),
6434 tmpdir.clone(),
6435 Arc::new(LanceCache::no_cache()),
6436 ));
6437
6438 let half = DEFAULT_BTREE_BATCH_SIZE;
6439 let total = (2 * half) as i32;
6440
6441 let part0 = gen_batch()
6443 .col("value", array::step::<Int32Type>())
6444 .col("_rowid", array::step::<UInt64Type>())
6445 .into_df_stream(RowCount::from(half), BatchCount::from(1));
6446 train_btree_index(part0, store.as_ref(), half, None, Some(0u32))
6447 .await
6448 .unwrap();
6449
6450 let values: Vec<i32> = (half as i32..total).collect();
6452 let row_ids: Vec<u64> = (half..total as u64).collect();
6453 let part1 = gen_batch()
6454 .col("value", array::cycle::<Int32Type>(values))
6455 .col("_rowid", array::cycle::<UInt64Type>(row_ids))
6456 .into_df_stream(RowCount::from(half), BatchCount::from(1));
6457 train_btree_index(part1, store.as_ref(), half, None, Some(1u32))
6458 .await
6459 .unwrap();
6460
6461 super::merge_metadata_files(
6462 store.as_ref(),
6463 &[
6464 part_page_data_file_path(0 << 32),
6465 part_page_data_file_path(1 << 32),
6466 ],
6467 &[
6468 part_lookup_file_path(0 << 32),
6469 part_lookup_file_path(1 << 32),
6470 ],
6471 Some(1usize),
6472 noop_progress(),
6473 )
6474 .await
6475 .unwrap();
6476
6477 let index = BTreeIndex::load(store.clone(), None, &LanceCache::no_cache())
6478 .await
6479 .unwrap();
6480 assert!(
6481 index.ranges_to_files.is_some(),
6482 "test setup should produce a range-partitioned index",
6483 );
6484
6485 let cache = LanceCache::with_capacity(64 * 1024 * 1024);
6486 let plugin = BTreeIndexPlugin;
6487 plugin.put_in_cache(&cache, index.clone()).await.unwrap();
6488 let from_cache = plugin
6489 .get_from_cache(store.clone(), None, &cache)
6490 .await
6491 .unwrap()
6492 .expect("index should be served from the cache");
6493
6494 for value in [0i32, total - 1] {
6496 let query = SargableQuery::Equals(ScalarValue::Int32(Some(value)));
6497 let expected = index.search(&query, &NoOpMetricsCollector).await.unwrap();
6498 let actual = from_cache
6499 .search(&query, &NoOpMetricsCollector)
6500 .await
6501 .unwrap();
6502 assert_eq!(expected, actual, "value {value}");
6503 }
6504 }
6505}