1use crate::pbold;
16use crate::scalar::expression::{SargableQueryParser, ScalarQueryParser};
17use crate::scalar::registry::{
18 BasicTrainer, ScalarIndexPlugin, TrainingCriteria, TrainingOrdering, TrainingRequest,
19};
20use crate::scalar::seed::IndexSeedWriter;
21use crate::scalar::{
22 BuiltinIndexType, CreatedIndex, IndexFile, SargableQuery, ScalarIndexParams, UpdateCriteria,
23 compute_next_prefix,
24};
25use lance_arrow_stats::StatisticsAccumulator;
26use lance_core::cache::{LanceCache, WeakLanceCache};
27use lance_core::utils::row_addr_remap::RowAddrRemap;
28use serde::{Deserialize, Serialize};
29use std::any::Any;
30use std::sync::LazyLock;
31
32use arrow_array::{
33 ArrayRef, RecordBatch, UInt32Array, UInt64Array, new_empty_array, new_null_array,
34};
35use arrow_schema::{DataType, Field};
36use datafusion::execution::SendableRecordBatchStream;
37use datafusion_common::ScalarValue;
38use lance_select::RowAddrTreeMap;
39use std::{collections::HashMap, sync::Arc};
40
41use super::{AnyQuery, IndexStore, MetricsCollector, ScalarIndex, SearchResult};
42use crate::scalar::RowIdRemapper;
43use crate::{Index, IndexType};
44use async_trait::async_trait;
45use lance_core::Error;
46use lance_core::Result;
47use lance_core::deepsize::DeepSizeOf;
48use roaring::RoaringBitmap;
49
50use super::zoned::{ZoneBound, ZoneProcessor, ZoneTrainer, rebuild_zones, search_zones};
51const ROWS_PER_ZONE_DEFAULT: u64 = 8192; const ZONEMAP_FILENAME: &str = "zonemap.lance";
54const ZONEMAP_SIZE_META_KEY: &str = "rows_per_zone";
55const NULL_BITMAP_META_KEY: &str = "null_bitmap";
56const ZONEMAP_INDEX_VERSION: u32 = 0;
57
58#[derive(Debug, PartialEq, Clone)]
60pub(crate) struct ZoneMapStatistics {
61 min: ScalarValue,
62 max: ScalarValue,
63 null_count: u32,
64 nan_count: u32,
66 bound: ZoneBound,
69}
70
71impl DeepSizeOf for ZoneMapStatistics {
72 fn deep_size_of_children(&self, _context: &mut lance_core::deepsize::Context) -> usize {
73 let min_size = self.min.size() - std::mem::size_of::<ScalarValue>();
75 let max_size = self.max.size() - std::mem::size_of::<ScalarValue>();
76
77 min_size + max_size
78 }
79}
80
81impl AsRef<ZoneBound> for ZoneMapStatistics {
82 fn as_ref(&self) -> &ZoneBound {
83 &self.bound
84 }
85}
86
87pub struct ZoneMapIndex {
109 zones: Vec<ZoneMapStatistics>,
110 data_type: DataType,
111 rows_per_zone: u64,
113 use_seeds: bool,
114 store: Arc<dyn IndexStore>,
115 fri: Option<Arc<dyn RowIdRemapper>>,
116 index_cache: WeakLanceCache,
117 null_rows: Option<RowAddrTreeMap>,
120}
121
122impl std::fmt::Debug for ZoneMapIndex {
123 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
124 f.debug_struct("ZoneMapIndex")
125 .field("zones", &self.zones)
126 .field("data_type", &self.data_type)
127 .field("rows_per_zone", &self.rows_per_zone)
128 .field("use_seeds", &self.use_seeds)
129 .field("store", &self.store)
130 .field("fri", &self.fri)
131 .field("index_cache", &self.index_cache)
132 .finish()
133 }
134}
135
136impl DeepSizeOf for ZoneMapIndex {
137 fn deep_size_of_children(&self, context: &mut lance_core::deepsize::Context) -> usize {
138 self.zones.deep_size_of_children(context) + self.null_rows.deep_size_of_children(context)
139 }
140}
141
142impl ZoneMapIndex {
143 pub fn rows_per_zone(&self) -> u64 {
145 self.rows_per_zone
146 }
147
148 fn scalar_is_nan(value: &ScalarValue) -> bool {
149 match value {
150 ScalarValue::Float16(Some(value)) => value.is_nan(),
151 ScalarValue::Float32(Some(value)) => value.is_nan(),
152 ScalarValue::Float64(Some(value)) => value.is_nan(),
153 _ => false,
154 }
155 }
156
157 fn zone_has_finite_min(zone: &ZoneMapStatistics) -> bool {
159 !(zone.min.is_null() || Self::scalar_is_nan(&zone.min))
160 }
161
162 fn zone_has_finite_extrema(zone: &ZoneMapStatistics) -> bool {
164 Self::zone_has_finite_min(zone) && !(zone.max.is_null() || Self::scalar_is_nan(&zone.max))
165 }
166
167 pub fn value_range_over<'a>(
182 segments: impl IntoIterator<Item = &'a Self>,
183 ) -> Option<(ScalarValue, ScalarValue)> {
184 let mut min: Option<&ScalarValue> = None;
185 let mut max: Option<&ScalarValue> = None;
186 for zone in segments.into_iter().flat_map(|seg| seg.zones.iter()) {
187 if Self::scalar_is_nan(&zone.max) {
188 return None;
189 }
190 if Self::scalar_is_finite_bound(&zone.min)
191 && min.is_none_or(|cur| zone.min.partial_cmp(cur).is_some_and(|o| o.is_lt()))
192 {
193 min = Some(&zone.min);
194 }
195 if Self::scalar_is_finite_bound(&zone.max)
196 && max.is_none_or(|cur| zone.max.partial_cmp(cur).is_some_and(|o| o.is_gt()))
197 {
198 max = Some(&zone.max);
199 }
200 }
201 Some((min?.clone(), max?.clone()))
202 }
203
204 fn scalar_is_finite_bound(v: &ScalarValue) -> bool {
206 !v.is_null() && !Self::scalar_is_nan(v)
207 }
208
209 fn evaluate_zone_against_query(
215 &self,
216 zone: &ZoneMapStatistics,
217 query: &SargableQuery,
218 ) -> Result<bool> {
219 use std::ops::Bound;
220
221 match query {
222 SargableQuery::IsNull() => {
223 Ok(zone.null_count > 0)
225 }
226 SargableQuery::Equals(target) => {
227 if target.is_null() {
230 return Ok(zone.null_count > 0);
231 }
232
233 let is_nan = match target {
235 ScalarValue::Float16(Some(f)) => f.is_nan(),
236 ScalarValue::Float32(Some(f)) => f.is_nan(),
237 ScalarValue::Float64(Some(f)) => f.is_nan(),
238 _ => false,
239 };
240
241 if is_nan {
242 return Ok(zone.nan_count > 0);
243 }
244
245 if !Self::zone_has_finite_min(zone) {
246 return Ok(false);
247 }
248
249 Ok(target >= &zone.min && target <= &zone.max)
250 }
251 SargableQuery::Range(start, end) => {
252 if !Self::zone_has_finite_min(zone) {
255 return Ok(false);
256 }
257
258 let zone_min = &zone.min;
259 let zone_max = &zone.max;
260
261 let start_check = match start {
262 Bound::Unbounded => true,
263 Bound::Included(s) => {
264 match s {
266 ScalarValue::Float16(Some(f)) => {
267 if f.is_nan() {
268 return Ok(zone.nan_count > 0);
269 }
270 }
271 ScalarValue::Float32(Some(f)) => {
272 if f.is_nan() {
273 return Ok(zone.nan_count > 0);
274 }
275 }
276 ScalarValue::Float64(Some(f)) if f.is_nan() => {
277 return Ok(zone.nan_count > 0);
278 }
279 _ => {}
280 }
281 match zone_max {
285 ScalarValue::Float16(Some(f)) if f.is_nan() => true,
286 ScalarValue::Float32(Some(f)) if f.is_nan() => true,
287 ScalarValue::Float64(Some(f)) if f.is_nan() => true,
288 _ => zone_max >= s,
289 }
290 }
291 Bound::Excluded(s) => {
292 match s {
294 ScalarValue::Float16(Some(f)) => {
295 if f.is_nan() {
296 return Ok(false); }
298 }
299 ScalarValue::Float32(Some(f)) => {
300 if f.is_nan() {
301 return Ok(false); }
303 }
304 ScalarValue::Float64(Some(f)) if f.is_nan() => {
305 return Ok(false); }
307 _ => {}
308 }
309 zone_max > s
310 }
311 };
312
313 let end_check = match end {
314 Bound::Unbounded => true,
315 Bound::Included(e) => {
316 match e {
318 ScalarValue::Float16(Some(f)) => {
319 if f.is_nan() {
320 return Ok(zone.nan_count > 0 || zone_min <= e);
322 }
323 }
324 ScalarValue::Float32(Some(f)) => {
325 if f.is_nan() {
326 return Ok(zone.nan_count > 0 || zone_min <= e);
327 }
328 }
329 ScalarValue::Float64(Some(f)) if f.is_nan() => {
330 return Ok(zone.nan_count > 0 || zone_min <= e);
331 }
332 _ => {}
333 }
334 zone_min <= e
335 }
336 Bound::Excluded(e) => {
337 match e {
339 ScalarValue::Float16(Some(f)) => {
340 if f.is_nan() {
341 return Ok(true);
343 }
344 }
345 ScalarValue::Float32(Some(f)) => {
346 if f.is_nan() {
347 return Ok(true);
348 }
349 }
350 ScalarValue::Float64(Some(f)) if f.is_nan() => {
351 return Ok(true);
352 }
353 _ => {}
354 }
355 zone_min < e
356 }
357 };
358
359 Ok(start_check && end_check)
360 }
361 SargableQuery::IsIn(values) => {
362 Ok(values.iter().any(|value| {
364 if value.is_null() {
365 zone.null_count > 0
366 } else {
367 match value {
368 ScalarValue::Float16(Some(f)) => {
369 if f.is_nan() {
370 zone.nan_count > 0
371 } else if !Self::zone_has_finite_min(zone) {
372 false
373 } else {
374 value >= &zone.min && value <= &zone.max
375 }
376 }
377 ScalarValue::Float32(Some(f)) => {
378 if f.is_nan() {
379 zone.nan_count > 0
380 } else if !Self::zone_has_finite_min(zone) {
381 false
382 } else {
383 value >= &zone.min && value <= &zone.max
384 }
385 }
386 ScalarValue::Float64(Some(f)) => {
387 if f.is_nan() {
388 zone.nan_count > 0
389 } else if !Self::zone_has_finite_min(zone) {
390 false
391 } else {
392 value >= &zone.min && value <= &zone.max
393 }
394 }
395 _ => {
396 Self::zone_has_finite_extrema(zone)
397 && value >= &zone.min
398 && value <= &zone.max
399 }
400 }
401 }
402 }))
403 }
404 SargableQuery::FullTextSearch(_) => Err(Error::not_supported_source(
405 "full text search is not supported for zonemap indexes".into(),
406 )),
407 SargableQuery::LikePrefix(prefix) => {
408 let prefix_str = match prefix {
418 ScalarValue::Utf8(Some(s)) => s.as_str(),
419 ScalarValue::LargeUtf8(Some(s)) => s.as_str(),
420 _ => return Ok(true), };
422
423 if prefix_str.is_empty() {
425 return Ok(true);
426 }
427
428 let max_check = &zone.max >= prefix;
430 if !max_check {
431 return Ok(false);
432 }
433
434 let next_prefix = compute_next_prefix(prefix_str);
437
438 match next_prefix {
439 Some(next) => {
440 let next_scalar = match prefix {
442 ScalarValue::Utf8(_) => ScalarValue::Utf8(Some(next)),
443 ScalarValue::LargeUtf8(_) => ScalarValue::LargeUtf8(Some(next)),
444 _ => return Ok(true),
445 };
446 Ok(zone.min < next_scalar)
447 }
448 None => {
449 Ok(true)
451 }
452 }
453 }
454 }
455 }
456
457 async fn load(
459 store: Arc<dyn IndexStore>,
460 fri: Option<Arc<dyn RowIdRemapper>>,
461 index_cache: &LanceCache,
462 use_seeds: bool,
463 ) -> Result<Arc<Self>>
464 where
465 Self: Sized,
466 {
467 let index_file = store.open_index_file(ZONEMAP_FILENAME).await?;
468 let zone_maps = index_file
469 .read_range(0..index_file.num_rows(), None)
470 .await?;
471 let file_schema = index_file.schema();
472
473 let rows_per_zone: u64 = file_schema
474 .metadata
475 .get(ZONEMAP_SIZE_META_KEY)
476 .and_then(|bs| bs.parse().ok())
477 .unwrap_or(ROWS_PER_ZONE_DEFAULT);
478
479 let null_rows = if let Some(idx_str) = file_schema.metadata.get(NULL_BITMAP_META_KEY) {
480 let idx = idx_str.parse::<u32>().map_err(|e| {
481 Error::invalid_input(format!("invalid null bitmap buffer index: {e}"))
482 })?;
483 let bytes = index_file.read_global_buffer(idx).await?;
484 Some(RowAddrTreeMap::deserialize_from(bytes.as_ref())?)
485 } else {
486 None
487 };
488
489 Ok(Arc::new(Self::try_from_serialized(
490 zone_maps,
491 store,
492 fri,
493 index_cache,
494 rows_per_zone,
495 null_rows,
496 use_seeds,
497 )?))
498 }
499
500 fn try_from_serialized(
501 data: RecordBatch,
502 store: Arc<dyn IndexStore>,
503 fri: Option<Arc<dyn RowIdRemapper>>,
504 index_cache: &LanceCache,
505 rows_per_zone: u64,
506 null_rows: Option<RowAddrTreeMap>,
507 use_seeds: bool,
508 ) -> Result<Self> {
509 let min_col = data
511 .column_by_name("min")
512 .ok_or_else(|| Error::invalid_input("ZoneMapIndex: missing 'min' column"))?;
513 let max_col = data
514 .column_by_name("max")
515 .ok_or_else(|| Error::invalid_input("ZoneMapIndex: missing 'max' column"))?;
516 let null_count_col = data
517 .column_by_name("null_count")
518 .ok_or_else(|| Error::invalid_input("ZoneMapIndex: missing 'null_count' column"))?
519 .as_any()
520 .downcast_ref::<arrow_array::UInt32Array>()
521 .ok_or_else(|| {
522 Error::invalid_input("ZoneMapIndex: 'null_count' column is not UInt32")
523 })?;
524 let nan_count_col = data
525 .column_by_name("nan_count")
526 .ok_or_else(|| Error::invalid_input("ZoneMapIndex: missing 'nan_count' column"))?
527 .as_any()
528 .downcast_ref::<arrow_array::UInt32Array>()
529 .ok_or_else(|| {
530 Error::invalid_input("ZoneMapIndex: 'nan_count' column is not UInt32")
531 })?;
532 let zone_length = data
533 .column_by_name("zone_length")
534 .ok_or_else(|| Error::invalid_input("ZoneMapIndex: missing 'zone_length' column"))?
535 .as_any()
536 .downcast_ref::<arrow_array::UInt64Array>()
537 .ok_or_else(|| {
538 Error::invalid_input("ZoneMapIndex: 'zone_length' column is not UInt64")
539 })?;
540
541 let fragment_id_col = data
542 .column_by_name("fragment_id")
543 .ok_or_else(|| Error::invalid_input("ZoneMapIndex: missing 'fragment_id' column"))?
544 .as_any()
545 .downcast_ref::<arrow_array::UInt64Array>()
546 .ok_or_else(|| {
547 Error::invalid_input("ZoneMapIndex: 'fragment_id' column is not UInt64")
548 })?;
549
550 let zone_start_col = data
551 .column_by_name("zone_start")
552 .ok_or_else(|| Error::invalid_input("ZoneMapIndex: missing 'zone_start' column"))?
553 .as_any()
554 .downcast_ref::<arrow_array::UInt64Array>()
555 .ok_or_else(|| {
556 Error::invalid_input("ZoneMapIndex: 'zone_start' column is not UInt64")
557 })?;
558
559 let data_type = min_col.data_type().clone();
560
561 if data.num_rows() == 0 {
562 return Ok(Self {
563 zones: Vec::new(),
564 data_type,
565 rows_per_zone,
566 use_seeds,
567 store,
568 fri,
569 index_cache: WeakLanceCache::from(index_cache),
570 null_rows,
571 });
572 }
573
574 let num_zones = data.num_rows();
575 let mut zones = Vec::with_capacity(num_zones);
576
577 for i in 0..num_zones {
578 let min = ScalarValue::try_from_array(min_col, i)?;
579 let max = ScalarValue::try_from_array(max_col, i)?;
580 let null_count = null_count_col.value(i);
581 let nan_count = nan_count_col.value(i);
582 zones.push(ZoneMapStatistics {
583 min,
584 max,
585 null_count,
586 nan_count,
587 bound: ZoneBound {
588 fragment_id: fragment_id_col.value(i),
589 start: zone_start_col.value(i),
590 length: zone_length.value(i) as usize,
591 },
592 });
593 }
594
595 Ok(Self {
596 zones,
597 data_type,
598 rows_per_zone,
599 use_seeds,
600 store,
601 fri,
602 index_cache: WeakLanceCache::from(index_cache),
603 null_rows,
604 })
605 }
606}
607
608#[async_trait]
609impl Index for ZoneMapIndex {
610 fn as_any(&self) -> &dyn Any {
611 self
612 }
613
614 fn as_index(self: Arc<Self>) -> Arc<dyn Index> {
615 self
616 }
617
618 async fn prewarm(&self) -> Result<()> {
619 Ok(())
621 }
622
623 fn statistics(&self) -> Result<serde_json::Value> {
624 Ok(serde_json::json!({
625 "num_zones": self.zones.len(),
626 "rows_per_zone": self.rows_per_zone,
627 }))
628 }
629
630 fn index_type(&self) -> IndexType {
631 IndexType::ZoneMap
632 }
633
634 async fn calculate_included_frags(&self) -> Result<RoaringBitmap> {
635 let mut frag_ids = RoaringBitmap::new();
636
637 for zone in &self.zones {
639 frag_ids.insert(zone.bound.fragment_id as u32);
640 }
641
642 Ok(frag_ids)
643 }
644}
645
646#[async_trait]
647impl ScalarIndex for ZoneMapIndex {
648 async fn search(
649 &self,
650 query: &dyn AnyQuery,
651 metrics: &dyn MetricsCollector,
652 ) -> Result<SearchResult> {
653 let query = query.as_any().downcast_ref::<SargableQuery>().unwrap();
654 if let SargableQuery::IsNull() = query
655 && let Some(null_rows) = &self.null_rows
656 {
657 return Ok(SearchResult::exact(null_rows.clone()));
658 }
659
660 search_zones(&self.zones, metrics, |zone| {
661 self.evaluate_zone_against_query(zone, query)
662 })
663 }
664
665 fn results_are_row_addresses(&self) -> bool {
666 true
667 }
668
669 fn can_remap(&self) -> bool {
670 false
671 }
672
673 async fn remap(
675 &self,
676 _mapping: &RowAddrRemap,
677 _dest_store: &dyn IndexStore,
678 ) -> Result<CreatedIndex> {
679 Err(Error::invalid_input_source(
680 "ZoneMapIndex does not support remap".into(),
681 ))
682 }
683
684 async fn update(
686 &self,
687 new_data: SendableRecordBatchStream,
688 dest_store: &dyn IndexStore,
689 _old_data_filter: Option<super::OldIndexDataFilter>,
690 ) -> Result<CreatedIndex> {
691 let schema = new_data.schema();
693 let value_type = schema.field(0).data_type().clone();
694
695 let options = ZoneMapIndexBuilderParams::new(self.rows_per_zone);
696 let processor = ZoneMapProcessor::new(value_type.clone())?;
697 let trainer = ZoneTrainer::new(processor, self.rows_per_zone)?;
698 let (updated_zones, new_null_rows) = rebuild_zones(&self.zones, trainer, new_data).await?;
699
700 let merged_null_rows = self.null_rows.as_ref().map(|existing| {
705 let mut merged = existing.clone();
706 merged |= &new_null_rows;
707 merged
708 });
709
710 let mut builder = ZoneMapIndexBuilder::try_new(options, self.data_type.clone())?;
712 builder.options.rows_per_zone = self.rows_per_zone;
713 builder.maps = updated_zones;
714 builder.null_rows = merged_null_rows;
715 let files = builder.write_index(dest_store).await?;
716
717 Ok(CreatedIndex {
718 index_details: make_zone_map_index_details(self.rows_per_zone, self.use_seeds),
719 index_version: ZONEMAP_INDEX_VERSION,
720 files,
721 })
722 }
723
724 fn update_criteria(&self) -> UpdateCriteria {
725 UpdateCriteria::only_new_data(
726 TrainingCriteria::new(TrainingOrdering::Addresses).with_row_addr(),
727 )
728 }
729
730 fn derive_index_params(&self) -> Result<ScalarIndexParams> {
731 let params = serde_json::to_value(ZoneMapIndexBuilderParams {
732 rows_per_zone: self.rows_per_zone,
733 use_seeds: Some(self.use_seeds),
734 })?;
735 Ok(ScalarIndexParams::for_builtin(BuiltinIndexType::ZoneMap).with_params(¶ms))
736 }
737
738 fn value_range(&self) -> Option<(ScalarValue, ScalarValue)> {
741 Self::value_range_over([self])
742 }
743}
744
745impl ZoneMapIndex {
746 async fn try_update_with_seeds(
747 &self,
748 seeds: &[crate::scalar::seed::FragmentSeed],
749 dest_store: &dyn IndexStore,
750 ) -> Result<Option<CreatedIndex>> {
751 let mut new_zones = self.zones.clone();
752 for seed in seeds {
753 let mut zones = ZoneMapSeedWriter::deserialize_seed(
754 seed.fragment_id,
755 &seed.bytes,
756 self.rows_per_zone,
757 )?;
758 new_zones.append(&mut zones);
759 }
760 new_zones.sort_by_key(|z| (z.bound.fragment_id, z.bound.start));
761
762 let mut builder = ZoneMapIndexBuilder::try_new(
763 ZoneMapIndexBuilderParams::new(self.rows_per_zone),
764 self.data_type.clone(),
765 )?;
766 builder.maps = new_zones;
767 let files = builder.write_index(dest_store).await?;
768
769 Ok(Some(CreatedIndex {
770 index_details: make_zone_map_index_details(self.rows_per_zone, self.use_seeds),
771 index_version: ZONEMAP_INDEX_VERSION,
772 files,
773 }))
774 }
775}
776
777pub async fn merge_zonemap_indices(
779 source_indices: &[&ZoneMapIndex],
780 dest_store: &dyn IndexStore,
781 fragment_filter: &RoaringBitmap,
782) -> Result<CreatedIndex> {
783 let first = source_indices.first().ok_or_else(|| {
784 Error::invalid_input("merge_zonemap_indices requires at least one source index")
785 })?;
786 let rows_per_zone = first.rows_per_zone;
787 let use_seeds = first.use_seeds;
788 let data_type = first.data_type.clone();
789
790 let mut zones = Vec::new();
791 let mut merged_null_rows = RowAddrTreeMap::new();
792 let mut any_missing_bitmap = false;
793 for source in source_indices {
794 if source.rows_per_zone != rows_per_zone {
795 return Err(Error::invalid_input(format!(
796 "cannot merge ZoneMap segments with different rows_per_zone values: {} and {}",
797 rows_per_zone, source.rows_per_zone
798 )));
799 }
800 if source.data_type != data_type {
801 return Err(Error::invalid_input(format!(
802 "cannot merge ZoneMap segments with different value types: {:?} and {:?}",
803 data_type, source.data_type
804 )));
805 }
806 zones.extend(
807 source
808 .zones
809 .iter()
810 .filter(|zone| {
811 u32::try_from(zone.bound.fragment_id)
812 .is_ok_and(|fragment_id| fragment_filter.contains(fragment_id))
813 })
814 .cloned(),
815 );
816 match &source.null_rows {
817 Some(null_rows) => {
818 let mut filtered = null_rows.clone();
819 filtered.retain_fragments(fragment_filter.iter());
820 merged_null_rows |= &filtered;
821 }
822 None => any_missing_bitmap = true,
823 }
824 }
825 zones.sort_by_key(|zone| (zone.bound.fragment_id, zone.bound.start));
826
827 let mut builder =
828 ZoneMapIndexBuilder::try_new(ZoneMapIndexBuilderParams::new(rows_per_zone), data_type)?;
829 builder.maps = zones;
830 if !any_missing_bitmap {
831 builder.null_rows = Some(merged_null_rows);
832 }
833 let files = builder.write_index(dest_store).await?;
834
835 Ok(CreatedIndex {
836 index_details: make_zone_map_index_details(rows_per_zone, use_seeds),
837 index_version: ZONEMAP_INDEX_VERSION,
838 files,
839 })
840}
841
842fn default_rows_per_zone() -> u64 {
843 *DEFAULT_ROWS_PER_ZONE
844}
845
846#[derive(Debug, Clone, Serialize, Deserialize)]
847pub struct ZoneMapIndexBuilderParams {
848 #[serde(default = "default_rows_per_zone")]
849 rows_per_zone: u64,
850 #[serde(default)]
855 use_seeds: Option<bool>,
856}
857
858static DEFAULT_ROWS_PER_ZONE: LazyLock<u64> = LazyLock::new(|| {
859 std::env::var("LANCE_ZONEMAP_DEFAULT_ROWS_PER_ZONE")
860 .unwrap_or_else(|_| (ROWS_PER_ZONE_DEFAULT).to_string())
861 .parse()
862 .expect("failed to parse LANCE_ZONEMAP_DEFAULT_ROWS_PER_ZONE")
863});
864
865impl Default for ZoneMapIndexBuilderParams {
866 fn default() -> Self {
867 Self {
868 rows_per_zone: *DEFAULT_ROWS_PER_ZONE,
869 use_seeds: None,
870 }
871 }
872}
873
874impl ZoneMapIndexBuilderParams {
875 pub fn new(rows_per_zone: u64) -> Self {
876 Self {
877 rows_per_zone,
878 use_seeds: None,
879 }
880 }
881
882 pub fn rows_per_zone(&self) -> u64 {
883 self.rows_per_zone
884 }
885}
886
887pub struct ZoneMapIndexBuilder {
889 options: ZoneMapIndexBuilderParams,
890
891 items_type: DataType,
892 maps: Vec<ZoneMapStatistics>,
893 null_rows: Option<RowAddrTreeMap>,
897}
898
899impl ZoneMapIndexBuilder {
900 pub fn try_new(options: ZoneMapIndexBuilderParams, items_type: DataType) -> Result<Self> {
901 Ok(Self {
902 options,
903 items_type,
904 maps: Vec::new(),
905 null_rows: None,
906 })
907 }
908
909 pub async fn train(&mut self, batches_source: SendableRecordBatchStream) -> Result<()> {
913 let processor = ZoneMapProcessor::new(self.items_type.clone())?;
914 let trainer = ZoneTrainer::new(processor, self.options.rows_per_zone)?;
915 let (maps, null_rows) = trainer.train(batches_source).await?;
916 self.maps = maps;
917 self.null_rows = Some(null_rows);
918 Ok(())
919 }
920
921 fn zonemap_stats_as_batch(&self) -> Result<RecordBatch> {
922 let mins = if self.maps.is_empty() {
924 new_empty_array(&self.items_type)
925 } else {
926 ScalarValue::iter_to_array(self.maps.iter().map(|stat| stat.min.clone()))?
927 };
928 let maxs = if self.maps.is_empty() {
929 new_empty_array(&self.items_type)
930 } else {
931 ScalarValue::iter_to_array(self.maps.iter().map(|stat| stat.max.clone()))?
932 };
933 let null_counts =
934 UInt32Array::from_iter_values(self.maps.iter().map(|stat| stat.null_count));
935
936 let nan_counts = UInt32Array::from_iter_values(self.maps.iter().map(|stat| stat.nan_count));
937
938 let fragment_ids =
939 UInt64Array::from_iter_values(self.maps.iter().map(|stat| stat.bound.fragment_id));
940
941 let zone_lengths =
942 UInt64Array::from_iter_values(self.maps.iter().map(|stat| stat.bound.length as u64));
943
944 let zone_starts =
945 UInt64Array::from_iter_values(self.maps.iter().map(|stat| stat.bound.start));
946
947 let schema = Arc::new(arrow_schema::Schema::new(vec![
948 Field::new("min", self.items_type.clone(), true),
950 Field::new("max", self.items_type.clone(), true),
951 Field::new("null_count", DataType::UInt32, false),
952 Field::new("nan_count", DataType::UInt32, false),
953 Field::new("fragment_id", DataType::UInt64, false),
954 Field::new("zone_start", DataType::UInt64, false),
955 Field::new("zone_length", DataType::UInt64, false),
956 ]));
957
958 let columns: Vec<ArrayRef> = vec![
959 mins,
960 maxs,
961 Arc::new(null_counts) as ArrayRef,
962 Arc::new(nan_counts) as ArrayRef,
963 Arc::new(fragment_ids) as ArrayRef,
964 Arc::new(zone_starts) as ArrayRef,
965 Arc::new(zone_lengths) as ArrayRef,
966 ];
967 Ok(RecordBatch::try_new(schema, columns)?)
968 }
969
970 pub async fn write_index(self, index_store: &dyn IndexStore) -> Result<Vec<IndexFile>> {
971 let record_batch = self.zonemap_stats_as_batch()?;
972
973 let mut file_schema = record_batch.schema().as_ref().clone();
974 file_schema.metadata.insert(
975 ZONEMAP_SIZE_META_KEY.to_string(),
976 self.options.rows_per_zone.to_string(),
977 );
978
979 let mut index_file = index_store
980 .new_index_file(ZONEMAP_FILENAME, Arc::new(file_schema))
981 .await?;
982 index_file.write_record_batch(record_batch).await?;
983
984 let zonemap_file = if let Some(null_rows) = self.null_rows {
985 let mut null_bitmap_bytes = Vec::with_capacity(null_rows.serialized_size());
986 null_rows.serialize_into(&mut null_bitmap_bytes)?;
987 let null_bitmap_idx = index_file
988 .add_global_buffer(bytes::Bytes::from(null_bitmap_bytes))
989 .await?;
990 index_file
991 .finish_with_metadata(HashMap::from([(
992 NULL_BITMAP_META_KEY.to_string(),
993 null_bitmap_idx.to_string(),
994 )]))
995 .await?
996 } else {
997 index_file.finish_with_metadata(HashMap::new()).await?
998 };
999
1000 Ok(vec![zonemap_file])
1001 }
1002}
1003
1004#[derive(Debug)]
1007struct ZoneMapProcessor {
1008 data_type: DataType,
1009 statistics: StatisticsAccumulator,
1010}
1011
1012impl ZoneMapProcessor {
1013 fn new(data_type: DataType) -> Result<Self> {
1014 Ok(Self {
1015 statistics: StatisticsAccumulator::new(&data_type),
1016 data_type,
1017 })
1018 }
1019
1020 fn scalar_value_from_stat(
1021 value: Option<&ArrayRef>,
1022 data_type: &DataType,
1023 ) -> Result<ScalarValue> {
1024 let array = value
1025 .cloned()
1026 .unwrap_or_else(|| new_null_array(data_type, 1));
1027 Ok(ScalarValue::try_from_array(&array, 0)?)
1028 }
1029
1030 fn stat_count_to_u32(name: &str, value: u64) -> Result<u32> {
1031 u32::try_from(value).map_err(|_| {
1032 Error::invalid_input(format!(
1033 "{} value {} exceeds the supported UInt32 range",
1034 name, value
1035 ))
1036 })
1037 }
1038
1039 fn nan_scalar(data_type: &DataType) -> Option<ScalarValue> {
1040 match data_type {
1041 DataType::Float16 => Some(ScalarValue::Float16(Some(half::f16::NAN))),
1042 DataType::Float32 => Some(ScalarValue::Float32(Some(f32::NAN))),
1043 DataType::Float64 => Some(ScalarValue::Float64(Some(f64::NAN))),
1044 _ => None,
1045 }
1046 }
1047
1048 fn max_value_from_stats(
1049 value: Option<&ArrayRef>,
1050 data_type: &DataType,
1051 nan_count: u32,
1052 ) -> Result<ScalarValue> {
1053 if nan_count > 0
1054 && let Some(nan) = Self::nan_scalar(data_type)
1055 {
1056 return Ok(nan);
1060 }
1061 Self::scalar_value_from_stat(value, data_type)
1062 }
1063}
1064
1065impl ZoneProcessor for ZoneMapProcessor {
1066 type ZoneStatistics = ZoneMapStatistics;
1067
1068 fn process_chunk(&mut self, array: &ArrayRef) -> Result<()> {
1069 self.statistics.update(array)?;
1070 Ok(())
1071 }
1072
1073 fn finish_zone(&mut self, bound: ZoneBound) -> Result<Self::ZoneStatistics> {
1074 let statistics = self.statistics.statistics();
1075 let nan_count = Self::stat_count_to_u32("nan_count", statistics.nan_count.unwrap_or(0))?;
1076 Ok(ZoneMapStatistics {
1077 min: Self::scalar_value_from_stat(
1078 statistics.min.as_ref().map(|scalar| scalar.as_array()),
1079 &self.data_type,
1080 )?,
1081 max: Self::max_value_from_stats(
1082 statistics.max.as_ref().map(|scalar| scalar.as_array()),
1083 &self.data_type,
1084 nan_count,
1085 )?,
1086 null_count: Self::stat_count_to_u32("null_count", statistics.null_count)?,
1087 nan_count,
1088 bound,
1089 })
1090 }
1091
1092 fn reset(&mut self) -> Result<()> {
1093 self.statistics.reset();
1094 Ok(())
1095 }
1096}
1097
1098fn make_zone_map_index_details(rows_per_zone: u64, use_seeds: bool) -> prost_types::Any {
1099 prost_types::Any::from_msg(&pbold::ZoneMapIndexDetails {
1100 rows_per_zone: Some(rows_per_zone),
1101 use_seeds: Some(use_seeds),
1102 })
1103 .unwrap()
1104}
1105
1106fn default_use_seeds(data_type: &DataType) -> bool {
1114 match data_type {
1115 DataType::Utf8
1117 | DataType::LargeUtf8
1118 | DataType::Utf8View
1119 | DataType::Binary
1120 | DataType::LargeBinary
1121 | DataType::BinaryView => true,
1122 DataType::Decimal128(_, _) | DataType::Decimal256(_, _) => true,
1124 DataType::FixedSizeBinary(n) => *n > 8,
1125 _ => false,
1126 }
1127}
1128
1129#[derive(Debug, Default)]
1130pub struct ZoneMapIndexPlugin;
1131
1132impl ZoneMapIndexPlugin {
1133 async fn train_zonemap_index(
1134 batches_source: SendableRecordBatchStream,
1135 index_store: &dyn IndexStore,
1136 options: Option<ZoneMapIndexBuilderParams>,
1137 ) -> Result<Vec<IndexFile>> {
1138 let value_type = batches_source.schema().field(0).data_type().clone();
1139
1140 let mut builder = ZoneMapIndexBuilder::try_new(options.unwrap_or_default(), value_type)?;
1141
1142 builder.train(batches_source).await?;
1143
1144 builder.write_index(index_store).await
1145 }
1146}
1147
1148pub struct ZoneMapIndexTrainingRequest {
1149 pub params: ZoneMapIndexBuilderParams,
1150 pub criteria: TrainingCriteria,
1151}
1152
1153impl ZoneMapIndexTrainingRequest {
1154 pub fn new(params: ZoneMapIndexBuilderParams) -> Self {
1155 Self {
1156 params,
1157 criteria: TrainingCriteria::new(TrainingOrdering::Addresses).with_row_addr(),
1158 }
1159 }
1160}
1161
1162impl TrainingRequest for ZoneMapIndexTrainingRequest {
1163 fn as_any(&self) -> &dyn std::any::Any {
1164 self
1165 }
1166 fn criteria(&self) -> &TrainingCriteria {
1167 &self.criteria
1168 }
1169}
1170
1171#[async_trait]
1172impl BasicTrainer for ZoneMapIndexPlugin {
1173 fn new_training_request(
1174 &self,
1175 params: &str,
1176 field: &Field,
1177 ) -> Result<Box<dyn TrainingRequest>> {
1178 if field.data_type().is_nested() {
1179 return Err(Error::invalid_input_source(
1180 "A zone map index can only be created on a non-nested field.".into(),
1181 ));
1182 }
1183
1184 let mut params = serde_json::from_str::<ZoneMapIndexBuilderParams>(params)?;
1185 if params.use_seeds.is_none() {
1187 params.use_seeds = Some(default_use_seeds(field.data_type()));
1188 }
1189
1190 Ok(Box::new(ZoneMapIndexTrainingRequest::new(params)))
1191 }
1192
1193 async fn train_index(
1194 &self,
1195 data: SendableRecordBatchStream,
1196 index_store: &dyn IndexStore,
1197 request: Box<dyn TrainingRequest>,
1198 _fragment_ids: Option<Vec<u32>>,
1199 _progress: Arc<dyn crate::progress::IndexBuildProgress>,
1200 ) -> Result<CreatedIndex> {
1201 let request = (request as Box<dyn std::any::Any>)
1202 .downcast::<ZoneMapIndexTrainingRequest>()
1203 .map_err(|_| {
1204 Error::invalid_input_source(
1205 "must provide training request created by new_training_request".into(),
1206 )
1207 })?;
1208 let rows_per_zone = request.params.rows_per_zone;
1209 let use_seeds = request.params.use_seeds.unwrap_or(false);
1210 let files = Self::train_zonemap_index(data, index_store, Some(request.params)).await?;
1211 Ok(CreatedIndex {
1212 index_details: make_zone_map_index_details(rows_per_zone, use_seeds),
1213 index_version: ZONEMAP_INDEX_VERSION,
1214 files,
1215 })
1216 }
1217}
1218
1219#[async_trait]
1220impl ScalarIndexPlugin for ZoneMapIndexPlugin {
1221 fn basic_trainer(&self) -> Option<&dyn BasicTrainer> {
1222 Some(self)
1223 }
1224
1225 fn name(&self) -> &str {
1226 "ZoneMap"
1227 }
1228
1229 fn provides_exact_answer(&self) -> bool {
1230 false
1231 }
1232
1233 fn version(&self) -> u32 {
1234 ZONEMAP_INDEX_VERSION
1235 }
1236
1237 fn new_query_parser(
1238 &self,
1239 index_name: String,
1240 _index_details: &prost_types::Any,
1241 ) -> Option<Box<dyn ScalarQueryParser>> {
1242 Some(Box::new(SargableQueryParser::new(
1243 index_name,
1244 self.name().to_string(),
1245 true,
1246 )))
1247 }
1248
1249 async fn load_index(
1250 &self,
1251 index_store: Arc<dyn IndexStore>,
1252 index_details: &prost_types::Any,
1253 frag_reuse_index: Option<Arc<dyn RowIdRemapper>>,
1254 cache: &LanceCache,
1255 ) -> Result<Arc<dyn ScalarIndex>> {
1256 let use_seeds = index_details
1257 .to_msg::<pbold::ZoneMapIndexDetails>()
1258 .ok()
1259 .and_then(|d| d.use_seeds)
1260 .unwrap_or(false);
1261 Ok(
1262 ZoneMapIndex::load(index_store, frag_reuse_index, cache, use_seeds).await?
1263 as Arc<dyn ScalarIndex>,
1264 )
1265 }
1266
1267 fn might_use_seeds(&self, index_details: &prost_types::Any) -> bool {
1268 index_details
1269 .to_msg::<pbold::ZoneMapIndexDetails>()
1270 .ok()
1271 .and_then(|d| d.use_seeds)
1272 .unwrap_or(false)
1273 }
1274
1275 async fn create_seed_writer(
1276 &self,
1277 field_path: &str,
1278 data_type: &DataType,
1279 index_details: &prost_types::Any,
1280 ) -> Result<Option<Box<dyn crate::scalar::seed::IndexSeedWriter>>> {
1281 if data_type.is_nested() {
1282 return Ok(None);
1283 }
1284 let details = index_details.to_msg::<pbold::ZoneMapIndexDetails>().ok();
1285 let Some(rows_per_zone) = details.as_ref().and_then(|d| d.rows_per_zone) else {
1286 return Ok(None);
1287 };
1288 if !details.as_ref().and_then(|d| d.use_seeds).unwrap_or(false) {
1289 return Ok(None);
1290 }
1291 Ok(Some(Box::new(ZoneMapSeedWriter::new(
1292 field_path,
1293 rows_per_zone,
1294 data_type.clone(),
1295 )?)))
1296 }
1297
1298 async fn update_from_seeds(
1299 &self,
1300 seeds: Vec<crate::scalar::seed::FragmentSeed>,
1301 reference_index: Arc<dyn ScalarIndex>,
1302 index_details: &prost_types::Any,
1303 dest_store: &dyn IndexStore,
1304 ) -> Result<Option<CreatedIndex>> {
1305 let Some(rows_per_zone) = index_details
1306 .to_msg::<pbold::ZoneMapIndexDetails>()
1307 .ok()
1308 .and_then(|d| d.rows_per_zone)
1309 else {
1310 return Ok(None);
1311 };
1312
1313 for seed in &seeds {
1315 let rpz_in_seed = seed
1316 .metadata_value
1317 .split_once(':')
1318 .and_then(|(_, rpz)| rpz.parse::<u64>().ok());
1319 if rpz_in_seed != Some(rows_per_zone) {
1320 return Ok(None);
1321 }
1322 }
1323
1324 let Some(zone_map) = reference_index.as_any().downcast_ref::<ZoneMapIndex>() else {
1325 return Ok(None);
1326 };
1327 zone_map.try_update_with_seeds(&seeds, dest_store).await
1328 }
1329}
1330
1331#[derive(Debug)]
1337pub struct ZoneMapSeedWriter {
1338 column_name: String,
1339 rows_per_zone: u64,
1340 data_type: DataType,
1341 completed_zones: Vec<ZoneMapStatistics>,
1342 processor: ZoneMapProcessor,
1343 rows_in_current_zone: u64,
1344 next_zone_start: u64,
1345}
1346
1347impl ZoneMapSeedWriter {
1348 pub fn new(
1350 column_name: impl Into<String>,
1351 rows_per_zone: u64,
1352 data_type: DataType,
1353 ) -> Result<Self> {
1354 if rows_per_zone == 0 {
1355 return Err(lance_core::Error::invalid_input(
1356 "rows_per_zone must be greater than zero",
1357 ));
1358 }
1359 let processor = ZoneMapProcessor::new(data_type.clone())?;
1360 Ok(Self {
1361 column_name: column_name.into(),
1362 rows_per_zone,
1363 data_type,
1364 completed_zones: Vec::new(),
1365 processor,
1366 rows_in_current_zone: 0,
1367 next_zone_start: 0,
1368 })
1369 }
1370
1371 fn seed_batch_from_zones(
1372 zones: &[ZoneMapStatistics],
1373 data_type: &DataType,
1374 ) -> Result<arrow_array::RecordBatch> {
1375 let mins = if zones.is_empty() {
1376 arrow_array::new_empty_array(data_type)
1377 } else {
1378 datafusion_common::ScalarValue::iter_to_array(zones.iter().map(|s| s.min.clone()))?
1379 };
1380 let maxs = if zones.is_empty() {
1381 arrow_array::new_empty_array(data_type)
1382 } else {
1383 datafusion_common::ScalarValue::iter_to_array(zones.iter().map(|s| s.max.clone()))?
1384 };
1385 let null_counts =
1386 arrow_array::UInt32Array::from_iter_values(zones.iter().map(|s| s.null_count));
1387 let nan_counts =
1388 arrow_array::UInt32Array::from_iter_values(zones.iter().map(|s| s.nan_count));
1389 let zone_lengths =
1390 arrow_array::UInt64Array::from_iter_values(zones.iter().map(|s| s.bound.length as u64));
1391
1392 let schema = Arc::new(arrow_schema::Schema::new(vec![
1393 Field::new("min", data_type.clone(), true),
1394 Field::new("max", data_type.clone(), true),
1395 Field::new("null_count", DataType::UInt32, false),
1396 Field::new("nan_count", DataType::UInt32, false),
1397 Field::new("zone_length", DataType::UInt64, false),
1398 ]));
1399
1400 let columns: Vec<ArrayRef> = vec![
1401 mins,
1402 maxs,
1403 Arc::new(null_counts) as ArrayRef,
1404 Arc::new(nan_counts) as ArrayRef,
1405 Arc::new(zone_lengths) as ArrayRef,
1406 ];
1407 Ok(arrow_array::RecordBatch::try_new(schema, columns)?)
1408 }
1409
1410 pub(crate) fn deserialize_seed(
1415 fragment_id: u64,
1416 bytes: &bytes::Bytes,
1417 rows_per_zone: u64,
1418 ) -> Result<Vec<ZoneMapStatistics>> {
1419 use arrow_ipc::reader::FileReader;
1420 use std::io::Cursor;
1421
1422 let cursor = Cursor::new(bytes.as_ref());
1423 let mut reader = FileReader::try_new(cursor, None).map_err(|e| {
1424 lance_core::Error::invalid_input(format!("failed to read zone map seed IPC: {}", e))
1425 })?;
1426
1427 let batch = match reader.next() {
1428 Some(Ok(batch)) => batch,
1429 Some(Err(e)) => {
1430 return Err(lance_core::Error::invalid_input(format!(
1431 "failed to read zone map seed batch: {}",
1432 e
1433 )));
1434 }
1435 None => return Ok(Vec::new()),
1436 };
1437
1438 let min_col = batch
1439 .column_by_name("min")
1440 .ok_or_else(|| lance_core::Error::invalid_input("seed batch missing 'min' column"))?;
1441 let max_col = batch
1442 .column_by_name("max")
1443 .ok_or_else(|| lance_core::Error::invalid_input("seed batch missing 'max' column"))?;
1444 let null_count_col = batch
1445 .column_by_name("null_count")
1446 .ok_or_else(|| {
1447 lance_core::Error::invalid_input("seed batch missing 'null_count' column")
1448 })?
1449 .as_any()
1450 .downcast_ref::<arrow_array::UInt32Array>()
1451 .ok_or_else(|| lance_core::Error::invalid_input("seed 'null_count' is not UInt32"))?;
1452 let nan_count_col = batch
1453 .column_by_name("nan_count")
1454 .ok_or_else(|| {
1455 lance_core::Error::invalid_input("seed batch missing 'nan_count' column")
1456 })?
1457 .as_any()
1458 .downcast_ref::<arrow_array::UInt32Array>()
1459 .ok_or_else(|| lance_core::Error::invalid_input("seed 'nan_count' is not UInt32"))?;
1460 let zone_length_col = batch
1461 .column_by_name("zone_length")
1462 .ok_or_else(|| {
1463 lance_core::Error::invalid_input("seed batch missing 'zone_length' column")
1464 })?
1465 .as_any()
1466 .downcast_ref::<arrow_array::UInt64Array>()
1467 .ok_or_else(|| lance_core::Error::invalid_input("seed 'zone_length' is not UInt64"))?;
1468
1469 let num_zones = batch.num_rows();
1470 let mut zones = Vec::with_capacity(num_zones);
1471 for i in 0..num_zones {
1472 let zone_start = i as u64 * rows_per_zone;
1473 let zone_length = zone_length_col.value(i) as usize;
1474 zones.push(ZoneMapStatistics {
1475 min: datafusion_common::ScalarValue::try_from_array(min_col, i)?,
1476 max: datafusion_common::ScalarValue::try_from_array(max_col, i)?,
1477 null_count: null_count_col.value(i),
1478 nan_count: nan_count_col.value(i),
1479 bound: ZoneBound {
1480 fragment_id,
1481 start: zone_start,
1482 length: zone_length,
1483 },
1484 });
1485 }
1486 Ok(zones)
1487 }
1488}
1489
1490impl IndexSeedWriter for ZoneMapSeedWriter {
1491 fn column_name(&self) -> &str {
1492 &self.column_name
1493 }
1494
1495 fn observe_batch(&mut self, values: &ArrayRef) -> lance_core::Result<()> {
1496 let mut offset = 0usize;
1497
1498 while offset < values.len() {
1499 let remaining_in_zone = self.rows_per_zone - self.rows_in_current_zone;
1500 let chunk_len = ((values.len() as u64 - offset as u64).min(remaining_in_zone)) as usize;
1501 let chunk = values.slice(offset, chunk_len);
1502 self.processor.process_chunk(&chunk)?;
1503 self.rows_in_current_zone += chunk_len as u64;
1504 offset += chunk_len;
1505
1506 if self.rows_in_current_zone >= self.rows_per_zone {
1507 let bound = ZoneBound {
1508 fragment_id: 0,
1509 start: self.next_zone_start,
1510 length: self.rows_per_zone as usize,
1511 };
1512 let stats = self.processor.finish_zone(bound)?;
1513 self.processor.reset()?;
1514 self.completed_zones.push(stats);
1515 self.next_zone_start += self.rows_per_zone;
1516 self.rows_in_current_zone = 0;
1517 }
1518 }
1519 Ok(())
1520 }
1521
1522 fn finish(&mut self) -> lance_core::Result<Option<bytes::Bytes>> {
1523 use arrow_ipc::writer::FileWriter;
1524 use std::io::Cursor;
1525
1526 if self.rows_in_current_zone > 0 {
1528 let bound = ZoneBound {
1529 fragment_id: 0,
1530 start: self.next_zone_start,
1531 length: self.rows_in_current_zone as usize,
1532 };
1533 let stats = self.processor.finish_zone(bound)?;
1534 self.processor.reset()?;
1535 self.completed_zones.push(stats);
1536 self.next_zone_start += self.rows_in_current_zone;
1537 self.rows_in_current_zone = 0;
1538 }
1539
1540 if self.completed_zones.is_empty() {
1541 return Ok(None);
1542 }
1543
1544 let batch = Self::seed_batch_from_zones(&self.completed_zones, &self.data_type)?;
1545
1546 let mut buf = Cursor::new(Vec::new());
1548 {
1549 let mut writer = FileWriter::try_new(&mut buf, batch.schema_ref()).map_err(|e| {
1550 lance_core::Error::invalid_input(format!("failed to create IPC writer: {}", e))
1551 })?;
1552 writer.write(&batch).map_err(|e| {
1553 lance_core::Error::invalid_input(format!("failed to write IPC batch: {}", e))
1554 })?;
1555 writer.finish().map_err(|e| {
1556 lance_core::Error::invalid_input(format!("failed to finish IPC writer: {}", e))
1557 })?;
1558 }
1559
1560 self.completed_zones.clear();
1562 self.next_zone_start = 0;
1563 self.processor = ZoneMapProcessor::new(self.data_type.clone())?;
1564
1565 Ok(Some(bytes::Bytes::from(buf.into_inner())))
1566 }
1567
1568 fn schema_metadata_key(&self) -> String {
1569 format!(
1570 "{}{}",
1571 crate::scalar::seed::SEED_META_KEY_PREFIX,
1572 self.column_name
1573 )
1574 }
1575
1576 fn schema_metadata_value(&self, buf_index: u32) -> String {
1577 format!("{}:{}", buf_index, self.rows_per_zone)
1578 }
1579}
1580
1581#[cfg(test)]
1582mod tests {
1583 use crate::scalar::registry::VALUE_COLUMN_NAME;
1584 use crate::scalar::{IndexStore, zonemap::ROWS_PER_ZONE_DEFAULT};
1585 use std::sync::Arc;
1586
1587 use crate::scalar::zoned::ZoneBound;
1588 use crate::scalar::zonemap::{ZoneMapIndexPlugin, ZoneMapStatistics};
1589 use arrow::datatypes::{ArrowPrimitiveType, Float32Type, Int64Type};
1590 use arrow_array::{Array, PrimitiveArray, RecordBatch, UInt64Array, record_batch};
1591 use arrow_schema::{DataType, Field, Schema};
1592 use datafusion::execution::SendableRecordBatchStream;
1593 use datafusion::physical_plan::stream::RecordBatchStreamAdapter;
1594 use datafusion_common::ScalarValue;
1595 use futures::{StreamExt, TryStreamExt, stream};
1596 use lance_core::utils::tempfile::TempObjDir;
1597 use lance_core::{
1598 ROW_ADDR,
1599 cache::{LanceCache, WeakLanceCache},
1600 };
1601 use lance_datafusion::datagen::DatafusionDatagenExt;
1602 use lance_datagen::ArrayGeneratorExt;
1603 use lance_datagen::{BatchCount, RowCount, array};
1604 use lance_io::object_store::ObjectStore;
1605 use lance_select::RowAddrTreeMap;
1606
1607 use crate::scalar::{
1608 SargableQuery, ScalarIndex, SearchResult,
1609 lance_format::LanceIndexStore,
1610 zonemap::{
1611 ZONEMAP_FILENAME, ZONEMAP_SIZE_META_KEY, ZoneMapIndex, ZoneMapIndexBuilderParams,
1612 merge_zonemap_indices,
1613 },
1614 };
1615
1616 use crate::Index; use crate::metrics::NoOpMetricsCollector;
1619 use roaring::RoaringBitmap; use std::collections::Bound;
1621
1622 fn add_row_addr(stream: SendableRecordBatchStream) -> SendableRecordBatchStream {
1624 let schema = stream.schema();
1625 let schema_with_row_addr = Arc::new(Schema::new(vec![
1626 schema.field(0).clone(),
1627 Field::new(ROW_ADDR, DataType::UInt64, false),
1628 ]));
1629 let schema = schema_with_row_addr.clone();
1630 let stream = stream.enumerate().map(move |(frag_id, batch)| {
1631 let batch = batch.unwrap();
1632 let row_addr = Arc::new(UInt64Array::from_iter_values(
1633 (0..batch.num_rows() as u64).map(|off| off + ((frag_id as u64) << 32)),
1634 ));
1635 Ok(RecordBatch::try_new(
1636 schema_with_row_addr.clone(),
1637 vec![batch.column(0).clone(), row_addr],
1638 )?)
1639 });
1640 Box::pin(RecordBatchStreamAdapter::new(schema, stream))
1641 }
1642
1643 async fn train_and_load<T: ArrowPrimitiveType>(
1646 fragments: Vec<Vec<Option<T::Native>>>,
1647 ) -> Arc<ZoneMapIndex>
1648 where
1649 PrimitiveArray<T>: From<Vec<Option<T::Native>>>,
1650 {
1651 let tmpdir = TempObjDir::default();
1652 let test_store = Arc::new(LanceIndexStore::new(
1653 Arc::new(ObjectStore::local()),
1654 tmpdir.clone(),
1655 Arc::new(LanceCache::no_cache()),
1656 ));
1657 let schema = Arc::new(Schema::new(vec![Field::new(
1658 VALUE_COLUMN_NAME,
1659 T::DATA_TYPE,
1660 true,
1661 )]));
1662 let batches: Vec<RecordBatch> = fragments
1663 .into_iter()
1664 .map(|vals| {
1665 RecordBatch::try_new(
1666 schema.clone(),
1667 vec![Arc::new(PrimitiveArray::<T>::from(vals))],
1668 )
1669 .unwrap()
1670 })
1671 .collect();
1672 let stream: SendableRecordBatchStream = Box::pin(RecordBatchStreamAdapter::new(
1673 schema.clone(),
1674 stream::iter(batches.into_iter().map(Ok)),
1675 ));
1676 let stream = add_row_addr(stream);
1677
1678 ZoneMapIndexPlugin::train_zonemap_index(
1679 stream,
1680 test_store.as_ref(),
1681 Some(ZoneMapIndexBuilderParams::new(2)),
1682 )
1683 .await
1684 .unwrap();
1685
1686 ZoneMapIndex::load(test_store.clone(), None, &LanceCache::no_cache(), false)
1687 .await
1688 .expect("Failed to load ZoneMapIndex")
1689 }
1690
1691 #[tokio::test]
1692 async fn test_value_range_spans_fragments() {
1693 let index = train_and_load::<Int64Type>(vec![
1695 vec![Some(10), Some(50), Some(30)],
1696 vec![Some(5), Some(99), Some(42)],
1697 ])
1698 .await;
1699 assert_eq!(
1700 index.value_range(),
1701 Some((ScalarValue::Int64(Some(5)), ScalarValue::Int64(Some(99))))
1702 );
1703 }
1704
1705 #[tokio::test]
1706 async fn test_value_range_all_null_is_none() {
1707 let index = train_and_load::<Int64Type>(vec![vec![None, None, None]]).await;
1708 assert_eq!(index.value_range(), None);
1709 }
1710
1711 #[tokio::test]
1712 async fn test_value_range_nan_max_is_none() {
1713 let index = train_and_load::<Float32Type>(vec![vec![
1716 Some(1.0),
1717 Some(2.0),
1718 Some(100.0),
1719 Some(f32::NAN),
1720 ]])
1721 .await;
1722 assert_eq!(index.value_range(), None);
1723 }
1724
1725 #[tokio::test]
1726 async fn test_value_range_over_folds_segments() {
1727 let a = train_and_load::<Int64Type>(vec![vec![Some(5), Some(9)]]).await;
1730 let b = train_and_load::<Int64Type>(vec![vec![Some(1), Some(20)]]).await;
1731 assert_eq!(
1732 ZoneMapIndex::value_range_over([a.as_ref(), b.as_ref()]),
1733 Some((ScalarValue::Int64(Some(1)), ScalarValue::Int64(Some(20))))
1734 );
1735 }
1736
1737 #[tokio::test]
1738 async fn test_value_range_over_nan_in_any_segment_is_none() {
1739 let a = train_and_load::<Float32Type>(vec![vec![Some(1.0), Some(2.0)]]).await;
1742 let b = train_and_load::<Float32Type>(vec![vec![Some(100.0), Some(f32::NAN)]]).await;
1743 assert_eq!(
1744 ZoneMapIndex::value_range_over([a.as_ref(), b.as_ref()]),
1745 None
1746 );
1747 }
1748
1749 #[tokio::test]
1750 async fn test_value_range_over_skips_all_null_segment() {
1751 let a = train_and_load::<Int64Type>(vec![vec![None, None]]).await;
1754 let b = train_and_load::<Int64Type>(vec![vec![Some(3), Some(7)]]).await;
1755 assert_eq!(
1756 ZoneMapIndex::value_range_over([a.as_ref(), b.as_ref()]),
1757 Some((ScalarValue::Int64(Some(3)), ScalarValue::Int64(Some(7))))
1758 );
1759 }
1760
1761 #[tokio::test]
1762 async fn test_empty_zonemap_index() {
1763 let tmpdir = TempObjDir::default();
1764 let test_store = Arc::new(LanceIndexStore::new(
1765 Arc::new(ObjectStore::local()),
1766 tmpdir.clone(),
1767 Arc::new(LanceCache::no_cache()),
1768 ));
1769
1770 let data = arrow_array::Int32Array::from(Vec::<i32>::new());
1771 let row_ids = arrow_array::UInt64Array::from(Vec::<u64>::new());
1772 let schema = Arc::new(Schema::new(vec![
1773 Field::new(VALUE_COLUMN_NAME, DataType::Int32, false),
1774 Field::new(ROW_ADDR, DataType::UInt64, false),
1775 ]));
1776 let data =
1777 RecordBatch::try_new(schema.clone(), vec![Arc::new(data), Arc::new(row_ids)]).unwrap();
1778
1779 let data_stream: SendableRecordBatchStream = Box::pin(RecordBatchStreamAdapter::new(
1780 schema,
1781 stream::once(std::future::ready(Ok(data))),
1782 ));
1783
1784 ZoneMapIndexPlugin::train_zonemap_index(data_stream, test_store.as_ref(), None)
1785 .await
1786 .unwrap();
1787
1788 log::debug!("Successfully wrote the index file");
1789
1790 let index = ZoneMapIndex::load(test_store.clone(), None, &LanceCache::no_cache(), false)
1792 .await
1793 .expect("Failed to load ZoneMapIndex");
1794 assert_eq!(index.zones.len(), 0);
1795 assert_eq!(index.data_type, DataType::Int32);
1796 assert_eq!(index.rows_per_zone, ROWS_PER_ZONE_DEFAULT);
1797
1798 let query = SargableQuery::Equals(ScalarValue::Int32(None));
1800 let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
1801 assert_eq!(result, SearchResult::at_most(RowAddrTreeMap::new()));
1802 }
1803
1804 #[tokio::test]
1805 async fn test_null_zonemap_index() {
1807 let tmpdir = TempObjDir::default();
1808 let test_store = Arc::new(LanceIndexStore::new(
1809 Arc::new(ObjectStore::local()),
1810 tmpdir.clone(),
1811 Arc::new(LanceCache::no_cache()),
1812 ));
1813
1814 let stream = lance_datagen::gen_batch()
1815 .col(
1816 VALUE_COLUMN_NAME,
1817 array::rand::<Float32Type>().with_nulls(&[true, false, false, false, false]),
1818 )
1819 .into_df_stream(RowCount::from(5000), BatchCount::from(10));
1820
1821 let stream = add_row_addr(stream);
1823
1824 ZoneMapIndexPlugin::train_zonemap_index(
1825 stream,
1826 test_store.as_ref(),
1827 Some(ZoneMapIndexBuilderParams::new(5000)),
1828 )
1829 .await
1830 .unwrap();
1831
1832 log::debug!("Successfully wrote the index file");
1833
1834 let index = ZoneMapIndex::load(test_store.clone(), None, &LanceCache::no_cache(), false)
1836 .await
1837 .expect("Failed to load ZoneMapIndex");
1838 assert_eq!(index.zones.len(), 10);
1839 for (i, zone) in index.zones.iter().enumerate() {
1840 assert_eq!(zone.null_count, 1000);
1841 assert_eq!(zone.nan_count, 0, "Zone {} should have nan_count = 0", i);
1842 assert_eq!(zone.bound.length, 5000);
1843 assert_eq!(zone.bound.fragment_id, i as u64);
1844 }
1845
1846 let query = SargableQuery::Equals(ScalarValue::Int32(None));
1848 let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
1849
1850 let mut expected = RowAddrTreeMap::new();
1852 for fragment_id in 0..10 {
1853 let start = (fragment_id as u64) << 32;
1854 let end = start + 5000;
1855 expected.insert_range(start..end);
1856 }
1857 assert_eq!(result, SearchResult::at_most(expected));
1858
1859 let new_data =
1861 arrow_array::Float32Array::from_iter_values((0..5000).map(|i| i as f32 / 1000.0));
1862 let new_row_addr =
1864 UInt64Array::from_iter_values((0..5000).map(|i| (10u64 << 32) | (i as u64)));
1865 let new_schema = Arc::new(Schema::new(vec![
1866 Field::new(VALUE_COLUMN_NAME, DataType::Float32, false), Field::new(ROW_ADDR, DataType::UInt64, false), ]));
1869 let new_data_batch = RecordBatch::try_new(
1870 new_schema.clone(),
1871 vec![Arc::new(new_data), Arc::new(new_row_addr)],
1872 )
1873 .unwrap();
1874 let new_data_stream: SendableRecordBatchStream = Box::pin(RecordBatchStreamAdapter::new(
1875 new_schema,
1876 stream::once(std::future::ready(Ok(new_data_batch))),
1877 ));
1878
1879 index
1882 .update(new_data_stream, test_store.as_ref(), None)
1883 .await
1884 .unwrap();
1885
1886 let updated_index =
1888 ZoneMapIndex::load(test_store.clone(), None, &LanceCache::no_cache(), false)
1889 .await
1890 .expect("Failed to load updated ZoneMapIndex");
1891
1892 assert_eq!(updated_index.zones.len(), 11);
1894
1895 let new_zone = &updated_index.zones[10]; assert_eq!(new_zone.bound.fragment_id, 10u64); assert_eq!(new_zone.bound.length, 5000);
1899 assert_eq!(new_zone.null_count, 0); assert_eq!(new_zone.nan_count, 0); let query = SargableQuery::Equals(ScalarValue::Float32(None));
1904 let result = updated_index
1905 .search(&query, &NoOpMetricsCollector)
1906 .await
1907 .unwrap();
1908
1909 let mut expected = RowAddrTreeMap::new();
1911 for fragment_id in 0..10 {
1912 let start = (fragment_id as u64) << 32;
1913 let end = start + 5000;
1914 expected.insert_range(start..end);
1915 }
1916 assert_eq!(result, SearchResult::at_most(expected));
1917
1918 let query = SargableQuery::Equals(ScalarValue::Float32(Some(2.5))); let result = updated_index
1921 .search(&query, &NoOpMetricsCollector)
1922 .await
1923 .unwrap();
1924
1925 let mut expected = RowAddrTreeMap::new();
1927 let start = 10u64 << 32;
1928 let end = start + 5000;
1929 expected.insert_range(start..end);
1930 assert_eq!(result, SearchResult::at_most(expected));
1931 }
1932
1933 #[tokio::test]
1934 async fn test_zonemap_null_handling_in_queries() {
1935 let tmpdir = TempObjDir::default();
1937 let store = Arc::new(LanceIndexStore::new(
1938 Arc::new(ObjectStore::local()),
1939 tmpdir.clone(),
1940 Arc::new(LanceCache::no_cache()),
1941 ));
1942
1943 let batch = record_batch!(
1945 (VALUE_COLUMN_NAME, Int64, [Some(0), Some(5), None]),
1946 (ROW_ADDR, UInt64, [0, 1, 2])
1947 )
1948 .unwrap();
1949 let schema = batch.schema();
1950 let stream = stream::once(async move { Ok(batch) });
1951 let stream = Box::pin(RecordBatchStreamAdapter::new(schema, stream));
1952
1953 ZoneMapIndexPlugin::train_zonemap_index(stream, store.as_ref(), None)
1955 .await
1956 .unwrap();
1957
1958 let cache = LanceCache::with_capacity(1024 * 1024);
1959 let index = ZoneMapIndex::load(store.clone(), None, &cache, false)
1960 .await
1961 .unwrap();
1962
1963 let query = SargableQuery::Equals(ScalarValue::Int64(Some(5)));
1966 let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
1967
1968 match result {
1969 SearchResult::AtMost(row_ids) => {
1970 let all_rows: Vec<u64> = row_ids
1973 .true_rows()
1974 .row_addrs()
1975 .unwrap()
1976 .map(u64::from)
1977 .collect();
1978 assert_eq!(
1979 all_rows,
1980 vec![0, 1, 2],
1981 "Should return all rows (including nulls) since ZoneMap is inexact"
1982 );
1983
1984 }
1987 _ => panic!("Expected AtMost search result from zonemap"),
1988 }
1989
1990 let query = SargableQuery::Range(
1992 std::ops::Bound::Included(ScalarValue::Int64(Some(0))),
1993 std::ops::Bound::Included(ScalarValue::Int64(Some(3))),
1994 );
1995 let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
1996
1997 match result {
1998 SearchResult::AtMost(row_ids) => {
1999 let all_rows: Vec<u64> = row_ids
2001 .true_rows()
2002 .row_addrs()
2003 .unwrap()
2004 .map(u64::from)
2005 .collect();
2006 assert_eq!(
2007 all_rows,
2008 vec![0, 1, 2],
2009 "Should return all rows in zone as possible matches"
2010 );
2011 }
2012 _ => panic!("Expected AtMost search result from zonemap"),
2013 }
2014 }
2015
2016 #[tokio::test]
2017 async fn test_nan_zonemap_index() {
2018 let tmpdir = TempObjDir::default();
2019 let test_store = Arc::new(LanceIndexStore::new(
2020 Arc::new(ObjectStore::local()),
2021 tmpdir.clone(),
2022 Arc::new(LanceCache::no_cache()),
2023 ));
2024
2025 let mut values = Vec::new();
2028 for i in 0..500 {
2029 if i % 5 == 2 {
2030 values.push(f32::NAN);
2031 } else {
2032 values.push(i as f32);
2034 }
2035 }
2036
2037 let float_data = arrow_array::Float32Array::from(values);
2038 let row_ids = UInt64Array::from_iter_values((0..float_data.len()).map(|i| i as u64));
2039 let schema = Arc::new(Schema::new(vec![
2040 Field::new(VALUE_COLUMN_NAME, DataType::Float32, true),
2041 Field::new(ROW_ADDR, DataType::UInt64, false),
2042 ]));
2043 let data = RecordBatch::try_new(
2044 schema.clone(),
2045 vec![Arc::new(float_data.clone()), Arc::new(row_ids)],
2046 )
2047 .unwrap();
2048 let data_stream: SendableRecordBatchStream = Box::pin(RecordBatchStreamAdapter::new(
2049 schema,
2050 stream::once(std::future::ready(Ok(data))),
2051 ));
2052
2053 ZoneMapIndexPlugin::train_zonemap_index(
2054 data_stream,
2055 test_store.as_ref(),
2056 Some(ZoneMapIndexBuilderParams::new(100)),
2057 )
2058 .await
2059 .unwrap();
2060
2061 let index = ZoneMapIndex::load(test_store.clone(), None, &LanceCache::no_cache(), false)
2063 .await
2064 .expect("Failed to load ZoneMapIndex");
2065
2066 assert_eq!(index.zones.len(), 5);
2068
2069 for (i, zone) in index.zones.iter().enumerate() {
2073 assert_eq!(zone.nan_count, 20, "Zone {} should have 20 NaN values", i);
2074 assert_eq!(
2075 zone.bound.length, 100,
2076 "Zone {} should have zone_length 100",
2077 i
2078 );
2079 assert_eq!(
2080 zone.bound.fragment_id, 0u64,
2081 "Zone {} should have fragment_id 0",
2082 i
2083 );
2084 }
2085
2086 let zone = &index.zones[0];
2087 assert!(matches!(
2088 zone.max,
2089 ScalarValue::Float32(Some(value)) if value.is_nan()
2090 ));
2091 let finite_target = ScalarValue::Float32(Some(1000.0));
2092 assert!(
2093 finite_target >= zone.min && finite_target <= zone.max,
2094 "ScalarValue total ordering keeps finite values below NaN max"
2095 );
2096
2097 let query = SargableQuery::Equals(ScalarValue::Float32(Some(f32::NAN)));
2099 let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
2100
2101 let mut expected = RowAddrTreeMap::new();
2103 expected.insert_range(0..500); assert_eq!(result, SearchResult::at_most(expected));
2105
2106 let query = SargableQuery::Equals(ScalarValue::Float32(Some(5.0)));
2108 let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
2109
2110 let mut expected = RowAddrTreeMap::new();
2112 expected.insert_range(0..100);
2113 assert_eq!(result, SearchResult::at_most(expected));
2114
2115 let query = SargableQuery::Equals(ScalarValue::Float32(Some(1000.0)));
2117 let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
2118
2119 let mut expected = RowAddrTreeMap::new();
2122 expected.insert_range(0..500);
2123 assert_eq!(result, SearchResult::at_most(expected));
2124
2125 let query = SargableQuery::Range(
2127 Bound::Included(ScalarValue::Float32(Some(0.0))),
2128 Bound::Included(ScalarValue::Float32(Some(250.0))),
2129 );
2130 let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
2131
2132 let mut expected = RowAddrTreeMap::new();
2134 expected.insert_range(0..300);
2135 assert_eq!(result, SearchResult::at_most(expected));
2136
2137 let query = SargableQuery::IsIn(vec![
2139 ScalarValue::Float32(Some(f32::NAN)),
2140 ScalarValue::Float32(Some(5.0)),
2141 ScalarValue::Float32(Some(150.0)), ]);
2143 let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
2144
2145 let mut expected = RowAddrTreeMap::new();
2147 expected.insert_range(0..500);
2148 assert_eq!(result, SearchResult::at_most(expected));
2149
2150 let query = SargableQuery::Range(
2152 Bound::Included(ScalarValue::Float32(Some(1000.0))),
2153 Bound::Included(ScalarValue::Float32(Some(2000.0))),
2154 );
2155 let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
2156
2157 let mut expected = RowAddrTreeMap::new();
2160 expected.insert_range(0..500);
2161 assert_eq!(result, SearchResult::at_most(expected));
2162
2163 let query = SargableQuery::IsNull();
2165 let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
2166 assert_eq!(result, SearchResult::exact(RowAddrTreeMap::new()));
2167
2168 let query = SargableQuery::Range(
2171 Bound::Included(ScalarValue::Float32(Some(f32::NAN))),
2172 Bound::Unbounded,
2173 );
2174 let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
2175 let mut expected = RowAddrTreeMap::new();
2177 expected.insert_range(0..500);
2178 assert_eq!(result, SearchResult::at_most(expected));
2179
2180 let query = SargableQuery::Range(
2182 Bound::Unbounded,
2183 Bound::Included(ScalarValue::Float32(Some(f32::NAN))),
2184 );
2185 let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
2186 let mut expected = RowAddrTreeMap::new();
2188 expected.insert_range(0..500);
2189 assert_eq!(result, SearchResult::at_most(expected));
2190
2191 let query = SargableQuery::Range(
2193 Bound::Unbounded,
2194 Bound::Excluded(ScalarValue::Float32(Some(f32::NAN))),
2195 );
2196 let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
2197 let mut expected = RowAddrTreeMap::new();
2199 expected.insert_range(0..500);
2200 assert_eq!(result, SearchResult::at_most(expected));
2201
2202 let query = SargableQuery::Range(
2204 Bound::Excluded(ScalarValue::Float32(Some(f32::NAN))),
2205 Bound::Unbounded,
2206 );
2207 let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
2208 assert_eq!(result, SearchResult::at_most(RowAddrTreeMap::new()));
2210
2211 let query = SargableQuery::IsIn(vec![
2213 ScalarValue::Float16(Some(half::f16::NAN)),
2214 ScalarValue::Float32(Some(f32::NAN)),
2215 ScalarValue::Float64(Some(f64::NAN)),
2216 ScalarValue::Float32(Some(5.0)),
2217 ]);
2218 let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
2219 let mut expected = RowAddrTreeMap::new();
2221 expected.insert_range(0..500);
2222 assert_eq!(result, SearchResult::at_most(expected));
2223 }
2224
2225 #[tokio::test]
2226 async fn test_basic_zonemap_index() {
2228 let tmpdir = TempObjDir::default();
2229 let test_store = Arc::new(LanceIndexStore::new(
2230 Arc::new(ObjectStore::local()),
2231 tmpdir.clone(),
2232 Arc::new(LanceCache::no_cache()),
2233 ));
2234
2235 let data = arrow_array::Int32Array::from_iter_values(0..=100);
2236 let row_ids = UInt64Array::from_iter_values((0..data.len()).map(|i| i as u64));
2237 let schema = Arc::new(Schema::new(vec![
2238 Field::new(VALUE_COLUMN_NAME, DataType::Int32, false),
2239 Field::new(ROW_ADDR, DataType::UInt64, false),
2240 ]));
2241 let data =
2242 RecordBatch::try_new(schema.clone(), vec![Arc::new(data), Arc::new(row_ids)]).unwrap();
2243 let data_stream: SendableRecordBatchStream = Box::pin(RecordBatchStreamAdapter::new(
2244 schema,
2245 stream::once(std::future::ready(Ok(data))),
2246 ));
2247
2248 ZoneMapIndexPlugin::train_zonemap_index(
2249 data_stream,
2250 test_store.as_ref(),
2251 Some(ZoneMapIndexBuilderParams::new(100)),
2252 )
2253 .await
2254 .unwrap();
2255
2256 log::debug!("Successfully wrote the index file");
2257
2258 let index_file = test_store.open_index_file(ZONEMAP_FILENAME).await.unwrap();
2260 let metadata = index_file.schema().metadata.clone();
2262 let record_batch = index_file
2263 .read_record_batch(0, index_file.num_rows() as u64)
2264 .await
2265 .unwrap();
2266 assert_eq!(record_batch.num_rows(), 2);
2267 assert_eq!(
2268 record_batch
2269 .column(0)
2270 .as_any()
2271 .downcast_ref::<arrow_array::Int32Array>()
2272 .unwrap()
2273 .values(),
2274 &[0, 100]
2275 );
2276 assert_eq!(
2277 record_batch
2278 .column(1)
2279 .as_any()
2280 .downcast_ref::<arrow_array::Int32Array>()
2281 .unwrap()
2282 .values(),
2283 &[99, 100]
2284 );
2285 assert_eq!(
2286 record_batch
2287 .column(2)
2288 .as_any()
2289 .downcast_ref::<arrow_array::UInt32Array>()
2290 .unwrap()
2291 .values(),
2292 &[0, 0]
2293 );
2294 assert_eq!(metadata.get(ZONEMAP_SIZE_META_KEY).unwrap(), "100");
2295
2296 let index = ZoneMapIndex::load(test_store.clone(), None, &LanceCache::no_cache(), false)
2298 .await
2299 .expect("Failed to load ZoneMapIndex");
2300 assert_eq!(index.zones.len(), 2);
2301 assert_eq!(
2302 index.zones,
2303 vec![
2304 ZoneMapStatistics {
2305 min: ScalarValue::Int32(Some(0)),
2306 max: ScalarValue::Int32(Some(99)),
2307 null_count: 0,
2308 nan_count: 0,
2309 bound: ZoneBound {
2310 fragment_id: 0,
2311 start: 0,
2312 length: 100,
2313 },
2314 },
2315 ZoneMapStatistics {
2316 min: ScalarValue::Int32(Some(100)),
2317 max: ScalarValue::Int32(Some(100)),
2318 null_count: 0,
2319 nan_count: 0,
2320 bound: ZoneBound {
2321 fragment_id: 0,
2322 start: 100,
2323 length: 1,
2324 },
2325 }
2326 ]
2327 );
2328 for (i, zone) in index.zones.iter().enumerate() {
2330 assert_eq!(zone.nan_count, 0, "Zone {} should have nan_count = 0", i);
2331 }
2332
2333 assert_eq!(index.data_type, DataType::Int32);
2334 assert_eq!(index.rows_per_zone, 100);
2335 assert_eq!(
2336 index.calculate_included_frags().await.unwrap(),
2337 RoaringBitmap::from_iter(0..1)
2338 );
2339
2340 let query = SargableQuery::Range(
2344 Bound::Excluded(ScalarValue::Int32(Some(50))),
2345 Bound::Unbounded,
2346 );
2347 let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
2348 assert_eq!(result, SearchResult::at_most(0..=100));
2349
2350 let query = SargableQuery::Range(
2352 Bound::Included(ScalarValue::Int32(Some(0))),
2353 Bound::Included(ScalarValue::Int32(Some(50))),
2354 );
2355 let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
2356 assert_eq!(result, SearchResult::at_most(0..=99));
2357
2358 let query = SargableQuery::Range(
2360 Bound::Included(ScalarValue::Int32(Some(101))),
2361 Bound::Included(ScalarValue::Int32(Some(200))),
2362 );
2363 let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
2364 assert_eq!(result, SearchResult::at_most(RowAddrTreeMap::new()));
2366
2367 let query = SargableQuery::Range(
2369 Bound::Included(ScalarValue::Int32(Some(100))),
2370 Bound::Included(ScalarValue::Int32(Some(100))),
2371 );
2372 let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
2373 assert_eq!(result, SearchResult::at_most(100..=100));
2374
2375 let query = SargableQuery::Equals(ScalarValue::Int32(Some(0)));
2377 let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
2378 assert_eq!(result, SearchResult::at_most(0..=99));
2379
2380 let query = SargableQuery::Equals(ScalarValue::Int32(Some(100)));
2382 let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
2383 assert_eq!(result, SearchResult::at_most(100..=100));
2384
2385 let query = SargableQuery::Equals(ScalarValue::Int32(Some(101)));
2387 let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
2388 assert_eq!(result, SearchResult::at_most(RowAddrTreeMap::new()));
2389
2390 let query = SargableQuery::IsNull();
2392 let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
2393 assert_eq!(result, SearchResult::exact(RowAddrTreeMap::new()));
2394 let query = SargableQuery::IsIn(vec![
2396 ScalarValue::Int32(Some(0)),
2397 ScalarValue::Int32(Some(100)),
2398 ScalarValue::Int32(Some(101)),
2399 ScalarValue::Int32(Some(50)),
2400 ]);
2401 let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
2402 assert_eq!(result, SearchResult::at_most(0..=100));
2404
2405 let query = SargableQuery::IsIn(vec![
2407 ScalarValue::Int32(Some(101)),
2408 ScalarValue::Int32(Some(102)),
2409 ]);
2410 let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
2411 assert_eq!(result, SearchResult::at_most(RowAddrTreeMap::new()));
2412
2413 let query = SargableQuery::IsIn(vec![ScalarValue::Int32(None)]);
2415 let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
2416 assert_eq!(result, SearchResult::at_most(RowAddrTreeMap::new()));
2417
2418 let query = SargableQuery::Equals(ScalarValue::Int32(None));
2420 let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
2421 assert_eq!(result, SearchResult::at_most(RowAddrTreeMap::new()));
2422 }
2423
2424 #[tokio::test]
2425 async fn test_complex_zonemap_index() {
2427 let tmpdir = TempObjDir::default();
2428 let test_store = Arc::new(LanceIndexStore::new(
2429 Arc::new(ObjectStore::local()),
2430 tmpdir.clone(),
2431 Arc::new(LanceCache::no_cache()),
2432 ));
2433
2434 let data =
2436 arrow_array::Int64Array::from_iter_values(0..(ROWS_PER_ZONE_DEFAULT * 2 + 42) as i64);
2437 let row_ids = UInt64Array::from_iter_values((0..data.len()).map(|i| i as u64));
2438 let schema = Arc::new(Schema::new(vec![
2439 Field::new(VALUE_COLUMN_NAME, DataType::Int64, false),
2440 Field::new(ROW_ADDR, DataType::UInt64, false),
2441 ]));
2442 let data =
2443 RecordBatch::try_new(schema.clone(), vec![Arc::new(data), Arc::new(row_ids)]).unwrap();
2444 let data_stream: SendableRecordBatchStream = Box::pin(RecordBatchStreamAdapter::new(
2445 schema,
2446 stream::once(std::future::ready(Ok(data))),
2447 ));
2448
2449 ZoneMapIndexPlugin::train_zonemap_index(
2450 data_stream,
2451 test_store.as_ref(),
2452 Some(ZoneMapIndexBuilderParams::default()),
2453 )
2454 .await
2455 .unwrap();
2456
2457 log::debug!("Successfully wrote the index file");
2458
2459 let index = ZoneMapIndex::load(test_store.clone(), None, &LanceCache::no_cache(), false)
2461 .await
2462 .expect("Failed to load ZoneMapIndex");
2463 assert_eq!(index.zones.len(), 3);
2464 assert_eq!(
2465 index.zones,
2466 vec![
2467 ZoneMapStatistics {
2468 min: ScalarValue::Int64(Some(0)),
2469 max: ScalarValue::Int64(Some(8191)),
2470 null_count: 0,
2471 nan_count: 0,
2472 bound: ZoneBound {
2473 fragment_id: 0,
2474 start: 0,
2475 length: 8192,
2476 },
2477 },
2478 ZoneMapStatistics {
2479 min: ScalarValue::Int64(Some(8192)),
2480 max: ScalarValue::Int64(Some(16383)),
2481 null_count: 0,
2482 nan_count: 0,
2483 bound: ZoneBound {
2484 fragment_id: 0,
2485 start: 8192,
2486 length: 8192,
2487 },
2488 },
2489 ZoneMapStatistics {
2490 min: ScalarValue::Int64(Some(16384)),
2491 max: ScalarValue::Int64(Some(16425)),
2492 null_count: 0,
2493 nan_count: 0,
2494 bound: ZoneBound {
2495 fragment_id: 0,
2496 start: 16384,
2497 length: 42,
2498 },
2499 }
2500 ]
2501 );
2502 for (i, zone) in index.zones.iter().enumerate() {
2504 assert_eq!(zone.nan_count, 0, "Zone {} should have nan_count = 0", i);
2505 }
2506
2507 assert_eq!(index.data_type, DataType::Int64);
2508 assert_eq!(index.rows_per_zone, ROWS_PER_ZONE_DEFAULT);
2509 assert_eq!(
2510 index.calculate_included_frags().await.unwrap(),
2511 RoaringBitmap::from_iter(0..1)
2512 );
2513
2514 let query = SargableQuery::Equals(ScalarValue::Int64(Some(1000)));
2519 let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
2520 let mut expected = RowAddrTreeMap::new();
2522 expected.insert_range(0..=8191);
2523 assert_eq!(result, SearchResult::at_most(expected));
2524
2525 let query = SargableQuery::Equals(ScalarValue::Int64(Some(9000)));
2527 let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
2528 let mut expected = RowAddrTreeMap::new();
2530 expected.insert_range(8192..=16383);
2531 assert_eq!(result, SearchResult::at_most(expected));
2532
2533 let query = SargableQuery::Equals(ScalarValue::Int64(Some(20000)));
2535 let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
2536 assert_eq!(result, SearchResult::at_most(RowAddrTreeMap::new()));
2537
2538 let query = SargableQuery::Range(
2540 Bound::Included(ScalarValue::Int64(Some(9000))),
2541 Bound::Included(ScalarValue::Int64(Some(16400))),
2542 );
2543 let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
2544 let mut expected = RowAddrTreeMap::new();
2546 expected.insert_range(8192..=16425);
2547 assert_eq!(result, SearchResult::at_most(expected));
2548 }
2549
2550 #[tokio::test]
2551 async fn test_multiple_fragments_zonemap() {
2553 let tmpdir = TempObjDir::default();
2554 let test_store = Arc::new(LanceIndexStore::new(
2555 Arc::new(ObjectStore::local()),
2556 tmpdir.clone(),
2557 Arc::new(LanceCache::no_cache()),
2558 ));
2559
2560 let schema = Arc::new(Schema::new(vec![
2561 Field::new(VALUE_COLUMN_NAME, DataType::Int64, false),
2562 Field::new(ROW_ADDR, DataType::UInt64, false),
2563 ]));
2564
2565 let fragment0_data =
2568 arrow_array::Int64Array::from_iter_values(0..ROWS_PER_ZONE_DEFAULT as i64);
2569 let fragment0_row_ids = UInt64Array::from_iter_values(0..ROWS_PER_ZONE_DEFAULT);
2570 let fragment0_batch = RecordBatch::try_new(
2571 schema.clone(),
2572 vec![Arc::new(fragment0_data), Arc::new(fragment0_row_ids)],
2573 )
2574 .unwrap();
2575
2576 let fragment1_data = arrow_array::Int64Array::from_iter_values(
2578 (ROWS_PER_ZONE_DEFAULT as i64)..((ROWS_PER_ZONE_DEFAULT * 2) as i64),
2579 );
2580 let fragment1_row_ids =
2581 UInt64Array::from_iter_values((0..ROWS_PER_ZONE_DEFAULT).map(|i| i + (1 << 32)));
2582 let fragment1_batch = RecordBatch::try_new(
2583 schema.clone(),
2584 vec![Arc::new(fragment1_data), Arc::new(fragment1_row_ids)],
2585 )
2586 .unwrap();
2587
2588 let fragment2_data = arrow_array::Int64Array::from_iter_values(
2590 ((ROWS_PER_ZONE_DEFAULT * 2) as i64)..((ROWS_PER_ZONE_DEFAULT * 2 + 42) as i64),
2591 );
2592 let fragment2_row_ids =
2593 UInt64Array::from_iter_values((0..42).map(|i| (i as u64) + (2 << 32)));
2594 let fragment2_batch = RecordBatch::try_new(
2595 schema.clone(),
2596 vec![Arc::new(fragment2_data), Arc::new(fragment2_row_ids)],
2597 )
2598 .unwrap();
2599
2600 {
2602 let data_stream: SendableRecordBatchStream = Box::pin(RecordBatchStreamAdapter::new(
2604 schema.clone(),
2605 stream::iter(vec![
2606 Ok(fragment0_batch.clone()),
2607 Ok(fragment1_batch.clone()),
2608 Ok(fragment2_batch.clone()),
2609 ]),
2610 ));
2611 ZoneMapIndexPlugin::train_zonemap_index(
2612 data_stream,
2613 test_store.as_ref(),
2614 Some(ZoneMapIndexBuilderParams::new(5000)),
2615 )
2616 .await
2617 .unwrap();
2618
2619 let index =
2621 ZoneMapIndex::load(test_store.clone(), None, &LanceCache::no_cache(), false)
2622 .await
2623 .expect("Failed to load ZoneMapIndex");
2624 assert_eq!(index.zones.len(), 5);
2625 assert_eq!(
2626 index.zones,
2627 vec![
2628 ZoneMapStatistics {
2629 min: ScalarValue::Int64(Some(0)),
2630 max: ScalarValue::Int64(Some(4999)),
2631 null_count: 0,
2632 nan_count: 0,
2633 bound: ZoneBound {
2634 fragment_id: 0,
2635 start: 0,
2636 length: 5000,
2637 },
2638 },
2639 ZoneMapStatistics {
2640 min: ScalarValue::Int64(Some(5000)),
2641 max: ScalarValue::Int64(Some(8191)),
2642 null_count: 0,
2643 nan_count: 0,
2644 bound: ZoneBound {
2645 fragment_id: 0,
2646 start: 5000,
2647 length: 3192,
2648 },
2649 },
2650 ZoneMapStatistics {
2651 min: ScalarValue::Int64(Some(8192)),
2652 max: ScalarValue::Int64(Some(13191)),
2653 null_count: 0,
2654 nan_count: 0,
2655 bound: ZoneBound {
2656 fragment_id: 1,
2657 start: 0,
2658 length: 5000,
2659 },
2660 },
2661 ZoneMapStatistics {
2662 min: ScalarValue::Int64(Some(13192)),
2663 max: ScalarValue::Int64(Some(16383)),
2664 null_count: 0,
2665 nan_count: 0,
2666 bound: ZoneBound {
2667 fragment_id: 1,
2668 start: 5000,
2669 length: 3192,
2670 },
2671 },
2672 ZoneMapStatistics {
2673 min: ScalarValue::Int64(Some(16384)),
2674 max: ScalarValue::Int64(Some(16425)),
2675 null_count: 0,
2676 nan_count: 0,
2677 bound: ZoneBound {
2678 fragment_id: 2,
2679 start: 0,
2680 length: 42,
2681 },
2682 }
2683 ]
2684 );
2685 for (i, zone) in index.zones.iter().enumerate() {
2687 assert_eq!(zone.nan_count, 0, "Zone {} should have nan_count = 0", i);
2688 }
2689
2690 assert_eq!(index.data_type, DataType::Int64);
2691 assert_eq!(index.rows_per_zone, 5000);
2692 assert_eq!(
2693 index.calculate_included_frags().await.unwrap(),
2694 RoaringBitmap::from_iter(0..3)
2695 );
2696
2697 let verify_data_stream: SendableRecordBatchStream =
2699 Box::pin(RecordBatchStreamAdapter::new(
2700 schema.clone(),
2701 stream::iter(vec![
2702 Ok(fragment0_batch.clone()),
2703 Ok(fragment1_batch.clone()),
2704 Ok(fragment2_batch.clone()),
2705 ]),
2706 ));
2707 let batches: Vec<RecordBatch> = verify_data_stream.try_collect().await.unwrap();
2708
2709 assert_eq!(batches.len(), 3);
2710
2711 let fragment0_rowaddr_col = batches[0].column_by_name(ROW_ADDR).unwrap();
2713 let fragment0_rowaddrs = fragment0_rowaddr_col
2714 .as_any()
2715 .downcast_ref::<UInt64Array>()
2716 .unwrap();
2717 assert_eq!(
2718 fragment0_rowaddrs.values().len(),
2719 ROWS_PER_ZONE_DEFAULT as usize
2720 );
2721 assert_eq!(fragment0_rowaddrs.values()[0], 0);
2722 assert_eq!(
2723 fragment0_rowaddrs.values()[fragment0_rowaddrs.values().len() - 1],
2724 8191
2725 );
2726
2727 let fragment1_rowaddr_col = batches[1].column_by_name(ROW_ADDR).unwrap();
2729 let fragment1_rowaddrs = fragment1_rowaddr_col
2730 .as_any()
2731 .downcast_ref::<UInt64Array>()
2732 .unwrap();
2733 assert_eq!(
2734 fragment1_rowaddrs.values().len(),
2735 ROWS_PER_ZONE_DEFAULT as usize
2736 );
2737 assert_eq!(fragment1_rowaddrs.values()[0], 1u64 << 32); assert_eq!(
2739 fragment1_rowaddrs.values()[fragment1_rowaddrs.values().len() - 1],
2740 8191 | (1u64 << 32)
2741 );
2742
2743 let fragment2_rowaddr_col = batches[2].column_by_name(ROW_ADDR).unwrap();
2745 let fragment2_rowaddrs = fragment2_rowaddr_col
2746 .as_any()
2747 .downcast_ref::<UInt64Array>()
2748 .unwrap();
2749 assert_eq!(fragment2_rowaddrs.values().len(), 42);
2750 assert_eq!(fragment2_rowaddrs.values()[0], 2u64 << 32); assert_eq!(
2752 fragment2_rowaddrs.values()[fragment2_rowaddrs.values().len() - 1],
2753 (2u64 << 32) | 41
2754 );
2755
2756 let query = SargableQuery::Range(
2760 Bound::Included(ScalarValue::Int64(Some(5000))),
2761 Bound::Included(ScalarValue::Int64(Some(12000))),
2762 );
2763 let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
2764 let mut expected = RowAddrTreeMap::new();
2766 expected.insert_range(5000..8192);
2768 expected.insert_range((1u64 << 32)..((1u64 << 32) + 5000));
2770 assert_eq!(result, SearchResult::at_most(expected));
2771
2772 let query = SargableQuery::Equals(ScalarValue::Int64(Some(8192)));
2774 let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
2775 let mut expected = RowAddrTreeMap::new();
2777 expected.insert_range((1u64 << 32)..((1u64 << 32) + 5000));
2778 assert_eq!(result, SearchResult::at_most(expected));
2779
2780 let query = SargableQuery::Equals(ScalarValue::Int64(Some(16385)));
2782 let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
2783 let mut expected = RowAddrTreeMap::new();
2785 expected.insert_range(2u64 << 32..((2u64 << 32) + 42));
2786 assert_eq!(result, SearchResult::at_most(expected));
2787
2788 let query = SargableQuery::Equals(ScalarValue::Int64(Some(99999)));
2790 let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
2791 assert_eq!(result, SearchResult::at_most(RowAddrTreeMap::new()));
2792
2793 let query = SargableQuery::IsIn(vec![ScalarValue::Int64(Some(16385))]);
2795 let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
2796 let mut expected = RowAddrTreeMap::new();
2797 expected.insert_range(2u64 << 32..((2u64 << 32) + 42));
2798 assert_eq!(result, SearchResult::at_most(expected));
2799
2800 let query = SargableQuery::Equals(ScalarValue::Int64(None));
2802 let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
2803 let mut expected = RowAddrTreeMap::new();
2804 expected.insert_range(0..=16425);
2805 assert_eq!(result, SearchResult::at_most(RowAddrTreeMap::new()));
2807 }
2808
2809 {
2811 let data_stream: SendableRecordBatchStream = Box::pin(RecordBatchStreamAdapter::new(
2813 schema.clone(),
2814 stream::iter(vec![
2815 Ok(fragment0_batch.clone()),
2816 Ok(fragment1_batch.clone()),
2817 Ok(fragment2_batch.clone()),
2818 ]),
2819 ));
2820 ZoneMapIndexPlugin::train_zonemap_index(
2821 data_stream,
2822 test_store.as_ref(),
2823 Some(ZoneMapIndexBuilderParams::default()),
2824 )
2825 .await
2826 .unwrap();
2827
2828 let index =
2830 ZoneMapIndex::load(test_store.clone(), None, &LanceCache::no_cache(), false)
2831 .await
2832 .expect("Failed to load ZoneMapIndex");
2833 assert_eq!(index.zones.len(), 3);
2834 assert_eq!(
2835 index.zones,
2836 vec![
2837 ZoneMapStatistics {
2838 min: ScalarValue::Int64(Some(0)),
2839 max: ScalarValue::Int64(Some(8191)),
2840 null_count: 0,
2841 nan_count: 0,
2842 bound: ZoneBound {
2843 fragment_id: 0,
2844 start: 0,
2845 length: 8192,
2846 },
2847 },
2848 ZoneMapStatistics {
2849 min: ScalarValue::Int64(Some(8192)),
2850 max: ScalarValue::Int64(Some(16383)),
2851 null_count: 0,
2852 nan_count: 0,
2853 bound: ZoneBound {
2854 fragment_id: 1,
2855 start: 0,
2856 length: 8192,
2857 },
2858 },
2859 ZoneMapStatistics {
2860 min: ScalarValue::Int64(Some(16384)),
2861 max: ScalarValue::Int64(Some(16425)),
2862 null_count: 0,
2863 nan_count: 0,
2864 bound: ZoneBound {
2865 fragment_id: 2,
2866 start: 0,
2867 length: 42,
2868 },
2869 }
2870 ]
2871 );
2872 for (i, zone) in index.zones.iter().enumerate() {
2874 assert_eq!(zone.nan_count, 0, "Zone {} should have nan_count = 0", i);
2875 }
2876
2877 assert_eq!(index.data_type, DataType::Int64);
2878 assert_eq!(index.rows_per_zone, ROWS_PER_ZONE_DEFAULT);
2879 assert_eq!(
2880 index.calculate_included_frags().await.unwrap(),
2881 RoaringBitmap::from_iter(0..3)
2882 );
2883 }
2884
2885 {
2887 let data_stream: SendableRecordBatchStream = Box::pin(RecordBatchStreamAdapter::new(
2889 schema.clone(),
2890 stream::iter(vec![
2891 Ok(fragment0_batch.clone()),
2892 Ok(fragment1_batch.clone()),
2893 Ok(fragment2_batch.clone()),
2894 ]),
2895 ));
2896 ZoneMapIndexPlugin::train_zonemap_index(
2897 data_stream,
2898 test_store.as_ref(),
2899 Some(ZoneMapIndexBuilderParams::new(ROWS_PER_ZONE_DEFAULT * 3)),
2900 )
2901 .await
2902 .unwrap();
2903
2904 let index =
2906 ZoneMapIndex::load(test_store.clone(), None, &LanceCache::no_cache(), false)
2907 .await
2908 .expect("Failed to load ZoneMapIndex");
2909 assert_eq!(index.zones.len(), 3);
2910 assert_eq!(
2911 index.zones,
2912 vec![
2913 ZoneMapStatistics {
2914 min: ScalarValue::Int64(Some(0)),
2915 max: ScalarValue::Int64(Some(8191)),
2916 null_count: 0,
2917 nan_count: 0,
2918 bound: ZoneBound {
2919 fragment_id: 0,
2920 start: 0,
2921 length: 8192,
2922 },
2923 },
2924 ZoneMapStatistics {
2925 min: ScalarValue::Int64(Some(8192)),
2926 max: ScalarValue::Int64(Some(16383)),
2927 null_count: 0,
2928 nan_count: 0,
2929 bound: ZoneBound {
2930 fragment_id: 1,
2931 start: 0,
2932 length: 8192,
2933 },
2934 },
2935 ZoneMapStatistics {
2936 min: ScalarValue::Int64(Some(16384)),
2937 max: ScalarValue::Int64(Some(16425)),
2938 null_count: 0,
2939 nan_count: 0,
2940 bound: ZoneBound {
2941 fragment_id: 2,
2942 start: 0,
2943 length: 42,
2944 },
2945 }
2946 ]
2947 );
2948 for (i, zone) in index.zones.iter().enumerate() {
2950 assert_eq!(zone.nan_count, 0, "Zone {} should have nan_count = 0", i);
2951 }
2952
2953 assert_eq!(index.data_type, DataType::Int64);
2954 assert_eq!(index.rows_per_zone, ROWS_PER_ZONE_DEFAULT * 3);
2955 }
2956 }
2957
2958 #[tokio::test]
2959 async fn test_fragment_id_assignment() {
2960 let schema = Arc::new(Schema::new(vec![Field::new(
2962 VALUE_COLUMN_NAME,
2963 DataType::Int32,
2964 false,
2965 )]));
2966
2967 let fragment0_data = arrow_array::Int32Array::from_iter_values(0..5);
2969 let fragment0_batch =
2970 RecordBatch::try_new(schema.clone(), vec![Arc::new(fragment0_data)]).unwrap();
2971
2972 let fragment1_data = arrow_array::Int32Array::from_iter_values(5..10);
2973 let fragment1_batch =
2974 RecordBatch::try_new(schema.clone(), vec![Arc::new(fragment1_data)]).unwrap();
2975
2976 let aligned_stream: SendableRecordBatchStream = Box::pin(RecordBatchStreamAdapter::new(
2977 schema,
2978 stream::iter(vec![Ok(fragment0_batch), Ok(fragment1_batch)]),
2979 ));
2980
2981 let aligned_stream = add_row_addr(aligned_stream);
2982
2983 let batches: Vec<RecordBatch> = aligned_stream.try_collect().await.unwrap();
2984
2985 assert_eq!(batches.len(), 2);
2986
2987 let fragment0_rowaddr_col = batches[0].column_by_name(ROW_ADDR).unwrap();
2989 let fragment0_rowaddrs = fragment0_rowaddr_col
2990 .as_any()
2991 .downcast_ref::<UInt64Array>()
2992 .unwrap();
2993
2994 assert_eq!(fragment0_rowaddrs.values(), &[0, 1, 2, 3, 4]);
2996
2997 let fragment1_rowaddr_col = batches[1].column_by_name(ROW_ADDR).unwrap();
2999 let fragment1_rowaddrs = fragment1_rowaddr_col
3000 .as_any()
3001 .downcast_ref::<UInt64Array>()
3002 .unwrap();
3003
3004 assert_eq!(
3007 fragment1_rowaddrs.values(),
3008 &[4294967296, 4294967297, 4294967298, 4294967299, 4294967300]
3009 );
3010 }
3011
3012 #[tokio::test]
3013 async fn test_like_prefix_query() {
3014 let tmpdir = TempObjDir::default();
3015 let test_store = Arc::new(LanceIndexStore::new(
3016 Arc::new(ObjectStore::local()),
3017 tmpdir.clone(),
3018 Arc::new(LanceCache::no_cache()),
3019 ));
3020
3021 let zones = vec![
3030 ZoneMapStatistics {
3031 min: ScalarValue::Utf8(Some("aaa".to_string())),
3032 max: ScalarValue::Utf8(Some("azz".to_string())),
3033 null_count: 0,
3034 nan_count: 0,
3035 bound: ZoneBound {
3036 fragment_id: 0,
3037 start: 0,
3038 length: 100,
3039 },
3040 },
3041 ZoneMapStatistics {
3042 min: ScalarValue::Utf8(Some("bar".to_string())),
3043 max: ScalarValue::Utf8(Some("baz".to_string())),
3044 null_count: 0,
3045 nan_count: 0,
3046 bound: ZoneBound {
3047 fragment_id: 1,
3048 start: 0,
3049 length: 100,
3050 },
3051 },
3052 ZoneMapStatistics {
3053 min: ScalarValue::Utf8(Some("fa".to_string())),
3054 max: ScalarValue::Utf8(Some("foz".to_string())),
3055 null_count: 0,
3056 nan_count: 0,
3057 bound: ZoneBound {
3058 fragment_id: 2,
3059 start: 0,
3060 length: 100,
3061 },
3062 },
3063 ZoneMapStatistics {
3064 min: ScalarValue::Utf8(Some("fop".to_string())),
3065 max: ScalarValue::Utf8(Some("fzz".to_string())),
3066 null_count: 0,
3067 nan_count: 0,
3068 bound: ZoneBound {
3069 fragment_id: 3,
3070 start: 0,
3071 length: 100,
3072 },
3073 },
3074 ZoneMapStatistics {
3075 min: ScalarValue::Utf8(Some("foo".to_string())),
3076 max: ScalarValue::Utf8(Some("foobar".to_string())),
3077 null_count: 0,
3078 nan_count: 0,
3079 bound: ZoneBound {
3080 fragment_id: 4,
3081 start: 0,
3082 length: 100,
3083 },
3084 },
3085 ZoneMapStatistics {
3086 min: ScalarValue::Utf8(Some("gaa".to_string())),
3087 max: ScalarValue::Utf8(Some("gzz".to_string())),
3088 null_count: 0,
3089 nan_count: 0,
3090 bound: ZoneBound {
3091 fragment_id: 5,
3092 start: 0,
3093 length: 100,
3094 },
3095 },
3096 ];
3097
3098 let index = ZoneMapIndex {
3099 zones,
3100 data_type: DataType::Utf8,
3101 rows_per_zone: ROWS_PER_ZONE_DEFAULT,
3102 use_seeds: false,
3103 store: test_store,
3104 fri: None,
3105 index_cache: WeakLanceCache::from(&LanceCache::no_cache()),
3106 null_rows: None,
3107 };
3108
3109 let query = SargableQuery::LikePrefix(ScalarValue::Utf8(Some("foo".to_string())));
3111 let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
3112
3113 let mut expected = RowAddrTreeMap::new();
3115 expected.insert_range((2u64 << 32)..((2u64 << 32) + 100));
3117 expected.insert_range((4u64 << 32)..((4u64 << 32) + 100));
3119
3120 assert_eq!(result, SearchResult::at_most(expected));
3121 }
3122
3123 #[tokio::test]
3124 async fn test_like_prefix_edge_cases() {
3125 let tmpdir = TempObjDir::default();
3126 let test_store = Arc::new(LanceIndexStore::new(
3127 Arc::new(ObjectStore::local()),
3128 tmpdir.clone(),
3129 Arc::new(LanceCache::no_cache()),
3130 ));
3131
3132 let zones = vec![
3134 ZoneMapStatistics {
3136 min: ScalarValue::Utf8(Some("test".to_string())),
3137 max: ScalarValue::Utf8(Some("test".to_string())),
3138 null_count: 0,
3139 nan_count: 0,
3140 bound: ZoneBound {
3141 fragment_id: 0,
3142 start: 0,
3143 length: 100,
3144 },
3145 },
3146 ZoneMapStatistics {
3148 min: ScalarValue::Utf8(Some("te".to_string())),
3149 max: ScalarValue::Utf8(Some("tf".to_string())),
3150 null_count: 0,
3151 nan_count: 0,
3152 bound: ZoneBound {
3153 fragment_id: 1,
3154 start: 0,
3155 length: 100,
3156 },
3157 },
3158 ZoneMapStatistics {
3160 min: ScalarValue::Utf8(Some("abc".to_string())),
3161 max: ScalarValue::Utf8(Some("def".to_string())),
3162 null_count: 0,
3163 nan_count: 0,
3164 bound: ZoneBound {
3165 fragment_id: 2,
3166 start: 0,
3167 length: 100,
3168 },
3169 },
3170 ];
3171
3172 let index = ZoneMapIndex {
3173 zones,
3174 data_type: DataType::Utf8,
3175 rows_per_zone: ROWS_PER_ZONE_DEFAULT,
3176 use_seeds: false,
3177 store: test_store,
3178 fri: None,
3179 index_cache: WeakLanceCache::from(&LanceCache::no_cache()),
3180 null_rows: None,
3181 };
3182
3183 let query = SargableQuery::LikePrefix(ScalarValue::Utf8(Some("test".to_string())));
3185 let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
3186
3187 let mut expected = RowAddrTreeMap::new();
3189 expected.insert_range(0..100); expected.insert_range((1u64 << 32)..((1u64 << 32) + 100));
3191
3192 assert_eq!(result, SearchResult::at_most(expected));
3193
3194 let query = SargableQuery::LikePrefix(ScalarValue::Utf8(Some("".to_string())));
3196 let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
3197
3198 let mut expected = RowAddrTreeMap::new();
3199 expected.insert_range(0..100); expected.insert_range((1u64 << 32)..((1u64 << 32) + 100));
3201 expected.insert_range((2u64 << 32)..((2u64 << 32) + 100));
3202
3203 assert_eq!(result, SearchResult::at_most(expected));
3204 }
3205
3206 #[tokio::test]
3207 async fn test_like_prefix_large_utf8() {
3208 let tmpdir = TempObjDir::default();
3209 let test_store = Arc::new(LanceIndexStore::new(
3210 Arc::new(ObjectStore::local()),
3211 tmpdir.clone(),
3212 Arc::new(LanceCache::no_cache()),
3213 ));
3214
3215 let zones = vec![
3217 ZoneMapStatistics {
3218 min: ScalarValue::LargeUtf8(Some("aaa".to_string())),
3219 max: ScalarValue::LargeUtf8(Some("azz".to_string())),
3220 null_count: 0,
3221 nan_count: 0,
3222 bound: ZoneBound {
3223 fragment_id: 0,
3224 start: 0,
3225 length: 100,
3226 },
3227 },
3228 ZoneMapStatistics {
3229 min: ScalarValue::LargeUtf8(Some("foo".to_string())),
3230 max: ScalarValue::LargeUtf8(Some("foobar".to_string())),
3231 null_count: 0,
3232 nan_count: 0,
3233 bound: ZoneBound {
3234 fragment_id: 1,
3235 start: 0,
3236 length: 100,
3237 },
3238 },
3239 ];
3240
3241 let index = ZoneMapIndex {
3242 zones,
3243 data_type: DataType::LargeUtf8,
3244 rows_per_zone: ROWS_PER_ZONE_DEFAULT,
3245 use_seeds: false,
3246 store: test_store,
3247 fri: None,
3248 index_cache: WeakLanceCache::from(&LanceCache::no_cache()),
3249 null_rows: None,
3250 };
3251
3252 let query = SargableQuery::LikePrefix(ScalarValue::LargeUtf8(Some("foo".to_string())));
3254 let result = index.search(&query, &NoOpMetricsCollector).await.unwrap();
3255
3256 let mut expected = RowAddrTreeMap::new();
3258 expected.insert_range((1u64 << 32)..((1u64 << 32) + 100));
3259
3260 assert_eq!(result, SearchResult::at_most(expected));
3261 }
3262
3263 #[test]
3264 fn test_compute_next_prefix() {
3265 use super::compute_next_prefix;
3266
3267 assert_eq!(compute_next_prefix("foo"), Some("fop".to_string()));
3269 assert_eq!(compute_next_prefix("abc"), Some("abd".to_string()));
3270 assert_eq!(compute_next_prefix("a"), Some("b".to_string()));
3271 assert_eq!(compute_next_prefix("z"), Some("{".to_string())); assert_eq!(compute_next_prefix("abz"), Some("ab{".to_string()));
3275
3276 assert_eq!(compute_next_prefix("ab~"), Some("ab\x7f".to_string()));
3278
3279 assert_eq!(compute_next_prefix(""), None);
3281
3282 assert_eq!(compute_next_prefix("café"), Some("cafê".to_string()));
3285 assert_eq!(compute_next_prefix("abc中"), Some("abc丮".to_string()));
3287 assert_eq!(compute_next_prefix("cafÿ"), Some("cafĀ".to_string()));
3289
3290 assert_eq!(
3293 compute_next_prefix("a\u{D7FF}"),
3294 Some("a\u{E000}".to_string())
3295 );
3296
3297 assert_eq!(compute_next_prefix("ab\u{10FFFF}"), Some("ac".to_string()));
3299 assert_eq!(compute_next_prefix("\u{10FFFF}\u{10FFFF}"), None);
3301 }
3302
3303 #[tokio::test]
3311 async fn test_merge_with_legacy_none_segment_not_treated_as_no_nulls() {
3312 let tmpdir = TempObjDir::default();
3313 let store = Arc::new(LanceIndexStore::new(
3314 Arc::new(ObjectStore::local()),
3315 tmpdir.clone(),
3316 Arc::new(LanceCache::no_cache()),
3317 ));
3318
3319 use arrow_array::{Int32Array, UInt32Array};
3320
3321 let schema_a = Arc::new(Schema::new(vec![
3323 Field::new("min", DataType::Int32, true),
3324 Field::new("max", DataType::Int32, true),
3325 Field::new("null_count", DataType::UInt32, false),
3326 Field::new("nan_count", DataType::UInt32, false),
3327 Field::new("fragment_id", DataType::UInt64, false),
3328 Field::new("zone_start", DataType::UInt64, false),
3329 Field::new("zone_length", DataType::UInt64, false),
3330 ]));
3331 let batch_a = RecordBatch::try_new(
3332 schema_a,
3333 vec![
3334 Arc::new(Int32Array::from(vec![Some(1i32)])) as _,
3335 Arc::new(Int32Array::from(vec![Some(5i32)])) as _,
3336 Arc::new(UInt32Array::from(vec![2u32])) as _,
3337 Arc::new(UInt32Array::from(vec![0u32])) as _,
3338 Arc::new(UInt64Array::from(vec![0u64])) as _,
3339 Arc::new(UInt64Array::from(vec![0u64])) as _,
3340 Arc::new(UInt64Array::from(vec![10u64])) as _,
3341 ],
3342 )
3343 .unwrap();
3344 let mut modern_null_rows = RowAddrTreeMap::new();
3345 modern_null_rows.insert(3); modern_null_rows.insert(7); let cache = LanceCache::no_cache();
3348 let index_a = Arc::new(
3349 ZoneMapIndex::try_from_serialized(
3350 batch_a,
3351 store.clone(),
3352 None,
3353 &cache,
3354 10,
3355 Some(modern_null_rows), false,
3357 )
3358 .unwrap(),
3359 );
3360
3361 let schema_b = Arc::new(Schema::new(vec![
3363 Field::new("min", DataType::Int32, true),
3364 Field::new("max", DataType::Int32, true),
3365 Field::new("null_count", DataType::UInt32, false),
3366 Field::new("nan_count", DataType::UInt32, false),
3367 Field::new("fragment_id", DataType::UInt64, false),
3368 Field::new("zone_start", DataType::UInt64, false),
3369 Field::new("zone_length", DataType::UInt64, false),
3370 ]));
3371 let batch_b = RecordBatch::try_new(
3372 schema_b,
3373 vec![
3374 Arc::new(Int32Array::from(vec![Some(10i32)])) as _,
3375 Arc::new(Int32Array::from(vec![Some(20i32)])) as _,
3376 Arc::new(UInt32Array::from(vec![3u32])) as _,
3377 Arc::new(UInt32Array::from(vec![0u32])) as _,
3378 Arc::new(UInt64Array::from(vec![1u64])) as _,
3379 Arc::new(UInt64Array::from(vec![0u64])) as _,
3380 Arc::new(UInt64Array::from(vec![10u64])) as _,
3381 ],
3382 )
3383 .unwrap();
3384 let index_b = Arc::new(
3385 ZoneMapIndex::try_from_serialized(
3386 batch_b,
3387 store.clone(),
3388 None,
3389 &cache,
3390 10,
3391 None, false,
3393 )
3394 .unwrap(),
3395 );
3396
3397 let dest_tmpdir = TempObjDir::default();
3398 let dest_store = Arc::new(LanceIndexStore::new(
3399 Arc::new(ObjectStore::local()),
3400 dest_tmpdir.clone(),
3401 Arc::new(LanceCache::no_cache()),
3402 ));
3403
3404 let all_frags = RoaringBitmap::from_iter([0u32, 1]);
3405 merge_zonemap_indices(
3406 &[index_a.as_ref(), index_b.as_ref()],
3407 dest_store.as_ref(),
3408 &all_frags,
3409 )
3410 .await
3411 .unwrap();
3412
3413 let merged = ZoneMapIndex::load(dest_store.clone(), None, &LanceCache::no_cache(), false)
3414 .await
3415 .unwrap();
3416
3417 let result = merged
3420 .search(&SargableQuery::IsNull(), &NoOpMetricsCollector)
3421 .await
3422 .unwrap();
3423
3424 assert!(
3429 !result.is_exact(),
3430 "IsNull on a merged index where one source had null_rows=None must not return \
3431 exact; the legacy segment had null_count=3 so its nulls exist at unknown positions"
3432 );
3433 }
3434
3435 async fn write_legacy_zonemap(store: &dyn IndexStore, null_count: u32) {
3438 use arrow_array::{Int32Array, UInt32Array};
3439 let schema = Arc::new(Schema::new(vec![
3440 Field::new("min", DataType::Int32, true),
3441 Field::new("max", DataType::Int32, true),
3442 Field::new("null_count", DataType::UInt32, false),
3443 Field::new("nan_count", DataType::UInt32, false),
3444 Field::new("fragment_id", DataType::UInt64, false),
3445 Field::new("zone_start", DataType::UInt64, false),
3446 Field::new("zone_length", DataType::UInt64, false),
3447 ]));
3448 let batch = RecordBatch::try_new(
3449 schema.clone(),
3450 vec![
3451 Arc::new(Int32Array::from(vec![Some(0)])) as _,
3452 Arc::new(Int32Array::from(vec![Some(99)])) as _,
3453 Arc::new(UInt32Array::from(vec![null_count])) as _,
3454 Arc::new(UInt32Array::from(vec![0u32])) as _,
3455 Arc::new(UInt64Array::from(vec![0u64])) as _,
3456 Arc::new(UInt64Array::from(vec![0u64])) as _,
3457 Arc::new(UInt64Array::from(vec![100u64])) as _,
3458 ],
3459 )
3460 .unwrap();
3461 let mut file_schema = schema.as_ref().clone();
3462 file_schema
3463 .metadata
3464 .insert(ZONEMAP_SIZE_META_KEY.to_string(), "8192".to_string());
3465 let mut writer = store
3466 .new_index_file(ZONEMAP_FILENAME, Arc::new(file_schema))
3467 .await
3468 .unwrap();
3469 writer.write_record_batch(batch).await.unwrap();
3470 writer.finish().await.unwrap();
3471 }
3472
3473 #[tokio::test]
3474 async fn test_legacy_zonemap_no_null_bitmap() {
3475 let tmpdir = TempObjDir::default();
3476 let store = Arc::new(LanceIndexStore::new(
3477 Arc::new(ObjectStore::local()),
3478 tmpdir.clone(),
3479 Arc::new(LanceCache::no_cache()),
3480 ));
3481
3482 write_legacy_zonemap(store.as_ref(), 10).await;
3484
3485 let index = ZoneMapIndex::load(store, None, &LanceCache::no_cache(), false)
3486 .await
3487 .expect("failed to load legacy zonemap");
3488
3489 assert!(
3490 index.null_rows.is_none(),
3491 "legacy index should have no null bitmap"
3492 );
3493
3494 let result = index
3496 .search(&SargableQuery::IsNull(), &NoOpMetricsCollector)
3497 .await
3498 .unwrap();
3499 assert!(
3500 !result.is_exact(),
3501 "IS NULL on a legacy index should not be exact"
3502 );
3503 }
3504
3505 #[tokio::test]
3506 async fn test_zone_map_seed_writer_round_trip() {
3507 use crate::scalar::seed::IndexSeedWriter;
3508 use crate::scalar::zonemap::ZoneMapSeedWriter;
3509 use arrow_array::{ArrayRef, Int32Array};
3510 use datafusion_common::ScalarValue;
3511
3512 let rows_per_zone = 4u64;
3513 let data_type = DataType::Int32;
3514 let mut writer = ZoneMapSeedWriter::new("test_col", rows_per_zone, data_type).unwrap();
3515
3516 let batch1: ArrayRef = Arc::new(Int32Array::from_iter_values(0..4));
3518 writer.observe_batch(&batch1).unwrap();
3519
3520 let batch2: ArrayRef = Arc::new(Int32Array::from_iter_values(10..14));
3522 writer.observe_batch(&batch2).unwrap();
3523
3524 let batch3: ArrayRef = Arc::new(Int32Array::from_iter_values(20..22));
3526 writer.observe_batch(&batch3).unwrap();
3527
3528 let bytes = writer.finish().unwrap().expect("should produce bytes");
3529
3530 assert_eq!(writer.schema_metadata_key(), "lance.seed.test_col");
3532 let meta_val = writer.schema_metadata_value(3);
3533 assert_eq!(meta_val, "3:4");
3534
3535 let zones = ZoneMapSeedWriter::deserialize_seed(42, &bytes, rows_per_zone).unwrap();
3537 assert_eq!(zones.len(), 3, "expected 3 zones");
3538
3539 assert_eq!(zones[0].bound.fragment_id, 42);
3541 assert_eq!(zones[0].bound.start, 0);
3542 assert_eq!(zones[0].min, ScalarValue::Int32(Some(0)));
3543 assert_eq!(zones[0].max, ScalarValue::Int32(Some(3)));
3544 assert_eq!(zones[0].null_count, 0);
3545
3546 assert_eq!(zones[1].bound.start, 4);
3548 assert_eq!(zones[1].min, ScalarValue::Int32(Some(10)));
3549 assert_eq!(zones[1].max, ScalarValue::Int32(Some(13)));
3550
3551 assert_eq!(zones[2].bound.start, 8);
3553 assert_eq!(
3554 zones[2].bound.length, 2,
3555 "partial zone length must be exact"
3556 );
3557 assert_eq!(zones[2].min, ScalarValue::Int32(Some(20)));
3558 assert_eq!(zones[2].max, ScalarValue::Int32(Some(21)));
3559
3560 assert_eq!(zones[0].bound.length, rows_per_zone as usize);
3562 assert_eq!(zones[1].bound.length, rows_per_zone as usize);
3563 }
3564
3565 #[tokio::test]
3566 async fn test_zone_map_seed_writer_spanning_batches() {
3567 use crate::scalar::seed::IndexSeedWriter;
3568 use crate::scalar::zonemap::ZoneMapSeedWriter;
3569 use arrow_array::{ArrayRef, Int32Array};
3570 use datafusion_common::ScalarValue;
3571
3572 let rows_per_zone = 5u64;
3573 let data_type = DataType::Int32;
3574 let mut writer = ZoneMapSeedWriter::new("val", rows_per_zone, data_type).unwrap();
3575
3576 let batch: ArrayRef = Arc::new(Int32Array::from_iter_values(0..12));
3578 writer.observe_batch(&batch).unwrap();
3579
3580 let bytes = writer.finish().unwrap().expect("should produce bytes");
3581 let zones = ZoneMapSeedWriter::deserialize_seed(1, &bytes, rows_per_zone).unwrap();
3582 assert_eq!(
3583 zones.len(),
3584 3,
3585 "expected 3 zones from 12 rows with zone size 5"
3586 );
3587
3588 assert_eq!(zones[0].bound.length, 5);
3590 assert_eq!(zones[0].min, ScalarValue::Int32(Some(0)));
3591 assert_eq!(zones[0].max, ScalarValue::Int32(Some(4)));
3592 assert_eq!(zones[1].bound.length, 5);
3594 assert_eq!(zones[1].min, ScalarValue::Int32(Some(5)));
3595 assert_eq!(zones[1].max, ScalarValue::Int32(Some(9)));
3596 assert_eq!(zones[2].bound.length, 2, "partial zone length must be 2");
3598 assert_eq!(zones[2].min, ScalarValue::Int32(Some(10)));
3599 assert_eq!(zones[2].max, ScalarValue::Int32(Some(11)));
3600 }
3601
3602 #[tokio::test]
3603 async fn test_zone_map_seed_writer_empty() {
3604 use crate::scalar::seed::IndexSeedWriter;
3605 use crate::scalar::zonemap::ZoneMapSeedWriter;
3606
3607 let mut writer = ZoneMapSeedWriter::new("col", 8, DataType::Int32).unwrap();
3608 let result = writer.finish().unwrap();
3609 assert!(result.is_none(), "empty fragment should return None");
3610 }
3611}