1use std::collections::HashMap;
5use std::num::NonZero;
6use std::sync::Arc;
7
8use lance_core::Error;
9use lance_core::deepsize::DeepSizeOf;
10use lance_file::version::ConcreteFileVersion;
11use lance_io::utils::CachedFileSize;
12use object_store::path::Path;
13use serde::{Deserialize, Deserializer, Serialize, Serializer};
14
15use super::overlay::{DataOverlayFile, TOMBSTONE_FIELD_ID, sort_overlays_newest_last};
16use super::row_ids::{
17 ROW_CREATED_AT_VERSION_FIELD_ID, ROW_ID_FIELD_ID, ROW_LAST_UPDATED_AT_VERSION_FIELD_ID,
18 RowIdMeta,
19};
20use crate::format::pb;
21
22use crate::rowids::version::{
23 RowDatasetVersionMeta, created_at_version_meta_to_pb, last_updated_at_version_meta_to_pb,
24};
25use lance_core::datatypes::Schema;
26use lance_core::error::Result;
27
28#[derive(Debug, Clone, PartialEq, Eq, DeepSizeOf)]
32pub struct DataFile {
33 pub path: String,
35 pub fields: Arc<[i32]>,
41 pub column_indices: Arc<[i32]>,
51 pub file_major_version: u32,
53 pub file_minor_version: u32,
55
56 pub file_size_bytes: CachedFileSize,
58
59 pub base_id: Option<u32>,
61}
62
63impl Serialize for DataFile {
65 fn serialize<S: Serializer>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error> {
66 use serde::ser::SerializeStruct;
67 let mut s = serializer.serialize_struct("DataFile", 7)?;
68 s.serialize_field("path", &self.path)?;
69 s.serialize_field("fields", self.fields.as_ref())?;
70 s.serialize_field("column_indices", self.column_indices.as_ref())?;
71 s.serialize_field("file_major_version", &self.file_major_version)?;
72 s.serialize_field("file_minor_version", &self.file_minor_version)?;
73 s.serialize_field("file_size_bytes", &self.file_size_bytes)?;
74 s.serialize_field("base_id", &self.base_id)?;
75 s.end()
76 }
77}
78
79impl<'de> Deserialize<'de> for DataFile {
81 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> std::result::Result<Self, D::Error> {
82 #[derive(Deserialize)]
83 struct DataFileHelper {
84 path: String,
85 fields: Vec<i32>,
86 #[serde(default)]
87 column_indices: Vec<i32>,
88 #[serde(default)]
89 file_major_version: u32,
90 #[serde(default)]
91 file_minor_version: u32,
92 file_size_bytes: CachedFileSize,
93 base_id: Option<u32>,
94 }
95
96 let helper = DataFileHelper::deserialize(deserializer)?;
97 Ok(Self {
98 path: helper.path,
99 fields: Arc::from(helper.fields),
100 column_indices: Arc::from(helper.column_indices),
101 file_major_version: helper.file_major_version,
102 file_minor_version: helper.file_minor_version,
103 file_size_bytes: helper.file_size_bytes,
104 base_id: helper.base_id,
105 })
106 }
107}
108
109impl DataFile {
110 pub fn new(
112 path: impl Into<String>,
113 fields: Vec<i32>,
114 column_indices: Vec<i32>,
115 file_version: ConcreteFileVersion,
116 file_size_bytes: Option<NonZero<u64>>,
117 base_id: Option<u32>,
118 ) -> Self {
119 let (file_major_version, file_minor_version) = file_version.to_data_file_numbers();
120 Self {
121 path: path.into(),
122 fields: Arc::from(fields),
123 column_indices: Arc::from(column_indices),
124 file_major_version,
125 file_minor_version,
126 file_size_bytes: file_size_bytes.into(),
127 base_id,
128 }
129 }
130
131 pub fn new_unstarted(path: impl Into<String>, file_version: ConcreteFileVersion) -> Self {
133 let (file_major_version, file_minor_version) = file_version.to_data_file_numbers();
134 Self {
135 path: path.into(),
136 fields: Arc::from([]),
137 column_indices: Arc::from([]),
138 file_major_version,
139 file_minor_version,
140 file_size_bytes: Default::default(),
141 base_id: None,
142 }
143 }
144
145 pub fn new_legacy_from_fields(
146 path: impl Into<String>,
147 fields: Vec<i32>,
148 base_id: Option<u32>,
149 ) -> Self {
150 Self::new(path, fields, vec![], ConcreteFileVersion::V1, None, base_id)
151 }
152
153 pub fn new_legacy(
154 path: impl Into<String>,
155 schema: &Schema,
156 file_size_bytes: Option<NonZero<u64>>,
157 base_id: Option<u32>,
158 ) -> Self {
159 let mut field_ids = schema.field_ids();
160 field_ids.sort();
161 Self::new(
162 path,
163 field_ids,
164 vec![],
165 ConcreteFileVersion::V1,
166 file_size_bytes,
167 base_id,
168 )
169 }
170
171 pub fn schema(&self, full_schema: &Schema) -> Schema {
172 fn project_field(
178 field: &lance_core::datatypes::Field,
179 mapped_columns: &HashMap<i32, i32>,
180 is_v2_0: bool,
181 ) -> Option<lance_core::datatypes::Field> {
182 if field.is_blob() || field.is_packed_struct() {
183 return mapped_columns
184 .contains_key(&field.id)
185 .then(|| field.clone());
186 }
187
188 let children = field
189 .children
190 .iter()
191 .filter_map(|child| project_field(child, mapped_columns, is_v2_0))
192 .collect::<Vec<_>>();
193 if !children.is_empty() {
194 return Some(lance_core::datatypes::Field {
195 children,
196 ..field.clone()
197 });
198 }
199
200 if field.children.is_empty() && mapped_columns.contains_key(&field.id) {
201 return Some(field.clone());
202 }
203
204 if is_v2_0 && mapped_columns.contains_key(&field.id) {
209 return Some(lance_core::datatypes::Field {
210 children: Vec::new(),
211 ..field.clone()
212 });
213 }
214 None
215 }
216
217 if self.uses_v1_data_file_encoding() {
218 return full_schema.project_by_ids(&self.fields, false);
219 }
220 let mapped_columns = self
221 .fields
222 .iter()
223 .copied()
224 .zip(self.column_indices.iter().copied())
225 .filter(|(_, column_index)| *column_index >= 0)
226 .collect::<HashMap<_, _>>();
227 let is_v2_0 = matches!(
228 (self.file_major_version, self.file_minor_version),
229 (0, 3) | (2, 0)
230 );
231 Schema {
232 fields: full_schema
233 .fields
234 .iter()
235 .filter_map(|field| project_field(field, &mapped_columns, is_v2_0))
236 .collect(),
237 metadata: full_schema.metadata.clone(),
238 }
239 }
240
241 fn uses_v1_data_file_encoding(&self) -> bool {
242 self.file_major_version == 0 && self.file_minor_version < 3
243 }
244
245 pub fn file_version(&self) -> Result<ConcreteFileVersion> {
247 ConcreteFileVersion::from_data_file_numbers(
248 self.file_major_version,
249 self.file_minor_version,
250 )
251 }
252
253 pub fn validate(&self, base_path: &Path) -> Result<()> {
254 if self.uses_v1_data_file_encoding() {
255 let live: Vec<i32> = self
259 .fields
260 .iter()
261 .copied()
262 .filter(|field| *field != TOMBSTONE_FIELD_ID)
263 .collect();
264 if !live.windows(2).all(|w| w[0] < w[1]) {
265 return Err(Error::corrupt_file(
266 base_path.clone().join(self.path.clone()),
267 "contained unsorted or duplicate field ids",
268 ));
269 }
270 } else if self.column_indices.len() < self.fields.len() {
271 return Err(Error::corrupt_file(
274 base_path.clone().join(self.path.clone()),
275 "contained fewer column_indices than fields",
276 ));
277 }
278 Ok(())
279 }
280}
281
282impl From<&DataFile> for pb::DataFile {
283 fn from(df: &DataFile) -> Self {
284 Self {
285 path: df.path.clone(),
286 fields: df.fields.to_vec(),
287 column_indices: df.column_indices.to_vec(),
288 file_major_version: df.file_major_version,
289 file_minor_version: df.file_minor_version,
290 file_size_bytes: df.file_size_bytes.get().map_or(0, |v| v.get()),
291 base_id: df.base_id,
292 }
293 }
294}
295
296impl TryFrom<pb::DataFile> for DataFile {
297 type Error = Error;
298
299 fn try_from(proto: pb::DataFile) -> Result<Self> {
300 Ok(Self {
301 path: proto.path,
302 fields: Arc::from(proto.fields),
303 column_indices: Arc::from(proto.column_indices),
304 file_major_version: proto.file_major_version,
305 file_minor_version: proto.file_minor_version,
306 file_size_bytes: CachedFileSize::new(proto.file_size_bytes),
307 base_id: proto.base_id,
308 })
309 }
310}
311
312#[derive(Default)]
323pub struct DataFileFieldInterner {
324 fields: InternCache<i32>,
325 column_indices: InternCache<i32>,
326 inline_bytes: InternCache<u8>,
327}
328
329enum InternCache<T: Eq + std::hash::Hash + Clone> {
333 Small(Vec<Arc<[T]>>),
334 Large(HashMap<Arc<[T]>, ()>),
335}
336
337const INTERN_CACHE_UPGRADE_THRESHOLD: usize = 16;
338
339impl<T: Eq + std::hash::Hash + Clone> Default for InternCache<T> {
340 fn default() -> Self {
341 Self::Small(Vec::new())
342 }
343}
344
345impl<T: Eq + std::hash::Hash + Clone> InternCache<T> {
346 fn intern(&mut self, v: Vec<T>) -> Arc<[T]> {
347 match self {
348 Self::Small(entries) => {
349 for existing in entries.iter() {
350 if existing.as_ref() == v.as_slice() {
351 return existing.clone();
352 }
353 }
354 let arc: Arc<[T]> = Arc::from(v);
355 entries.push(arc.clone());
356 if entries.len() > INTERN_CACHE_UPGRADE_THRESHOLD {
357 let mut map = HashMap::with_capacity(entries.len());
358 for e in entries.drain(..) {
359 map.insert(e, ());
360 }
361 *self = Self::Large(map);
362 }
363 arc
364 }
365 Self::Large(map) => {
366 if let Some((existing, _)) = map.get_key_value(v.as_slice()) {
367 existing.clone()
368 } else {
369 let arc: Arc<[T]> = Arc::from(v);
370 map.insert(arc.clone(), ());
371 arc
372 }
373 }
374 }
375 }
376}
377
378impl DataFileFieldInterner {
379 fn intern_last_updated_version_meta(
383 cache: &mut InternCache<u8>,
384 pb: pb::data_fragment::LastUpdatedAtVersionSequence,
385 ) -> Result<RowDatasetVersionMeta> {
386 match pb {
387 pb::data_fragment::LastUpdatedAtVersionSequence::InlineLastUpdatedAtVersions(data) => {
388 Ok(RowDatasetVersionMeta::Inline(cache.intern(data)))
389 }
390 pb::data_fragment::LastUpdatedAtVersionSequence::ColumnLastUpdatedAtVersions(_) => {
391 Ok(RowDatasetVersionMeta::Column)
392 }
393 }
394 }
395
396 fn intern_created_version_meta(
398 cache: &mut InternCache<u8>,
399 pb: pb::data_fragment::CreatedAtVersionSequence,
400 ) -> Result<RowDatasetVersionMeta> {
401 match pb {
402 pb::data_fragment::CreatedAtVersionSequence::InlineCreatedAtVersions(data) => {
403 Ok(RowDatasetVersionMeta::Inline(cache.intern(data)))
404 }
405 pb::data_fragment::CreatedAtVersionSequence::ColumnCreatedAtVersions(_) => {
406 Ok(RowDatasetVersionMeta::Column)
407 }
408 }
409 }
410
411 pub fn intern_data_file(&mut self, proto: pb::DataFile) -> Result<DataFile> {
413 Ok(DataFile {
414 path: proto.path,
415 fields: self.fields.intern(proto.fields),
416 column_indices: self.column_indices.intern(proto.column_indices),
417 file_major_version: proto.file_major_version,
418 file_minor_version: proto.file_minor_version,
419 file_size_bytes: CachedFileSize::new(proto.file_size_bytes),
420 base_id: proto.base_id,
421 })
422 }
423
424 pub fn intern_fragment(&mut self, p: pb::DataFragment) -> Result<Fragment> {
426 let physical_rows = if p.physical_rows > 0 {
427 Some(p.physical_rows as usize)
428 } else {
429 None
430 };
431 let last_updated_at_version_meta = p
432 .last_updated_at_version_sequence
433 .map(|pb| Self::intern_last_updated_version_meta(&mut self.inline_bytes, pb))
434 .transpose()?;
435 let created_at_version_meta = p
436 .created_at_version_sequence
437 .map(|pb| Self::intern_created_version_meta(&mut self.inline_bytes, pb))
438 .transpose()?;
439 Ok(Fragment {
440 id: p.id,
441 files: p
442 .files
443 .into_iter()
444 .map(|f| self.intern_data_file(f))
445 .collect::<Result<_>>()?,
446 overlays: {
447 let mut overlays = p
448 .overlays
449 .into_iter()
450 .map(DataOverlayFile::try_from)
451 .collect::<Result<Vec<_>>>()?;
452 sort_overlays_newest_last(&mut overlays);
453 overlays
454 },
455 deletion_file: p.deletion_file.map(DeletionFile::try_from).transpose()?,
456 row_id_meta: p.row_id_sequence.map(RowIdMeta::try_from).transpose()?,
457 physical_rows,
458 last_updated_at_version_meta,
459 created_at_version_meta,
460 })
461 }
462}
463
464#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, DeepSizeOf)]
465#[serde(rename_all = "lowercase")]
466pub enum DeletionFileType {
467 Array,
468 Bitmap,
469}
470
471impl DeletionFileType {
472 pub fn suffix(&self) -> &str {
474 match self {
475 Self::Array => "arrow",
476 Self::Bitmap => "bin",
477 }
478 }
479}
480
481#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, DeepSizeOf)]
482pub struct DeletionFile {
483 pub read_version: u64,
484 pub id: u64,
485 pub file_type: DeletionFileType,
486 pub num_deleted_rows: Option<usize>,
488 pub base_id: Option<u32>,
489}
490
491impl TryFrom<pb::DeletionFile> for DeletionFile {
492 type Error = Error;
493
494 fn try_from(value: pb::DeletionFile) -> Result<Self> {
495 let file_type = match value.file_type {
496 0 => DeletionFileType::Array,
497 1 => DeletionFileType::Bitmap,
498 _ => {
499 return Err(Error::not_supported_source(
500 "Unknown deletion file type".into(),
501 ));
502 }
503 };
504 let num_deleted_rows = if value.num_deleted_rows == 0 {
505 None
506 } else {
507 Some(value.num_deleted_rows as usize)
508 };
509 Ok(Self {
510 read_version: value.read_version,
511 id: value.id,
512 file_type,
513 num_deleted_rows,
514 base_id: value.base_id,
515 })
516 }
517}
518
519#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, DeepSizeOf)]
524pub struct Fragment {
525 pub id: u64,
527
528 pub files: Vec<DataFile>,
530
531 #[serde(default, skip_serializing_if = "Vec::is_empty")]
535 pub overlays: Vec<DataOverlayFile>,
536
537 #[serde(skip_serializing_if = "Option::is_none")]
539 pub deletion_file: Option<DeletionFile>,
540
541 #[serde(skip_serializing_if = "Option::is_none")]
543 pub row_id_meta: Option<RowIdMeta>,
544
545 pub physical_rows: Option<usize>,
549
550 #[serde(skip_serializing_if = "Option::is_none")]
552 pub last_updated_at_version_meta: Option<RowDatasetVersionMeta>,
553
554 #[serde(skip_serializing_if = "Option::is_none")]
556 pub created_at_version_meta: Option<RowDatasetVersionMeta>,
557}
558
559impl Fragment {
560 pub fn new(id: u64) -> Self {
561 Self {
562 id,
563 files: vec![],
564 overlays: vec![],
565 deletion_file: None,
566 row_id_meta: None,
567 physical_rows: None,
568 last_updated_at_version_meta: None,
569 created_at_version_meta: None,
570 }
571 }
572
573 pub fn num_rows(&self) -> Option<usize> {
574 match (self.physical_rows, &self.deletion_file) {
575 (Some(len), None) => Some(len),
577 (
579 Some(len),
580 Some(DeletionFile {
581 num_deleted_rows: Some(num_deleted_rows),
582 ..
583 }),
584 ) => Some(len - num_deleted_rows),
585 _ => None,
586 }
587 }
588
589 pub fn referenced_lance_files(&self) -> impl Iterator<Item = &DataFile> + '_ {
597 let Self {
600 id: _,
601 files,
602 overlays,
603 deletion_file: _,
604 row_id_meta: _,
605 physical_rows: _,
606 last_updated_at_version_meta: _,
607 created_at_version_meta: _,
608 } = self;
609 files
610 .iter()
611 .chain(overlays.iter().map(|overlay| &overlay.data_file))
612 }
613
614 pub fn referenced_lance_files_mut(&mut self) -> impl Iterator<Item = &mut DataFile> + '_ {
618 let Self {
621 id: _,
622 files,
623 overlays,
624 deletion_file: _,
625 row_id_meta: _,
626 physical_rows: _,
627 last_updated_at_version_meta: _,
628 created_at_version_meta: _,
629 } = self;
630 files
631 .iter_mut()
632 .chain(overlays.iter_mut().map(|overlay| &mut overlay.data_file))
633 }
634
635 pub fn has_spilled_row_lineage(&self) -> bool {
638 matches!(self.row_id_meta, Some(RowIdMeta::Column))
639 || matches!(
640 self.created_at_version_meta,
641 Some(RowDatasetVersionMeta::Column)
642 )
643 || matches!(
644 self.last_updated_at_version_meta,
645 Some(RowDatasetVersionMeta::Column)
646 )
647 }
648
649 pub fn row_lineage_file(&self, field_id: i32) -> Result<Option<&DataFile>> {
655 let mut carriers = self
656 .files
657 .iter()
658 .filter(|file| file.fields.contains(&field_id));
659 let file = carriers.next();
660 if let Some(extra) = carriers.next() {
661 return Err(Error::corrupt_file_named(
662 &extra.path,
663 format!(
664 "fragment {} has more than one data file carrying row lineage field {}",
665 self.id, field_id
666 ),
667 ));
668 }
669 Ok(file)
670 }
671
672 pub fn spilled_row_lineage_field_ids(&self) -> Vec<i32> {
681 let mut field_ids = Vec::new();
682 if matches!(self.row_id_meta, Some(RowIdMeta::Column)) {
683 field_ids.push(ROW_ID_FIELD_ID);
684 }
685 if matches!(
686 self.created_at_version_meta,
687 Some(RowDatasetVersionMeta::Column)
688 ) {
689 field_ids.push(ROW_CREATED_AT_VERSION_FIELD_ID);
690 }
691 if matches!(
692 self.last_updated_at_version_meta,
693 Some(RowDatasetVersionMeta::Column)
694 ) {
695 field_ids.push(ROW_LAST_UPDATED_AT_VERSION_FIELD_ID);
696 }
697 field_ids
698 }
699
700 pub(crate) fn validate_row_lineage_carriers(&self) -> Result<()> {
708 for field_id in self.spilled_row_lineage_field_ids() {
709 let Some(file) = self.row_lineage_file(field_id)? else {
710 return Err(Error::internal(format!(
711 "cannot commit fragment {}: it marks row lineage field {} as spilled but \
712 none of its data files carries it",
713 self.id, field_id
714 )));
715 };
716 if file.file_version()? == ConcreteFileVersion::V1 {
717 return Err(Error::internal(format!(
718 "cannot commit fragment {}: its spilled row lineage field {} is carried by \
719 legacy v1 data file {}, which cannot hold row lineage columns",
720 self.id, field_id, file.path
721 )));
722 }
723 }
724 Ok(())
725 }
726
727 pub fn from_json(json: &str) -> Result<Self> {
728 let fragment: Self = serde_json::from_str(json)?;
729 Ok(fragment)
730 }
731
732 pub fn with_file_legacy(
734 id: u64,
735 path: &str,
736 schema: &Schema,
737 physical_rows: Option<usize>,
738 ) -> Self {
739 Self {
740 id,
741 files: vec![DataFile::new_legacy(path, schema, None, None)],
742 overlays: vec![],
743 deletion_file: None,
744 physical_rows,
745 row_id_meta: None,
746 last_updated_at_version_meta: None,
747 created_at_version_meta: None,
748 }
749 }
750
751 pub fn with_file(
752 mut self,
753 path: impl Into<String>,
754 field_ids: Vec<i32>,
755 column_indices: Vec<i32>,
756 version: ConcreteFileVersion,
757 file_size_bytes: Option<NonZero<u64>>,
758 ) -> Self {
759 let data_file = DataFile::new(
760 path,
761 field_ids,
762 column_indices,
763 version,
764 file_size_bytes,
765 None,
766 );
767 self.files.push(data_file);
768 self
769 }
770
771 pub fn with_physical_rows(mut self, physical_rows: usize) -> Self {
772 self.physical_rows = Some(physical_rows);
773 self
774 }
775
776 pub fn add_file(
777 &mut self,
778 path: impl Into<String>,
779 field_ids: Vec<i32>,
780 column_indices: Vec<i32>,
781 version: ConcreteFileVersion,
782 file_size_bytes: Option<NonZero<u64>>,
783 ) {
784 self.files.push(DataFile::new(
785 path,
786 field_ids,
787 column_indices,
788 version,
789 file_size_bytes,
790 None,
791 ));
792 }
793
794 pub fn add_file_legacy(&mut self, path: &str, schema: &Schema) {
796 self.files
797 .push(DataFile::new_legacy(path, schema, None, None));
798 }
799
800 pub fn try_infer_version(fragments: &[Self]) -> Result<Option<ConcreteFileVersion>> {
805 let Some(sample_file) = fragments
808 .iter()
809 .flat_map(Self::referenced_lance_files)
810 .next()
811 else {
812 return Ok(None);
813 };
814 let file_version = sample_file.file_version()?;
815 for frag in fragments {
817 for file in frag.referenced_lance_files() {
818 let this_file_version = file.file_version()?;
819 if file_version != this_file_version {
820 return Err(Error::invalid_input(format!(
821 "All data files must have the same version. Detected both {} and {}",
822 file_version, this_file_version
823 )));
824 }
825 }
826 }
827 Ok(Some(file_version))
828 }
829}
830
831impl TryFrom<pb::DataFragment> for Fragment {
832 type Error = Error;
833
834 fn try_from(p: pb::DataFragment) -> Result<Self> {
835 let physical_rows = if p.physical_rows > 0 {
836 Some(p.physical_rows as usize)
837 } else {
838 None
839 };
840 Ok(Self {
841 id: p.id,
842 files: p
843 .files
844 .into_iter()
845 .map(DataFile::try_from)
846 .collect::<Result<_>>()?,
847 overlays: {
848 let mut overlays = p
849 .overlays
850 .into_iter()
851 .map(DataOverlayFile::try_from)
852 .collect::<Result<Vec<_>>>()?;
853 sort_overlays_newest_last(&mut overlays);
854 overlays
855 },
856 deletion_file: p.deletion_file.map(DeletionFile::try_from).transpose()?,
857 row_id_meta: p.row_id_sequence.map(RowIdMeta::try_from).transpose()?,
858 physical_rows,
859 last_updated_at_version_meta: p
860 .last_updated_at_version_sequence
861 .map(RowDatasetVersionMeta::try_from)
862 .transpose()?,
863 created_at_version_meta: p
864 .created_at_version_sequence
865 .map(RowDatasetVersionMeta::try_from)
866 .transpose()?,
867 })
868 }
869}
870
871impl From<&Fragment> for pb::DataFragment {
872 fn from(f: &Fragment) -> Self {
873 let deletion_file = f.deletion_file.as_ref().map(|f| {
874 let file_type = match f.file_type {
875 DeletionFileType::Array => pb::deletion_file::DeletionFileType::ArrowArray,
876 DeletionFileType::Bitmap => pb::deletion_file::DeletionFileType::Bitmap,
877 };
878 pb::DeletionFile {
879 read_version: f.read_version,
880 id: f.id,
881 file_type: file_type.into(),
882 num_deleted_rows: f.num_deleted_rows.unwrap_or_default() as u64,
883 base_id: f.base_id,
884 }
885 });
886
887 let row_id_sequence = f.row_id_meta.as_ref().map(|m| match m {
888 RowIdMeta::Inline(data) => {
889 pb::data_fragment::RowIdSequence::InlineRowIds(data.bytes().clone())
890 }
891 RowIdMeta::Column => {
892 pb::data_fragment::RowIdSequence::ColumnRowIds(pb::RowLineageColumn {})
893 }
894 });
895 let last_updated_at_version_sequence =
896 last_updated_at_version_meta_to_pb(&f.last_updated_at_version_meta);
897 let created_at_version_sequence = created_at_version_meta_to_pb(&f.created_at_version_meta);
898 Self {
899 id: f.id,
900 files: f.files.iter().map(pb::DataFile::from).collect(),
901 overlays: f.overlays.iter().map(pb::DataOverlayFile::from).collect(),
902 deletion_file,
903 row_id_sequence,
904 physical_rows: f.physical_rows.unwrap_or_default() as u64,
905 last_updated_at_version_sequence,
906 created_at_version_sequence,
907 }
908 }
909}
910
911#[cfg(test)]
912mod tests {
913 use super::*;
914 use crate::format::overlay::OverlayCoverage;
915 use arrow_schema::{
916 DataType, Field as ArrowField, Fields as ArrowFields, Schema as ArrowSchema,
917 };
918 use lance_file::format::{MAJOR_VERSION, MINOR_VERSION};
919 use object_store::path::Path;
920 use roaring::RoaringBitmap;
921 use serde_json::{Value, json};
922
923 #[test]
924 fn test_data_overlay_roundtrip() {
925 let mut bitmap = RoaringBitmap::new();
928 bitmap.insert(1);
929 bitmap.insert(3);
930
931 let overlay = DataOverlayFile {
932 data_file: DataFile::new_legacy_from_fields("overlay-0.lance", vec![3], None),
933 coverage: OverlayCoverage::dense(bitmap.clone()),
934 committed_version: 7,
935 };
936 let mut fragment = Fragment::new(0);
937 fragment.files = vec![DataFile::new_legacy_from_fields(
938 "base.lance",
939 vec![1, 3],
940 None,
941 )];
942 fragment.overlays = vec![overlay];
943
944 let proto = pb::DataFragment::from(&fragment);
945 assert_eq!(proto.overlays.len(), 1);
946 let round_tripped = Fragment::try_from(proto).unwrap();
947 assert_eq!(round_tripped, fragment);
948
949 let recovered = round_tripped.overlays[0].coverage_for_field(0).unwrap();
951 assert_eq!(*recovered, bitmap);
952 assert_eq!(
953 *round_tripped.overlays[0].coverage_for_field(5).unwrap(),
954 bitmap
955 );
956 }
957
958 #[test]
959 fn test_data_overlay_sparse_per_field_coverage() {
960 let name_coverage = RoaringBitmap::from_iter([2u32, 3]);
962 let embedding_coverage = RoaringBitmap::from_iter([1u32]);
963 let overlay = DataOverlayFile {
964 data_file: DataFile::new_legacy_from_fields("overlay-1.lance", vec![2, 4], None),
965 coverage: OverlayCoverage::sparse(vec![
966 name_coverage.clone(),
967 embedding_coverage.clone(),
968 ]),
969 committed_version: 3,
970 };
971 let mut fragment = Fragment::new(1);
972 fragment.overlays = vec![overlay];
973
974 let round_tripped = Fragment::try_from(pb::DataFragment::from(&fragment)).unwrap();
975 assert_eq!(
976 *round_tripped.overlays[0].coverage_for_field(0).unwrap(),
977 name_coverage
978 );
979 assert_eq!(
980 *round_tripped.overlays[0].coverage_for_field(1).unwrap(),
981 embedding_coverage
982 );
983 }
984
985 #[test]
986 fn test_overlays_sorted_newest_last_on_load() {
987 let mk = |version: u64, field: i32| DataOverlayFile {
990 data_file: DataFile::new_legacy_from_fields("o.lance", vec![field], None),
991 coverage: OverlayCoverage::dense(RoaringBitmap::from_iter([0u32])),
992 committed_version: version,
993 };
994 let mut fragment = Fragment::new(0);
995 fragment.overlays = vec![mk(5, 1), mk(2, 2), mk(2, 3), mk(3, 4)];
997
998 let loaded = Fragment::try_from(pb::DataFragment::from(&fragment)).unwrap();
999 let versions: Vec<u64> = loaded
1000 .overlays
1001 .iter()
1002 .map(|o| o.committed_version)
1003 .collect();
1004 assert_eq!(versions, vec![2, 2, 3, 5]);
1005 assert_eq!(
1008 loaded.overlays[0].data_file.fields.as_ref(),
1009 [2i32].as_slice()
1010 );
1011 assert_eq!(
1012 loaded.overlays[1].data_file.fields.as_ref(),
1013 [3i32].as_slice()
1014 );
1015 }
1016
1017 #[test]
1018 fn test_new_fragment() {
1019 let path = "foobar.lance";
1020
1021 let arrow_schema = ArrowSchema::new(vec![
1022 ArrowField::new(
1023 "s",
1024 DataType::Struct(ArrowFields::from(vec![
1025 ArrowField::new("si", DataType::Int32, false),
1026 ArrowField::new("sb", DataType::Binary, true),
1027 ])),
1028 true,
1029 ),
1030 ArrowField::new("bool", DataType::Boolean, true),
1031 ]);
1032 let schema = Schema::try_from(&arrow_schema).unwrap();
1033 let fragment = Fragment::with_file_legacy(123, path, &schema, Some(10));
1034
1035 assert_eq!(123, fragment.id);
1036 assert_eq!(
1037 fragment.files,
1038 vec![DataFile::new_legacy_from_fields(
1039 path.to_string(),
1040 vec![0, 1, 2, 3],
1041 None,
1042 )]
1043 )
1044 }
1045
1046 #[test]
1047 fn test_roundtrip_fragment() {
1048 let mut fragment = Fragment::new(123);
1049 let schema = ArrowSchema::new(vec![ArrowField::new("x", DataType::Float16, true)]);
1050 fragment.add_file_legacy("foobar.lance", &Schema::try_from(&schema).unwrap());
1051 fragment.deletion_file = Some(DeletionFile {
1052 read_version: 123,
1053 id: 456,
1054 file_type: DeletionFileType::Array,
1055 num_deleted_rows: Some(10),
1056 base_id: None,
1057 });
1058
1059 let proto = pb::DataFragment::from(&fragment);
1060 let fragment2 = Fragment::try_from(proto).unwrap();
1061 assert_eq!(fragment, fragment2);
1062
1063 fragment.deletion_file = None;
1064 let proto = pb::DataFragment::from(&fragment);
1065 let fragment2 = Fragment::try_from(proto).unwrap();
1066 assert_eq!(fragment, fragment2);
1067 }
1068
1069 #[test]
1070 fn infer_exact_file_version_and_reject_mixed_fragments() {
1071 assert_eq!(Fragment::try_infer_version(&[]).unwrap(), None);
1072
1073 let v2_0 = Fragment::new(0).with_file(
1074 "v2_0.lance",
1075 vec![0],
1076 vec![0],
1077 ConcreteFileVersion::V2_0,
1078 None,
1079 );
1080 assert_eq!(
1081 (
1082 v2_0.files[0].file_major_version,
1083 v2_0.files[0].file_minor_version
1084 ),
1085 (2, 0)
1086 );
1087 let unstarted = DataFile::new_unstarted("unstarted.lance", ConcreteFileVersion::V2_0);
1088 assert_eq!(
1089 (unstarted.file_major_version, unstarted.file_minor_version),
1090 (2, 0)
1091 );
1092 let v2_0_second = Fragment::new(1).with_file(
1093 "v2_0_second.lance",
1094 vec![0],
1095 vec![0],
1096 ConcreteFileVersion::V2_0,
1097 None,
1098 );
1099 assert_eq!(
1100 Fragment::try_infer_version(&[v2_0.clone(), v2_0_second]).unwrap(),
1101 Some(ConcreteFileVersion::V2_0)
1102 );
1103
1104 let mut mixed_overlay = v2_0.clone();
1105 mixed_overlay.overlays.push(DataOverlayFile {
1106 data_file: DataFile::new(
1107 "overlay-v2_1.lance",
1108 vec![0],
1109 vec![0],
1110 ConcreteFileVersion::V2_1,
1111 None,
1112 None,
1113 ),
1114 coverage: OverlayCoverage::dense(RoaringBitmap::from_iter([0])),
1115 committed_version: 1,
1116 });
1117 let error = Fragment::try_infer_version(&[mixed_overlay]).unwrap_err();
1118 assert!(error.to_string().contains("2.0"));
1119 assert!(error.to_string().contains("2.1"));
1120
1121 let v2_1 = Fragment::new(2).with_file(
1122 "v2_1.lance",
1123 vec![0],
1124 vec![0],
1125 ConcreteFileVersion::V2_1,
1126 None,
1127 );
1128 let error = Fragment::try_infer_version(&[v2_0, v2_1]).unwrap_err();
1129 assert!(matches!(error, Error::InvalidInput { .. }));
1130 let message = error.to_string();
1131 assert!(message.contains("All data files must have the same version"));
1132 assert!(message.contains("2.0"));
1133 assert!(message.contains("2.1"));
1134 }
1135
1136 #[test]
1137 fn data_file_schema_does_not_substitute_evolved_struct_children() {
1138 let mut schema = Schema::try_from(&ArrowSchema::new(vec![
1139 ArrowField::new(
1140 "values",
1141 DataType::Struct(ArrowFields::from(vec![ArrowField::new(
1142 "later",
1143 DataType::Int32,
1144 true,
1145 )])),
1146 true,
1147 ),
1148 ArrowField::new("id", DataType::Int32, false),
1149 ]))
1150 .unwrap();
1151 schema.set_field_id(None);
1152 let parent_id = schema.fields[0].id;
1153 let later_id = schema.fields[0].children[0].id;
1154 let id_field_id = schema.fields[1].id;
1155 schema.fields[0].children[0].id = later_id + 2;
1156
1157 let stale_file = DataFile::new(
1158 "stale.lance",
1159 vec![parent_id, later_id, id_field_id],
1160 vec![0, 1, 2],
1161 ConcreteFileVersion::V2_0,
1162 None,
1163 None,
1164 );
1165 let stale_schema = stale_file.schema(&schema);
1166 assert_eq!(
1167 stale_schema
1168 .fields
1169 .iter()
1170 .map(|field| field.name.as_str())
1171 .collect::<Vec<_>>(),
1172 vec!["values", "id"]
1173 );
1174 assert!(stale_schema.fields[0].children.is_empty());
1175
1176 let stale_v2_1_file = DataFile::new(
1177 "stale-v2-1.lance",
1178 vec![parent_id, later_id, id_field_id],
1179 vec![-1, 0, 1],
1180 ConcreteFileVersion::V2_1,
1181 None,
1182 None,
1183 );
1184 assert_eq!(
1185 stale_v2_1_file.schema(&schema).fields,
1186 vec![schema.fields[1].clone()]
1187 );
1188
1189 let current_file = DataFile::new(
1190 "current.lance",
1191 vec![parent_id, later_id + 2, id_field_id],
1192 vec![0, 1, 2],
1193 ConcreteFileVersion::V2_0,
1194 None,
1195 None,
1196 );
1197 assert_eq!(current_file.schema(&schema), schema);
1198 }
1199
1200 #[test]
1201 fn test_to_json() {
1202 let mut fragment = Fragment::new(123);
1203 let schema = ArrowSchema::new(vec![ArrowField::new("x", DataType::Float16, true)]);
1204 fragment.add_file_legacy("foobar.lance", &Schema::try_from(&schema).unwrap());
1205 fragment.deletion_file = Some(DeletionFile {
1206 read_version: 123,
1207 id: 456,
1208 file_type: DeletionFileType::Array,
1209 num_deleted_rows: Some(10),
1210 base_id: None,
1211 });
1212
1213 let json = serde_json::to_string(&fragment).unwrap();
1214
1215 let value: Value = serde_json::from_str(&json).unwrap();
1216 assert_eq!(
1217 value,
1218 json!({
1219 "id": 123,
1220 "files":[
1221 {"path": "foobar.lance", "fields": [0], "column_indices": [],
1222 "file_major_version": MAJOR_VERSION, "file_minor_version": MINOR_VERSION,
1223 "file_size_bytes": null, "base_id": null }
1224 ],
1225 "deletion_file": {"read_version": 123, "id": 456, "file_type": "array",
1226 "num_deleted_rows": 10, "base_id": null},
1227 "physical_rows": None::<usize>}),
1228 );
1229
1230 let frag2 = Fragment::from_json(&json).unwrap();
1231 assert_eq!(fragment, frag2);
1232 }
1233
1234 #[test]
1235 fn data_file_validate_allows_extra_columns() {
1236 let data_file = DataFile {
1237 path: "foo.lance".to_string(),
1238 fields: Arc::from([1, 2]),
1239 column_indices: Arc::from([0, 1, 2]),
1241 file_major_version: MAJOR_VERSION as u32,
1242 file_minor_version: MINOR_VERSION as u32,
1243 file_size_bytes: Default::default(),
1244 base_id: None,
1245 };
1246
1247 let base_path = Path::from("base");
1248 data_file
1249 .validate(&base_path)
1250 .expect("validation should allow extra columns without field ids");
1251 }
1252}