Skip to main content

lance_table/format/
index.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright The Lance Authors
3
4//! Metadata for index
5
6use std::collections::{HashMap, HashSet};
7use std::sync::Arc;
8
9use chrono::{DateTime, Utc};
10use futures::StreamExt;
11use lance_core::deepsize::DeepSizeOf;
12use lance_io::object_store::ObjectStore;
13use object_store::path::Path;
14use roaring::RoaringBitmap;
15use uuid::Uuid;
16
17use super::pb;
18use lance_core::cache::{CacheEntryReader, CacheEntryWriter};
19use lance_core::{Error, Result};
20
21/// Metadata about a single file within an index segment.
22#[derive(Debug, Clone, PartialEq, DeepSizeOf)]
23pub struct IndexFile {
24    /// Path relative to the index directory (e.g., "index.idx", "auxiliary.idx")
25    pub path: String,
26    /// Size of the file in bytes
27    pub size_bytes: u64,
28}
29
30/// Index metadata
31#[derive(Debug, Clone, PartialEq)]
32pub struct IndexMetadata {
33    /// Unique ID across all dataset versions.
34    pub uuid: Uuid,
35
36    /// Fields to build the index.
37    ///
38    /// `fields[0]` is always a column the index is keyed on. Trailing entries
39    /// may instead be merely carried, not keyed on -- see [`Self::covering_fields`].
40    pub fields: Vec<i32>,
41
42    /// Fields whose values this index carries but is not keyed on.
43    ///
44    /// Always a suffix of [`Self::fields`], and never all of it, so
45    /// `fields[0]` is always a column the index is keyed on. Empty for an
46    /// index that carries no extra columns.
47    ///
48    /// These ids also appear in [`Self::fields`]. That is deliberate: every
49    /// consumer that reads `fields` as the index's dependency set then covers
50    /// them with no change.
51    pub covering_fields: Vec<i32>,
52
53    /// Human readable index name
54    pub name: String,
55
56    /// The version of the dataset this index was last updated on
57    ///
58    /// This is set when the index is created (based on the version used to train the index)
59    /// This is updated when the index is updated or remapped
60    pub dataset_version: u64,
61
62    /// The fragment ids this index covers.
63    ///
64    /// This may contain fragment ids that no longer exist in the dataset.
65    ///
66    /// If this is None, then this is unknown.
67    pub fragment_bitmap: Option<RoaringBitmap>,
68
69    /// Metadata specific to the index type
70    ///
71    /// This is an Option because older versions of Lance may not have this defined.  However, it should always
72    /// be present in newer versions.
73    pub index_details: Option<Arc<prost_types::Any>>,
74
75    /// The index version.
76    pub index_version: i32,
77
78    /// Timestamp when the index was created
79    ///
80    /// This field is optional for backward compatibility. For existing indices created before
81    /// this field was added, this will be None.
82    pub created_at: Option<DateTime<Utc>>,
83
84    /// The base path index of the index files. Used when the index is imported or referred from another dataset.
85    /// Lance uses it as key of the base_paths field in Manifest to determine the actual base path of the index files.
86    pub base_id: Option<u32>,
87
88    /// List of files and their sizes for this index segment.
89    /// This enables skipping HEAD calls when opening indices and provides
90    /// visibility into index storage size via describe_indices().
91    /// This is None if the file sizes are unknown. This happens for indices created
92    /// before this field was added.
93    pub files: Option<Vec<IndexFile>>,
94}
95
96impl IndexMetadata {
97    pub fn effective_fragment_bitmap(
98        &self,
99        existing_fragments: &RoaringBitmap,
100    ) -> Option<RoaringBitmap> {
101        let fragment_bitmap = self.fragment_bitmap.as_ref()?;
102        Some(fragment_bitmap & existing_fragments)
103    }
104
105    /// Returns a map of relative file paths to their sizes.
106    /// Returns an empty map if file information is not available.
107    pub fn file_size_map(&self) -> HashMap<String, u64> {
108        self.files
109            .as_ref()
110            .map(|files| {
111                files
112                    .iter()
113                    .map(|f| (f.path.clone(), f.size_bytes))
114                    .collect()
115            })
116            .unwrap_or_default()
117    }
118
119    /// Returns the total size of all files in this index segment in bytes.
120    /// Returns None if file information is not available.
121    pub fn total_size_bytes(&self) -> Option<u64> {
122        self.files
123            .as_ref()
124            .map(|files| files.iter().map(|f| f.size_bytes).sum())
125    }
126
127    /// Returns the set of fragments which are part of the fragment bitmap
128    /// but no longer in the dataset.
129    pub fn deleted_fragment_bitmap(
130        &self,
131        existing_fragments: &RoaringBitmap,
132    ) -> Option<RoaringBitmap> {
133        let fragment_bitmap = self.fragment_bitmap.as_ref()?;
134        Some(fragment_bitmap - existing_fragments)
135    }
136
137    /// True when the index reports matches as physical row addresses rather than row ids
138    /// (`ScalarIndex::results_are_row_addresses`).
139    ///
140    /// Such an index cannot follow its data through a rewrite: the addresses it stores
141    /// name fragments and offsets, and neither kind supports remap.
142    pub fn results_are_row_addrs(&self) -> bool {
143        self.index_details.as_ref().is_some_and(|details| {
144            details.type_url.ends_with("ZoneMapIndexDetails")
145                || details.type_url.ends_with("BloomFilterIndexDetails")
146        })
147    }
148
149    /// The prefix of [`Self::fields`] this index is keyed on, with the carried
150    /// columns of [`Self::covering_fields`] removed.
151    ///
152    /// Only this prefix decides which column an index answers for; the full
153    /// `fields` vector answers what invalidates it. Empty for a system index
154    /// that declares no fields, and empty for a declaration longer than
155    /// `fields`: decoding validates, but metadata built by a caller this build
156    /// never validated does not, and failing closed beats an underflow.
157    pub fn keyed_fields(&self) -> &[i32] {
158        let keyed = self.fields.len().saturating_sub(self.covering_fields.len());
159        &self.fields[..keyed]
160    }
161
162    /// The single column this index is keyed on, or `None` when it is keyed on
163    /// several -- a genuinely composite index -- or on none at all.
164    ///
165    /// Most selection paths are only defined for one keyed column, so they can
166    /// compare this against the column they are resolving.
167    pub fn keyed_field(&self) -> Option<i32> {
168        match self.keyed_fields() {
169            [only] => Some(*only),
170            _ => None,
171        }
172    }
173
174    /// Check the covering declaration against [`Self::fields`].
175    ///
176    /// Carried columns must be a suffix of `fields` and must not consume all of
177    /// it, so `fields[0]` is always a column the index is keyed on. An empty
178    /// declaration is always valid, which is what keeps the system indices --
179    /// `mem_wal` and `frag_reuse`, both of which commit no fields at all --
180    /// passing this check.
181    ///
182    /// The rules are checked from most to least specific, because one bad
183    /// declaration usually trips several: an id that is not a field at all is
184    /// reported ahead of the length and ordering rules, which would otherwise
185    /// name a consequence instead of the cause.
186    pub fn validate_covering_fields(&self) -> Result<()> {
187        if self.covering_fields.is_empty() {
188            return Ok(());
189        }
190
191        let missing: Vec<i32> = self
192            .covering_fields
193            .iter()
194            .copied()
195            .filter(|f| !self.fields.contains(f))
196            .collect();
197        if !missing.is_empty() {
198            return Err(Error::invalid_input(format!(
199                "index '{}' declares covering fields {:?} but {:?} are not \
200                 among its fields {:?}",
201                self.name, self.covering_fields, missing, self.fields,
202            )));
203        }
204
205        // A column carried twice would be projected twice. The suffix check
206        // below cannot stand in for this, because `fields` may repeat the id in
207        // the same positions -- `fields = [7, 11, 11]` has `[11, 11]` as a
208        // genuine tail.
209        let mut seen = HashSet::with_capacity(self.covering_fields.len());
210        if let Some(duplicate) = self.covering_fields.iter().find(|f| !seen.insert(**f)) {
211            return Err(Error::invalid_input(format!(
212                "index '{}' declares covering field {} more than once in {:?}",
213                self.name, duplicate, self.covering_fields,
214            )));
215        }
216
217        if self.covering_fields.len() >= self.fields.len() {
218            return Err(Error::invalid_input(format!(
219                "index '{}' declares covering fields {:?} but its fields are {:?}; \
220                 at least one field must remain indexed",
221                self.name, self.covering_fields, self.fields,
222            )));
223        }
224
225        let suffix_start = self.fields.len() - self.covering_fields.len();
226        if self.fields[suffix_start..] != self.covering_fields[..] {
227            return Err(Error::invalid_input(format!(
228                "index '{}' declares covering fields {:?} which are not the trailing \
229                 entries of {:?}; covering fields must come last",
230                self.name, self.covering_fields, self.fields,
231            )));
232        }
233
234        Ok(())
235    }
236}
237
238impl DeepSizeOf for IndexMetadata {
239    fn deep_size_of_children(&self, context: &mut lance_core::deepsize::Context) -> usize {
240        self.uuid.as_bytes().deep_size_of_children(context)
241            + self.fields.deep_size_of_children(context)
242            + self.covering_fields.deep_size_of_children(context)
243            + self.name.deep_size_of_children(context)
244            + self.dataset_version.deep_size_of_children(context)
245            + self
246                .fragment_bitmap
247                .as_ref()
248                .map(|fragment_bitmap| fragment_bitmap.serialized_size())
249                .unwrap_or(0)
250            + self.files.deep_size_of_children(context)
251    }
252}
253
254impl TryFrom<pb::IndexMetadata> for IndexMetadata {
255    type Error = Error;
256
257    fn try_from(proto: pb::IndexMetadata) -> Result<Self> {
258        let fragment_bitmap = if proto.fragment_bitmap.is_empty() {
259            None
260        } else {
261            Some(RoaringBitmap::deserialize_from(
262                &mut proto.fragment_bitmap.as_slice(),
263            )?)
264        };
265
266        let files = if proto.files.is_empty() {
267            None
268        } else {
269            Some(
270                proto
271                    .files
272                    .into_iter()
273                    .map(|f| IndexFile {
274                        path: f.path,
275                        size_bytes: f.size_bytes,
276                    })
277                    .collect(),
278            )
279        };
280
281        let metadata = Self {
282            uuid: proto.uuid.as_ref().map(Uuid::try_from).ok_or_else(|| {
283                Error::invalid_input("uuid field does not exist in Index metadata".to_string())
284            })??,
285            name: proto.name,
286            fields: proto.fields,
287            covering_fields: proto.covering_fields,
288            dataset_version: proto.dataset_version,
289            fragment_bitmap,
290            index_details: proto.index_details.map(Arc::new),
291            index_version: proto.index_version.unwrap_or_default(),
292            created_at: proto.created_at.map(|ts| {
293                DateTime::from_timestamp_millis(ts as i64)
294                    .expect("Invalid timestamp in index metadata")
295            }),
296            base_id: proto.base_id,
297            files,
298        };
299
300        // This is the single boundary between manifest bytes and
301        // `IndexMetadata`, so validating once here is what lets every reader
302        // treat the declaration as a trailing slice of `fields`. A manifest
303        // that fails this was written by something that did not follow the
304        // format contract; refuse it rather than let each use site quietly
305        // ignore the index.
306        metadata.validate_covering_fields()?;
307
308        Ok(metadata)
309    }
310}
311
312impl From<&IndexMetadata> for pb::IndexMetadata {
313    fn from(idx: &IndexMetadata) -> Self {
314        let mut fragment_bitmap = Vec::new();
315        if let Some(bitmap) = &idx.fragment_bitmap
316            && let Err(e) = bitmap.serialize_into(&mut fragment_bitmap)
317        {
318            // In theory, this should never error. But if we do, just
319            // recover gracefully.
320            log::error!("Failed to serialize fragment bitmap: {}", e);
321            fragment_bitmap.clear();
322        }
323
324        let files = idx
325            .files
326            .as_ref()
327            .map(|files| {
328                files
329                    .iter()
330                    .map(|f| pb::IndexFile {
331                        path: f.path.clone(),
332                        size_bytes: f.size_bytes,
333                    })
334                    .collect()
335            })
336            .unwrap_or_default();
337
338        Self {
339            uuid: Some((&idx.uuid).into()),
340            name: idx.name.clone(),
341            fields: idx.fields.clone(),
342            covering_fields: idx.covering_fields.clone(),
343            dataset_version: idx.dataset_version,
344            fragment_bitmap,
345            index_details: idx
346                .index_details
347                .as_ref()
348                .map(|details| details.as_ref().clone()),
349            index_version: Some(idx.index_version),
350            created_at: idx.created_at.map(|dt| dt.timestamp_millis() as u64),
351            base_id: idx.base_id,
352            files,
353        }
354    }
355}
356
357/// Returns a [`CacheCodec`](lance_core::cache::CacheCodec) for `Vec<IndexMetadata>`.
358///
359/// Uses `pb::IndexSection` (which wraps `repeated IndexMetadata`) as the wire
360/// format, reusing the existing `TryFrom`/`From` conversions.
361///
362/// Uses [`CacheCodec::new`](lance_core::cache::CacheCodec::new) because the
363/// orphan rule prevents `impl CacheCodecImpl for Vec<IndexMetadata>`.
364type ArcAny = Arc<dyn std::any::Any + Send + Sync>;
365
366/// Stable type identifier for the `Vec<IndexMetadata>` cache entry.
367const INDEX_METADATA_TYPE_ID: &str = "lance.table.IndexMetadataList";
368/// Body schema version written by this build.
369const INDEX_METADATA_VERSION: u32 = 1;
370
371fn serialize_index_metadata(
372    any: &ArcAny,
373    writer: &mut CacheEntryWriter<'_>,
374) -> lance_core::Result<()> {
375    let vec = any
376        .downcast_ref::<Vec<IndexMetadata>>()
377        .expect("index_metadata_codec: wrong type (this is a bug in the cache layer)");
378    let section = pb::IndexSection {
379        indices: vec.iter().map(pb::IndexMetadata::from).collect(),
380    };
381    writer.write_header(&section)
382}
383
384fn deserialize_index_metadata(reader: &mut CacheEntryReader<'_>) -> lance_core::Result<ArcAny> {
385    let section: pb::IndexSection = reader.read_header()?;
386    let indices: Vec<IndexMetadata> = section
387        .indices
388        .into_iter()
389        .map(IndexMetadata::try_from)
390        .collect::<lance_core::Result<_>>()?;
391    Ok(Arc::new(indices))
392}
393
394pub fn index_metadata_codec() -> lance_core::cache::CacheCodec {
395    lance_core::cache::CacheCodec::new(
396        INDEX_METADATA_TYPE_ID,
397        INDEX_METADATA_VERSION,
398        serialize_index_metadata,
399        deserialize_index_metadata,
400    )
401}
402
403/// List all files in an index directory with their sizes.
404///
405/// Returns a list of `IndexFile` structs containing relative paths and sizes.
406/// This is used to capture file metadata after index creation/modification.
407pub async fn list_index_files_with_sizes(
408    object_store: &ObjectStore,
409    index_dir: &Path,
410) -> Result<Vec<IndexFile>> {
411    let mut files = Vec::new();
412    let mut stream = object_store.read_dir_all(index_dir, None);
413    while let Some(meta) = stream.next().await {
414        let meta = meta?;
415        // Get relative path by stripping the index_dir prefix
416        let relative_path = meta
417            .location
418            .as_ref()
419            .strip_prefix(index_dir.as_ref())
420            .map(|s| s.trim_start_matches('/').to_string())
421            .unwrap_or_else(|| meta.location.filename().unwrap_or("").to_string());
422        files.push(IndexFile {
423            path: relative_path,
424            size_bytes: meta.size,
425        });
426    }
427    Ok(files)
428}
429
430#[cfg(test)]
431mod tests {
432    use super::*;
433    use rstest::rstest;
434    use std::collections::HashMap;
435
436    /// Demonstrates the pattern a disk-backed cache backend would use:
437    /// serialize entries to bytes, store in a key-value map, then
438    /// deserialize on retrieval.
439    #[test]
440    fn test_index_metadata_codec_roundtrip() {
441        let codec = index_metadata_codec();
442
443        let original = vec![
444            IndexMetadata {
445                uuid: Uuid::new_v4(),
446                name: "my_index".to_string(),
447                fields: vec![0, 1],
448                covering_fields: vec![],
449                dataset_version: 42,
450                fragment_bitmap: Some(RoaringBitmap::from_iter([1, 2, 3])),
451                index_details: None,
452                index_version: 1,
453                created_at: None,
454                base_id: None,
455                files: Some(vec![IndexFile {
456                    path: "index.idx".to_string(),
457                    size_bytes: 1024,
458                }]),
459            },
460            IndexMetadata {
461                uuid: Uuid::new_v4(),
462                name: "second_index".to_string(),
463                fields: vec![2],
464                covering_fields: vec![],
465                dataset_version: 43,
466                fragment_bitmap: None,
467                index_details: None,
468                index_version: 2,
469                created_at: None,
470                base_id: Some(7),
471                files: None,
472            },
473        ];
474
475        // Simulate a disk-backed store: HashMap<String, Vec<u8>>
476        let mut store: HashMap<String, Vec<u8>> = HashMap::new();
477
478        // Serialize into the store
479        let key = "dataset/v42/Vec<IndexMetadata>".to_string();
480        let mut buf = Vec::new();
481        let entry: Arc<dyn std::any::Any + Send + Sync> = Arc::new(original.clone());
482        codec.serialize(&entry, &mut buf).unwrap();
483        store.insert(key.clone(), buf);
484
485        // Deserialize from the store
486        let bytes = store.get(&key).unwrap();
487        let recovered = codec
488            .deserialize(&bytes::Bytes::copy_from_slice(bytes))
489            .hit()
490            .expect("entry should decode as a hit");
491        let recovered = recovered
492            .downcast::<Vec<IndexMetadata>>()
493            .expect("downcast should succeed");
494
495        assert_eq!(original.len(), recovered.len());
496        for (orig, rec) in original.iter().zip(recovered.iter()) {
497            assert_eq!(orig.uuid, rec.uuid);
498            assert_eq!(orig.name, rec.name);
499            assert_eq!(orig.fields, rec.fields);
500            assert_eq!(orig.dataset_version, rec.dataset_version);
501            assert_eq!(orig.fragment_bitmap, rec.fragment_bitmap);
502            assert_eq!(orig.index_version, rec.index_version);
503            assert_eq!(orig.base_id, rec.base_id);
504            assert_eq!(orig.files, rec.files);
505        }
506    }
507
508    /// The covering declaration must survive both conversion directions.
509    /// A dropped `covering_fields` would leave index files holding carried
510    /// columns the manifest no longer names.
511    #[test]
512    fn test_covering_fields_survives_proto_roundtrip() {
513        let original = IndexMetadata {
514            uuid: Uuid::new_v4(),
515            name: "covered".to_string(),
516            fields: vec![7, 11, 13],
517            covering_fields: vec![11, 13],
518            dataset_version: 1,
519            fragment_bitmap: None,
520            index_details: None,
521            index_version: 0,
522            created_at: None,
523            base_id: None,
524            files: None,
525        };
526
527        let proto = pb::IndexMetadata::from(&original);
528        assert_eq!(proto.covering_fields, vec![11, 13]);
529
530        let recovered = IndexMetadata::try_from(proto).unwrap();
531        assert_eq!(recovered, original);
532    }
533
534    /// `TryFrom<pb::IndexMetadata>` is the only path from manifest bytes to
535    /// `IndexMetadata`, so validating here is what lets every reader downstream
536    /// assume the declaration really is a trailing slice of `fields`. Without
537    /// it, a malformed declaration reaches each use site instead, where the
538    /// keyed count saturates to zero and the index is silently ignored.
539    #[test]
540    fn test_try_from_proto_rejects_a_malformed_covering_declaration() {
541        let mut proto = pb::IndexMetadata::from(&index_metadata_with(vec![7, 11], vec![11]));
542        // The leading entry, not the trailing one: claims the keyed column is
543        // carried.
544        proto.covering_fields = vec![7];
545
546        let err = IndexMetadata::try_from(proto)
547            .expect_err("a malformed covering declaration must not decode");
548        assert!(
549            matches!(err, Error::InvalidInput { .. }),
550            "expected InvalidInput, got {:?}",
551            err
552        );
553        assert!(
554            err.to_string().contains("must come last"),
555            "unexpected message: {err}"
556        );
557    }
558
559    /// Only the keyed prefix decides which column an index answers for, and
560    /// nearly every selection path needs exactly one such column. Metadata read
561    /// from a manifest this build never wrote can still be malformed, so both
562    /// accessors must fail closed -- no keyed field -- rather than underflow.
563    #[rstest]
564    #[case::not_covered(vec![7], vec![], vec![7], Some(7))]
565    #[case::covered(vec![7, 11], vec![11], vec![7], Some(7))]
566    #[case::covered_multi(vec![7, 11, 13], vec![11, 13], vec![7], Some(7))]
567    #[case::covered_composite(vec![7, 11, 13], vec![13], vec![7, 11], None)]
568    #[case::composite(vec![7, 11], vec![], vec![7, 11], None)]
569    #[case::system_index_no_fields(vec![], vec![], vec![], None)]
570    #[case::malformed_longer_than_fields(vec![7], vec![11, 13], vec![], None)]
571    fn test_keyed_fields(
572        #[case] fields: Vec<i32>,
573        #[case] covering_fields: Vec<i32>,
574        #[case] expected_keyed: Vec<i32>,
575        #[case] expected_single: Option<i32>,
576    ) {
577        let metadata = index_metadata_with(fields, covering_fields);
578
579        assert_eq!(metadata.keyed_fields(), expected_keyed.as_slice());
580        assert_eq!(metadata.keyed_field(), expected_single);
581    }
582
583    fn index_metadata_with(fields: Vec<i32>, covering_fields: Vec<i32>) -> IndexMetadata {
584        IndexMetadata {
585            uuid: Uuid::new_v4(),
586            name: "idx".to_string(),
587            fields,
588            covering_fields,
589            dataset_version: 1,
590            fragment_bitmap: None,
591            index_details: None,
592            index_version: 0,
593            created_at: None,
594            base_id: None,
595            files: None,
596        }
597    }
598
599    #[rstest]
600    #[case::empty_is_valid(vec![7], vec![], None)]
601    // mem_wal and frag_reuse both commit no fields at all; a bare
602    // `covering_fields.len() < fields.len()` check would reject them.
603    #[case::system_index_no_fields(vec![], vec![], None)]
604    #[case::valid_single_covered(vec![7, 11], vec![11], None)]
605    #[case::valid_suffix(vec![7, 11, 13], vec![11, 13], None)]
606    #[case::not_a_suffix(vec![7, 11, 13], vec![11], Some("must come last"))]
607    #[case::not_a_subset(vec![7, 11], vec![99], Some("are not among its fields"))]
608    #[case::wrong_order(vec![7, 11, 13], vec![13, 11], Some("must come last"))]
609    #[case::all_fields_covered(vec![7], vec![7], Some("at least one field must remain indexed"))]
610    #[case::covers_the_search_key(vec![7, 11], vec![7, 11], Some("at least one field must remain indexed"))]
611    // `fields` repeats the id in the same positions, so `[11, 11]` is a genuine
612    // tail of it -- only the duplicate check rejects this.
613    #[case::duplicate_covered(vec![7, 11, 11], vec![11, 11], Some("more than once"))]
614    // Both over-long and naming an unknown id: the unknown id is the cause, so
615    // it must be reported ahead of "at least one field must remain indexed".
616    #[case::unknown_id_reported_before_length(vec![7, 11], vec![99, 11], Some("are not among its fields"))]
617    fn test_validate_covering_fields(
618        #[case] fields: Vec<i32>,
619        #[case] covering_fields: Vec<i32>,
620        #[case] expected_error: Option<&str>,
621    ) {
622        let metadata = index_metadata_with(fields, covering_fields);
623        let result = metadata.validate_covering_fields();
624
625        match expected_error {
626            None => assert!(result.is_ok(), "expected valid, got {:?}", result),
627            Some(fragment) => {
628                let err = result.expect_err("expected a validation error");
629                assert!(
630                    matches!(err, Error::InvalidInput { .. }),
631                    "expected InvalidInput, got {:?}",
632                    err
633                );
634                let message = err.to_string();
635                assert!(
636                    message.contains(fragment),
637                    "expected message to contain {:?}, got {:?}",
638                    fragment,
639                    message
640                );
641            }
642        }
643    }
644}