1use std::{
5 any::Any,
6 cmp::Ordering,
7 collections::{BTreeMap, BinaryHeap, HashMap, HashSet},
8 fmt::{Debug, Display},
9 ops::Bound,
10 sync::Arc,
11};
12
13use super::{
14 AnyQuery, BuiltinIndexType, IndexReader, IndexStore, IndexWriter, MetricsCollector,
15 OldIndexDataFilter, SargableQuery, ScalarIndex, ScalarIndexParams, SearchResult,
16 compute_next_prefix,
17};
18use crate::{Index, IndexType};
19use crate::{
20 frag_reuse::FragReuseIndex,
21 progress::{IndexBuildProgress, noop_progress},
22 scalar::{
23 CreatedIndex, UpdateCriteria,
24 expression::{SargableQueryParser, ScalarQueryParser},
25 registry::{ScalarIndexPlugin, TrainingOrdering, TrainingRequest, VALUE_COLUMN_NAME},
26 },
27};
28use crate::{metrics::NoOpMetricsCollector, scalar::registry::TrainingCriteria};
29use crate::{pbold, scalar::btree::flat::FlatIndex};
30use arrow_arith::numeric::add;
31use arrow_array::{Array, RecordBatch, UInt32Array, new_empty_array};
32use arrow_schema::{DataType, Field, Schema, SortOptions};
33use async_trait::async_trait;
34use datafusion::physical_plan::{
35 ExecutionPlan, SendableRecordBatchStream,
36 sorts::sort_preserving_merge::SortPreservingMergeExec, stream::RecordBatchStreamAdapter,
37 union::UnionExec,
38};
39use datafusion_common::{DataFusionError, ScalarValue};
40use datafusion_physical_expr::{PhysicalSortExpr, expressions::Column};
41use deepsize::DeepSizeOf;
42use futures::{
43 FutureExt, Stream, StreamExt, TryFutureExt, TryStreamExt,
44 future::BoxFuture,
45 stream::{self},
46};
47use lance_arrow::ipc::{read_ipc_stream_single_at, write_ipc_stream};
48use lance_core::{
49 Error, ROW_ID, Result,
50 cache::{CacheCodec, CacheCodecImpl, CacheKey, LanceCache, WeakLanceCache},
51 error::LanceOptionExt,
52 utils::{
53 mask::NullableRowAddrSet,
54 tokio::get_num_compute_intensive_cpus,
55 tracing::{IO_TYPE_LOAD_SCALAR_PART, TRACE_IO_EVENTS},
56 },
57};
58use lance_datafusion::{
59 chunker::chunk_concat_stream,
60 exec::{LanceExecutionOptions, OneShotExec, execute_plan},
61};
62use lance_io::object_store::ObjectStore;
63use log::{debug, warn};
64use object_store::{Error as ObjectStoreError, path::Path};
65use rangemap::RangeInclusiveMap;
66use roaring::RoaringBitmap;
67use serde::{Deserialize, Serialize, Serializer};
68use tracing::{info, instrument};
69
70mod flat;
71
72const BTREE_LOOKUP_NAME: &str = "page_lookup.lance";
73const BTREE_PAGES_NAME: &str = "page_data.lance";
74pub const DEFAULT_BTREE_BATCH_SIZE: u64 = 4096;
75const BATCH_SIZE_META_KEY: &str = "batch_size";
76const DEFAULT_RANGE_PARTITIONED: bool = false;
77const RANGE_PARTITIONED_META_KEY: &str = "range_partitioned";
78const PAGE_NUM_PER_RANGE_PARTITION_META_KEY: &str = "page_num_per_range_partition";
79const BTREE_INDEX_VERSION: u32 = 0;
80pub(crate) const BTREE_VALUES_COLUMN: &str = "values";
81pub(crate) const BTREE_IDS_COLUMN: &str = "ids";
82
83#[derive(Clone, Debug)]
85pub struct OrderableScalarValue(pub ScalarValue);
86
87impl DeepSizeOf for OrderableScalarValue {
88 fn deep_size_of_children(&self, _context: &mut deepsize::Context) -> usize {
89 self.0.size() - std::mem::size_of::<ScalarValue>()
91 }
92}
93
94impl Display for OrderableScalarValue {
95 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
96 std::fmt::Display::fmt(&self.0, f)
97 }
98}
99
100impl PartialEq for OrderableScalarValue {
101 fn eq(&self, other: &Self) -> bool {
102 self.0.eq(&other.0)
103 }
104}
105
106impl Eq for OrderableScalarValue {}
107
108impl PartialOrd for OrderableScalarValue {
109 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
110 Some(self.cmp(other))
111 }
112}
113
114impl Ord for OrderableScalarValue {
120 fn cmp(&self, other: &Self) -> Ordering {
121 use ScalarValue::*;
122 match (&self.0, &other.0) {
126 (Decimal32(v1, p1, s1), Decimal32(v2, p2, s2)) => {
127 if p1.eq(p2) && s1.eq(s2) {
128 v1.cmp(v2)
129 } else {
130 panic!("Attempt to compare decimals with unequal precision / scale")
132 }
133 }
134 (Decimal32(v1, _, _), Null) => {
135 if v1.is_none() {
136 Ordering::Equal
137 } else {
138 Ordering::Greater
139 }
140 }
141 (Decimal32(_, _, _), _) => panic!("Attempt to compare decimal with non-decimal"),
142 (Decimal64(v1, p1, s1), Decimal64(v2, p2, s2)) => {
143 if p1.eq(p2) && s1.eq(s2) {
144 v1.cmp(v2)
145 } else {
146 panic!("Attempt to compare decimals with unequal precision / scale")
148 }
149 }
150 (Decimal64(v1, _, _), Null) => {
151 if v1.is_none() {
152 Ordering::Equal
153 } else {
154 Ordering::Greater
155 }
156 }
157 (Decimal64(_, _, _), _) => panic!("Attempt to compare decimal with non-decimal"),
158 (Decimal128(v1, p1, s1), Decimal128(v2, p2, s2)) => {
159 if p1.eq(p2) && s1.eq(s2) {
160 v1.cmp(v2)
161 } else {
162 panic!("Attempt to compare decimals with unequal precision / scale")
164 }
165 }
166 (Decimal128(v1, _, _), Null) => {
167 if v1.is_none() {
168 Ordering::Equal
169 } else {
170 Ordering::Greater
171 }
172 }
173 (Decimal128(_, _, _), _) => panic!("Attempt to compare decimal with non-decimal"),
174 (Decimal256(v1, p1, s1), Decimal256(v2, p2, s2)) => {
175 if p1.eq(p2) && s1.eq(s2) {
176 v1.cmp(v2)
177 } else {
178 panic!("Attempt to compare decimals with unequal precision / scale")
180 }
181 }
182 (Decimal256(v1, _, _), Null) => {
183 if v1.is_none() {
184 Ordering::Equal
185 } else {
186 Ordering::Greater
187 }
188 }
189 (Decimal256(_, _, _), _) => panic!("Attempt to compare decimal with non-decimal"),
190
191 (Boolean(v1), Boolean(v2)) => v1.cmp(v2),
192 (Boolean(v1), Null) => {
193 if v1.is_none() {
194 Ordering::Equal
195 } else {
196 Ordering::Greater
197 }
198 }
199 (Boolean(_), _) => panic!("Attempt to compare boolean with non-boolean"),
200 (Float32(v1), Float32(v2)) => match (v1, v2) {
201 (Some(f1), Some(f2)) => f1.total_cmp(f2),
202 (None, Some(_)) => Ordering::Less,
203 (Some(_), None) => Ordering::Greater,
204 (None, None) => Ordering::Equal,
205 },
206 (Float32(v1), Null) => {
207 if v1.is_none() {
208 Ordering::Equal
209 } else {
210 Ordering::Greater
211 }
212 }
213 (Float32(_), _) => panic!("Attempt to compare f32 with non-f32"),
214 (Float64(v1), Float64(v2)) => match (v1, v2) {
215 (Some(f1), Some(f2)) => f1.total_cmp(f2),
216 (None, Some(_)) => Ordering::Less,
217 (Some(_), None) => Ordering::Greater,
218 (None, None) => Ordering::Equal,
219 },
220 (Float64(v1), Null) => {
221 if v1.is_none() {
222 Ordering::Equal
223 } else {
224 Ordering::Greater
225 }
226 }
227 (Float64(_), _) => panic!("Attempt to compare f64 with non-f64"),
228 (Float16(v1), Float16(v2)) => match (v1, v2) {
229 (Some(f1), Some(f2)) => f1.total_cmp(f2),
230 (None, Some(_)) => Ordering::Less,
231 (Some(_), None) => Ordering::Greater,
232 (None, None) => Ordering::Equal,
233 },
234 (Float16(v1), Null) => {
235 if v1.is_none() {
236 Ordering::Equal
237 } else {
238 Ordering::Greater
239 }
240 }
241 (Float16(_), _) => panic!("Attempt to compare f16 with non-f16"),
242 (Int8(v1), Int8(v2)) => v1.cmp(v2),
243 (Int8(v1), Null) => {
244 if v1.is_none() {
245 Ordering::Equal
246 } else {
247 Ordering::Greater
248 }
249 }
250 (Int8(_), _) => panic!("Attempt to compare Int8 with non-Int8"),
251 (Int16(v1), Int16(v2)) => v1.cmp(v2),
252 (Int16(v1), Null) => {
253 if v1.is_none() {
254 Ordering::Equal
255 } else {
256 Ordering::Greater
257 }
258 }
259 (Int16(_), _) => panic!("Attempt to compare Int16 with non-Int16"),
260 (Int32(v1), Int32(v2)) => v1.cmp(v2),
261 (Int32(v1), Null) => {
262 if v1.is_none() {
263 Ordering::Equal
264 } else {
265 Ordering::Greater
266 }
267 }
268 (Int32(_), _) => panic!("Attempt to compare Int32 with non-Int32"),
269 (Int64(v1), Int64(v2)) => v1.cmp(v2),
270 (Int64(v1), Null) => {
271 if v1.is_none() {
272 Ordering::Equal
273 } else {
274 Ordering::Greater
275 }
276 }
277 (Int64(_), _) => panic!("Attempt to compare Int64 with non-Int64"),
278 (UInt8(v1), UInt8(v2)) => v1.cmp(v2),
279 (UInt8(v1), Null) => {
280 if v1.is_none() {
281 Ordering::Equal
282 } else {
283 Ordering::Greater
284 }
285 }
286 (UInt8(_), _) => panic!("Attempt to compare UInt8 with non-UInt8"),
287 (UInt16(v1), UInt16(v2)) => v1.cmp(v2),
288 (UInt16(v1), Null) => {
289 if v1.is_none() {
290 Ordering::Equal
291 } else {
292 Ordering::Greater
293 }
294 }
295 (UInt16(_), _) => panic!("Attempt to compare UInt16 with non-UInt16"),
296 (UInt32(v1), UInt32(v2)) => v1.cmp(v2),
297 (UInt32(v1), Null) => {
298 if v1.is_none() {
299 Ordering::Equal
300 } else {
301 Ordering::Greater
302 }
303 }
304 (UInt32(_), _) => panic!("Attempt to compare UInt32 with non-UInt32"),
305 (UInt64(v1), UInt64(v2)) => v1.cmp(v2),
306 (UInt64(v1), Null) => {
307 if v1.is_none() {
308 Ordering::Equal
309 } else {
310 Ordering::Greater
311 }
312 }
313 (UInt64(_), _) => panic!("Attempt to compare UInt64 with non-UInt64"),
314 (Utf8(v1) | Utf8View(v1) | LargeUtf8(v1), Utf8(v2) | Utf8View(v2) | LargeUtf8(v2)) => {
315 v1.cmp(v2)
316 }
317 (Utf8(v1) | Utf8View(v1) | LargeUtf8(v1), Null) => {
318 if v1.is_none() {
319 Ordering::Equal
320 } else {
321 Ordering::Greater
322 }
323 }
324 (Utf8(_) | Utf8View(_) | LargeUtf8(_), _) => {
325 panic!("Attempt to compare Utf8 with non-Utf8")
326 }
327 (
328 Binary(v1) | LargeBinary(v1) | BinaryView(v1),
329 Binary(v2) | LargeBinary(v2) | BinaryView(v2),
330 ) => v1.cmp(v2),
331 (Binary(v1) | LargeBinary(v1) | BinaryView(v1), Null) => {
332 if v1.is_none() {
333 Ordering::Equal
334 } else {
335 Ordering::Greater
336 }
337 }
338 (Binary(_) | LargeBinary(_) | BinaryView(_), _) => {
339 panic!("Attempt to compare Binary with non-Binary")
340 }
341 (FixedSizeBinary(_, v1), FixedSizeBinary(_, v2)) => v1.cmp(v2),
342 (FixedSizeBinary(_, v1), Null) => {
343 if v1.is_none() {
344 Ordering::Equal
345 } else {
346 Ordering::Greater
347 }
348 }
349 (FixedSizeBinary(_, _), _) => {
350 panic!("Attempt to compare FixedSizeBinary with non-FixedSizeBinary")
351 }
352 (FixedSizeList(left), FixedSizeList(right)) => {
353 if left.eq(right) {
354 todo!()
355 } else {
356 panic!(
357 "Attempt to compare fixed size list elements with different widths/fields"
358 )
359 }
360 }
361 (FixedSizeList(left), Null) => {
362 if left.is_null(0) {
363 Ordering::Equal
364 } else {
365 Ordering::Greater
366 }
367 }
368 (FixedSizeList(_), _) => {
369 panic!("Attempt to compare FixedSizeList with non-FixedSizeList")
370 }
371 (List(_), List(_)) => todo!(),
372 (List(left), Null) => {
373 if left.is_null(0) {
374 Ordering::Equal
375 } else {
376 Ordering::Greater
377 }
378 }
379 (List(_), _) => {
380 panic!("Attempt to compare List with non-List")
381 }
382 (LargeList(_), _) => todo!(),
383 (Map(_), Map(_)) => todo!(),
384 (Map(left), Null) => {
385 if left.is_null(0) {
386 Ordering::Equal
387 } else {
388 Ordering::Greater
389 }
390 }
391 (Map(_), _) => {
392 panic!("Attempt to compare Map with non-Map")
393 }
394 (Date32(v1), Date32(v2)) => v1.cmp(v2),
395 (Date32(v1), Null) => {
396 if v1.is_none() {
397 Ordering::Equal
398 } else {
399 Ordering::Greater
400 }
401 }
402 (Date32(_), _) => panic!("Attempt to compare Date32 with non-Date32"),
403 (Date64(v1), Date64(v2)) => v1.cmp(v2),
404 (Date64(v1), Null) => {
405 if v1.is_none() {
406 Ordering::Equal
407 } else {
408 Ordering::Greater
409 }
410 }
411 (Date64(_), _) => panic!("Attempt to compare Date64 with non-Date64"),
412 (Time32Second(v1), Time32Second(v2)) => v1.cmp(v2),
413 (Time32Second(v1), Null) => {
414 if v1.is_none() {
415 Ordering::Equal
416 } else {
417 Ordering::Greater
418 }
419 }
420 (Time32Second(_), _) => panic!("Attempt to compare Time32Second with non-Time32Second"),
421 (Time32Millisecond(v1), Time32Millisecond(v2)) => v1.cmp(v2),
422 (Time32Millisecond(v1), Null) => {
423 if v1.is_none() {
424 Ordering::Equal
425 } else {
426 Ordering::Greater
427 }
428 }
429 (Time32Millisecond(_), _) => {
430 panic!("Attempt to compare Time32Millisecond with non-Time32Millisecond")
431 }
432 (Time64Microsecond(v1), Time64Microsecond(v2)) => v1.cmp(v2),
433 (Time64Microsecond(v1), Null) => {
434 if v1.is_none() {
435 Ordering::Equal
436 } else {
437 Ordering::Greater
438 }
439 }
440 (Time64Microsecond(_), _) => {
441 panic!("Attempt to compare Time64Microsecond with non-Time64Microsecond")
442 }
443 (Time64Nanosecond(v1), Time64Nanosecond(v2)) => v1.cmp(v2),
444 (Time64Nanosecond(v1), Null) => {
445 if v1.is_none() {
446 Ordering::Equal
447 } else {
448 Ordering::Greater
449 }
450 }
451 (Time64Nanosecond(_), _) => {
452 panic!("Attempt to compare Time64Nanosecond with non-Time64Nanosecond")
453 }
454 (TimestampSecond(v1, _), TimestampSecond(v2, _)) => v1.cmp(v2),
455 (TimestampSecond(v1, _), Null) => {
456 if v1.is_none() {
457 Ordering::Equal
458 } else {
459 Ordering::Greater
460 }
461 }
462 (TimestampSecond(_, _), _) => {
463 panic!("Attempt to compare TimestampSecond with non-TimestampSecond")
464 }
465 (TimestampMillisecond(v1, _), TimestampMillisecond(v2, _)) => v1.cmp(v2),
466 (TimestampMillisecond(v1, _), Null) => {
467 if v1.is_none() {
468 Ordering::Equal
469 } else {
470 Ordering::Greater
471 }
472 }
473 (TimestampMillisecond(_, _), _) => {
474 panic!("Attempt to compare TimestampMillisecond with non-TimestampMillisecond")
475 }
476 (TimestampMicrosecond(v1, _), TimestampMicrosecond(v2, _)) => v1.cmp(v2),
477 (TimestampMicrosecond(v1, _), Null) => {
478 if v1.is_none() {
479 Ordering::Equal
480 } else {
481 Ordering::Greater
482 }
483 }
484 (TimestampMicrosecond(_, _), _) => {
485 panic!("Attempt to compare TimestampMicrosecond with non-TimestampMicrosecond")
486 }
487 (TimestampNanosecond(v1, _), TimestampNanosecond(v2, _)) => v1.cmp(v2),
488 (TimestampNanosecond(v1, _), Null) => {
489 if v1.is_none() {
490 Ordering::Equal
491 } else {
492 Ordering::Greater
493 }
494 }
495 (TimestampNanosecond(_, _), _) => {
496 panic!("Attempt to compare TimestampNanosecond with non-TimestampNanosecond")
497 }
498 (IntervalYearMonth(v1), IntervalYearMonth(v2)) => v1.cmp(v2),
499 (IntervalYearMonth(v1), Null) => {
500 if v1.is_none() {
501 Ordering::Equal
502 } else {
503 Ordering::Greater
504 }
505 }
506 (IntervalYearMonth(_), _) => {
507 panic!("Attempt to compare IntervalYearMonth with non-IntervalYearMonth")
508 }
509 (IntervalDayTime(v1), IntervalDayTime(v2)) => v1.cmp(v2),
510 (IntervalDayTime(v1), Null) => {
511 if v1.is_none() {
512 Ordering::Equal
513 } else {
514 Ordering::Greater
515 }
516 }
517 (IntervalDayTime(_), _) => {
518 panic!("Attempt to compare IntervalDayTime with non-IntervalDayTime")
519 }
520 (IntervalMonthDayNano(v1), IntervalMonthDayNano(v2)) => v1.cmp(v2),
521 (IntervalMonthDayNano(v1), Null) => {
522 if v1.is_none() {
523 Ordering::Equal
524 } else {
525 Ordering::Greater
526 }
527 }
528 (IntervalMonthDayNano(_), _) => {
529 panic!("Attempt to compare IntervalMonthDayNano with non-IntervalMonthDayNano")
530 }
531 (DurationSecond(v1), DurationSecond(v2)) => v1.cmp(v2),
532 (DurationSecond(v1), Null) => {
533 if v1.is_none() {
534 Ordering::Equal
535 } else {
536 Ordering::Greater
537 }
538 }
539 (DurationSecond(_), _) => {
540 panic!("Attempt to compare DurationSecond with non-DurationSecond")
541 }
542 (DurationMillisecond(v1), DurationMillisecond(v2)) => v1.cmp(v2),
543 (DurationMillisecond(v1), Null) => {
544 if v1.is_none() {
545 Ordering::Equal
546 } else {
547 Ordering::Greater
548 }
549 }
550 (DurationMillisecond(_), _) => {
551 panic!("Attempt to compare DurationMillisecond with non-DurationMillisecond")
552 }
553 (DurationMicrosecond(v1), DurationMicrosecond(v2)) => v1.cmp(v2),
554 (DurationMicrosecond(v1), Null) => {
555 if v1.is_none() {
556 Ordering::Equal
557 } else {
558 Ordering::Greater
559 }
560 }
561 (DurationMicrosecond(_), _) => {
562 panic!("Attempt to compare DurationMicrosecond with non-DurationMicrosecond")
563 }
564 (DurationNanosecond(v1), DurationNanosecond(v2)) => v1.cmp(v2),
565 (DurationNanosecond(v1), Null) => {
566 if v1.is_none() {
567 Ordering::Equal
568 } else {
569 Ordering::Greater
570 }
571 }
572 (DurationNanosecond(_), _) => {
573 panic!("Attempt to compare DurationNanosecond with non-DurationNanosecond")
574 }
575 (Struct(_arr), Struct(_arr2)) => todo!(),
576 (Struct(arr), Null) => {
577 if arr.is_empty() {
578 Ordering::Equal
579 } else {
580 Ordering::Greater
581 }
582 }
583 (Struct(_arr), _) => panic!("Attempt to compare Struct with non-Struct"),
584 (Dictionary(_k1, _v1), Dictionary(_k2, _v2)) => todo!(),
585 (Dictionary(_, v1), Null) => Self(*v1.clone()).cmp(&Self(ScalarValue::Null)),
586 (Dictionary(_, _), _) => panic!("Attempt to compare Dictionary with non-Dictionary"),
587 (Union(_, _, _), _) => todo!("Support for union scalars"),
589 (RunEndEncoded(_, _, _), _) => {
590 todo!("Support for run-end encoded scalars")
591 }
592 (Null, Null) => Ordering::Equal,
593 (Null, _) => todo!(),
594 }
595 }
596}
597
598#[derive(Debug, DeepSizeOf, PartialEq, Eq)]
599struct PageRecord {
600 max: OrderableScalarValue,
601 page_number: u32,
602}
603
604trait BTreeMapExt<K, V> {
605 fn largest_node_less(&self, key: &K) -> Option<(&K, &V)>;
606}
607
608impl<K: Ord, V> BTreeMapExt<K, V> for BTreeMap<K, V> {
609 fn largest_node_less(&self, key: &K) -> Option<(&K, &V)> {
610 self.range((Bound::Unbounded, Bound::Excluded(key)))
611 .next_back()
612 }
613}
614
615#[derive(Debug, DeepSizeOf, PartialEq, Eq)]
617pub struct BTreeLookup {
618 tree: BTreeMap<OrderableScalarValue, Vec<PageRecord>>,
619 null_pages: Vec<u32>,
621 all_null_pages: Vec<u32>,
623}
624
625impl BTreeLookup {
626 fn empty() -> Self {
627 Self {
628 tree: BTreeMap::new(),
629 null_pages: Vec::new(),
630 all_null_pages: Vec::new(),
631 }
632 }
633}
634
635#[derive(Debug, Copy, Clone)]
636enum Matches {
637 Some(u32),
638 All(u32),
639}
640
641impl Matches {
642 fn page_id(&self) -> u32 {
643 match self {
644 Self::Some(page_id) => *page_id,
645 Self::All(page_id) => *page_id,
646 }
647 }
648}
649
650impl BTreeLookup {
651 fn new(
652 tree: BTreeMap<OrderableScalarValue, Vec<PageRecord>>,
653 null_pages: Vec<u32>,
654 all_null_pages: Vec<u32>,
655 ) -> Self {
656 Self {
657 tree,
658 null_pages,
659 all_null_pages,
660 }
661 }
662
663 fn pages_eq(&self, query: &OrderableScalarValue) -> Vec<Matches> {
665 if query.0.is_null() {
666 self.pages_null()
667 } else {
668 self.pages_between((Bound::Included(query), Bound::Excluded(query)))
669 }
670 }
671
672 fn pages_in(&self, values: impl IntoIterator<Item = OrderableScalarValue>) -> Vec<Matches> {
674 let page_lists = values
677 .into_iter()
678 .map(|val| {
679 self.pages_eq(&val)
680 .into_iter()
681 .map(|matches| matches.page_id())
682 })
683 .collect::<Vec<_>>();
684 let total_size = page_lists.iter().map(|set| set.len()).sum();
685 let mut heap = BinaryHeap::with_capacity(total_size);
686 for page_list in page_lists {
687 heap.extend(page_list);
688 }
689 let mut all_pages = heap.into_sorted_vec();
690 all_pages.dedup();
691 all_pages.into_iter().map(Matches::Some).collect()
692 }
693
694 fn pages_between(
696 &self,
697 range: (Bound<&OrderableScalarValue>, Bound<&OrderableScalarValue>),
698 ) -> Vec<Matches> {
699 let lower_bound = match range.0 {
702 Bound::Unbounded => Bound::Unbounded,
703 Bound::Included(lower) => self
710 .tree
711 .largest_node_less(lower)
712 .map(|val| Bound::Included(val.0))
713 .unwrap_or(Bound::Unbounded),
714 Bound::Excluded(lower) => self
715 .tree
716 .largest_node_less(lower)
717 .map(|val| Bound::Included(val.0))
718 .unwrap_or(Bound::Unbounded),
719 };
720 let upper_bound = match range.1 {
721 Bound::Unbounded => Bound::Unbounded,
722 Bound::Included(upper) => Bound::Included(upper),
723 Bound::Excluded(upper) => Bound::Included(upper),
732 };
733
734 match (lower_bound, upper_bound) {
735 (Bound::Excluded(lower), Bound::Excluded(upper))
736 | (Bound::Excluded(lower), Bound::Included(upper))
737 | (Bound::Included(lower), Bound::Excluded(upper)) => {
738 if lower >= upper {
741 return vec![];
742 }
743 }
744 (Bound::Included(lower), Bound::Included(upper)) => {
745 if lower > upper {
746 return vec![];
747 }
748 }
749 _ => {}
750 }
751
752 let mut matches = Vec::new();
753
754 for (min, page_records) in self.tree.range((lower_bound, upper_bound)) {
755 for page_record in page_records {
756 match lower_bound {
757 Bound::Unbounded => {}
758 Bound::Included(lower) => {
759 if page_record.max.cmp(lower) == Ordering::Less {
760 continue;
761 }
762 }
763 Bound::Excluded(lower) => {
764 if page_record.max.cmp(lower) != Ordering::Greater {
765 continue;
766 }
767 }
768 }
769 if min.0.is_null() || page_record.max.0.is_null() {
773 matches.push(Matches::Some(page_record.page_number));
775 continue;
776 }
777
778 match range.0 {
779 Bound::Excluded(lower) => {
782 if min.cmp(lower) != Ordering::Greater {
783 matches.push(Matches::Some(page_record.page_number));
784 continue;
785 }
786 }
787 Bound::Included(lower) => {
790 if min.cmp(lower) == Ordering::Less {
791 matches.push(Matches::Some(page_record.page_number));
792 continue;
793 }
794 }
795 Bound::Unbounded => {}
796 }
797 match range.1 {
798 Bound::Excluded(upper) => {
801 if page_record.max.cmp(upper) != Ordering::Less {
802 matches.push(Matches::Some(page_record.page_number));
803 continue;
804 }
805 }
806 Bound::Included(upper) => {
809 if page_record.max.cmp(upper) == Ordering::Greater {
810 matches.push(Matches::Some(page_record.page_number));
811 continue;
812 }
813 }
814 Bound::Unbounded => {}
815 }
816 matches.push(Matches::All(page_record.page_number));
819 }
820 }
821
822 matches
823 }
824
825 fn pages_null(&self) -> Vec<Matches> {
826 self.null_pages
827 .iter()
828 .map(|page_id| Matches::Some(*page_id))
829 .chain(self.all_null_pages.iter().copied().map(Matches::All))
830 .collect()
831 }
832}
833
834#[derive(Clone)]
837struct LazyIndexReader {
838 index_reader: Arc<tokio::sync::Mutex<Option<Arc<dyn IndexReader>>>>,
839 store: Arc<dyn IndexStore>,
840 ranges_to_files: Option<Arc<RangeInclusiveMap<u32, (String, u32)>>>,
841}
842
843impl LazyIndexReader {
844 fn new(
845 store: Arc<dyn IndexStore>,
846 ranges_to_files: Option<Arc<RangeInclusiveMap<u32, (String, u32)>>>,
847 ) -> Self {
848 Self {
849 index_reader: Arc::new(tokio::sync::Mutex::new(None)),
850 store,
851 ranges_to_files,
852 }
853 }
854
855 async fn get(&self) -> Result<Arc<dyn IndexReader>> {
856 let mut reader = self.index_reader.lock().await;
857 if reader.is_none() {
858 let index_reader = if let Some(ranges_to_files) = &self.ranges_to_files {
859 Arc::new(LazyRangedIndexReader::new(
860 self.store.clone(),
861 ranges_to_files.clone(),
862 ))
863 } else {
864 self.store.open_index_file(BTREE_PAGES_NAME).await?
865 };
866 *reader = Some(index_reader);
867 }
868 Ok(reader.as_ref().unwrap().clone())
869 }
870}
871
872struct LazyRangedIndexReader {
874 #[allow(clippy::type_complexity)]
875 readers:
876 Arc<tokio::sync::Mutex<HashMap<String, Arc<tokio::sync::OnceCell<Arc<dyn IndexReader>>>>>>,
877 store: Arc<dyn IndexStore>,
878 ranges_to_files: Arc<RangeInclusiveMap<u32, (String, u32)>>,
879}
880
881impl LazyRangedIndexReader {
882 fn new(
883 store: Arc<dyn IndexStore>,
884 ranges_to_files: Arc<RangeInclusiveMap<u32, (String, u32)>>,
885 ) -> Self {
886 Self {
887 readers: Arc::new(tokio::sync::Mutex::new(HashMap::new())),
888 store,
889 ranges_to_files,
890 }
891 }
892
893 async fn get_reader(&self, file_name: &str) -> Result<Arc<dyn IndexReader>> {
894 let reader_cell = {
895 let mut guard = self.readers.lock().await;
896 guard
897 .entry(file_name.to_string())
898 .or_insert_with(|| Arc::new(tokio::sync::OnceCell::new()))
899 .clone()
900 };
901 let reader = reader_cell
902 .get_or_try_init(|| async { self.store.open_index_file(file_name).await })
903 .await?;
904 Ok(reader.clone())
905 }
906
907 async fn get_reader_and_local_page_idx(
908 &self,
909 page_idx: u32,
910 ) -> Result<(Arc<dyn IndexReader>, u32)> {
911 let (page_file_name, offset) = self.ranges_to_files.get(&page_idx).ok_or_else(|| {
912 Error::internal(format!(
913 "Unexpected page index, index {} is out of range.",
914 page_idx
915 ))
916 })?;
917 let reader = self.get_reader(page_file_name).await?;
918 Ok((reader.clone(), page_idx - *offset))
919 }
920}
921
922#[async_trait]
923impl IndexReader for LazyRangedIndexReader {
924 async fn read_record_batch(&self, n: u64, batch_size: u64) -> Result<RecordBatch> {
925 let (reader, local_page_idx) = self.get_reader_and_local_page_idx(n as u32).await?;
926 reader
927 .read_record_batch(local_page_idx as u64, batch_size)
928 .await
929 }
930
931 async fn read_range(
932 &self,
933 _range: std::ops::Range<usize>,
934 _projection: Option<&[&str]>,
935 ) -> Result<RecordBatch> {
936 unimplemented!("Read range is not implemented for lazy page file reader.");
937 }
938
939 async fn num_batches(&self, batch_size: u64) -> u32 {
940 let mut total_batches = 0;
941 for (_, (file_name, _)) in self.ranges_to_files.iter() {
942 let reader = self
943 .get_reader(file_name)
944 .await
945 .unwrap_or_else(|_| panic!("Cannot open page file {}.", file_name));
946 total_batches += reader.as_ref().num_batches(batch_size).await;
947 }
948 total_batches
949 }
950
951 fn num_rows(&self) -> usize {
952 unimplemented!("only async functions are available for lazy page index reader.");
953 }
954
955 fn schema(&self) -> &lance_core::datatypes::Schema {
956 unimplemented!("only async functions are available for lazy page index reader.");
957 }
958}
959
960#[derive(Debug, Clone, DeepSizeOf)]
975pub struct CachedScalarIndex(Arc<dyn ScalarIndex>);
976
977impl CachedScalarIndex {
978 pub fn new(index: Arc<dyn ScalarIndex>) -> Self {
979 Self(index)
980 }
981
982 pub fn into_inner(self) -> Arc<dyn ScalarIndex> {
983 self.0
984 }
985}
986
987#[derive(Debug, Clone)]
988pub struct BTreePageKey {
989 pub page_number: u32,
990}
991
992impl CacheKey for BTreePageKey {
993 type ValueType = FlatIndex;
994
995 fn key(&self) -> std::borrow::Cow<'_, str> {
996 format!("page-{}", self.page_number).into()
997 }
998
999 fn type_name() -> &'static str {
1000 "BTreePage"
1001 }
1002
1003 fn codec() -> Option<CacheCodec> {
1004 Some(CacheCodec::from_impl::<FlatIndex>())
1006 }
1007}
1008
1009#[derive(Debug, Clone)]
1017pub struct BTreeIndexState {
1018 lookup_batch: RecordBatch,
1019 batch_size: u64,
1020 ranges_to_files: Option<Arc<RangeInclusiveMap<u32, (String, u32)>>>,
1021}
1022
1023impl DeepSizeOf for BTreeIndexState {
1024 fn deep_size_of_children(&self, _context: &mut deepsize::Context) -> usize {
1025 self.lookup_batch.get_array_memory_size()
1028 }
1029}
1030
1031impl BTreeIndexState {
1032 fn reconstruct(
1033 &self,
1034 store: Arc<dyn IndexStore>,
1035 index_cache: &LanceCache,
1036 frag_reuse_index: Option<Arc<FragReuseIndex>>,
1037 ) -> Result<Arc<dyn ScalarIndex>> {
1038 let index = BTreeIndex::try_from_serialized(
1039 self.lookup_batch.clone(),
1040 store,
1041 index_cache,
1042 self.batch_size,
1043 self.ranges_to_files.clone(),
1044 frag_reuse_index,
1045 )?;
1046 Ok(Arc::new(index) as Arc<dyn ScalarIndex>)
1047 }
1048}
1049
1050impl CacheCodecImpl for BTreeIndexState {
1051 fn serialize(&self, writer: &mut dyn std::io::Write) -> Result<()> {
1062 writer.write_all(&self.batch_size.to_le_bytes())?;
1063 match &self.ranges_to_files {
1064 None => writer.write_all(&[0u8])?,
1065 Some(ranges) => {
1066 writer.write_all(&[1u8])?;
1067 let count = u32::try_from(ranges.len()).map_err(|_| {
1068 Error::io("BTreeIndexState: ranges_to_files exceeds u32::MAX entries")
1069 })?;
1070 writer.write_all(&count.to_le_bytes())?;
1071 for (range, (path, page_offset)) in ranges.iter() {
1072 writer.write_all(&range.start().to_le_bytes())?;
1073 writer.write_all(&range.end().to_le_bytes())?;
1074 writer.write_all(&page_offset.to_le_bytes())?;
1075 let path_len = u32::try_from(path.len()).map_err(|_| {
1076 Error::io("BTreeIndexState: ranges_to_files path exceeds u32::MAX bytes")
1077 })?;
1078 writer.write_all(&path_len.to_le_bytes())?;
1079 writer.write_all(path.as_bytes())?;
1080 }
1081 }
1082 }
1083 write_ipc_stream(&self.lookup_batch, writer)?;
1084 Ok(())
1085 }
1086
1087 fn deserialize(data: &bytes::Bytes) -> Result<Self> {
1088 let mut offset = 0;
1089 let batch_size = read_u64_le(data, &mut offset)?;
1090 let has_ranges = read_u8(data, &mut offset)?;
1091 let ranges_to_files = match has_ranges {
1092 0 => None,
1093 1 => {
1094 let count = read_u32_le(data, &mut offset)? as usize;
1095 let mut entries = Vec::with_capacity(count);
1096 for _ in 0..count {
1097 let start = read_u32_le(data, &mut offset)?;
1098 let end = read_u32_le(data, &mut offset)?;
1099 let page_offset = read_u32_le(data, &mut offset)?;
1100 let path_len = read_u32_le(data, &mut offset)? as usize;
1101 let path = read_bytes(data, &mut offset, path_len)?;
1102 let path = std::str::from_utf8(&path)
1103 .map_err(|e| Error::io(format!("BTreeIndexState path: {e}")))?
1104 .to_string();
1105 entries.push((start..=end, (path, page_offset)));
1106 }
1107 Some(Arc::new(entries.into_iter().collect()))
1108 }
1109 other => {
1110 return Err(Error::io(format!(
1111 "BTreeIndexState: invalid has_ranges tag {other}"
1112 )));
1113 }
1114 };
1115 let lookup_batch = read_ipc_stream_single_at(data, &mut offset)?;
1116 Ok(Self {
1117 lookup_batch,
1118 batch_size,
1119 ranges_to_files,
1120 })
1121 }
1122}
1123
1124fn read_bytes(data: &bytes::Bytes, offset: &mut usize, len: usize) -> Result<bytes::Bytes> {
1125 if data.len() < *offset + len {
1126 return Err(Error::io(format!(
1127 "BTreeIndexState: short read of {len} bytes at offset {offset} (have {})",
1128 data.len()
1129 )));
1130 }
1131 let slice = data.slice(*offset..*offset + len);
1132 *offset += len;
1133 Ok(slice)
1134}
1135
1136fn read_u8(data: &bytes::Bytes, offset: &mut usize) -> Result<u8> {
1137 let bytes = read_bytes(data, offset, 1)?;
1138 Ok(bytes[0])
1139}
1140
1141fn read_u32_le(data: &bytes::Bytes, offset: &mut usize) -> Result<u32> {
1142 let bytes = read_bytes(data, offset, 4)?;
1143 Ok(u32::from_le_bytes(bytes.as_ref().try_into().unwrap()))
1144}
1145
1146fn read_u64_le(data: &bytes::Bytes, offset: &mut usize) -> Result<u64> {
1147 let bytes = read_bytes(data, offset, 8)?;
1148 Ok(u64::from_le_bytes(bytes.as_ref().try_into().unwrap()))
1149}
1150
1151struct BTreeIndexStateKey;
1154
1155impl CacheKey for BTreeIndexStateKey {
1156 type ValueType = BTreeIndexState;
1157
1158 fn key(&self) -> std::borrow::Cow<'_, str> {
1159 "state".into()
1160 }
1161
1162 fn type_name() -> &'static str {
1163 "BTreeIndexState"
1164 }
1165
1166 fn codec() -> Option<CacheCodec> {
1167 Some(CacheCodec::from_impl::<BTreeIndexState>())
1168 }
1169}
1170
1171#[derive(Clone, Debug)]
1174pub struct BTreeIndex {
1175 page_lookup: Arc<BTreeLookup>,
1176 index_cache: WeakLanceCache,
1177 store: Arc<dyn IndexStore>,
1178 data_type: DataType,
1179 batch_size: u64,
1180
1181 ranges_to_files: Option<Arc<RangeInclusiveMap<u32, (String, u32)>>>,
1210 frag_reuse_index: Option<Arc<FragReuseIndex>>,
1211
1212 lookup_batch: RecordBatch,
1222}
1223
1224impl DeepSizeOf for BTreeIndex {
1225 fn deep_size_of_children(&self, context: &mut deepsize::Context) -> usize {
1226 self.page_lookup.deep_size_of_children(context)
1229 + self.store.deep_size_of_children(context)
1230 + self.lookup_batch.get_array_memory_size()
1231 }
1232}
1233
1234impl BTreeIndex {
1235 #[allow(clippy::too_many_arguments)]
1236 fn new(
1237 page_lookup: Arc<BTreeLookup>,
1238 store: Arc<dyn IndexStore>,
1239 data_type: DataType,
1240 index_cache: WeakLanceCache,
1241 batch_size: u64,
1242 ranges_to_files: Option<Arc<RangeInclusiveMap<u32, (String, u32)>>>,
1243 frag_reuse_index: Option<Arc<FragReuseIndex>>,
1244 lookup_batch: RecordBatch,
1245 ) -> Self {
1246 Self {
1247 page_lookup,
1248 store,
1249 data_type,
1250 index_cache,
1251 batch_size,
1252 ranges_to_files,
1253 frag_reuse_index,
1254 lookup_batch,
1255 }
1256 }
1257
1258 async fn lookup_page(
1259 &self,
1260 page_number: u32,
1261 index_reader: LazyIndexReader,
1262 metrics: &dyn MetricsCollector,
1263 ) -> Result<Arc<FlatIndex>> {
1264 self.index_cache
1265 .get_or_insert_with_key(BTreePageKey { page_number }, move || async move {
1266 self.read_page(page_number, index_reader, metrics).await
1267 })
1268 .await
1269 }
1270
1271 #[instrument(level = "debug", skip_all)]
1272 async fn read_page(
1273 &self,
1274 page_number: u32,
1275 index_reader: LazyIndexReader,
1276 metrics: &dyn MetricsCollector,
1277 ) -> Result<FlatIndex> {
1278 metrics.record_part_load();
1279 info!(target: TRACE_IO_EVENTS, r#type=IO_TYPE_LOAD_SCALAR_PART, index_type="btree", part_id=page_number);
1280 let index_reader = index_reader.get().await?;
1281 let mut serialized_page = index_reader
1282 .read_record_batch(page_number as u64, self.batch_size)
1283 .await?;
1284 if let Some(frag_reuse_index_ref) = self.frag_reuse_index.as_ref() {
1285 serialized_page =
1286 frag_reuse_index_ref.remap_row_ids_record_batch(serialized_page, 1)?;
1287 }
1288 FlatIndex::try_new(serialized_page)
1289 }
1290
1291 async fn search_page(
1292 &self,
1293 query: &SargableQuery,
1294 matches: Matches,
1295 index_reader: LazyIndexReader,
1296 metrics: &dyn MetricsCollector,
1297 ) -> Result<NullableRowAddrSet> {
1298 let subindex = self
1299 .lookup_page(matches.page_id(), index_reader, metrics)
1300 .await?;
1301
1302 match matches {
1303 Matches::Some(_) => {
1304 subindex.search(query, metrics)
1309 }
1310 Matches::All(_) => Ok(match query {
1311 SargableQuery::IsNull() => subindex.all_ignore_nulls(),
1313 _ => subindex.all(),
1314 }),
1315 }
1316 }
1317
1318 #[instrument(level = "debug", skip_all)]
1319 fn try_from_serialized(
1320 data: RecordBatch,
1321 store: Arc<dyn IndexStore>,
1322 index_cache: &LanceCache,
1323 batch_size: u64,
1324 ranges_to_files: Option<Arc<RangeInclusiveMap<u32, (String, u32)>>>,
1325 frag_reuse_index: Option<Arc<FragReuseIndex>>,
1326 ) -> Result<Self> {
1327 let mut map = BTreeMap::<OrderableScalarValue, Vec<PageRecord>>::new();
1328 let mut null_pages = Vec::<u32>::new();
1330 let mut all_null_pages = Vec::<u32>::new();
1332
1333 if data.num_rows() == 0 {
1334 let data_type = data.column(0).data_type().clone();
1335 let page_lookup = Arc::new(BTreeLookup::empty());
1336 return Ok(Self::new(
1337 page_lookup,
1338 store,
1339 data_type,
1340 WeakLanceCache::from(index_cache),
1341 batch_size,
1342 ranges_to_files,
1343 frag_reuse_index,
1344 data,
1345 ));
1346 }
1347
1348 let mins = data.column(0);
1349 let maxs = data.column(1);
1350 let null_counts = data
1351 .column(2)
1352 .as_any()
1353 .downcast_ref::<UInt32Array>()
1354 .unwrap();
1355 let page_numbers = data
1356 .column(3)
1357 .as_any()
1358 .downcast_ref::<UInt32Array>()
1359 .unwrap();
1360
1361 for idx in 0..data.num_rows() {
1362 let min = OrderableScalarValue(ScalarValue::try_from_array(&mins, idx)?);
1363 let max = OrderableScalarValue(ScalarValue::try_from_array(&maxs, idx)?);
1364 let null_count = null_counts.values()[idx];
1365 let page_number = page_numbers.values()[idx];
1366
1367 if max.0.is_null() {
1369 all_null_pages.push(page_number);
1370 continue;
1372 } else {
1373 map.entry(min)
1374 .or_default()
1375 .push(PageRecord { max, page_number });
1376 }
1377
1378 if null_count > 0 {
1379 null_pages.push(page_number);
1380 }
1381 }
1382
1383 let last_max = ScalarValue::try_from_array(&maxs, data.num_rows() - 1)?;
1384 map.entry(OrderableScalarValue(last_max)).or_default();
1385
1386 let data_type = mins.data_type().clone();
1387
1388 let page_lookup = Arc::new(BTreeLookup::new(map, null_pages, all_null_pages));
1389
1390 Ok(Self::new(
1391 page_lookup,
1392 store,
1393 data_type,
1394 WeakLanceCache::from(index_cache),
1395 batch_size,
1396 ranges_to_files,
1397 frag_reuse_index,
1398 data,
1399 ))
1400 }
1401
1402 async fn load(
1403 store: Arc<dyn IndexStore>,
1404 frag_reuse_index: Option<Arc<FragReuseIndex>>,
1405 index_cache: &LanceCache,
1406 ) -> Result<Arc<Self>> {
1407 let (page_lookup_file, standalone_partition_page_file) =
1408 match store.open_index_file(BTREE_LOOKUP_NAME).await {
1409 Ok(page_lookup_file) => (page_lookup_file, None),
1410 Err(original_err) if is_missing_lookup_error(&original_err) => {
1411 let files = store.list_files_with_sizes().await?;
1412 let Some((lookup_file, page_file)) = find_single_partition_files(&files)?
1413 else {
1414 return Err(original_err);
1415 };
1416 (
1417 store.open_index_file(lookup_file).await?,
1418 Some(page_file.to_string()),
1419 )
1420 }
1421 Err(other_err) => return Err(other_err),
1422 };
1423 let num_rows_in_lookup = page_lookup_file.num_rows();
1424 let serialized_lookup = page_lookup_file
1425 .read_range(0..num_rows_in_lookup, None)
1426 .await?;
1427 let file_schema = page_lookup_file.schema();
1428 let batch_size = file_schema
1429 .metadata
1430 .get(BATCH_SIZE_META_KEY)
1431 .map(|bs| bs.parse().unwrap_or(DEFAULT_BTREE_BATCH_SIZE))
1432 .unwrap_or(DEFAULT_BTREE_BATCH_SIZE);
1433
1434 let range_partitioned = file_schema
1435 .metadata
1436 .get(RANGE_PARTITIONED_META_KEY)
1437 .map(|bs| bs.parse().unwrap_or(DEFAULT_RANGE_PARTITIONED))
1438 .unwrap_or(DEFAULT_RANGE_PARTITIONED);
1439 let ranges_to_files = if let Some(page_file_name) = standalone_partition_page_file {
1443 let page_numbers = serialized_lookup
1444 .column(3)
1445 .as_any()
1446 .downcast_ref::<UInt32Array>()
1447 .unwrap();
1448 let max_page_number = page_numbers.values().iter().copied().max().unwrap_or(0);
1449 let mut range_map = RangeInclusiveMap::new();
1450 range_map.insert(0..=max_page_number, (page_file_name, 0));
1451 Some(Arc::new(range_map))
1452 } else if range_partitioned {
1453 let part_sizes_str = file_schema
1454 .metadata
1455 .get(PAGE_NUM_PER_RANGE_PARTITION_META_KEY)
1456 .expect("Range-partitioned Btree lookup file must have page-number-per-range-file metadata!");
1457 let part_sizes_vec: Vec<(u64, u32)> = serde_json::from_str(part_sizes_str)?;
1458 let mut offset: u32 = 0;
1459
1460 let range_map = part_sizes_vec
1461 .into_iter()
1462 .map(|(id, size)| {
1463 let range = offset..=(offset + size - 1);
1464 let file_with_size = (part_page_data_file_path(id), offset);
1465 offset += size;
1466 (range, file_with_size)
1467 })
1468 .collect();
1469
1470 Some(Arc::new(range_map))
1471 } else {
1472 None
1473 };
1474
1475 Ok(Arc::new(Self::try_from_serialized(
1476 serialized_lookup,
1477 store,
1478 index_cache,
1479 batch_size,
1480 ranges_to_files,
1481 frag_reuse_index,
1482 )?))
1483 }
1484
1485 fn train_schema(&self) -> Schema {
1487 let value_field = Field::new(VALUE_COLUMN_NAME, self.data_type.clone(), true);
1488 let row_id_field = Field::new(ROW_ID, DataType::UInt64, false);
1489 Schema::new(vec![value_field, row_id_field])
1490 }
1491
1492 async fn into_data_stream(self) -> Result<SendableRecordBatchStream> {
1494 let lazy_reader = LazyIndexReader::new(self.store.clone(), self.ranges_to_files.clone());
1495 let reader = lazy_reader.get().await?;
1496 let new_schema = Arc::new(self.train_schema());
1497 let new_schema_clone = new_schema.clone();
1498 let reader_stream = IndexReaderStream::new(reader, self.batch_size).await;
1499 let batches = reader_stream
1500 .map(|fut| fut.map_err(DataFusionError::from))
1501 .buffered(self.store.io_parallelism())
1502 .map_ok(move |batch| {
1503 RecordBatch::try_new(
1504 new_schema.clone(),
1505 vec![batch.column(0).clone(), batch.column(1).clone()],
1506 )
1507 .unwrap()
1508 })
1509 .boxed();
1510 Ok(Box::pin(RecordBatchStreamAdapter::new(
1511 new_schema_clone,
1512 batches,
1513 )))
1514 }
1515
1516 async fn combine_old_new(
1517 self,
1518 new_data: SendableRecordBatchStream,
1519 chunk_size: u64,
1520 old_data_filter: Option<OldIndexDataFilter>,
1521 ) -> Result<SendableRecordBatchStream> {
1522 let value_column_index = new_data.schema().index_of(VALUE_COLUMN_NAME)?;
1523
1524 let new_input = Arc::new(OneShotExec::new(new_data));
1525 let old_stream = self.into_data_stream().await?;
1526 let old_stream = match old_data_filter {
1527 Some(filter) => filter_row_ids(old_stream, filter),
1528 None => old_stream,
1529 };
1530 let old_input = Arc::new(OneShotExec::new(old_stream));
1531 debug_assert_eq!(
1532 old_input.schema().flattened_fields().len(),
1533 new_input.schema().flattened_fields().len()
1534 );
1535
1536 let sort_expr = PhysicalSortExpr {
1537 expr: Arc::new(Column::new(VALUE_COLUMN_NAME, value_column_index)),
1538 options: SortOptions {
1539 descending: false,
1540 nulls_first: true,
1541 },
1542 };
1543 let all_data = UnionExec::try_new(vec![old_input, new_input])?;
1546 let ordered = Arc::new(SortPreservingMergeExec::new([sort_expr].into(), all_data));
1547
1548 let unchunked = execute_plan(
1549 ordered,
1550 LanceExecutionOptions {
1551 use_spilling: true,
1552 ..Default::default()
1553 },
1554 )?;
1555 Ok(chunk_concat_stream(unchunked, chunk_size as usize))
1556 }
1557}
1558
1559fn filter_row_ids(
1562 stream: SendableRecordBatchStream,
1563 old_data_filter: OldIndexDataFilter,
1564) -> SendableRecordBatchStream {
1565 let schema = stream.schema();
1566 let filtered = stream.map(move |batch_result| {
1567 let batch = batch_result?;
1568 let row_ids = batch[ROW_ID]
1569 .as_any()
1570 .downcast_ref::<arrow_array::UInt64Array>()
1571 .ok_or_else(|| Error::internal("expected UInt64Array for row_id column"))?;
1572 let mask = old_data_filter.filter_row_ids(row_ids);
1573 Ok(arrow_select::filter::filter_record_batch(&batch, &mask)?)
1574 });
1575 Box::pin(RecordBatchStreamAdapter::new(schema, filtered))
1576}
1577
1578fn wrap_bound(bound: &Bound<ScalarValue>) -> Bound<OrderableScalarValue> {
1579 match bound {
1580 Bound::Unbounded => Bound::Unbounded,
1581 Bound::Included(val) => Bound::Included(OrderableScalarValue(val.clone())),
1582 Bound::Excluded(val) => Bound::Excluded(OrderableScalarValue(val.clone())),
1583 }
1584}
1585
1586fn serialize_with_display<T: Display, S: Serializer>(
1587 value: &Option<T>,
1588 serializer: S,
1589) -> std::result::Result<S::Ok, S::Error> {
1590 if let Some(value) = value {
1591 serializer.collect_str(value)
1592 } else {
1593 serializer.collect_str("N/A")
1594 }
1595}
1596
1597#[derive(Serialize)]
1598struct BTreeStatistics {
1599 #[serde(serialize_with = "serialize_with_display")]
1600 min: Option<OrderableScalarValue>,
1601 #[serde(serialize_with = "serialize_with_display")]
1602 max: Option<OrderableScalarValue>,
1603 num_pages: u32,
1604}
1605
1606#[async_trait]
1607impl Index for BTreeIndex {
1608 fn as_any(&self) -> &dyn Any {
1609 self
1610 }
1611
1612 fn as_index(self: Arc<Self>) -> Arc<dyn Index> {
1613 self
1614 }
1615
1616 fn as_vector_index(self: Arc<Self>) -> Result<Arc<dyn crate::vector::VectorIndex>> {
1617 Err(Error::not_supported_source(
1618 "BTreeIndex is not vector index".into(),
1619 ))
1620 }
1621
1622 async fn prewarm(&self) -> Result<()> {
1623 let index_reader = LazyIndexReader::new(self.store.clone(), self.ranges_to_files.clone());
1624 let reader = index_reader.get().await?;
1625 let num_pages = reader.num_batches(self.batch_size).await;
1626 let mut pages = stream::iter(0..num_pages)
1627 .map(|page_idx| {
1628 let index_reader = index_reader.clone();
1629 async move {
1630 let page = self
1631 .read_page(page_idx, index_reader, &NoOpMetricsCollector)
1632 .await?;
1633 Result::Ok((page_idx, page))
1634 }
1635 })
1636 .buffer_unordered(get_num_compute_intensive_cpus());
1637
1638 while let Some((page_idx, page)) = pages.try_next().await? {
1639 let inserted = self
1640 .index_cache
1641 .insert_with_key(
1642 &BTreePageKey {
1643 page_number: page_idx,
1644 },
1645 Arc::new(page),
1646 )
1647 .await;
1648
1649 if !inserted {
1650 return Err(Error::internal(
1651 "Failed to prewarm index: cache is no longer available".to_string(),
1652 ));
1653 }
1654 }
1655
1656 Ok(())
1657 }
1658
1659 fn index_type(&self) -> IndexType {
1660 IndexType::BTree
1661 }
1662
1663 fn statistics(&self) -> Result<serde_json::Value> {
1664 let min = self
1665 .page_lookup
1666 .tree
1667 .first_key_value()
1668 .map(|(k, _)| k.clone());
1669 let max = self
1670 .page_lookup
1671 .tree
1672 .last_key_value()
1673 .map(|(k, _)| k.clone());
1674 serde_json::to_value(&BTreeStatistics {
1675 num_pages: self.page_lookup.tree.len() as u32,
1676 min,
1677 max,
1678 })
1679 .map_err(|err| err.into())
1680 }
1681
1682 async fn calculate_included_frags(&self) -> Result<RoaringBitmap> {
1683 let mut frag_ids = RoaringBitmap::default();
1684
1685 let lazy_reader = LazyIndexReader::new(self.store.clone(), self.ranges_to_files.clone());
1686 let sub_index_reader = lazy_reader.get().await?;
1687 let mut reader_stream = IndexReaderStream::new(sub_index_reader, self.batch_size)
1688 .await
1689 .buffered(self.store.io_parallelism());
1690 while let Some(serialized) = reader_stream.try_next().await? {
1691 let page = FlatIndex::try_new(serialized)?;
1692 frag_ids |= page.calculate_included_frags()?;
1693 }
1694
1695 Ok(frag_ids)
1696 }
1697}
1698
1699#[async_trait]
1700impl ScalarIndex for BTreeIndex {
1701 async fn search(
1702 &self,
1703 query: &dyn AnyQuery,
1704 metrics: &dyn MetricsCollector,
1705 ) -> Result<SearchResult> {
1706 let query = query.as_any().downcast_ref::<SargableQuery>().unwrap();
1707 let mut pages = match query {
1708 SargableQuery::Equals(val) => self
1709 .page_lookup
1710 .pages_eq(&OrderableScalarValue(val.clone())),
1711 SargableQuery::Range(start, end) => self
1712 .page_lookup
1713 .pages_between((wrap_bound(start).as_ref(), wrap_bound(end).as_ref())),
1714 SargableQuery::IsIn(values) => self
1715 .page_lookup
1716 .pages_in(values.iter().map(|val| OrderableScalarValue(val.clone()))),
1717 SargableQuery::FullTextSearch(_) => {
1718 return Err(Error::invalid_input(
1719 "full text search is not supported for BTree index, build a inverted index for it",
1720 ));
1721 }
1722 SargableQuery::IsNull() => self.page_lookup.pages_null(),
1723 SargableQuery::LikePrefix(prefix) => {
1724 match prefix {
1726 ScalarValue::Utf8(Some(s)) => {
1727 let start = Bound::Included(OrderableScalarValue(prefix.clone()));
1728 let end = match compute_next_prefix(s) {
1729 Some(next) => {
1730 Bound::Excluded(OrderableScalarValue(ScalarValue::Utf8(Some(next))))
1731 }
1732 None => Bound::Unbounded,
1733 };
1734 self.page_lookup
1735 .pages_between((start.as_ref(), end.as_ref()))
1736 }
1737 ScalarValue::LargeUtf8(Some(s)) => {
1738 let start = Bound::Included(OrderableScalarValue(prefix.clone()));
1739 let end = match compute_next_prefix(s) {
1740 Some(next) => Bound::Excluded(OrderableScalarValue(
1741 ScalarValue::LargeUtf8(Some(next)),
1742 )),
1743 None => Bound::Unbounded,
1744 };
1745 self.page_lookup
1746 .pages_between((start.as_ref(), end.as_ref()))
1747 }
1748 _ => {
1749 self.page_lookup
1752 .pages_between((Bound::Unbounded, Bound::Unbounded))
1753 }
1754 }
1755 }
1756 };
1757
1758 if !matches!(query, SargableQuery::IsNull()) {
1768 let existing: HashSet<u32> = pages.iter().map(|m| m.page_id()).collect();
1769 for &page_id in self
1770 .page_lookup
1771 .null_pages
1772 .iter()
1773 .chain(self.page_lookup.all_null_pages.iter())
1774 {
1775 if !existing.contains(&page_id) {
1776 pages.push(Matches::Some(page_id));
1777 }
1778 }
1779 }
1780
1781 let lazy_index_reader =
1782 LazyIndexReader::new(self.store.clone(), self.ranges_to_files.clone());
1783 let page_tasks = pages
1784 .into_iter()
1785 .map(|page_index| {
1786 self.search_page(query, page_index, lazy_index_reader.clone(), metrics)
1787 .boxed()
1788 })
1789 .collect::<Vec<_>>();
1790 debug!("Searching {} btree pages", page_tasks.len());
1791
1792 let results: Vec<NullableRowAddrSet> = stream::iter(page_tasks)
1794 .buffered(get_num_compute_intensive_cpus())
1797 .try_collect()
1798 .await?;
1799
1800 let selection = NullableRowAddrSet::union_all(&results);
1802
1803 Ok(SearchResult::Exact(selection))
1804 }
1805
1806 fn can_remap(&self) -> bool {
1807 true
1808 }
1809
1810 async fn remap(
1811 &self,
1812 mapping: &HashMap<u64, Option<u64>>,
1813 dest_store: &dyn IndexStore,
1814 ) -> Result<CreatedIndex> {
1815 let part_page_files: Vec<(Option<u32>, &str)> =
1820 if let Some(ranges_to_files) = &self.ranges_to_files {
1821 ranges_to_files
1823 .iter()
1824 .enumerate()
1825 .map(|(part_id, (_, (path, _)))| (Some(part_id as u32), path.as_str()))
1826 .collect()
1827 } else {
1828 vec![(None, BTREE_PAGES_NAME)]
1830 };
1831
1832 let mapping = Arc::new(mapping.clone());
1833 let train_schema = Arc::new(self.train_schema());
1834
1835 for (part_id, page_file) in part_page_files {
1837 let sub_index_reader = self.store.open_index_file(page_file).await?;
1839 let mapping = mapping.clone();
1840
1841 let train_schema_clone = train_schema.clone();
1842 let train_schema = train_schema.clone();
1843
1844 let remapped_stream = IndexReaderStream::new(sub_index_reader, self.batch_size)
1845 .await
1846 .buffered(self.store.io_parallelism())
1847 .map_err(DataFusionError::from)
1848 .and_then(move |batch| {
1849 let remapped =
1851 FlatIndex::remap_batch(batch, &mapping).map_err(DataFusionError::from);
1852 let with_train_schema = remapped.and_then(|batch| {
1853 RecordBatch::try_new(train_schema.clone(), batch.columns().to_vec())
1854 .map_err(DataFusionError::from)
1855 });
1856 std::future::ready(with_train_schema)
1857 });
1858
1859 let remapped_stream = Box::pin(RecordBatchStreamAdapter::new(
1860 train_schema_clone,
1861 remapped_stream,
1862 ));
1863
1864 train_btree_index(remapped_stream, dest_store, self.batch_size, None, part_id).await?;
1865 }
1866
1867 if let Some(ranges_to_files) = &self.ranges_to_files {
1868 let num_parts = ranges_to_files.len();
1869 let page_files = (0..num_parts)
1871 .map(|part_id| part_page_data_file_path((part_id as u64) << 32))
1872 .collect::<Vec<_>>();
1873 let lookup_files = (0..num_parts)
1874 .map(|part_id| part_lookup_file_path((part_id as u64) << 32))
1875 .collect::<Vec<_>>();
1876 merge_metadata_files(
1877 dest_store,
1878 &page_files,
1879 &lookup_files,
1880 None,
1881 noop_progress(),
1882 )
1883 .await?;
1884 }
1885
1886 Ok(CreatedIndex {
1887 index_details: prost_types::Any::from_msg(&pbold::BTreeIndexDetails::default())
1888 .unwrap(),
1889 index_version: BTREE_INDEX_VERSION,
1890 files: Some(dest_store.list_files_with_sizes().await?),
1891 })
1892 }
1893
1894 async fn update(
1895 &self,
1896 new_data: SendableRecordBatchStream,
1897 dest_store: &dyn IndexStore,
1898 old_data_filter: Option<OldIndexDataFilter>,
1899 ) -> Result<CreatedIndex> {
1900 let merged_data_source = self
1902 .clone()
1903 .combine_old_new(new_data, self.batch_size, old_data_filter)
1904 .await?;
1905 train_btree_index(merged_data_source, dest_store, self.batch_size, None, None).await?;
1906
1907 Ok(CreatedIndex {
1908 index_details: prost_types::Any::from_msg(&pbold::BTreeIndexDetails::default())
1909 .unwrap(),
1910 index_version: BTREE_INDEX_VERSION,
1911 files: Some(dest_store.list_files_with_sizes().await?),
1912 })
1913 }
1914
1915 fn update_criteria(&self) -> UpdateCriteria {
1916 UpdateCriteria::only_new_data(TrainingCriteria::new(TrainingOrdering::Values).with_row_id())
1917 }
1918
1919 fn derive_index_params(&self) -> Result<ScalarIndexParams> {
1920 let params = serde_json::to_value(BTreeParameters {
1921 zone_size: Some(self.batch_size),
1922 range_id: None,
1923 })?;
1924 Ok(ScalarIndexParams::for_builtin(BuiltinIndexType::BTree).with_params(¶ms))
1925 }
1926}
1927
1928struct BatchStats {
1929 min: ScalarValue,
1930 max: ScalarValue,
1931 null_count: u32,
1932}
1933
1934fn analyze_batch(batch: &RecordBatch) -> Result<BatchStats> {
1935 let values = batch.column_by_name(VALUE_COLUMN_NAME).expect_ok()?;
1936 if values.is_empty() {
1937 return Err(Error::internal(
1938 "received an empty batch in btree training".to_string(),
1939 ));
1940 }
1941 let min = ScalarValue::try_from_array(&values, 0)
1942 .map_err(|e| Error::internal(format!("failed to get min value from batch: {}", e)))?;
1943 let max = ScalarValue::try_from_array(&values, values.len() - 1)
1944 .map_err(|e| Error::internal(format!("failed to get max value from batch: {}", e)))?;
1945
1946 Ok(BatchStats {
1947 min,
1948 max,
1949 null_count: values.null_count() as u32,
1950 })
1951}
1952
1953#[async_trait]
1955pub trait BTreeSubIndex: Debug + Send + Sync + DeepSizeOf {
1956 async fn train(&self, batch: RecordBatch) -> Result<RecordBatch>;
1958
1959 async fn load_subindex(&self, serialized: RecordBatch) -> Result<Arc<dyn ScalarIndex>>;
1961
1962 async fn retrieve_data(&self, serialized: RecordBatch) -> Result<RecordBatch>;
1969
1970 fn schema(&self) -> &Arc<Schema>;
1972
1973 async fn remap_subindex(
1975 &self,
1976 serialized: RecordBatch,
1977 mapping: &HashMap<u64, Option<u64>>,
1978 ) -> Result<RecordBatch>;
1979}
1980
1981struct EncodedBatch {
1982 stats: BatchStats,
1983 page_number: u32,
1984}
1985
1986async fn train_btree_page(
1987 batch: RecordBatch,
1988 batch_idx: u32,
1989 writer: &mut dyn IndexWriter,
1990 schema: Arc<Schema>,
1991) -> Result<EncodedBatch> {
1992 let stats = analyze_batch(&batch)?;
1993
1994 let trained = RecordBatch::try_new(
1996 schema.clone(),
1997 vec![
1998 batch.column_by_name(VALUE_COLUMN_NAME).expect_ok()?.clone(),
1999 batch.column_by_name(ROW_ID).expect_ok()?.clone(),
2000 ],
2001 )?;
2002
2003 writer.write_record_batch(trained).await?;
2004 Ok(EncodedBatch {
2005 stats,
2006 page_number: batch_idx,
2007 })
2008}
2009
2010fn btree_stats_as_batch(stats: Vec<EncodedBatch>, value_type: &DataType) -> Result<RecordBatch> {
2011 let mins = if stats.is_empty() {
2012 new_empty_array(value_type)
2013 } else {
2014 ScalarValue::iter_to_array(stats.iter().map(|stat| stat.stats.min.clone()))?
2015 };
2016 let maxs = if stats.is_empty() {
2017 new_empty_array(value_type)
2018 } else {
2019 ScalarValue::iter_to_array(stats.iter().map(|stat| stat.stats.max.clone()))?
2020 };
2021 let null_counts = UInt32Array::from_iter_values(stats.iter().map(|stat| stat.stats.null_count));
2022 let page_numbers = UInt32Array::from_iter_values(stats.iter().map(|stat| stat.page_number));
2023
2024 let schema = Arc::new(Schema::new(vec![
2025 Field::new("min", mins.data_type().clone(), true),
2027 Field::new("max", maxs.data_type().clone(), true),
2028 Field::new("null_count", null_counts.data_type().clone(), false),
2029 Field::new("page_idx", page_numbers.data_type().clone(), false),
2030 ]));
2031
2032 let columns = vec![
2033 mins,
2034 maxs,
2035 Arc::new(null_counts) as Arc<dyn Array>,
2036 Arc::new(page_numbers) as Arc<dyn Array>,
2037 ];
2038
2039 Ok(RecordBatch::try_new(schema, columns)?)
2040}
2041
2042pub async fn train_btree_index(
2044 batches_source: SendableRecordBatchStream,
2045 index_store: &dyn IndexStore,
2046 batch_size: u64,
2047 fragment_ids: Option<Vec<u32>>,
2048 range_id: Option<u32>,
2049) -> Result<()> {
2050 let partition_id = fragment_ids
2055 .as_ref()
2056 .and_then(|frag_ids| frag_ids.first())
2060 .map(|&first_frag_id| (first_frag_id as u64) << 32)
2061 .or_else(|| range_id.map(|id| (id as u64) << 32));
2065
2066 let flat_schema = Arc::new(Schema::new(vec![
2067 Field::new(
2068 BTREE_VALUES_COLUMN,
2069 batches_source.schema().field(0).data_type().clone(),
2070 true,
2071 ),
2072 Field::new(BTREE_IDS_COLUMN, DataType::UInt64, false),
2073 ]));
2074
2075 let mut sub_index_file = match partition_id {
2076 None => {
2077 index_store
2078 .new_index_file(BTREE_PAGES_NAME, flat_schema.clone())
2079 .await?
2080 }
2081 Some(partition_id) => {
2082 index_store
2083 .new_index_file(
2084 part_page_data_file_path(partition_id).as_str(),
2085 flat_schema.clone(),
2086 )
2087 .await?
2088 }
2089 };
2090
2091 let mut encoded_batches = Vec::new();
2092 let mut batch_idx = 0;
2093
2094 let value_type = batches_source
2095 .schema()
2096 .field_with_name(VALUE_COLUMN_NAME)?
2097 .data_type()
2098 .clone();
2099
2100 let mut batches_source = chunk_concat_stream(batches_source, batch_size as usize);
2101
2102 while let Some(batch) = batches_source.try_next().await? {
2103 encoded_batches.push(
2104 train_btree_page(
2105 batch,
2106 batch_idx,
2107 sub_index_file.as_mut(),
2108 flat_schema.clone(),
2109 )
2110 .await?,
2111 );
2112 batch_idx += 1;
2113 }
2114 sub_index_file.finish().await?;
2115 let record_batch = btree_stats_as_batch(encoded_batches, &value_type)?;
2116 let mut file_schema = record_batch.schema().as_ref().clone();
2117 file_schema
2118 .metadata
2119 .insert(BATCH_SIZE_META_KEY.to_string(), batch_size.to_string());
2120 file_schema.metadata.insert(
2121 RANGE_PARTITIONED_META_KEY.to_string(),
2122 range_id.is_some().to_string(),
2123 );
2124 let mut btree_index_file = match partition_id {
2125 None => {
2126 index_store
2127 .new_index_file(BTREE_LOOKUP_NAME, Arc::new(file_schema))
2128 .await?
2129 }
2130 Some(partition_id) => {
2131 index_store
2132 .new_index_file(
2133 part_lookup_file_path(partition_id).as_str(),
2134 Arc::new(file_schema),
2135 )
2136 .await?
2137 }
2138 };
2139 btree_index_file.write_record_batch(record_batch).await?;
2140 btree_index_file.finish().await?;
2141 Ok(())
2142}
2143
2144pub async fn merge_index_files(
2145 object_store: &ObjectStore,
2146 index_dir: &Path,
2147 store: Arc<dyn IndexStore>,
2148 batch_readhead: Option<usize>,
2149 progress: Arc<dyn IndexBuildProgress>,
2150) -> Result<()> {
2151 let (part_page_files, part_lookup_files) =
2153 list_page_lookup_files(object_store, index_dir).await?;
2154 merge_metadata_files(
2155 store.as_ref(),
2156 &part_page_files,
2157 &part_lookup_files,
2158 batch_readhead,
2159 progress,
2160 )
2161 .await
2162}
2163
2164async fn list_page_lookup_files(
2167 object_store: &ObjectStore,
2168 index_dir: &Path,
2169) -> Result<(Vec<String>, Vec<String>)> {
2170 let mut part_page_files = Vec::new();
2171 let mut part_lookup_files = Vec::new();
2172
2173 let mut list_stream = object_store.list(Some(index_dir.clone()));
2174
2175 while let Some(item) = list_stream.next().await {
2176 match item {
2177 Ok(meta) => {
2178 let file_name = meta.location.filename().unwrap_or_default();
2179 if file_name.starts_with("part_") && file_name.ends_with("_page_data.lance") {
2181 part_page_files.push(file_name.to_string());
2182 }
2183 if file_name.starts_with("part_") && file_name.ends_with("_page_lookup.lance") {
2185 part_lookup_files.push(file_name.to_string());
2186 }
2187 }
2188 Err(_) => continue,
2189 }
2190 }
2191
2192 if part_page_files.is_empty() || part_lookup_files.is_empty() {
2193 return Err(Error::internal(format!(
2194 "No partition metadata files found in index directory: {} (page_files: {}, lookup_files: {})",
2195 index_dir,
2196 part_page_files.len(),
2197 part_lookup_files.len()
2198 )));
2199 }
2200
2201 Ok((part_page_files, part_lookup_files))
2202}
2203
2204fn find_single_partition_files(
2205 files: &[lance_table::format::IndexFile],
2206) -> Result<Option<(&str, &str)>> {
2207 let lookup_files = files
2208 .iter()
2209 .filter_map(|file| {
2210 (file.path.starts_with("part_") && file.path.ends_with("_page_lookup.lance"))
2211 .then_some(file.path.as_str())
2212 })
2213 .collect::<Vec<_>>();
2214 let page_files = files
2215 .iter()
2216 .filter_map(|file| {
2217 (file.path.starts_with("part_") && file.path.ends_with("_page_data.lance"))
2218 .then_some(file.path.as_str())
2219 })
2220 .collect::<Vec<_>>();
2221
2222 if lookup_files.len() != 1 || page_files.len() != 1 {
2223 return Ok(None);
2224 }
2225
2226 let lookup_partition_id = extract_partition_id(lookup_files[0])?;
2227 let page_partition_id = extract_partition_id(page_files[0])?;
2228 if lookup_partition_id != page_partition_id {
2229 return Ok(None);
2230 }
2231
2232 Ok(Some((lookup_files[0], page_files[0])))
2233}
2234
2235fn is_missing_lookup_error(err: &Error) -> bool {
2236 matches!(err, Error::NotFound { .. })
2237 || matches!(
2238 err,
2239 Error::IO { source, .. }
2240 if source
2241 .downcast_ref::<ObjectStoreError>()
2242 .map(|os_err| matches!(os_err, ObjectStoreError::NotFound { .. }))
2243 .unwrap_or(false)
2244 )
2245}
2246
2247async fn merge_metadata_files(
2254 store: &dyn IndexStore,
2255 part_page_files: &[String],
2256 part_lookup_files: &[String],
2257 batch_readhead: Option<usize>,
2258 progress: Arc<dyn IndexBuildProgress>,
2259) -> Result<()> {
2260 if part_lookup_files.is_empty() || part_page_files.is_empty() {
2261 return Err(Error::internal(
2262 "No partition files provided for merging".to_string(),
2263 ));
2264 }
2265
2266 if part_lookup_files.len() != part_page_files.len() {
2268 return Err(Error::internal(format!(
2269 "Number of partition lookup files ({}) does not match number of partition page files ({})",
2270 part_lookup_files.len(),
2271 part_page_files.len()
2272 )));
2273 }
2274 let mut page_files_map = HashMap::new();
2275 for page_file in part_page_files {
2276 let partition_id = extract_partition_id(page_file)?;
2277 page_files_map.insert(partition_id, page_file);
2278 }
2279
2280 for lookup_file in part_lookup_files {
2282 let partition_id = extract_partition_id(lookup_file)?;
2283 if !page_files_map.contains_key(&partition_id) {
2284 return Err(Error::internal(format!(
2285 "No corresponding page file found for lookup file: {} (partition_id: {})",
2286 lookup_file, partition_id
2287 )));
2288 }
2289 }
2290
2291 let first_lookup_reader = store.open_index_file(&part_lookup_files[0]).await?;
2293 let batch_size = first_lookup_reader
2294 .schema()
2295 .metadata
2296 .get(BATCH_SIZE_META_KEY)
2297 .map(|bs| bs.parse().unwrap_or(DEFAULT_BTREE_BATCH_SIZE))
2298 .unwrap_or(DEFAULT_BTREE_BATCH_SIZE);
2299 let range_partitioned = first_lookup_reader
2300 .schema()
2301 .metadata
2302 .get(RANGE_PARTITIONED_META_KEY)
2303 .map(|bs| bs.parse().unwrap_or(DEFAULT_RANGE_PARTITIONED))
2304 .unwrap_or(DEFAULT_RANGE_PARTITIONED);
2305
2306 let value_type = first_lookup_reader
2308 .schema()
2309 .fields
2310 .first()
2311 .unwrap()
2312 .data_type();
2313
2314 let mut metadata = HashMap::new();
2315 metadata.insert(BATCH_SIZE_META_KEY.to_string(), batch_size.to_string());
2316 let lookup_schema = Arc::new(Schema::new(vec![
2317 Field::new("min", value_type.clone(), true),
2318 Field::new("max", value_type.clone(), true),
2319 Field::new("null_count", DataType::UInt32, false),
2320 Field::new("page_idx", DataType::UInt32, false),
2321 ]));
2322
2323 if range_partitioned {
2325 merge_range_partitioned_lookups(
2326 store,
2327 part_lookup_files,
2328 lookup_schema,
2329 metadata,
2330 batch_size,
2331 batch_readhead,
2332 progress,
2333 )
2334 .await
2335 } else {
2336 merge_pages_and_lookups(
2337 store,
2338 part_page_files,
2339 part_lookup_files,
2340 &page_files_map,
2341 lookup_schema,
2342 metadata,
2343 batch_size,
2344 batch_readhead,
2345 progress,
2346 )
2347 .await
2348 }
2349}
2350
2351async fn merge_range_partitioned_lookups(
2381 store: &dyn IndexStore,
2382 part_lookup_files: &[String],
2383 lookup_schema: Arc<Schema>,
2384 mut metadata: HashMap<String, String>,
2385 batch_size: u64,
2386 batch_readhead: Option<usize>,
2387 progress: Arc<dyn IndexBuildProgress>,
2388) -> Result<()> {
2389 let sorted_part_lookup_files = sort_files_by_partition_id(part_lookup_files)?;
2390 let mut lookup_file = store
2391 .new_index_file(BTREE_LOOKUP_NAME, lookup_schema)
2392 .await?;
2393
2394 let mut pages_per_file: Vec<(u64, u32)> = Vec::with_capacity(sorted_part_lookup_files.len());
2396 let mut num_pages_written = 0u32;
2397
2398 progress
2399 .stage_start(
2400 "merge_lookups",
2401 Some(sorted_part_lookup_files.len() as u64),
2402 "files",
2403 )
2404 .await?;
2405
2406 for (idx, (part_id, part_lookup_file)) in sorted_part_lookup_files.into_iter().enumerate() {
2407 let lookup_reader = store.open_index_file(&part_lookup_file).await?;
2408 let reader_stream = IndexReaderStream::new(lookup_reader.clone(), batch_size).await;
2409 let mut stream = reader_stream.buffered(batch_readhead.unwrap_or(1)).boxed();
2410 while let Some(batch) = stream.next().await {
2411 let original_batch = batch?;
2412 let modified_batch = add_offset_to_page_idx(&original_batch, num_pages_written)?;
2413 lookup_file.write_record_batch(modified_batch).await?;
2414 }
2415 pages_per_file.push((part_id, lookup_reader.num_rows() as u32));
2416 num_pages_written += lookup_reader.num_rows() as u32;
2417 progress
2418 .stage_progress("merge_lookups", idx as u64 + 1)
2419 .await?;
2420 }
2421
2422 metadata.insert(RANGE_PARTITIONED_META_KEY.to_string(), "true".to_string());
2423 metadata.insert(
2424 PAGE_NUM_PER_RANGE_PARTITION_META_KEY.to_string(),
2425 serde_json::to_string(&pages_per_file)?,
2426 );
2427
2428 lookup_file.finish_with_metadata(metadata).await?;
2429 progress.stage_complete("merge_lookups").await?;
2430
2431 cleanup_partition_files(store, part_lookup_files, &[]).await;
2433 Ok(())
2434}
2435
2436#[allow(clippy::too_many_arguments)]
2442async fn merge_pages_and_lookups(
2443 store: &dyn IndexStore,
2444 part_page_files: &[String],
2445 part_lookup_files: &[String],
2446 page_files_map: &HashMap<u64, &String>,
2447 lookup_schema: Arc<Schema>,
2448 metadata: HashMap<String, String>,
2449 batch_size: u64,
2450 batch_readhead: Option<usize>,
2451 progress: Arc<dyn IndexBuildProgress>,
2452) -> Result<()> {
2453 let partition_id = extract_partition_id(part_lookup_files[0].as_str())?;
2455 let page_file = page_files_map.get(&partition_id).unwrap();
2456 let page_reader = store.open_index_file(page_file).await?;
2457 let page_schema = page_reader.schema().clone();
2458
2459 let arrow_schema = Arc::new(Schema::from(&page_schema));
2460 let mut page_file = store
2461 .new_index_file(BTREE_PAGES_NAME, arrow_schema.clone())
2462 .await?;
2463 progress.stage_start("merge_pages", None, "pages").await?;
2464 let lookup_entries = merge_pages(
2465 part_lookup_files,
2466 page_files_map,
2467 store,
2468 batch_size,
2469 &mut page_file,
2470 arrow_schema.clone(),
2471 batch_readhead,
2472 progress.clone(),
2473 )
2474 .await?;
2475 page_file.finish().await?;
2476 progress.stage_complete("merge_pages").await?;
2477
2478 let lookup_batch = RecordBatch::try_new(
2479 lookup_schema.clone(),
2480 vec![
2481 ScalarValue::iter_to_array(lookup_entries.iter().map(|(min, _, _, _)| min.clone()))?,
2482 ScalarValue::iter_to_array(lookup_entries.iter().map(|(_, max, _, _)| max.clone()))?,
2483 Arc::new(UInt32Array::from_iter_values(
2484 lookup_entries
2485 .iter()
2486 .map(|(_, _, null_count, _)| *null_count),
2487 )),
2488 Arc::new(UInt32Array::from_iter_values(
2489 lookup_entries.iter().map(|(_, _, _, page_idx)| *page_idx),
2490 )),
2491 ],
2492 )?;
2493 let mut lookup_file = store
2494 .new_index_file(BTREE_LOOKUP_NAME, lookup_schema)
2495 .await?;
2496 progress
2497 .stage_start("write_lookup_file", Some(1), "files")
2498 .await?;
2499 lookup_file.write_record_batch(lookup_batch).await?;
2500 lookup_file.finish_with_metadata(metadata).await?;
2501 progress.stage_progress("write_lookup_file", 1).await?;
2502 progress.stage_complete("write_lookup_file").await?;
2503
2504 cleanup_partition_files(store, part_lookup_files, part_page_files).await;
2507
2508 Ok(())
2509}
2510
2511fn add_offset_to_page_idx(batch: &RecordBatch, offset: u32) -> Result<RecordBatch> {
2513 let (page_idx_pos, _) = batch.schema().column_with_name("page_idx").ok_or_else(|| {
2514 Error::internal("Column 'page_idx' not found in RecordBatch schema".to_string())
2515 })?;
2516 let page_idx_array = batch
2517 .column(page_idx_pos)
2518 .as_any()
2519 .downcast_ref::<UInt32Array>()
2520 .ok_or_else(|| {
2521 Error::internal("Failed to downcast 'page_idx' column to UInt32Array".to_string())
2522 })?;
2523 let offset_array = UInt32Array::from(vec![offset; page_idx_array.len()]);
2524 let new_page_idx_array_ref = add(page_idx_array, &offset_array)?;
2525 let mut new_columns = batch.columns().to_vec();
2526 new_columns[page_idx_pos] = new_page_idx_array_ref;
2527 let new_batch = RecordBatch::try_new(batch.schema(), new_columns)?;
2528 Ok(new_batch)
2529}
2530
2531#[allow(clippy::too_many_arguments)]
2534async fn merge_pages(
2535 part_lookup_files: &[String],
2536 page_files_map: &HashMap<u64, &String>,
2537 store: &dyn IndexStore,
2538 batch_size: u64,
2539 page_file: &mut Box<dyn IndexWriter>,
2540 arrow_schema: Arc<Schema>,
2541 batch_readhead: Option<usize>,
2542 progress: Arc<dyn IndexBuildProgress>,
2543) -> Result<Vec<(ScalarValue, ScalarValue, u32, u32)>> {
2544 let mut lookup_entries = Vec::new();
2545 let mut page_idx = 0u32;
2546
2547 debug!(
2548 "Starting SortPreservingMerge with {} partitions",
2549 part_lookup_files.len()
2550 );
2551
2552 let value_field = arrow_schema.field(0).clone().with_name(VALUE_COLUMN_NAME);
2553 let row_id_field = arrow_schema.field(1).clone().with_name(ROW_ID);
2554 let stream_schema = Arc::new(Schema::new(vec![value_field, row_id_field]));
2555
2556 let mut inputs: Vec<Arc<dyn ExecutionPlan>> = Vec::new();
2558 for lookup_file in part_lookup_files {
2559 let partition_id = extract_partition_id(lookup_file)?;
2560 let page_file_name = (*page_files_map.get(&partition_id).ok_or_else(|| {
2561 Error::internal(format!(
2562 "Page file not found for partition ID: {}",
2563 partition_id
2564 ))
2565 })?)
2566 .clone();
2567
2568 let reader = store.open_index_file(&page_file_name).await?;
2569
2570 let reader_stream = IndexReaderStream::new(reader, batch_size).await;
2571
2572 let stream = reader_stream
2573 .map(|fut| fut.map_err(DataFusionError::from))
2574 .buffered(batch_readhead.unwrap_or(1))
2575 .boxed();
2576
2577 let sendable_stream =
2578 Box::pin(RecordBatchStreamAdapter::new(stream_schema.clone(), stream));
2579 inputs.push(Arc::new(OneShotExec::new(sendable_stream)));
2580 }
2581
2582 let union_inputs = UnionExec::try_new(inputs)?;
2584
2585 let value_column_index = stream_schema.index_of(VALUE_COLUMN_NAME)?;
2587 let sort_expr = PhysicalSortExpr {
2588 expr: Arc::new(Column::new(VALUE_COLUMN_NAME, value_column_index)),
2589 options: SortOptions {
2590 descending: false,
2591 nulls_first: true,
2592 },
2593 };
2594
2595 let merge_exec = Arc::new(SortPreservingMergeExec::new(
2596 [sort_expr].into(),
2597 union_inputs,
2598 ));
2599
2600 let unchunked = execute_plan(
2601 merge_exec,
2602 LanceExecutionOptions {
2603 use_spilling: false,
2604 ..Default::default()
2605 },
2606 )?;
2607
2608 let mut chunked_stream = chunk_concat_stream(unchunked, batch_size as usize);
2610
2611 while let Some(batch) = chunked_stream.try_next().await? {
2613 let writer_batch = RecordBatch::try_new(
2614 arrow_schema.clone(),
2615 vec![batch.column(0).clone(), batch.column(1).clone()],
2616 )?;
2617
2618 page_file.write_record_batch(writer_batch).await?;
2619
2620 let min_val = ScalarValue::try_from_array(batch.column(0), 0)?;
2621 let max_val = ScalarValue::try_from_array(batch.column(0), batch.num_rows() - 1)?;
2622 let null_count = batch.column(0).null_count() as u32;
2623
2624 lookup_entries.push((min_val, max_val, null_count, page_idx));
2625 page_idx += 1;
2626 progress
2627 .stage_progress("merge_pages", page_idx as u64)
2628 .await?;
2629 }
2630
2631 Ok(lookup_entries)
2632}
2633
2634fn sort_files_by_partition_id(part_files: &[String]) -> Result<Vec<(u64, String)>> {
2636 let mut files_with_ids: Vec<(u64, &String)> = part_files
2637 .iter()
2638 .map(|file| extract_partition_id(file).map(|id| (id, file)))
2639 .collect::<Result<Vec<_>>>()?;
2640
2641 files_with_ids.sort_unstable_by_key(|k| k.0);
2642
2643 let sorted_files = files_with_ids
2644 .into_iter()
2645 .map(|(id, file)| (id, file.clone()))
2646 .collect();
2647
2648 Ok(sorted_files)
2649}
2650
2651fn extract_partition_id(filename: &str) -> Result<u64> {
2654 if !filename.starts_with("part_") {
2655 return Err(Error::internal(format!(
2656 "Invalid partition file name format: {}",
2657 filename
2658 )));
2659 }
2660
2661 let parts: Vec<&str> = filename.split('_').collect();
2662 if parts.len() < 3 {
2663 return Err(Error::internal(format!(
2664 "Invalid partition file name format: {}",
2665 filename
2666 )));
2667 }
2668
2669 parts[1].parse::<u64>().map_err(|_| {
2670 Error::internal(format!(
2671 "Failed to parse partition ID from filename: {}",
2672 filename
2673 ))
2674 })
2675}
2676
2677async fn cleanup_partition_files(
2682 store: &dyn IndexStore,
2683 part_lookup_files: &[String],
2684 part_page_files: &[String],
2685) {
2686 for file_name in part_lookup_files {
2688 cleanup_single_file(
2689 store,
2690 file_name,
2691 "part_",
2692 "_page_lookup.lance",
2693 "partition lookup",
2694 )
2695 .await;
2696 }
2697
2698 for file_name in part_page_files {
2700 cleanup_single_file(
2701 store,
2702 file_name,
2703 "part_",
2704 "_page_data.lance",
2705 "partition page",
2706 )
2707 .await;
2708 }
2709}
2710
2711async fn cleanup_single_file(
2715 store: &dyn IndexStore,
2716 file_name: &str,
2717 expected_prefix: &str,
2718 expected_suffix: &str,
2719 file_type: &str,
2720) {
2721 if file_name.starts_with(expected_prefix) && file_name.ends_with(expected_suffix) {
2722 match store.delete_index_file(file_name).await {
2723 Ok(()) => {
2724 debug!("Successfully deleted {} file: {}", file_type, file_name);
2725 }
2726 Err(e) => {
2727 warn!(
2728 "Failed to delete {} file '{}': {}. \
2729 This does not affect the merge operation, but may leave \
2730 partition files that should be cleaned up manually.",
2731 file_type, file_name, e
2732 );
2733 }
2734 }
2735 } else {
2736 warn!(
2738 "Skipping deletion of file '{}' as it does not match the expected \
2739 {} file pattern ({}*{})",
2740 file_name, file_type, expected_prefix, expected_suffix
2741 );
2742 }
2743}
2744
2745pub(crate) fn part_page_data_file_path(partition_id: u64) -> String {
2746 format!("part_{}_{}", partition_id, BTREE_PAGES_NAME)
2747}
2748
2749pub(crate) fn part_lookup_file_path(partition_id: u64) -> String {
2750 format!("part_{}_{}", partition_id, BTREE_LOOKUP_NAME)
2751}
2752
2753struct IndexReaderStream {
2757 reader: Arc<dyn IndexReader>,
2758 batch_size: u64,
2759 num_batches: u32,
2760 batch_idx: u32,
2761}
2762
2763impl IndexReaderStream {
2764 async fn new(reader: Arc<dyn IndexReader>, batch_size: u64) -> Self {
2765 let num_batches = reader.num_batches(batch_size).await;
2766 Self {
2767 reader,
2768 batch_size,
2769 num_batches,
2770 batch_idx: 0,
2771 }
2772 }
2773}
2774
2775impl Stream for IndexReaderStream {
2776 type Item = BoxFuture<'static, Result<RecordBatch>>;
2777
2778 fn poll_next(
2779 self: std::pin::Pin<&mut Self>,
2780 _cx: &mut std::task::Context<'_>,
2781 ) -> std::task::Poll<Option<Self::Item>> {
2782 let this = self.get_mut();
2783 if this.batch_idx >= this.num_batches {
2784 return std::task::Poll::Ready(None);
2785 }
2786 let batch_num = this.batch_idx;
2787 this.batch_idx += 1;
2788 let reader_copy = this.reader.clone();
2789 let batch_size = this.batch_size;
2790 let read_task = async move {
2791 reader_copy
2792 .read_record_batch(batch_num as u64, batch_size)
2793 .await
2794 }
2795 .boxed();
2796 std::task::Poll::Ready(Some(read_task))
2797 }
2798}
2799
2800#[derive(Debug, Serialize, Deserialize)]
2802pub struct BTreeParameters {
2803 pub zone_size: Option<u64>,
2805
2806 pub range_id: Option<u32>,
2830}
2831
2832struct BTreeTrainingRequest {
2833 parameters: BTreeParameters,
2834 criteria: TrainingCriteria,
2835}
2836
2837impl BTreeTrainingRequest {
2838 pub fn new(parameters: BTreeParameters) -> Self {
2839 Self {
2840 parameters,
2841 criteria: TrainingCriteria::new(TrainingOrdering::Values).with_row_id(),
2843 }
2844 }
2845}
2846
2847impl TrainingRequest for BTreeTrainingRequest {
2848 fn as_any(&self) -> &dyn std::any::Any {
2849 self
2850 }
2851
2852 fn criteria(&self) -> &TrainingCriteria {
2853 &self.criteria
2854 }
2855}
2856
2857#[derive(Debug, Default)]
2858pub struct BTreeIndexPlugin;
2859
2860#[async_trait]
2861impl ScalarIndexPlugin for BTreeIndexPlugin {
2862 fn name(&self) -> &str {
2863 "BTree"
2864 }
2865
2866 fn new_training_request(
2867 &self,
2868 params: &str,
2869 field: &Field,
2870 ) -> Result<Box<dyn TrainingRequest>> {
2871 if field.data_type().is_nested() {
2872 return Err(Error::invalid_input_source(
2873 "A btree index can only be created on a non-nested field.".into(),
2874 ));
2875 }
2876
2877 let params = serde_json::from_str::<BTreeParameters>(params)?;
2878 Ok(Box::new(BTreeTrainingRequest::new(params)))
2879 }
2880
2881 fn provides_exact_answer(&self) -> bool {
2882 true
2883 }
2884
2885 fn version(&self) -> u32 {
2886 BTREE_INDEX_VERSION
2887 }
2888
2889 fn new_query_parser(
2890 &self,
2891 index_name: String,
2892 _index_details: &prost_types::Any,
2893 ) -> Option<Box<dyn ScalarQueryParser>> {
2894 Some(Box::new(SargableQueryParser::new(
2895 index_name,
2896 self.name().to_string(),
2897 false,
2898 )))
2899 }
2900
2901 async fn train_index(
2902 &self,
2903 data: SendableRecordBatchStream,
2904 index_store: &dyn IndexStore,
2905 request: Box<dyn TrainingRequest>,
2906 fragment_ids: Option<Vec<u32>>,
2907 _progress: Arc<dyn crate::progress::IndexBuildProgress>,
2908 ) -> Result<CreatedIndex> {
2909 let request = request
2910 .as_any()
2911 .downcast_ref::<BTreeTrainingRequest>()
2912 .unwrap();
2913 train_btree_index(
2914 data,
2915 index_store,
2916 request
2917 .parameters
2918 .zone_size
2919 .unwrap_or(DEFAULT_BTREE_BATCH_SIZE),
2920 fragment_ids,
2921 request.parameters.range_id,
2922 )
2923 .await?;
2924 Ok(CreatedIndex {
2925 index_details: prost_types::Any::from_msg(&pbold::BTreeIndexDetails::default())
2926 .unwrap(),
2927 index_version: BTREE_INDEX_VERSION,
2928 files: Some(index_store.list_files_with_sizes().await?),
2929 })
2930 }
2931
2932 async fn load_index(
2933 &self,
2934 index_store: Arc<dyn IndexStore>,
2935 _index_details: &prost_types::Any,
2936 frag_reuse_index: Option<Arc<FragReuseIndex>>,
2937 cache: &LanceCache,
2938 ) -> Result<Arc<dyn ScalarIndex>> {
2939 Ok(BTreeIndex::load(index_store, frag_reuse_index, cache).await? as Arc<dyn ScalarIndex>)
2940 }
2941
2942 async fn get_from_cache(
2943 &self,
2944 index_store: Arc<dyn IndexStore>,
2945 frag_reuse_index: Option<Arc<FragReuseIndex>>,
2946 cache: &LanceCache,
2947 ) -> Result<Option<Arc<dyn ScalarIndex>>> {
2948 let Some(state) = cache.get_with_key(&BTreeIndexStateKey).await else {
2949 return Ok(None);
2950 };
2951 Ok(Some(state.reconstruct(
2952 index_store,
2953 cache,
2954 frag_reuse_index,
2955 )?))
2956 }
2957
2958 async fn put_in_cache(&self, cache: &LanceCache, index: Arc<dyn ScalarIndex>) -> Result<()> {
2959 let btree = index.as_any().downcast_ref::<BTreeIndex>().ok_or_else(|| {
2960 Error::internal("BTreeIndexPlugin::put_in_cache called with a non-BTree index")
2961 })?;
2962 let state = BTreeIndexState {
2963 lookup_batch: btree.lookup_batch.clone(),
2964 batch_size: btree.batch_size,
2965 ranges_to_files: btree.ranges_to_files.clone(),
2966 };
2967 cache
2968 .insert_with_key(&BTreeIndexStateKey, Arc::new(state))
2969 .await;
2970 Ok(())
2971 }
2972}
2973
2974#[cfg(test)]
2975mod tests {
2976 use std::sync::atomic::Ordering;
2977 use std::{collections::HashMap, sync::Arc};
2978
2979 use arrow::datatypes::{Float32Type, Float64Type, Int32Type, UInt64Type};
2980 use arrow_array::{FixedSizeListArray, record_batch};
2981 use datafusion::{
2982 execution::{SendableRecordBatchStream, TaskContext},
2983 physical_plan::{ExecutionPlan, sorts::sort::SortExec, stream::RecordBatchStreamAdapter},
2984 };
2985 use datafusion_common::{DataFusionError, ScalarValue};
2986 use datafusion_physical_expr::{PhysicalSortExpr, expressions::col};
2987 use deepsize::DeepSizeOf;
2988 use futures::TryStreamExt;
2989 use futures::stream;
2990 use lance_core::utils::mask::RowSetOps;
2991 use lance_core::utils::tempfile::TempObjDir;
2992 use lance_core::{cache::LanceCache, utils::mask::RowAddrTreeMap};
2993 use lance_datafusion::{chunker::break_stream, datagen::DatafusionDatagenExt};
2994 use lance_datagen::{ArrayGeneratorExt, BatchCount, RowCount, array, gen_batch};
2995 use lance_io::object_store::ObjectStore;
2996 use object_store::path::Path;
2997
2998 use crate::metrics::LocalMetricsCollector;
2999 use crate::progress::{IndexBuildProgress, noop_progress};
3000 use crate::{
3001 metrics::NoOpMetricsCollector,
3002 scalar::{
3003 IndexStore, OldIndexDataFilter, SargableQuery, ScalarIndex, SearchResult,
3004 btree::{BTREE_PAGES_NAME, BTreeIndex},
3005 lance_format::LanceIndexStore,
3006 },
3007 };
3008
3009 use super::{
3010 BTreeIndexPlugin, BTreeIndexState, BTreePageKey, DEFAULT_BTREE_BATCH_SIZE,
3011 OrderableScalarValue, part_lookup_file_path, part_page_data_file_path, train_btree_index,
3012 };
3013 use crate::scalar::registry::ScalarIndexPlugin;
3014 use arrow_array::RecordBatch;
3015 use lance_core::cache::{CacheCodecImpl, CacheKey};
3016 use rangemap::RangeInclusiveMap;
3017
3018 lance_testing::define_stage_event_progress!(
3019 RecordingProgress,
3020 IndexBuildProgress,
3021 lance_core::Result<()>
3022 );
3023 #[test]
3024 fn test_scalar_value_size() {
3025 let size_of_i32 = OrderableScalarValue(ScalarValue::Int32(Some(0))).deep_size_of();
3026 let size_of_many_i32 = OrderableScalarValue(ScalarValue::FixedSizeList(Arc::new(
3027 FixedSizeListArray::from_iter_primitive::<Int32Type, _, _>(
3028 vec![Some(vec![Some(0); 128])],
3029 128,
3030 ),
3031 )))
3032 .deep_size_of();
3033
3034 assert!(size_of_i32 > 4);
3036 assert!(size_of_many_i32 > 128 * 4);
3037 }
3038
3039 #[tokio::test]
3040 async fn test_null_ids() {
3041 let tmpdir = TempObjDir::default();
3042 let test_store = Arc::new(LanceIndexStore::new(
3043 Arc::new(ObjectStore::local()),
3044 tmpdir.clone(),
3045 Arc::new(LanceCache::no_cache()),
3046 ));
3047
3048 let stream = gen_batch()
3050 .col(
3051 "value",
3052 array::rand::<Float32Type>().with_nulls(&[true, false, false, false, false]),
3053 )
3054 .col("_rowid", array::step::<UInt64Type>())
3055 .into_df_stream(RowCount::from(5000), BatchCount::from(10));
3056
3057 train_btree_index(stream, test_store.as_ref(), 5000, None, None)
3058 .await
3059 .unwrap();
3060
3061 let index = BTreeIndex::load(test_store.clone(), None, &LanceCache::no_cache())
3062 .await
3063 .unwrap();
3064
3065 assert_eq!(index.page_lookup.null_pages.len(), 10);
3066
3067 let remap_dir = TempObjDir::default();
3068 let remap_store = Arc::new(LanceIndexStore::new(
3069 Arc::new(ObjectStore::local()),
3070 remap_dir.clone(),
3071 Arc::new(LanceCache::no_cache()),
3072 ));
3073
3074 index
3076 .remap(&HashMap::default(), remap_store.as_ref())
3077 .await
3078 .unwrap();
3079
3080 let remap_index = BTreeIndex::load(remap_store.clone(), None, &LanceCache::no_cache())
3081 .await
3082 .unwrap();
3083
3084 assert_eq!(remap_index.page_lookup, index.page_lookup);
3085
3086 let original_pages = test_store.open_index_file(BTREE_PAGES_NAME).await.unwrap();
3087 let remapped_pages = remap_store.open_index_file(BTREE_PAGES_NAME).await.unwrap();
3088
3089 assert_eq!(original_pages.num_rows(), remapped_pages.num_rows());
3090
3091 let original_data = original_pages
3092 .read_record_batch(0, original_pages.num_rows() as u64)
3093 .await
3094 .unwrap();
3095 let remapped_data = remapped_pages
3096 .read_record_batch(0, remapped_pages.num_rows() as u64)
3097 .await
3098 .unwrap();
3099
3100 assert_eq!(original_data, remapped_data);
3101 }
3102
3103 #[tokio::test]
3104 async fn test_nan_ordering() {
3105 let tmpdir = TempObjDir::default();
3106 let test_store = Arc::new(LanceIndexStore::new(
3107 Arc::new(ObjectStore::local()),
3108 tmpdir.clone(),
3109 Arc::new(LanceCache::no_cache()),
3110 ));
3111
3112 let values = vec![
3113 0.0,
3114 1.0,
3115 2.0,
3116 3.0,
3117 f64::NAN,
3118 f64::NEG_INFINITY,
3119 f64::INFINITY,
3120 ];
3121
3122 let data = gen_batch()
3126 .col("value", array::cycle::<Float64Type>(values.clone()))
3127 .col("_rowid", array::step::<UInt64Type>())
3128 .into_df_exec(RowCount::from(10), BatchCount::from(100));
3129 let schema = data.schema();
3130 let sort_expr = PhysicalSortExpr::new_default(col("value", schema.as_ref()).unwrap());
3131 let plan = Arc::new(SortExec::new([sort_expr].into(), data));
3132 let stream = plan.execute(0, Arc::new(TaskContext::default())).unwrap();
3133 let stream = break_stream(stream, 64);
3134 let stream = stream.map_err(DataFusionError::from);
3135 let stream =
3136 Box::pin(RecordBatchStreamAdapter::new(schema, stream)) as SendableRecordBatchStream;
3137
3138 train_btree_index(stream, test_store.as_ref(), 64, None, None)
3139 .await
3140 .unwrap();
3141
3142 let index = BTreeIndex::load(test_store, None, &LanceCache::no_cache())
3143 .await
3144 .unwrap();
3145
3146 for (idx, value) in values.into_iter().enumerate() {
3147 let query = SargableQuery::Equals(ScalarValue::Float64(Some(value)));
3148 let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
3149 assert_eq!(
3150 result,
3151 SearchResult::exact(RowAddrTreeMap::from_iter(((idx as u64)..1000).step_by(7)))
3152 );
3153 }
3154 }
3155
3156 #[tokio::test]
3157 async fn test_page_cache() {
3158 let tmpdir = TempObjDir::default();
3159 let test_store = Arc::new(LanceIndexStore::new(
3160 Arc::new(ObjectStore::local()),
3161 tmpdir.clone(),
3162 Arc::new(LanceCache::no_cache()),
3163 ));
3164
3165 let data = gen_batch()
3166 .col("value", array::step::<Float32Type>())
3167 .col("_rowid", array::step::<UInt64Type>())
3168 .into_df_exec(RowCount::from(1000), BatchCount::from(10));
3169 let schema = data.schema();
3170 let sort_expr = PhysicalSortExpr::new_default(col("value", schema.as_ref()).unwrap());
3171 let plan = Arc::new(SortExec::new([sort_expr].into(), data));
3172 let stream = plan.execute(0, Arc::new(TaskContext::default())).unwrap();
3173 let stream = break_stream(stream, 64);
3174 let stream = stream.map_err(DataFusionError::from);
3175 let stream =
3176 Box::pin(RecordBatchStreamAdapter::new(schema, stream)) as SendableRecordBatchStream;
3177
3178 train_btree_index(stream, test_store.as_ref(), 64, None, None)
3179 .await
3180 .unwrap();
3181
3182 let cache = Arc::new(LanceCache::with_capacity(100 * 1024 * 1024));
3183 let index = BTreeIndex::load(test_store, None, cache.as_ref())
3184 .await
3185 .unwrap();
3186
3187 let query = SargableQuery::Equals(ScalarValue::Float32(Some(0.0)));
3188 let metrics = LocalMetricsCollector::default();
3189 let query1 = index.search(&query, &metrics);
3190 let query2 = index.search(&query, &metrics);
3191 tokio::join!(query1, query2).0.unwrap();
3192 assert_eq!(metrics.parts_loaded.load(Ordering::Relaxed), 1);
3193 }
3194
3195 #[tokio::test]
3196 async fn test_like_prefix_search() {
3197 use arrow::datatypes::DataType;
3198 use arrow_array::StringArray;
3199
3200 let tmpdir = TempObjDir::default();
3201 let test_store = Arc::new(LanceIndexStore::new(
3202 Arc::new(ObjectStore::local()),
3203 tmpdir.clone(),
3204 Arc::new(LanceCache::no_cache()),
3205 ));
3206
3207 let values = vec![
3209 "apple",
3210 "app",
3211 "application",
3212 "banana",
3213 "band",
3214 "test_ns$table1",
3215 "test_ns$table2",
3216 "test_ns2$table1",
3217 "test",
3218 "testing",
3219 ];
3220 let row_ids: Vec<u64> = (0..values.len() as u64).collect();
3221
3222 let schema = Arc::new(arrow::datatypes::Schema::new(vec![
3223 arrow::datatypes::Field::new("value", DataType::Utf8, false),
3224 arrow::datatypes::Field::new("_rowid", DataType::UInt64, false),
3225 ]));
3226
3227 let batch = arrow::record_batch::RecordBatch::try_new(
3228 schema.clone(),
3229 vec![
3230 Arc::new(StringArray::from(values.clone())),
3231 Arc::new(arrow_array::UInt64Array::from(row_ids)),
3232 ],
3233 )
3234 .unwrap();
3235
3236 let stream: SendableRecordBatchStream = Box::pin(RecordBatchStreamAdapter::new(
3237 schema,
3238 stream::once(async { Ok(batch) }),
3239 ));
3240
3241 train_btree_index(stream, test_store.as_ref(), 100, None, None)
3242 .await
3243 .unwrap();
3244
3245 let index = BTreeIndex::load(test_store, None, &LanceCache::no_cache())
3246 .await
3247 .unwrap();
3248
3249 let query = SargableQuery::LikePrefix(ScalarValue::Utf8(Some("app".to_string())));
3251 let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
3252
3253 match &result {
3254 SearchResult::Exact(row_ids) => {
3255 let ids: Vec<u64> = row_ids
3256 .true_rows()
3257 .row_addrs()
3258 .unwrap()
3259 .map(u64::from)
3260 .collect();
3261 assert!(ids.contains(&0), "Should contain row 0 (apple)");
3262 assert!(ids.contains(&1), "Should contain row 1 (app)");
3263 assert!(ids.contains(&2), "Should contain row 2 (application)");
3264 assert!(!ids.contains(&3), "Should not contain row 3 (banana)");
3265 }
3266 _ => panic!("Expected Exact result"),
3267 }
3268
3269 let query = SargableQuery::LikePrefix(ScalarValue::Utf8(Some("test_ns$".to_string())));
3271 let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
3272
3273 match &result {
3274 SearchResult::Exact(row_ids) => {
3275 let ids: Vec<u64> = row_ids
3276 .true_rows()
3277 .row_addrs()
3278 .unwrap()
3279 .map(u64::from)
3280 .collect();
3281 assert!(ids.contains(&5), "Should contain row 5 (test_ns$table1)");
3282 assert!(ids.contains(&6), "Should contain row 6 (test_ns$table2)");
3283 assert!(
3284 !ids.contains(&7),
3285 "Should not contain row 7 (test_ns2$table1)"
3286 );
3287 }
3288 _ => panic!("Expected Exact result"),
3289 }
3290
3291 let query = SargableQuery::LikePrefix(ScalarValue::Utf8(Some("test".to_string())));
3293 let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
3294
3295 match &result {
3296 SearchResult::Exact(row_ids) => {
3297 let ids: Vec<u64> = row_ids
3298 .true_rows()
3299 .row_addrs()
3300 .unwrap()
3301 .map(u64::from)
3302 .collect();
3303 assert!(
3304 ids.contains(&5),
3305 "Should contain row 5 (test_ns$table1): {:?}",
3306 ids
3307 );
3308 assert!(
3309 ids.contains(&6),
3310 "Should contain row 6 (test_ns$table2): {:?}",
3311 ids
3312 );
3313 assert!(
3314 ids.contains(&7),
3315 "Should contain row 7 (test_ns2$table1): {:?}",
3316 ids
3317 );
3318 assert!(ids.contains(&8), "Should contain row 8 (test): {:?}", ids);
3319 assert!(
3320 ids.contains(&9),
3321 "Should contain row 9 (testing): {:?}",
3322 ids
3323 );
3324 }
3325 _ => panic!("Expected Exact result"),
3326 }
3327 }
3328
3329 #[tokio::test]
3330 async fn test_like_prefix_search_large_utf8() {
3331 use arrow::datatypes::DataType;
3332 use arrow_array::LargeStringArray;
3333
3334 let tmpdir = TempObjDir::default();
3335 let test_store = Arc::new(LanceIndexStore::new(
3336 Arc::new(ObjectStore::local()),
3337 tmpdir.clone(),
3338 Arc::new(LanceCache::no_cache()),
3339 ));
3340
3341 let values = vec!["apple", "app", "application", "banana"];
3342 let row_ids: Vec<u64> = (0..values.len() as u64).collect();
3343
3344 let schema = Arc::new(arrow::datatypes::Schema::new(vec![
3345 arrow::datatypes::Field::new("value", DataType::LargeUtf8, false),
3346 arrow::datatypes::Field::new("_rowid", DataType::UInt64, false),
3347 ]));
3348
3349 let batch = arrow::record_batch::RecordBatch::try_new(
3350 schema.clone(),
3351 vec![
3352 Arc::new(LargeStringArray::from(values)),
3353 Arc::new(arrow_array::UInt64Array::from(row_ids)),
3354 ],
3355 )
3356 .unwrap();
3357
3358 let stream: SendableRecordBatchStream = Box::pin(RecordBatchStreamAdapter::new(
3359 schema,
3360 stream::once(async { Ok(batch) }),
3361 ));
3362
3363 train_btree_index(stream, test_store.as_ref(), 100, None, None)
3364 .await
3365 .unwrap();
3366
3367 let index = BTreeIndex::load(test_store, None, &LanceCache::no_cache())
3368 .await
3369 .unwrap();
3370
3371 let query = SargableQuery::LikePrefix(ScalarValue::LargeUtf8(Some("app".to_string())));
3373 let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
3374
3375 match &result {
3376 SearchResult::Exact(row_ids) => {
3377 let ids: Vec<u64> = row_ids
3378 .true_rows()
3379 .row_addrs()
3380 .unwrap()
3381 .map(u64::from)
3382 .collect();
3383 assert!(ids.contains(&0), "Should contain row 0 (apple)");
3384 assert!(ids.contains(&1), "Should contain row 1 (app)");
3385 assert!(ids.contains(&2), "Should contain row 2 (application)");
3386 assert!(!ids.contains(&3), "Should not contain row 3 (banana)");
3387 }
3388 _ => panic!("Expected Exact result"),
3389 }
3390 }
3391
3392 #[tokio::test]
3393 async fn test_fragment_btree_index_consistency() {
3394 let full_tmpdir = TempObjDir::default();
3396 let full_store = Arc::new(LanceIndexStore::new(
3397 Arc::new(ObjectStore::local()),
3398 full_tmpdir.clone(),
3399 Arc::new(LanceCache::no_cache()),
3400 ));
3401
3402 let fragment_tmpdir = TempObjDir::default();
3403 let fragment_store = Arc::new(LanceIndexStore::new(
3404 Arc::new(ObjectStore::local()),
3405 fragment_tmpdir.clone(),
3406 Arc::new(LanceCache::no_cache()),
3407 ));
3408
3409 let total_count = 2 * DEFAULT_BTREE_BATCH_SIZE;
3412 let full_data_gen = gen_batch()
3413 .col("value", array::step::<Int32Type>())
3414 .col("_rowid", array::step::<UInt64Type>())
3415 .into_df_stream(RowCount::from(total_count / 2), BatchCount::from(2));
3416 let full_data_source = Box::pin(RecordBatchStreamAdapter::new(
3417 full_data_gen.schema(),
3418 full_data_gen,
3419 ));
3420
3421 train_btree_index(
3422 full_data_source,
3423 full_store.as_ref(),
3424 DEFAULT_BTREE_BATCH_SIZE,
3425 None,
3426 None,
3427 )
3428 .await
3429 .unwrap();
3430
3431 let half_count = DEFAULT_BTREE_BATCH_SIZE;
3434 let fragment1_gen = gen_batch()
3435 .col("value", array::step::<Int32Type>())
3436 .col("_rowid", array::step::<UInt64Type>())
3437 .into_df_stream(RowCount::from(half_count), BatchCount::from(1));
3438 let fragment1_data_source = Box::pin(RecordBatchStreamAdapter::new(
3439 fragment1_gen.schema(),
3440 fragment1_gen,
3441 ));
3442
3443 train_btree_index(
3444 fragment1_data_source,
3445 fragment_store.as_ref(),
3446 DEFAULT_BTREE_BATCH_SIZE,
3447 Some(vec![1]), None,
3449 )
3450 .await
3451 .unwrap();
3452
3453 let start_val = DEFAULT_BTREE_BATCH_SIZE as i32;
3455 let end_val = (2 * DEFAULT_BTREE_BATCH_SIZE) as i32;
3456 let values_second_half: Vec<i32> = (start_val..end_val).collect();
3457 let row_ids_second_half: Vec<u64> = (start_val as u64..end_val as u64).collect();
3458 let fragment2_gen = gen_batch()
3459 .col("value", array::cycle::<Int32Type>(values_second_half))
3460 .col("_rowid", array::cycle::<UInt64Type>(row_ids_second_half))
3461 .into_df_stream(RowCount::from(half_count), BatchCount::from(1));
3462 let fragment2_data_source = Box::pin(RecordBatchStreamAdapter::new(
3463 fragment2_gen.schema(),
3464 fragment2_gen,
3465 ));
3466
3467 train_btree_index(
3468 fragment2_data_source,
3469 fragment_store.as_ref(),
3470 DEFAULT_BTREE_BATCH_SIZE,
3471 Some(vec![2]), None,
3473 )
3474 .await
3475 .unwrap();
3476
3477 let part_page_files = vec![
3479 part_page_data_file_path(1 << 32),
3480 part_page_data_file_path(2 << 32),
3481 ];
3482
3483 let part_lookup_files = vec![
3484 part_lookup_file_path(1 << 32),
3485 part_lookup_file_path(2 << 32),
3486 ];
3487
3488 let progress = Arc::new(RecordingProgress::default());
3489 super::merge_metadata_files(
3490 fragment_store.as_ref(),
3491 &part_page_files,
3492 &part_lookup_files,
3493 Option::from(1usize),
3494 progress.clone(),
3495 )
3496 .await
3497 .unwrap();
3498
3499 let tags = progress
3500 .recorded_events()
3501 .iter()
3502 .map(|(kind, stage, _)| format!("{kind}:{stage}"))
3503 .collect::<Vec<_>>();
3504 let merge_start = tags
3505 .iter()
3506 .position(|e| e == "start:merge_pages")
3507 .expect("missing merge_pages start");
3508 let merge_complete = tags
3509 .iter()
3510 .position(|e| e == "complete:merge_pages")
3511 .expect("missing merge_pages complete");
3512 let lookup_start = tags
3513 .iter()
3514 .position(|e| e == "start:write_lookup_file")
3515 .expect("missing write_lookup_file start");
3516 let lookup_complete = tags
3517 .iter()
3518 .position(|e| e == "complete:write_lookup_file")
3519 .expect("missing write_lookup_file complete");
3520 assert!(merge_start < merge_complete);
3521 assert!(merge_complete < lookup_start);
3522 assert!(lookup_start < lookup_complete);
3523 assert!(
3524 tags.iter().any(|e| e == "progress:merge_pages"),
3525 "expected merge_pages progress callbacks"
3526 );
3527 assert!(
3528 tags.iter().any(|e| e == "progress:write_lookup_file"),
3529 "expected write_lookup_file progress callbacks"
3530 );
3531
3532 let full_index = BTreeIndex::load(full_store.clone(), None, &LanceCache::no_cache())
3534 .await
3535 .unwrap();
3536
3537 let merged_index = BTreeIndex::load(fragment_store.clone(), None, &LanceCache::no_cache())
3538 .await
3539 .unwrap();
3540
3541 let query_0 = SargableQuery::Equals(ScalarValue::Int32(Some(0)));
3545 let full_result_0 = full_index
3546 .search(&query_0, &NoOpMetricsCollector)
3547 .await
3548 .unwrap();
3549 let merged_result_0 = merged_index
3550 .search(&query_0, &NoOpMetricsCollector)
3551 .await
3552 .unwrap();
3553 assert_eq!(full_result_0, merged_result_0, "Query for value 0 failed");
3554
3555 let mid_first_batch = (DEFAULT_BTREE_BATCH_SIZE / 2) as i32;
3557 let query_mid_first = SargableQuery::Equals(ScalarValue::Int32(Some(mid_first_batch)));
3558 let full_result_mid_first = full_index
3559 .search(&query_mid_first, &NoOpMetricsCollector)
3560 .await
3561 .unwrap();
3562 let merged_result_mid_first = merged_index
3563 .search(&query_mid_first, &NoOpMetricsCollector)
3564 .await
3565 .unwrap();
3566 assert_eq!(
3567 full_result_mid_first, merged_result_mid_first,
3568 "Query for value {} failed",
3569 mid_first_batch
3570 );
3571
3572 let first_second_batch = DEFAULT_BTREE_BATCH_SIZE as i32;
3574 let query_first_second =
3575 SargableQuery::Equals(ScalarValue::Int32(Some(first_second_batch)));
3576 let full_result_first_second = full_index
3577 .search(&query_first_second, &NoOpMetricsCollector)
3578 .await
3579 .unwrap();
3580 let merged_result_first_second = merged_index
3581 .search(&query_first_second, &NoOpMetricsCollector)
3582 .await
3583 .unwrap();
3584 assert_eq!(
3585 full_result_first_second, merged_result_first_second,
3586 "Query for value {} failed",
3587 first_second_batch
3588 );
3589
3590 let mid_second_batch = (DEFAULT_BTREE_BATCH_SIZE + DEFAULT_BTREE_BATCH_SIZE / 2) as i32;
3592 let query_mid_second = SargableQuery::Equals(ScalarValue::Int32(Some(mid_second_batch)));
3593
3594 let full_result_mid_second = full_index
3595 .search(&query_mid_second, &NoOpMetricsCollector)
3596 .await
3597 .unwrap();
3598 let merged_result_mid_second = merged_index
3599 .search(&query_mid_second, &NoOpMetricsCollector)
3600 .await
3601 .unwrap();
3602 assert_eq!(
3603 full_result_mid_second, merged_result_mid_second,
3604 "Query for value {} failed",
3605 mid_second_batch
3606 );
3607 }
3608
3609 #[tokio::test]
3610 async fn test_fragment_btree_index_boundary_queries() {
3611 let full_tmpdir = TempObjDir::default();
3613 let full_store = Arc::new(LanceIndexStore::new(
3614 Arc::new(ObjectStore::local()),
3615 full_tmpdir.clone(),
3616 Arc::new(LanceCache::no_cache()),
3617 ));
3618
3619 let fragment_tmpdir = TempObjDir::default();
3620 let fragment_store = Arc::new(LanceIndexStore::new(
3621 Arc::new(ObjectStore::local()),
3622 fragment_tmpdir.clone(),
3623 Arc::new(LanceCache::no_cache()),
3624 ));
3625
3626 let total_count = 3 * DEFAULT_BTREE_BATCH_SIZE;
3628
3629 let full_data_gen = gen_batch()
3631 .col("value", array::step::<Int32Type>())
3632 .col("_rowid", array::step::<UInt64Type>())
3633 .into_df_stream(RowCount::from(total_count / 3), BatchCount::from(3));
3634 let full_data_source = Box::pin(RecordBatchStreamAdapter::new(
3635 full_data_gen.schema(),
3636 full_data_gen,
3637 ));
3638
3639 train_btree_index(
3640 full_data_source,
3641 full_store.as_ref(),
3642 DEFAULT_BTREE_BATCH_SIZE,
3643 None,
3644 None,
3645 )
3646 .await
3647 .unwrap();
3648
3649 let fragment_size = DEFAULT_BTREE_BATCH_SIZE;
3652 let fragment1_gen = gen_batch()
3653 .col("value", array::step::<Int32Type>())
3654 .col("_rowid", array::step::<UInt64Type>())
3655 .into_df_stream(RowCount::from(fragment_size), BatchCount::from(1));
3656 let fragment1_data_source = Box::pin(RecordBatchStreamAdapter::new(
3657 fragment1_gen.schema(),
3658 fragment1_gen,
3659 ));
3660
3661 train_btree_index(
3662 fragment1_data_source,
3663 fragment_store.as_ref(),
3664 DEFAULT_BTREE_BATCH_SIZE,
3665 Some(vec![1]),
3666 None,
3667 )
3668 .await
3669 .unwrap();
3670
3671 let start_val2 = DEFAULT_BTREE_BATCH_SIZE as i32;
3673 let end_val2 = (2 * DEFAULT_BTREE_BATCH_SIZE) as i32;
3674 let values_fragment2: Vec<i32> = (start_val2..end_val2).collect();
3675 let row_ids_fragment2: Vec<u64> = (start_val2 as u64..end_val2 as u64).collect();
3676 let fragment2_gen = gen_batch()
3677 .col("value", array::cycle::<Int32Type>(values_fragment2))
3678 .col("_rowid", array::cycle::<UInt64Type>(row_ids_fragment2))
3679 .into_df_stream(RowCount::from(fragment_size), BatchCount::from(1));
3680 let fragment2_data_source = Box::pin(RecordBatchStreamAdapter::new(
3681 fragment2_gen.schema(),
3682 fragment2_gen,
3683 ));
3684
3685 train_btree_index(
3686 fragment2_data_source,
3687 fragment_store.as_ref(),
3688 DEFAULT_BTREE_BATCH_SIZE,
3689 Some(vec![2]),
3690 None,
3691 )
3692 .await
3693 .unwrap();
3694
3695 let start_val3 = (2 * DEFAULT_BTREE_BATCH_SIZE) as i32;
3697 let end_val3 = (3 * DEFAULT_BTREE_BATCH_SIZE) as i32;
3698 let values_fragment3: Vec<i32> = (start_val3..end_val3).collect();
3699 let row_ids_fragment3: Vec<u64> = (start_val3 as u64..end_val3 as u64).collect();
3700 let fragment3_gen = gen_batch()
3701 .col("value", array::cycle::<Int32Type>(values_fragment3))
3702 .col("_rowid", array::cycle::<UInt64Type>(row_ids_fragment3))
3703 .into_df_stream(RowCount::from(fragment_size), BatchCount::from(1));
3704 let fragment3_data_source = Box::pin(RecordBatchStreamAdapter::new(
3705 fragment3_gen.schema(),
3706 fragment3_gen,
3707 ));
3708
3709 train_btree_index(
3710 fragment3_data_source,
3711 fragment_store.as_ref(),
3712 DEFAULT_BTREE_BATCH_SIZE,
3713 Some(vec![3]),
3714 None,
3715 )
3716 .await
3717 .unwrap();
3718
3719 let part_page_files = vec![
3721 part_page_data_file_path(1 << 32),
3722 part_page_data_file_path(2 << 32),
3723 part_page_data_file_path(3 << 32),
3724 ];
3725
3726 let part_lookup_files = vec![
3727 part_lookup_file_path(1 << 32),
3728 part_lookup_file_path(2 << 32),
3729 part_lookup_file_path(3 << 32),
3730 ];
3731
3732 super::merge_metadata_files(
3733 fragment_store.as_ref(),
3734 &part_page_files,
3735 &part_lookup_files,
3736 Option::from(1usize),
3737 noop_progress(),
3738 )
3739 .await
3740 .unwrap();
3741
3742 let full_index = BTreeIndex::load(full_store.clone(), None, &LanceCache::no_cache())
3744 .await
3745 .unwrap();
3746
3747 let merged_index = BTreeIndex::load(fragment_store.clone(), None, &LanceCache::no_cache())
3748 .await
3749 .unwrap();
3750
3751 let query_min = SargableQuery::Equals(ScalarValue::Int32(Some(0)));
3755 let full_result_min = full_index
3756 .search(&query_min, &NoOpMetricsCollector)
3757 .await
3758 .unwrap();
3759 let merged_result_min = merged_index
3760 .search(&query_min, &NoOpMetricsCollector)
3761 .await
3762 .unwrap();
3763 assert_eq!(
3764 full_result_min, merged_result_min,
3765 "Query for minimum value 0 failed"
3766 );
3767
3768 let max_val = (3 * DEFAULT_BTREE_BATCH_SIZE - 1) as i32;
3770 let query_max = SargableQuery::Equals(ScalarValue::Int32(Some(max_val)));
3771 let full_result_max = full_index
3772 .search(&query_max, &NoOpMetricsCollector)
3773 .await
3774 .unwrap();
3775 let merged_result_max = merged_index
3776 .search(&query_max, &NoOpMetricsCollector)
3777 .await
3778 .unwrap();
3779 assert_eq!(
3780 full_result_max, merged_result_max,
3781 "Query for maximum value {} failed",
3782 max_val
3783 );
3784
3785 let fragment1_last = (DEFAULT_BTREE_BATCH_SIZE - 1) as i32;
3787 let query_frag1_last = SargableQuery::Equals(ScalarValue::Int32(Some(fragment1_last)));
3788 let full_result_frag1_last = full_index
3789 .search(&query_frag1_last, &NoOpMetricsCollector)
3790 .await
3791 .unwrap();
3792 let merged_result_frag1_last = merged_index
3793 .search(&query_frag1_last, &NoOpMetricsCollector)
3794 .await
3795 .unwrap();
3796 assert_eq!(
3797 full_result_frag1_last, merged_result_frag1_last,
3798 "Query for fragment 1 last value {} failed",
3799 fragment1_last
3800 );
3801
3802 let fragment2_first = DEFAULT_BTREE_BATCH_SIZE as i32;
3804 let query_frag2_first = SargableQuery::Equals(ScalarValue::Int32(Some(fragment2_first)));
3805 let full_result_frag2_first = full_index
3806 .search(&query_frag2_first, &NoOpMetricsCollector)
3807 .await
3808 .unwrap();
3809 let merged_result_frag2_first = merged_index
3810 .search(&query_frag2_first, &NoOpMetricsCollector)
3811 .await
3812 .unwrap();
3813 assert_eq!(
3814 full_result_frag2_first, merged_result_frag2_first,
3815 "Query for fragment 2 first value {} failed",
3816 fragment2_first
3817 );
3818
3819 let fragment2_last = (2 * DEFAULT_BTREE_BATCH_SIZE - 1) as i32;
3821 let query_frag2_last = SargableQuery::Equals(ScalarValue::Int32(Some(fragment2_last)));
3822 let full_result_frag2_last = full_index
3823 .search(&query_frag2_last, &NoOpMetricsCollector)
3824 .await
3825 .unwrap();
3826 let merged_result_frag2_last = merged_index
3827 .search(&query_frag2_last, &NoOpMetricsCollector)
3828 .await
3829 .unwrap();
3830 assert_eq!(
3831 full_result_frag2_last, merged_result_frag2_last,
3832 "Query for fragment 2 last value {} failed",
3833 fragment2_last
3834 );
3835
3836 let fragment3_first = (2 * DEFAULT_BTREE_BATCH_SIZE) as i32;
3838 let query_frag3_first = SargableQuery::Equals(ScalarValue::Int32(Some(fragment3_first)));
3839 let full_result_frag3_first = full_index
3840 .search(&query_frag3_first, &NoOpMetricsCollector)
3841 .await
3842 .unwrap();
3843 let merged_result_frag3_first = merged_index
3844 .search(&query_frag3_first, &NoOpMetricsCollector)
3845 .await
3846 .unwrap();
3847 assert_eq!(
3848 full_result_frag3_first, merged_result_frag3_first,
3849 "Query for fragment 3 first value {} failed",
3850 fragment3_first
3851 );
3852
3853 let query_below_min = SargableQuery::Equals(ScalarValue::Int32(Some(-1)));
3857 let full_result_below = full_index
3858 .search(&query_below_min, &NoOpMetricsCollector)
3859 .await
3860 .unwrap();
3861 let merged_result_below = merged_index
3862 .search(&query_below_min, &NoOpMetricsCollector)
3863 .await
3864 .unwrap();
3865 assert_eq!(
3866 full_result_below, merged_result_below,
3867 "Query for value below minimum (-1) failed"
3868 );
3869
3870 let query_above_max = SargableQuery::Equals(ScalarValue::Int32(Some(max_val + 1)));
3872 let full_result_above = full_index
3873 .search(&query_above_max, &NoOpMetricsCollector)
3874 .await
3875 .unwrap();
3876 let merged_result_above = merged_index
3877 .search(&query_above_max, &NoOpMetricsCollector)
3878 .await
3879 .unwrap();
3880 assert_eq!(
3881 full_result_above,
3882 merged_result_above,
3883 "Query for value above maximum ({}) failed",
3884 max_val + 1
3885 );
3886
3887 let range_start = (DEFAULT_BTREE_BATCH_SIZE - 100) as i32;
3891 let range_end = (DEFAULT_BTREE_BATCH_SIZE + 100) as i32;
3892 let query_cross_frag = SargableQuery::Range(
3893 std::collections::Bound::Included(ScalarValue::Int32(Some(range_start))),
3894 std::collections::Bound::Excluded(ScalarValue::Int32(Some(range_end))),
3895 );
3896 let full_result_cross = full_index
3897 .search(&query_cross_frag, &NoOpMetricsCollector)
3898 .await
3899 .unwrap();
3900 let merged_result_cross = merged_index
3901 .search(&query_cross_frag, &NoOpMetricsCollector)
3902 .await
3903 .unwrap();
3904 assert_eq!(
3905 full_result_cross, merged_result_cross,
3906 "Cross-fragment range query [{}, {}] failed",
3907 range_start, range_end
3908 );
3909
3910 let single_frag_start = 100i32;
3912 let single_frag_end = 200i32;
3913 let query_single_frag = SargableQuery::Range(
3914 std::collections::Bound::Included(ScalarValue::Int32(Some(single_frag_start))),
3915 std::collections::Bound::Excluded(ScalarValue::Int32(Some(single_frag_end))),
3916 );
3917 let full_result_single = full_index
3918 .search(&query_single_frag, &NoOpMetricsCollector)
3919 .await
3920 .unwrap();
3921 let merged_result_single = merged_index
3922 .search(&query_single_frag, &NoOpMetricsCollector)
3923 .await
3924 .unwrap();
3925 assert_eq!(
3926 full_result_single, merged_result_single,
3927 "Single fragment range query [{}, {}] failed",
3928 single_frag_start, single_frag_end
3929 );
3930
3931 let large_range_start = 100i32;
3933 let large_range_end = (3 * DEFAULT_BTREE_BATCH_SIZE - 100) as i32;
3934 let query_large_range = SargableQuery::Range(
3935 std::collections::Bound::Included(ScalarValue::Int32(Some(large_range_start))),
3936 std::collections::Bound::Excluded(ScalarValue::Int32(Some(large_range_end))),
3937 );
3938 let full_result_large = full_index
3939 .search(&query_large_range, &NoOpMetricsCollector)
3940 .await
3941 .unwrap();
3942 let merged_result_large = merged_index
3943 .search(&query_large_range, &NoOpMetricsCollector)
3944 .await
3945 .unwrap();
3946 assert_eq!(
3947 full_result_large, merged_result_large,
3948 "Large range query [{}, {}] failed",
3949 large_range_start, large_range_end
3950 );
3951
3952 let lt_val = (DEFAULT_BTREE_BATCH_SIZE / 2) as i32;
3956 let query_lt = SargableQuery::Range(
3957 std::collections::Bound::Included(ScalarValue::Int32(Some(0))),
3958 std::collections::Bound::Excluded(ScalarValue::Int32(Some(lt_val))),
3959 );
3960 let full_result_lt = full_index
3961 .search(&query_lt, &NoOpMetricsCollector)
3962 .await
3963 .unwrap();
3964 let merged_result_lt = merged_index
3965 .search(&query_lt, &NoOpMetricsCollector)
3966 .await
3967 .unwrap();
3968 assert_eq!(
3969 full_result_lt, merged_result_lt,
3970 "Less than query (<{}) failed",
3971 lt_val
3972 );
3973
3974 let gt_val = (2 * DEFAULT_BTREE_BATCH_SIZE) as i32;
3976 let max_range_val = (3 * DEFAULT_BTREE_BATCH_SIZE) as i32;
3977 let query_gt = SargableQuery::Range(
3978 std::collections::Bound::Excluded(ScalarValue::Int32(Some(gt_val))),
3979 std::collections::Bound::Excluded(ScalarValue::Int32(Some(max_range_val))),
3980 );
3981 let full_result_gt = full_index
3982 .search(&query_gt, &NoOpMetricsCollector)
3983 .await
3984 .unwrap();
3985 let merged_result_gt = merged_index
3986 .search(&query_gt, &NoOpMetricsCollector)
3987 .await
3988 .unwrap();
3989 assert_eq!(
3990 full_result_gt, merged_result_gt,
3991 "Greater than query (>{}) failed",
3992 gt_val
3993 );
3994
3995 let lte_val = (DEFAULT_BTREE_BATCH_SIZE - 1) as i32;
3997 let query_lte = SargableQuery::Range(
3998 std::collections::Bound::Included(ScalarValue::Int32(Some(0))),
3999 std::collections::Bound::Included(ScalarValue::Int32(Some(lte_val))),
4000 );
4001 let full_result_lte = full_index
4002 .search(&query_lte, &NoOpMetricsCollector)
4003 .await
4004 .unwrap();
4005 let merged_result_lte = merged_index
4006 .search(&query_lte, &NoOpMetricsCollector)
4007 .await
4008 .unwrap();
4009 assert_eq!(
4010 full_result_lte, merged_result_lte,
4011 "Less than or equal query (<={}) failed",
4012 lte_val
4013 );
4014
4015 let gte_val = (2 * DEFAULT_BTREE_BATCH_SIZE) as i32;
4017 let query_gte = SargableQuery::Range(
4018 std::collections::Bound::Included(ScalarValue::Int32(Some(gte_val))),
4019 std::collections::Bound::Excluded(ScalarValue::Int32(Some(max_range_val))),
4020 );
4021 let full_result_gte = full_index
4022 .search(&query_gte, &NoOpMetricsCollector)
4023 .await
4024 .unwrap();
4025 let merged_result_gte = merged_index
4026 .search(&query_gte, &NoOpMetricsCollector)
4027 .await
4028 .unwrap();
4029 assert_eq!(
4030 full_result_gte, merged_result_gte,
4031 "Greater than or equal query (>={}) failed",
4032 gte_val
4033 );
4034 }
4035
4036 #[test]
4037 fn test_extract_partition_id() {
4038 assert_eq!(
4040 super::extract_partition_id("part_123_page_data.lance").unwrap(),
4041 123
4042 );
4043 assert_eq!(
4044 super::extract_partition_id("part_456_page_lookup.lance").unwrap(),
4045 456
4046 );
4047 assert_eq!(
4048 super::extract_partition_id("part_4294967296_page_data.lance").unwrap(),
4049 4294967296
4050 );
4051
4052 assert!(super::extract_partition_id("invalid_filename.lance").is_err());
4054 assert!(super::extract_partition_id("part_abc_page_data.lance").is_err());
4055 assert!(super::extract_partition_id("part_123").is_err());
4056 assert!(super::extract_partition_id("part_").is_err());
4057 }
4058
4059 #[tokio::test]
4060 async fn test_cleanup_partition_files() {
4061 let tmpdir = TempObjDir::default();
4063 let test_store: Arc<dyn crate::scalar::IndexStore> = Arc::new(LanceIndexStore::new(
4064 Arc::new(ObjectStore::local()),
4065 tmpdir.clone(),
4066 Arc::new(LanceCache::no_cache()),
4067 ));
4068
4069 let lookup_files = vec![
4071 "part_123_page_lookup.lance".to_string(),
4072 "invalid_lookup_file.lance".to_string(),
4073 "part_456_page_lookup.lance".to_string(),
4074 ];
4075
4076 let page_files = vec![
4077 "part_123_page_data.lance".to_string(),
4078 "invalid_page_file.lance".to_string(),
4079 "part_456_page_data.lance".to_string(),
4080 ];
4081
4082 super::cleanup_partition_files(test_store.as_ref(), &lookup_files, &page_files).await;
4085 }
4086
4087 #[tokio::test]
4088 async fn test_btree_null_handling_in_queries() {
4089 let store = Arc::new(LanceIndexStore::new(
4090 Arc::new(ObjectStore::memory()),
4091 Path::default(),
4092 Arc::new(LanceCache::no_cache()),
4093 ));
4094
4095 let batch = record_batch!(
4098 ("value", Int32, [None, Some(0), Some(5)]),
4099 ("_rowid", UInt64, [0, 1, 2])
4100 )
4101 .unwrap();
4102 let stream = stream::once(futures::future::ok(batch.clone()));
4103 let stream = Box::pin(RecordBatchStreamAdapter::new(batch.schema(), stream));
4104
4105 super::train_btree_index(stream, store.as_ref(), 256, None, None)
4107 .await
4108 .unwrap();
4109
4110 let cache = LanceCache::with_capacity(1024 * 1024);
4111 let index = super::BTreeIndex::load(store.clone(), None, &cache)
4112 .await
4113 .unwrap();
4114
4115 let query = SargableQuery::Equals(ScalarValue::Int32(Some(5)));
4117 let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
4118
4119 match result {
4120 SearchResult::Exact(row_ids) => {
4121 let actual_rows: Vec<u64> = row_ids
4122 .true_rows()
4123 .row_addrs()
4124 .unwrap()
4125 .map(u64::from)
4126 .collect();
4127 assert_eq!(actual_rows, vec![2], "Should find row 2 where value == 5");
4128
4129 let null_row_ids = row_ids.null_rows();
4131 assert!(!null_row_ids.is_empty(), "null_row_ids should be non-empty");
4132 let null_rows: Vec<u64> =
4133 null_row_ids.row_addrs().unwrap().map(u64::from).collect();
4134 assert_eq!(null_rows, vec![0], "Should report row 0 as null");
4135 }
4136 _ => panic!("Expected Exact search result"),
4137 }
4138
4139 let query = SargableQuery::Range(
4141 std::ops::Bound::Included(ScalarValue::Int32(Some(0))),
4142 std::ops::Bound::Included(ScalarValue::Int32(Some(3))),
4143 );
4144 let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
4145
4146 match result {
4147 SearchResult::Exact(row_ids) => {
4148 let actual_rows: Vec<u64> = row_ids
4149 .true_rows()
4150 .row_addrs()
4151 .unwrap()
4152 .map(u64::from)
4153 .collect();
4154 assert_eq!(actual_rows, vec![1], "Should find row 1 where value == 0");
4155
4156 let null_row_ids = row_ids.null_rows();
4158 assert!(!null_row_ids.is_empty(), "null_row_ids should be non-empty");
4159 let null_rows: Vec<u64> =
4160 null_row_ids.row_addrs().unwrap().map(u64::from).collect();
4161 assert_eq!(null_rows, vec![0], "Should report row 0 as null");
4162 }
4163 _ => panic!("Expected Exact search result"),
4164 }
4165
4166 let query = SargableQuery::IsIn(vec![
4168 ScalarValue::Int32(Some(0)),
4169 ScalarValue::Int32(Some(5)),
4170 ]);
4171 let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
4172
4173 match result {
4174 SearchResult::Exact(row_ids) => {
4175 let mut actual_rows: Vec<u64> = row_ids
4176 .true_rows()
4177 .row_addrs()
4178 .unwrap()
4179 .map(u64::from)
4180 .collect();
4181 actual_rows.sort();
4182 assert_eq!(
4183 actual_rows,
4184 vec![1, 2],
4185 "Should find rows 1 and 2 where value in [0, 5]"
4186 );
4187
4188 let null_row_ids = row_ids.null_rows();
4190 assert!(!null_row_ids.is_empty(), "null_row_ids should be non-empty");
4191 let null_rows: Vec<u64> =
4192 null_row_ids.row_addrs().unwrap().map(u64::from).collect();
4193 assert_eq!(null_rows, vec![0], "Should report row 0 as null");
4194 }
4195 _ => panic!("Expected Exact search result"),
4196 }
4197 }
4198
4199 #[tokio::test]
4200 async fn test_range_btree_index_consistency() {
4201 let full_tmpdir = TempObjDir::default();
4203 let full_store = Arc::new(LanceIndexStore::new(
4204 Arc::new(ObjectStore::local()),
4205 full_tmpdir.clone(),
4206 Arc::new(LanceCache::no_cache()),
4207 ));
4208
4209 let range_tmpdir = TempObjDir::default();
4210 let range_store = Arc::new(LanceIndexStore::new(
4211 Arc::new(ObjectStore::local()),
4212 range_tmpdir.clone(),
4213 Arc::new(LanceCache::no_cache()),
4214 ));
4215
4216 let total_count = 4 * DEFAULT_BTREE_BATCH_SIZE;
4219 let full_data_gen = gen_batch()
4220 .col("value", array::step::<Int32Type>())
4221 .col("_rowid", array::step::<UInt64Type>())
4222 .into_df_stream(RowCount::from(total_count / 4), BatchCount::from(4));
4223 let full_data_source = Box::pin(RecordBatchStreamAdapter::new(
4224 full_data_gen.schema(),
4225 full_data_gen,
4226 ));
4227
4228 train_btree_index(
4229 full_data_source,
4230 full_store.as_ref(),
4231 DEFAULT_BTREE_BATCH_SIZE,
4232 None,
4233 None,
4234 )
4235 .await
4236 .unwrap();
4237
4238 let range1_gen = gen_batch()
4241 .col("value", array::step::<Int32Type>())
4242 .col("_rowid", array::step::<UInt64Type>())
4243 .into_df_stream(
4244 RowCount::from(DEFAULT_BTREE_BATCH_SIZE / 2),
4245 BatchCount::from(5),
4246 );
4247 let range1_data_source = Box::pin(RecordBatchStreamAdapter::new(
4248 range1_gen.schema(),
4249 range1_gen,
4250 ));
4251
4252 train_btree_index(
4253 range1_data_source,
4254 range_store.as_ref(),
4255 DEFAULT_BTREE_BATCH_SIZE,
4256 None,
4257 Option::from(0u32),
4258 )
4259 .await
4260 .unwrap();
4261
4262 let start_val = (DEFAULT_BTREE_BATCH_SIZE * 2 + DEFAULT_BTREE_BATCH_SIZE / 2) as i32;
4264 let end_val = (4 * DEFAULT_BTREE_BATCH_SIZE) as i32;
4265 let values_second_half: Vec<i32> = (start_val..end_val).collect();
4266 let row_ids_second_half: Vec<u64> = (start_val as u64..end_val as u64).collect();
4267 let range2_gen = gen_batch()
4268 .col("value", array::cycle::<Int32Type>(values_second_half))
4269 .col("_rowid", array::cycle::<UInt64Type>(row_ids_second_half))
4270 .into_df_stream(
4271 RowCount::from(DEFAULT_BTREE_BATCH_SIZE / 2),
4272 BatchCount::from(3),
4273 );
4274 let range2_data_source = Box::pin(RecordBatchStreamAdapter::new(
4275 range2_gen.schema(),
4276 range2_gen,
4277 ));
4278
4279 train_btree_index(
4280 range2_data_source,
4281 range_store.as_ref(),
4282 DEFAULT_BTREE_BATCH_SIZE,
4283 None,
4284 Option::from(1u32),
4285 )
4286 .await
4287 .unwrap();
4288
4289 let part_page_files = vec![
4291 part_page_data_file_path(0 << 32),
4292 part_page_data_file_path(1 << 32),
4293 ];
4294
4295 let part_lookup_files = vec![
4296 part_lookup_file_path(0 << 32),
4297 part_lookup_file_path(1 << 32),
4298 ];
4299
4300 super::merge_metadata_files(
4301 range_store.as_ref(),
4302 &part_page_files,
4303 &part_lookup_files,
4304 Option::from(1usize),
4305 noop_progress(),
4306 )
4307 .await
4308 .unwrap();
4309
4310 let full_index = BTreeIndex::load(full_store.clone(), None, &LanceCache::no_cache())
4311 .await
4312 .unwrap();
4313
4314 let ranged_index = BTreeIndex::load(range_store.clone(), None, &LanceCache::no_cache())
4315 .await
4316 .unwrap();
4317
4318 let query_0 = SargableQuery::Equals(ScalarValue::Int32(Some(0)));
4322 let full_result_0 = full_index
4323 .search(&query_0, &NoOpMetricsCollector)
4324 .await
4325 .unwrap();
4326 let ranged_result_0 = ranged_index
4327 .search(&query_0, &NoOpMetricsCollector)
4328 .await
4329 .unwrap();
4330 assert_eq!(full_result_0, ranged_result_0, "Query for value 0 failed");
4331
4332 let mid_first_batch = (DEFAULT_BTREE_BATCH_SIZE / 2) as i32;
4334 let query_mid_first = SargableQuery::Equals(ScalarValue::Int32(Some(mid_first_batch)));
4335 let full_result_mid_first = full_index
4336 .search(&query_mid_first, &NoOpMetricsCollector)
4337 .await
4338 .unwrap();
4339 let ranged_result_mid_first = ranged_index
4340 .search(&query_mid_first, &NoOpMetricsCollector)
4341 .await
4342 .unwrap();
4343 assert_eq!(
4344 full_result_mid_first, ranged_result_mid_first,
4345 "Query for value {} failed",
4346 mid_first_batch
4347 );
4348
4349 let mid_last_batch = (DEFAULT_BTREE_BATCH_SIZE * 3 + (DEFAULT_BTREE_BATCH_SIZE / 2)) as i32;
4351 let query_mid_last = SargableQuery::Equals(ScalarValue::Int32(Some(mid_last_batch)));
4352 let full_result_mid_last = full_index
4353 .search(&query_mid_last, &NoOpMetricsCollector)
4354 .await
4355 .unwrap();
4356 let ranged_result_mid_last = ranged_index
4357 .search(&query_mid_last, &NoOpMetricsCollector)
4358 .await
4359 .unwrap();
4360 assert_eq!(
4361 full_result_mid_last, ranged_result_mid_last,
4362 "Query for value {} failed",
4363 mid_last_batch
4364 );
4365
4366 let max_val = (4 * DEFAULT_BTREE_BATCH_SIZE - 1) as i32;
4368 let query_max = SargableQuery::Equals(ScalarValue::Int32(Some(max_val)));
4369 let full_result_max = full_index
4370 .search(&query_max, &NoOpMetricsCollector)
4371 .await
4372 .unwrap();
4373 let ranged_result_max = ranged_index
4374 .search(&query_max, &NoOpMetricsCollector)
4375 .await
4376 .unwrap();
4377 assert_eq!(
4378 full_result_max, ranged_result_max,
4379 "Query for maximum value {} failed",
4380 max_val
4381 );
4382
4383 let second_first_val = (DEFAULT_BTREE_BATCH_SIZE * 2 + DEFAULT_BTREE_BATCH_SIZE / 2) as i32;
4385 let query_second_first = SargableQuery::Equals(ScalarValue::Int32(Some(second_first_val)));
4386 let full_result_second_first = full_index
4387 .search(&query_second_first, &NoOpMetricsCollector)
4388 .await
4389 .unwrap();
4390 let ranged_result_second_first = ranged_index
4391 .search(&query_second_first, &NoOpMetricsCollector)
4392 .await
4393 .unwrap();
4394 assert_eq!(
4395 full_result_second_first, ranged_result_second_first,
4396 "Query for first value of the second page file {} failed",
4397 second_first_val
4398 );
4399
4400 let query_below_min = SargableQuery::Equals(ScalarValue::Int32(Some(-1)));
4402 let full_result_below = full_index
4403 .search(&query_below_min, &NoOpMetricsCollector)
4404 .await
4405 .unwrap();
4406 let ranged_result_below = ranged_index
4407 .search(&query_below_min, &NoOpMetricsCollector)
4408 .await
4409 .unwrap();
4410 assert_eq!(
4411 full_result_below, ranged_result_below,
4412 "Query for value below minimum (-1) failed"
4413 );
4414
4415 let query_above_max = SargableQuery::Equals(ScalarValue::Int32(Some(max_val + 1)));
4417 let full_result_above = full_index
4418 .search(&query_above_max, &NoOpMetricsCollector)
4419 .await
4420 .unwrap();
4421 let ranged_result_above = ranged_index
4422 .search(&query_above_max, &NoOpMetricsCollector)
4423 .await
4424 .unwrap();
4425 assert_eq!(
4426 full_result_above,
4427 ranged_result_above,
4428 "Query for value above maximum ({}) failed",
4429 max_val + 1
4430 );
4431
4432 let range_start =
4436 (DEFAULT_BTREE_BATCH_SIZE * 2 + DEFAULT_BTREE_BATCH_SIZE / 2 - 100) as i32;
4437 let range_end = range_start + 200;
4438 let query_cross_range = SargableQuery::Range(
4439 std::collections::Bound::Included(ScalarValue::Int32(Some(range_start))),
4440 std::collections::Bound::Excluded(ScalarValue::Int32(Some(range_end))),
4441 );
4442 let full_result_cross = full_index
4443 .search(&query_cross_range, &NoOpMetricsCollector)
4444 .await
4445 .unwrap();
4446 let ranged_result_cross = ranged_index
4447 .search(&query_cross_range, &NoOpMetricsCollector)
4448 .await
4449 .unwrap();
4450 assert_eq!(
4451 full_result_cross, ranged_result_cross,
4452 "Cross-range range query [{}, {}] failed",
4453 range_start, range_end
4454 );
4455
4456 let single_range_start = (DEFAULT_BTREE_BATCH_SIZE * 4 - 300) as i32;
4458 let single_range_end = single_range_start + 200;
4459 let query_single_range = SargableQuery::Range(
4460 std::collections::Bound::Included(ScalarValue::Int32(Some(single_range_start))),
4461 std::collections::Bound::Excluded(ScalarValue::Int32(Some(single_range_end))),
4462 );
4463 let full_result_single = full_index
4464 .search(&query_single_range, &NoOpMetricsCollector)
4465 .await
4466 .unwrap();
4467 let ranged_result_single = ranged_index
4468 .search(&query_single_range, &NoOpMetricsCollector)
4469 .await
4470 .unwrap();
4471 assert_eq!(
4472 full_result_single, ranged_result_single,
4473 "Single range query [{}, {}] failed",
4474 single_range_start, single_range_end
4475 );
4476
4477 let large_range_start = 100_i32;
4479 let large_range_end = (DEFAULT_BTREE_BATCH_SIZE * 4 - 100) as i32;
4480 let query_large_range = SargableQuery::Range(
4481 std::collections::Bound::Included(ScalarValue::Int32(Some(large_range_start))),
4482 std::collections::Bound::Excluded(ScalarValue::Int32(Some(large_range_end))),
4483 );
4484 let full_result_single = full_index
4485 .search(&query_large_range, &NoOpMetricsCollector)
4486 .await
4487 .unwrap();
4488 let ranged_result_single = ranged_index
4489 .search(&query_large_range, &NoOpMetricsCollector)
4490 .await
4491 .unwrap();
4492 assert_eq!(
4493 full_result_single, ranged_result_single,
4494 "Single fragment range query [{}, {}] failed",
4495 large_range_start, large_range_end
4496 );
4497
4498 let remap_dir = TempObjDir::default();
4499 let remap_store = Arc::new(LanceIndexStore::new(
4500 Arc::new(ObjectStore::local()),
4501 remap_dir.clone(),
4502 Arc::new(LanceCache::no_cache()),
4503 ));
4504
4505 ranged_index
4507 .remap(&HashMap::default(), remap_store.as_ref())
4508 .await
4509 .unwrap();
4510
4511 let remap_index = BTreeIndex::load(remap_store.clone(), None, &LanceCache::no_cache())
4512 .await
4513 .unwrap();
4514
4515 assert_eq!(remap_index.page_lookup, ranged_index.page_lookup);
4516
4517 let ranged_pages = range_store
4518 .open_index_file(part_page_data_file_path(1 << 32).as_str())
4519 .await
4520 .unwrap();
4521 let remapped_pages = remap_store
4522 .open_index_file(part_page_data_file_path(1 << 32).as_str())
4523 .await
4524 .unwrap();
4525
4526 assert_eq!(ranged_pages.num_rows(), remapped_pages.num_rows());
4527
4528 let original_data = ranged_pages
4529 .read_record_batch(0, ranged_pages.num_rows() as u64)
4530 .await
4531 .unwrap();
4532 let remapped_data = remapped_pages
4533 .read_record_batch(0, remapped_pages.num_rows() as u64)
4534 .await
4535 .unwrap();
4536
4537 assert_eq!(original_data, remapped_data);
4538 }
4539
4540 #[tokio::test]
4541 async fn test_update_ranged_index() {
4542 let old_tmpdir = TempObjDir::default();
4544 let old_store = Arc::new(LanceIndexStore::new(
4545 Arc::new(ObjectStore::local()),
4546 old_tmpdir.clone(),
4547 Arc::new(LanceCache::no_cache()),
4548 ));
4549
4550 let new_tmpdir = TempObjDir::default();
4551 let new_store = Arc::new(LanceIndexStore::new(
4552 Arc::new(ObjectStore::local()),
4553 new_tmpdir.clone(),
4554 Arc::new(LanceCache::no_cache()),
4555 ));
4556
4557 let range1_gen = gen_batch()
4559 .col("value", array::step::<Int32Type>())
4560 .col("_rowid", array::step::<UInt64Type>())
4561 .into_df_stream(
4562 RowCount::from(DEFAULT_BTREE_BATCH_SIZE / 2),
4563 BatchCount::from(5),
4564 );
4565 let range1_data_source = Box::pin(RecordBatchStreamAdapter::new(
4566 range1_gen.schema(),
4567 range1_gen,
4568 ));
4569
4570 train_btree_index(
4571 range1_data_source,
4572 old_store.as_ref(),
4573 DEFAULT_BTREE_BATCH_SIZE,
4574 None,
4575 Option::from(1u32),
4576 )
4577 .await
4578 .unwrap();
4579
4580 let start_val = (DEFAULT_BTREE_BATCH_SIZE * 2 + DEFAULT_BTREE_BATCH_SIZE / 2) as i32;
4582 let end_val = (4 * DEFAULT_BTREE_BATCH_SIZE) as i32;
4583 let values_second_half: Vec<i32> = (start_val..end_val).collect();
4584 let row_ids_second_half: Vec<u64> = (start_val as u64..end_val as u64).collect();
4585 let range2_gen = gen_batch()
4586 .col("value", array::cycle::<Int32Type>(values_second_half))
4587 .col("_rowid", array::cycle::<UInt64Type>(row_ids_second_half))
4588 .into_df_stream(
4589 RowCount::from(DEFAULT_BTREE_BATCH_SIZE / 2),
4590 BatchCount::from(3),
4591 );
4592 let range2_data_source = Box::pin(RecordBatchStreamAdapter::new(
4593 range2_gen.schema(),
4594 range2_gen,
4595 ));
4596
4597 train_btree_index(
4598 range2_data_source,
4599 old_store.as_ref(),
4600 DEFAULT_BTREE_BATCH_SIZE,
4601 None,
4602 Option::from(2u32),
4603 )
4604 .await
4605 .unwrap();
4606
4607 let part_page_files = vec![
4609 part_page_data_file_path(1 << 32),
4610 part_page_data_file_path(2 << 32),
4611 ];
4612
4613 let part_lookup_files = vec![
4614 part_lookup_file_path(1 << 32),
4615 part_lookup_file_path(2 << 32),
4616 ];
4617
4618 super::merge_metadata_files(
4619 old_store.as_ref(),
4620 &part_page_files,
4621 &part_lookup_files,
4622 Option::from(1usize),
4623 noop_progress(),
4624 )
4625 .await
4626 .unwrap();
4627
4628 let start_val = (DEFAULT_BTREE_BATCH_SIZE * 2) as i32;
4630 let end_val = (DEFAULT_BTREE_BATCH_SIZE * 3) as i32;
4631 let row_id_delta = (DEFAULT_BTREE_BATCH_SIZE * 3) as i32;
4632 let values: Vec<i32> = (start_val..end_val).collect();
4633 let row_ids: Vec<u64> =
4634 ((start_val + row_id_delta) as u64..(end_val + row_id_delta) as u64).collect();
4635 let update_data = gen_batch()
4636 .col("value", array::cycle::<Int32Type>(values))
4637 .col("_rowid", array::cycle::<UInt64Type>(row_ids))
4638 .into_df_stream(
4639 RowCount::from(DEFAULT_BTREE_BATCH_SIZE / 2),
4640 BatchCount::from(2),
4641 );
4642 let update_data_source = Box::pin(RecordBatchStreamAdapter::new(
4643 update_data.schema(),
4644 update_data,
4645 ));
4646
4647 let ranged_index = BTreeIndex::load(old_store.clone(), None, &LanceCache::no_cache())
4648 .await
4649 .unwrap();
4650
4651 ranged_index
4653 .update(update_data_source, new_store.as_ref(), None)
4654 .await
4655 .expect("Error in updating ranged index");
4656
4657 let updated_index = BTreeIndex::load(new_store.clone(), None, &LanceCache::no_cache())
4658 .await
4659 .unwrap();
4660
4661 assert!(
4662 updated_index.ranges_to_files.is_none(),
4663 "Updated ranged-btree-index should fall back to non-ranged"
4664 );
4665
4666 let updated_value = (DEFAULT_BTREE_BATCH_SIZE * 2 + (DEFAULT_BTREE_BATCH_SIZE / 2)) as i32;
4667 let updated_query = SargableQuery::Equals(ScalarValue::Int32(Some(updated_value)));
4668
4669 let query_result = updated_index
4670 .search(&updated_query, &NoOpMetricsCollector)
4671 .await
4672 .unwrap();
4673 match query_result {
4674 SearchResult::Exact(row_id_map) => {
4675 assert!(
4676 row_id_map.selected(updated_value as u64),
4677 "Updated index should contain original rowids."
4678 );
4679 assert!(
4680 row_id_map.selected((updated_value + row_id_delta) as u64),
4681 "Updated index should contain new rowids"
4682 );
4683 }
4684 _ => {
4685 panic!("Btree search result should always be Exact.");
4686 }
4687 }
4688 }
4689
4690 #[tokio::test]
4691 async fn test_update_with_exact_row_id_filter() {
4692 let old_tmpdir = TempObjDir::default();
4693 let old_store = Arc::new(LanceIndexStore::new(
4694 Arc::new(ObjectStore::local()),
4695 old_tmpdir.clone(),
4696 Arc::new(LanceCache::no_cache()),
4697 ));
4698
4699 let new_tmpdir = TempObjDir::default();
4700 let new_store = Arc::new(LanceIndexStore::new(
4701 Arc::new(ObjectStore::local()),
4702 new_tmpdir.clone(),
4703 Arc::new(LanceCache::no_cache()),
4704 ));
4705
4706 let old_data = gen_batch()
4707 .col("value", array::step::<Int32Type>())
4708 .col("_rowid", array::step::<UInt64Type>())
4709 .into_df_stream(RowCount::from(512), BatchCount::from(2));
4710 let old_data_source = Box::pin(RecordBatchStreamAdapter::new(old_data.schema(), old_data));
4711 train_btree_index(
4712 old_data_source,
4713 old_store.as_ref(),
4714 DEFAULT_BTREE_BATCH_SIZE,
4715 None,
4716 None,
4717 )
4718 .await
4719 .unwrap();
4720
4721 let index = BTreeIndex::load(old_store.clone(), None, &LanceCache::no_cache())
4722 .await
4723 .unwrap();
4724
4725 let new_data = gen_batch()
4726 .col("value", array::step_custom::<Int32Type>(2000, 1))
4727 .col("_rowid", array::step_custom::<UInt64Type>(2000, 1))
4728 .into_df_stream(RowCount::from(100), BatchCount::from(1));
4729 let new_data_source = Box::pin(RecordBatchStreamAdapter::new(new_data.schema(), new_data));
4730
4731 let mut retained_old_rows = RowAddrTreeMap::new();
4732 retained_old_rows.insert_range(0..64);
4733 retained_old_rows.insert_range(300..364);
4734
4735 index
4736 .update(
4737 new_data_source,
4738 new_store.as_ref(),
4739 Some(OldIndexDataFilter::RowIds(retained_old_rows)),
4740 )
4741 .await
4742 .unwrap();
4743
4744 let updated_index = BTreeIndex::load(new_store.clone(), None, &LanceCache::no_cache())
4745 .await
4746 .unwrap();
4747
4748 let present = |value: i32| {
4749 let updated_index = updated_index.clone();
4750 async move {
4751 let query = SargableQuery::Equals(ScalarValue::Int32(Some(value)));
4752 match updated_index
4753 .search(&query, &NoOpMetricsCollector)
4754 .await
4755 .unwrap()
4756 {
4757 SearchResult::Exact(row_id_map) => row_id_map.selected(value as u64),
4758 _ => unreachable!("Btree search result should always be Exact"),
4759 }
4760 }
4761 };
4762
4763 assert!(present(12).await);
4764 assert!(present(320).await);
4765 assert!(!present(120).await);
4766 assert!(!present(420).await);
4767 assert!(present(2005).await);
4768 }
4769
4770 #[tokio::test]
4780 async fn test_btree_remap_big_deletions() {
4781 let tmpdir = TempObjDir::default();
4782 let test_store = Arc::new(LanceIndexStore::new(
4783 Arc::new(ObjectStore::local()),
4784 tmpdir.clone(),
4785 Arc::new(LanceCache::no_cache()),
4786 ));
4787
4788 let batch_size = 4096;
4791 let total_rows = 15000;
4792
4793 let stream = gen_batch()
4794 .col("value", array::step::<Int32Type>())
4795 .col("_rowid", array::step::<UInt64Type>())
4796 .into_df_stream(RowCount::from(total_rows), BatchCount::from(1));
4797
4798 train_btree_index(stream, test_store.as_ref(), batch_size, None, None)
4799 .await
4800 .unwrap();
4801
4802 let index = BTreeIndex::load(test_store.clone(), None, &LanceCache::no_cache())
4803 .await
4804 .unwrap();
4805
4806 let mut mapping: HashMap<u64, Option<u64>> = HashMap::new();
4812
4813 for old_id in 1001..10000 {
4815 mapping.insert(old_id, None);
4816 }
4817
4818 let mut new_id_counter = 100_000;
4819
4820 for old_id in (0..1000).chain(10000..15000) {
4822 let new_id = new_id_counter;
4823 new_id_counter += 1;
4824 mapping.insert(old_id, Some(new_id));
4825 }
4826
4827 let remap_dir = TempObjDir::default();
4828 let remap_store = Arc::new(LanceIndexStore::new(
4829 Arc::new(ObjectStore::local()),
4830 remap_dir.clone(),
4831 Arc::new(LanceCache::no_cache()),
4832 ));
4833
4834 index.remap(&mapping, remap_store.as_ref()).await.unwrap();
4836
4837 let remapped_index = BTreeIndex::load(remap_store.clone(), None, &LanceCache::no_cache())
4838 .await
4839 .unwrap();
4840
4841 let should_exist = vec![0, 500, 1000, 10000, 13000, 14000, 14999];
4845 for value in should_exist {
4846 let query = SargableQuery::Equals(ScalarValue::Int32(Some(value)));
4847 let result = remapped_index
4848 .search(&query, &NoOpMetricsCollector)
4849 .await
4850 .unwrap();
4851 match result {
4852 SearchResult::Exact(row_id_map) => {
4853 assert!(
4854 !row_id_map.is_empty(),
4855 "Value {} should exist in remapped index but was not found",
4856 value
4857 );
4858 }
4859 _ => {
4860 panic!("Btree search result should always be Exact.");
4861 }
4862 }
4863 }
4864
4865 let should_not_exist = vec![1001, 5000, 8000, 9999];
4867 for value in should_not_exist {
4868 let query = SargableQuery::Equals(ScalarValue::Int32(Some(value)));
4869 let result = remapped_index
4870 .search(&query, &NoOpMetricsCollector)
4871 .await
4872 .unwrap();
4873 match result {
4874 SearchResult::Exact(row_id_map) => {
4875 assert!(
4876 row_id_map.is_empty(),
4877 "Value {} should NOT exist in remapped index but was found",
4878 value
4879 );
4880 }
4881 _ => {
4882 panic!("Btree search result should always be Exact.");
4883 }
4884 }
4885 }
4886 }
4887
4888 #[tokio::test]
4894 async fn test_search_tracks_nulls_for_absent_value() {
4895 use arrow_array::{Int32Array, UInt64Array};
4896
4897 let tmpdir = TempObjDir::default();
4898 let test_store = Arc::new(LanceIndexStore::new(
4899 Arc::new(ObjectStore::local()),
4900 tmpdir.clone(),
4901 Arc::new(LanceCache::no_cache()),
4902 ));
4903
4904 let num_rows = 5000u64;
4908 let values: Int32Array = (0..num_rows)
4909 .map(|i| {
4910 if i % 5 != 0 {
4911 None } else {
4913 Some(100 + i as i32) }
4915 })
4916 .collect();
4917 let row_ids = UInt64Array::from_iter_values(0..num_rows);
4918 let data = arrow_array::RecordBatch::try_from_iter(vec![
4919 ("value", Arc::new(values) as arrow_array::ArrayRef),
4920 ("_rowid", Arc::new(row_ids) as arrow_array::ArrayRef),
4921 ])
4922 .unwrap();
4923
4924 let schema = data.schema();
4925 let stream: SendableRecordBatchStream = Box::pin(RecordBatchStreamAdapter::new(
4926 schema,
4927 stream::iter(vec![Ok(data)]),
4928 ));
4929 train_btree_index(stream, test_store.as_ref(), num_rows, None, None)
4930 .await
4931 .unwrap();
4932
4933 let index = BTreeIndex::load(test_store.clone(), None, &LanceCache::no_cache())
4934 .await
4935 .unwrap();
4936
4937 assert!(
4939 !index.page_lookup.all_null_pages.is_empty(),
4940 "Test setup requires all-null pages; got null_pages={}, all_null_pages={}",
4941 index.page_lookup.null_pages.len(),
4942 index.page_lookup.all_null_pages.len(),
4943 );
4944
4945 let metrics = NoOpMetricsCollector;
4946
4947 let result = index
4949 .search(
4950 &SargableQuery::Equals(ScalarValue::Int32(Some(0))),
4951 &metrics,
4952 )
4953 .await
4954 .unwrap();
4955
4956 match result {
4957 SearchResult::Exact(set) => {
4958 assert!(set.true_rows().is_empty(), "No rows should match Equals(0)");
4960 assert!(
4962 !set.null_rows().is_empty(),
4963 "Null rows must be tracked even when no pages match the value"
4964 );
4965 }
4966 _ => panic!("BTree search should return Exact"),
4967 }
4968
4969 let result = index
4971 .search(
4972 &SargableQuery::Range(
4973 std::ops::Bound::Unbounded,
4974 std::ops::Bound::Excluded(ScalarValue::Int32(Some(50))),
4975 ),
4976 &metrics,
4977 )
4978 .await
4979 .unwrap();
4980
4981 match result {
4982 SearchResult::Exact(set) => {
4983 assert!(set.true_rows().is_empty(), "No rows should be < 50");
4984 assert!(
4985 !set.null_rows().is_empty(),
4986 "Null rows must be tracked for range queries too"
4987 );
4988 }
4989 _ => panic!("BTree search should return Exact"),
4990 }
4991 }
4992
4993 fn sample_lookup_batch() -> RecordBatch {
4994 record_batch!(
4995 ("min", Int32, [Some(0), Some(10), Some(20)]),
4996 ("max", Int32, [Some(9), Some(19), Some(29)]),
4997 ("null_count", UInt32, [0, 2, 0]),
4998 ("page_idx", UInt32, [0, 1, 2])
4999 )
5000 .unwrap()
5001 }
5002
5003 fn assert_state_roundtrips(state: &BTreeIndexState) {
5004 let mut buf = Vec::new();
5005 state.serialize(&mut buf).unwrap();
5006 let restored = BTreeIndexState::deserialize(&bytes::Bytes::from(buf)).unwrap();
5007 assert_eq!(restored.lookup_batch, state.lookup_batch);
5008 assert_eq!(restored.batch_size, state.batch_size);
5009 assert_eq!(restored.ranges_to_files, state.ranges_to_files);
5010 }
5011
5012 #[test]
5013 fn test_btree_page_key_codec() {
5014 assert!(BTreePageKey::codec().is_some());
5016 }
5017
5018 #[test]
5019 fn test_btree_index_state_roundtrip() {
5020 assert_state_roundtrips(&BTreeIndexState {
5022 lookup_batch: sample_lookup_batch(),
5023 batch_size: DEFAULT_BTREE_BATCH_SIZE,
5024 ranges_to_files: None,
5025 });
5026
5027 let ranges: RangeInclusiveMap<u32, (String, u32)> = [
5029 (0..=99, ("part_0_page_file.lance".to_string(), 0)),
5030 (100..=199, ("part_1_page_file.lance".to_string(), 100)),
5031 ]
5032 .into_iter()
5033 .collect();
5034 assert_state_roundtrips(&BTreeIndexState {
5035 lookup_batch: sample_lookup_batch(),
5036 batch_size: 8192,
5037 ranges_to_files: Some(Arc::new(ranges)),
5038 });
5039
5040 assert_state_roundtrips(&BTreeIndexState {
5042 lookup_batch: RecordBatch::new_empty(sample_lookup_batch().schema()),
5043 batch_size: DEFAULT_BTREE_BATCH_SIZE,
5044 ranges_to_files: None,
5045 });
5046 }
5047
5048 #[tokio::test]
5049 async fn test_btree_index_state_reconstruct_and_plugin_cache() {
5050 let tmpdir = TempObjDir::default();
5051 let test_store = Arc::new(LanceIndexStore::new(
5052 Arc::new(ObjectStore::local()),
5053 tmpdir.clone(),
5054 Arc::new(LanceCache::no_cache()),
5055 ));
5056
5057 let stream = gen_batch()
5058 .col("value", array::step::<Int32Type>())
5059 .col("_rowid", array::step::<UInt64Type>())
5060 .into_df_stream(RowCount::from(1000), BatchCount::from(5));
5061 train_btree_index(stream, test_store.as_ref(), 1000, None, None)
5062 .await
5063 .unwrap();
5064
5065 let index = BTreeIndex::load(test_store.clone(), None, &LanceCache::no_cache())
5066 .await
5067 .unwrap();
5068
5069 let state = BTreeIndexState {
5071 lookup_batch: index.lookup_batch.clone(),
5072 batch_size: index.batch_size,
5073 ranges_to_files: index.ranges_to_files.clone(),
5074 };
5075 let mut buf = Vec::new();
5076 state.serialize(&mut buf).unwrap();
5077 let restored = BTreeIndexState::deserialize(&bytes::Bytes::from(buf)).unwrap();
5078 let reconstructed = restored
5079 .reconstruct(test_store.clone(), &LanceCache::no_cache(), None)
5080 .unwrap();
5081 assert_eq!(
5082 reconstructed
5083 .as_any()
5084 .downcast_ref::<BTreeIndex>()
5085 .unwrap()
5086 .page_lookup,
5087 index.page_lookup
5088 );
5089
5090 let cache = LanceCache::with_capacity(64 * 1024 * 1024);
5092 let plugin = BTreeIndexPlugin;
5093 plugin.put_in_cache(&cache, index.clone()).await.unwrap();
5094 let from_cache = plugin
5095 .get_from_cache(test_store.clone(), None, &cache)
5096 .await
5097 .unwrap()
5098 .expect("index should be served from the cache");
5099
5100 let query = SargableQuery::Range(
5102 std::ops::Bound::Included(ScalarValue::Int32(Some(100))),
5103 std::ops::Bound::Excluded(ScalarValue::Int32(Some(200))),
5104 );
5105 let expected = index.search(&query, &NoOpMetricsCollector).await.unwrap();
5106 let actual = from_cache
5107 .search(&query, &NoOpMetricsCollector)
5108 .await
5109 .unwrap();
5110 assert_eq!(expected, actual);
5111 }
5112
5113 #[test]
5114 fn test_btree_index_state_rejects_invalid_has_ranges_tag() {
5115 let mut buf = Vec::new();
5117 buf.extend_from_slice(&1000u64.to_le_bytes());
5118 buf.push(7u8);
5119 let err = BTreeIndexState::deserialize(&bytes::Bytes::from(buf)).unwrap_err();
5120 let msg = err.to_string();
5121 assert!(
5122 msg.contains("has_ranges") && msg.contains("7"),
5123 "expected error to mention the bad has_ranges tag, got: {msg}"
5124 );
5125 }
5126
5127 #[tokio::test]
5128 async fn test_btree_index_state_reconstruct_applies_frag_reuse_index() {
5129 use crate::frag_reuse::{FragReuseIndex, FragReuseIndexDetails};
5130 use std::collections::HashMap;
5131 use uuid::Uuid;
5132
5133 let tmpdir = TempObjDir::default();
5134 let test_store = Arc::new(LanceIndexStore::new(
5135 Arc::new(ObjectStore::local()),
5136 tmpdir.clone(),
5137 Arc::new(LanceCache::no_cache()),
5138 ));
5139
5140 let stream = gen_batch()
5142 .col("value", array::step::<Int32Type>())
5143 .col("_rowid", array::step::<UInt64Type>())
5144 .into_df_stream(RowCount::from(1000), BatchCount::from(1));
5145 train_btree_index(stream, test_store.as_ref(), 1000, None, None)
5146 .await
5147 .unwrap();
5148
5149 let index = BTreeIndex::load(test_store.clone(), None, &LanceCache::no_cache())
5150 .await
5151 .unwrap();
5152 let state = BTreeIndexState {
5153 lookup_batch: index.lookup_batch.clone(),
5154 batch_size: index.batch_size,
5155 ranges_to_files: index.ranges_to_files.clone(),
5156 };
5157
5158 let frag_reuse_index = Arc::new(FragReuseIndex::new(
5162 Uuid::new_v4(),
5163 vec![HashMap::from([(0u64, Some(5000u64))])],
5164 FragReuseIndexDetails { versions: vec![] },
5165 ));
5166 let reconstructed = state
5167 .reconstruct(
5168 test_store.clone(),
5169 &LanceCache::no_cache(),
5170 Some(frag_reuse_index),
5171 )
5172 .unwrap();
5173
5174 let result = reconstructed
5175 .search(
5176 &SargableQuery::Equals(ScalarValue::Int32(Some(0))),
5177 &NoOpMetricsCollector,
5178 )
5179 .await
5180 .unwrap();
5181 let row_ids: Vec<u64> = match &result {
5182 SearchResult::Exact(set) => set
5183 .true_rows()
5184 .row_addrs()
5185 .unwrap()
5186 .map(u64::from)
5187 .collect(),
5188 other => panic!("expected Exact, got {other:?}"),
5189 };
5190 assert_eq!(
5191 row_ids,
5192 vec![5000],
5193 "frag_reuse_index remap was not applied"
5194 );
5195 }
5196
5197 #[tokio::test]
5198 async fn test_btree_index_state_range_partitioned_plugin_cache_roundtrip() {
5199 let tmpdir = TempObjDir::default();
5203 let store = Arc::new(LanceIndexStore::new(
5204 Arc::new(ObjectStore::local()),
5205 tmpdir.clone(),
5206 Arc::new(LanceCache::no_cache()),
5207 ));
5208
5209 let half = DEFAULT_BTREE_BATCH_SIZE;
5210 let total = (2 * half) as i32;
5211
5212 let part0 = gen_batch()
5214 .col("value", array::step::<Int32Type>())
5215 .col("_rowid", array::step::<UInt64Type>())
5216 .into_df_stream(RowCount::from(half), BatchCount::from(1));
5217 train_btree_index(part0, store.as_ref(), half, None, Some(0u32))
5218 .await
5219 .unwrap();
5220
5221 let values: Vec<i32> = (half as i32..total).collect();
5223 let row_ids: Vec<u64> = (half..total as u64).collect();
5224 let part1 = gen_batch()
5225 .col("value", array::cycle::<Int32Type>(values))
5226 .col("_rowid", array::cycle::<UInt64Type>(row_ids))
5227 .into_df_stream(RowCount::from(half), BatchCount::from(1));
5228 train_btree_index(part1, store.as_ref(), half, None, Some(1u32))
5229 .await
5230 .unwrap();
5231
5232 super::merge_metadata_files(
5233 store.as_ref(),
5234 &[
5235 part_page_data_file_path(0 << 32),
5236 part_page_data_file_path(1 << 32),
5237 ],
5238 &[
5239 part_lookup_file_path(0 << 32),
5240 part_lookup_file_path(1 << 32),
5241 ],
5242 Some(1usize),
5243 noop_progress(),
5244 )
5245 .await
5246 .unwrap();
5247
5248 let index = BTreeIndex::load(store.clone(), None, &LanceCache::no_cache())
5249 .await
5250 .unwrap();
5251 assert!(
5252 index.ranges_to_files.is_some(),
5253 "test setup should produce a range-partitioned index",
5254 );
5255
5256 let cache = LanceCache::with_capacity(64 * 1024 * 1024);
5257 let plugin = BTreeIndexPlugin;
5258 plugin.put_in_cache(&cache, index.clone()).await.unwrap();
5259 let from_cache = plugin
5260 .get_from_cache(store.clone(), None, &cache)
5261 .await
5262 .unwrap()
5263 .expect("index should be served from the cache");
5264
5265 for value in [0i32, total - 1] {
5267 let query = SargableQuery::Equals(ScalarValue::Int32(Some(value)));
5268 let expected = index.search(&query, &NoOpMetricsCollector).await.unwrap();
5269 let actual = from_cache
5270 .search(&query, &NoOpMetricsCollector)
5271 .await
5272 .unwrap();
5273 assert_eq!(expected, actual, "value {value}");
5274 }
5275 }
5276}