1use 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#[derive(Debug, Clone, PartialEq, DeepSizeOf)]
23pub struct IndexFile {
24 pub path: String,
26 pub size_bytes: u64,
28}
29
30#[derive(Debug, Clone, PartialEq)]
32pub struct IndexMetadata {
33 pub uuid: Uuid,
35
36 pub fields: Vec<i32>,
41
42 pub covering_fields: Vec<i32>,
52
53 pub name: String,
55
56 pub dataset_version: u64,
61
62 pub fragment_bitmap: Option<RoaringBitmap>,
68
69 pub index_details: Option<Arc<prost_types::Any>>,
74
75 pub index_version: i32,
77
78 pub created_at: Option<DateTime<Utc>>,
83
84 pub base_id: Option<u32>,
87
88 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 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 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 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 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 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 pub fn keyed_field(&self) -> Option<i32> {
168 match self.keyed_fields() {
169 [only] => Some(*only),
170 _ => None,
171 }
172 }
173
174 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 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 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 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
357type ArcAny = Arc<dyn std::any::Any + Send + Sync>;
365
366const INDEX_METADATA_TYPE_ID: &str = "lance.table.IndexMetadataList";
368const 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(§ion)
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
403pub 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 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 #[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 let mut store: HashMap<String, Vec<u8>> = HashMap::new();
477
478 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 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 #[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 #[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 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 #[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 #[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 #[case::duplicate_covered(vec![7, 11, 11], vec![11, 11], Some("more than once"))]
614 #[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}