Skip to main content

lance_file/
datatypes.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright The Lance Authors
3
4use lance_arrow::ARROW_EXT_NAME_KEY;
5use lance_core::datatypes::{Dictionary, Encoding, Field, LogicalType, Schema};
6use lance_core::{Error, Result};
7use std::collections::HashMap;
8
9use crate::format::pb;
10
11#[allow(clippy::fallible_impl_from)]
12impl From<&pb::Field> for Field {
13    fn from(field: &pb::Field) -> Self {
14        let lance_metadata: HashMap<String, String> = field
15            .metadata
16            .iter()
17            .map(|(key, value)| {
18                let string_value = String::from_utf8_lossy(value).to_string();
19                (key.clone(), string_value)
20            })
21            .collect();
22        let mut lance_metadata = lance_metadata;
23        if !field.extension_name.is_empty() {
24            lance_metadata.insert(ARROW_EXT_NAME_KEY.to_string(), field.extension_name.clone());
25        }
26        Self {
27            name: field.name.clone(),
28            id: field.id,
29            parent_id: field.parent_id,
30            logical_type: LogicalType::from(field.logical_type.as_str()),
31            metadata: lance_metadata,
32            encoding: match field.encoding {
33                1 => Some(Encoding::Plain),
34                2 => Some(Encoding::VarBinary),
35                3 => Some(Encoding::Dictionary),
36                4 => Some(Encoding::RLE),
37                _ => None,
38            },
39            nullable: field.nullable,
40            children: vec![],
41            dictionary: field.dictionary.as_ref().map(Dictionary::from),
42            unenforced_primary_key_position: if field.unenforced_primary_key_position > 0 {
43                Some(field.unenforced_primary_key_position)
44            } else if field.unenforced_primary_key {
45                Some(0)
46            } else {
47                None
48            },
49            unenforced_clustering_key_position: if field.unenforced_clustering_key_position > 0 {
50                Some(field.unenforced_clustering_key_position)
51            } else {
52                None
53            },
54        }
55    }
56}
57
58impl From<&Field> for pb::Field {
59    fn from(field: &Field) -> Self {
60        let pb_metadata = field
61            .metadata
62            .iter()
63            .map(|(key, value)| (key.clone(), value.clone().into_bytes()))
64            .collect();
65        Self {
66            id: field.id,
67            parent_id: field.parent_id,
68            name: field.name.clone(),
69            logical_type: field.logical_type.to_string(),
70            encoding: match field.encoding {
71                Some(Encoding::Plain) => 1,
72                Some(Encoding::VarBinary) => 2,
73                Some(Encoding::Dictionary) => 3,
74                Some(Encoding::RLE) => 4,
75                _ => 0,
76            },
77            nullable: field.nullable,
78            dictionary: field.dictionary.as_ref().map(pb::Dictionary::from),
79            metadata: pb_metadata,
80            extension_name: field
81                .extension_name()
82                .map(|name| name.to_owned())
83                .unwrap_or_default(),
84            r#type: 0,
85            unenforced_primary_key: field.unenforced_primary_key_position.is_some(),
86            unenforced_primary_key_position: field.unenforced_primary_key_position.unwrap_or(0),
87            unenforced_clustering_key: false,
88            unenforced_clustering_key_position: field
89                .unenforced_clustering_key_position
90                .unwrap_or(0),
91        }
92    }
93}
94
95pub struct Fields(pub Vec<pb::Field>);
96
97struct FieldNode {
98    field: Field,
99    child_indices: Vec<usize>,
100}
101
102/// Searches in pre-order depth-first order and returns the first matching node,
103/// preserving the legacy parent tie-break for duplicate field IDs.
104fn first_field_index_by_id(
105    nodes: &[FieldNode],
106    root_indices: &[usize],
107    field_id: i32,
108) -> Option<usize> {
109    let mut to_visit = Vec::with_capacity(nodes.len());
110    to_visit.extend(root_indices.iter().rev().copied());
111
112    while let Some(node_index) = to_visit.pop() {
113        let node = &nodes[node_index];
114        if node.field.id == field_id {
115            return Some(node_index);
116        }
117        to_visit.extend(node.child_indices.iter().rev().copied());
118    }
119
120    None
121}
122
123impl From<&Field> for Fields {
124    fn from(field: &Field) -> Self {
125        let mut protos = vec![pb::Field::from(field)];
126        protos.extend(field.children.iter().flat_map(|val| Self::from(val).0));
127        Self(protos)
128    }
129}
130
131/// Reconstruct a schema from a flat, pre-order protobuf field list.
132///
133/// Parent fields must appear before their children. Historical manifests may
134/// contain duplicate field IDs, so an ID may not identify a unique parent. For
135/// those references, reconstruction preserves the legacy
136/// [`Schema::mut_field_by_id`] tie-break by selecting the first matching field
137/// in pre-order depth-first traversal.
138///
139/// # Examples
140///
141/// ```
142/// use lance_core::datatypes::Schema;
143/// use lance_file::{datatypes::Fields, format::pb};
144///
145/// let field = pb::Field {
146///     id: 0,
147///     parent_id: -1,
148///     name: "value".to_owned(),
149///     logical_type: "int32".to_owned(),
150///     ..Default::default()
151/// };
152/// let fields = Fields(vec![field]);
153/// let schema = Schema::try_from(&fields)?;
154/// assert_eq!(schema.fields[0].name, "value");
155/// # Ok::<(), lance_core::Error>(())
156/// ```
157impl TryFrom<&Fields> for Schema {
158    type Error = Error;
159
160    fn try_from(fields: &Fields) -> Result<Self> {
161        let mut nodes: Vec<FieldNode> = Vec::with_capacity(fields.0.len());
162        let mut root_indices = Vec::with_capacity(fields.0.len());
163        let mut field_indices: HashMap<i32, Option<usize>> = HashMap::with_capacity(fields.0.len());
164
165        for proto_field in &fields.0 {
166            let parent_index = if proto_field.parent_id == -1 {
167                None
168            } else {
169                let parent_index = match field_indices.get(&proto_field.parent_id) {
170                    Some(Some(parent_index)) => *parent_index,
171                    Some(None) => {
172                        // Duplicate IDs are invalid but occur in historical
173                        // manifests. Match the legacy tree traversal only for
174                        // these ambiguous parent references so valid schemas
175                        // retain the linear fast path.
176                        first_field_index_by_id(&nodes, &root_indices, proto_field.parent_id)
177                            .ok_or_else(|| {
178                                Error::internal(format!(
179                                    "Duplicate field id {} has no existing arena node",
180                                    proto_field.parent_id
181                                ))
182                            })?
183                    }
184                    None => {
185                        return Err(Error::schema(format!(
186                            "Field '{}' (id={}) references parent id {}, which must appear earlier in the protobuf field list",
187                            proto_field.name, proto_field.id, proto_field.parent_id
188                        )));
189                    }
190                };
191                Some(parent_index)
192            };
193
194            let node_index = nodes.len();
195            if let Some(parent_index) = parent_index {
196                nodes[parent_index].child_indices.push(node_index);
197            } else {
198                root_indices.push(node_index);
199            }
200            nodes.push(FieldNode {
201                field: Field::from(proto_field),
202                child_indices: Vec::new(),
203            });
204
205            field_indices
206                .entry(proto_field.id)
207                .and_modify(|field_index| *field_index = None)
208                .or_insert(Some(node_index));
209        }
210
211        let mut fields_by_node = Vec::with_capacity(nodes.len());
212        fields_by_node.resize_with(nodes.len(), || None);
213        for (node_index, mut node) in nodes.into_iter().enumerate().rev() {
214            node.field.children.reserve(node.child_indices.len());
215            for child_index in node.child_indices {
216                let child = fields_by_node
217                    .get_mut(child_index)
218                    .and_then(Option::take)
219                    .ok_or_else(|| {
220                        Error::internal(format!(
221                            "Schema field arena node {child_index} was not materialized before its parent"
222                        ))
223                    })?;
224                node.field.children.push(child);
225            }
226            fields_by_node[node_index] = Some(node.field);
227        }
228
229        let fields = root_indices
230            .into_iter()
231            .map(|root_index| {
232                fields_by_node[root_index].take().ok_or_else(|| {
233                    Error::internal(format!(
234                        "Schema field arena root node {root_index} was not materialized"
235                    ))
236                })
237            })
238            .collect::<Result<Vec<_>>>()?;
239
240        Ok(Self {
241            fields,
242            metadata: HashMap::default(),
243        })
244    }
245}
246
247pub struct FieldsWithMeta {
248    pub fields: Fields,
249    pub metadata: HashMap<String, Vec<u8>>,
250}
251
252/// Reconstruct a schema from flat protobuf fields and schema metadata.
253///
254/// # Examples
255///
256/// ```
257/// use std::collections::HashMap;
258///
259/// use lance_core::datatypes::Schema;
260/// use lance_file::datatypes::{Fields, FieldsWithMeta};
261///
262/// let fields = FieldsWithMeta {
263///     fields: Fields(Vec::new()),
264///     metadata: HashMap::from([("owner".to_owned(), b"lance".to_vec())]),
265/// };
266/// let schema = Schema::try_from(fields)?;
267/// assert_eq!(schema.metadata["owner"], "lance");
268/// # Ok::<(), lance_core::Error>(())
269/// ```
270impl TryFrom<FieldsWithMeta> for Schema {
271    type Error = Error;
272
273    fn try_from(fields_with_meta: FieldsWithMeta) -> Result<Self> {
274        let lance_metadata = fields_with_meta
275            .metadata
276            .into_iter()
277            .map(|(key, value)| {
278                let string_value = String::from_utf8_lossy(&value).to_string();
279                (key, string_value)
280            })
281            .collect();
282
283        let schema_with_fields = Self::try_from(&fields_with_meta.fields)?;
284        Ok(Self {
285            fields: schema_with_fields.fields,
286            metadata: lance_metadata,
287        })
288    }
289}
290
291/// Convert a Schema to a list of protobuf Field.
292impl From<&Schema> for Fields {
293    fn from(schema: &Schema) -> Self {
294        let mut protos = vec![];
295        schema.fields.iter().for_each(|f| {
296            protos.extend(Self::from(f).0);
297        });
298        Self(protos)
299    }
300}
301
302/// Convert a Schema to a list of protobuf Field and Metadata
303impl From<&Schema> for FieldsWithMeta {
304    fn from(schema: &Schema) -> Self {
305        let fields = schema.into();
306        let metadata = schema
307            .metadata
308            .clone()
309            .into_iter()
310            .map(|(key, value)| (key, value.into_bytes()))
311            .collect();
312        Self { fields, metadata }
313    }
314}
315
316impl From<&pb::Dictionary> for Dictionary {
317    fn from(proto: &pb::Dictionary) -> Self {
318        Self {
319            offset: proto.offset as usize,
320            length: proto.length as usize,
321            values: None,
322        }
323    }
324}
325
326impl From<&Dictionary> for pb::Dictionary {
327    fn from(d: &Dictionary) -> Self {
328        Self {
329            offset: d.offset as i64,
330            length: d.length as i64,
331        }
332    }
333}
334
335impl From<Encoding> for pb::Encoding {
336    fn from(e: Encoding) -> Self {
337        match e {
338            Encoding::Plain => Self::Plain,
339            Encoding::VarBinary => Self::VarBinary,
340            Encoding::Dictionary => Self::Dictionary,
341            Encoding::RLE => Self::Rle,
342        }
343    }
344}
345
346#[cfg(test)]
347mod tests {
348    use std::collections::HashMap;
349
350    use arrow_schema::DataType;
351    use arrow_schema::Field as ArrowField;
352    use arrow_schema::Fields as ArrowFields;
353    use arrow_schema::Schema as ArrowSchema;
354    use lance_core::Error;
355    use lance_core::datatypes::Schema;
356
357    use super::{Fields, FieldsWithMeta};
358    use crate::format::pb;
359
360    fn proto_field(id: i32, parent_id: i32, name: String, logical_type: &str) -> pb::Field {
361        pb::Field {
362            id,
363            parent_id,
364            name,
365            logical_type: logical_type.to_owned(),
366            ..Default::default()
367        }
368    }
369
370    #[test]
371    fn test_schema_set_ids() {
372        let arrow_schema = ArrowSchema::new(vec![
373            ArrowField::new("a", DataType::Int32, false),
374            ArrowField::new(
375                "b",
376                DataType::Struct(ArrowFields::from(vec![
377                    ArrowField::new("f1", DataType::Utf8, true),
378                    ArrowField::new("f2", DataType::Boolean, false),
379                    ArrowField::new("f3", DataType::Float32, false),
380                ])),
381                true,
382            ),
383            ArrowField::new("c", DataType::Float64, false),
384        ]);
385        let schema = Schema::try_from(&arrow_schema).unwrap();
386
387        let protos: Fields = (&schema).into();
388        assert_eq!(
389            protos.0.iter().map(|p| p.id).collect::<Vec<_>>(),
390            (0..6).collect::<Vec<_>>()
391        );
392    }
393
394    #[test]
395    fn test_schema_metadata() {
396        let mut metadata: HashMap<String, String> = HashMap::new();
397        metadata.insert(String::from("k1"), String::from("v1"));
398        metadata.insert(String::from("k2"), String::from("v2"));
399
400        let arrow_schema = ArrowSchema::new_with_metadata(
401            vec![ArrowField::new("a", DataType::Int32, false)],
402            metadata,
403        );
404
405        let expected_schema = Schema::try_from(&arrow_schema).unwrap();
406        let fields_with_meta: FieldsWithMeta = (&expected_schema).into();
407
408        let schema = Schema::try_from(fields_with_meta).unwrap();
409        assert_eq!(expected_schema, schema);
410    }
411
412    #[test]
413    fn test_reconstruct_wide_nested_schema() {
414        const NUM_STRUCTS: usize = 4096;
415
416        let mut proto_fields = Vec::with_capacity(NUM_STRUCTS * 3);
417        for struct_index in 0..NUM_STRUCTS {
418            let parent_id = (struct_index * 3) as i32;
419            proto_fields.push(proto_field(
420                parent_id,
421                -1,
422                format!("struct_{struct_index}"),
423                "struct",
424            ));
425            proto_fields.push(proto_field(
426                parent_id + 1,
427                parent_id,
428                format!("left_{struct_index}"),
429                "int32",
430            ));
431            proto_fields.push(proto_field(
432                parent_id + 2,
433                parent_id,
434                format!("right_{struct_index}"),
435                "int32",
436            ));
437        }
438
439        let fields = Fields(proto_fields);
440        let schema = Schema::try_from(&fields).unwrap();
441        assert_eq!(schema.fields.len(), NUM_STRUCTS);
442        for (struct_index, field) in schema.fields.iter().enumerate() {
443            let parent_id = (struct_index * 3) as i32;
444            assert_eq!(field.id, parent_id);
445            assert_eq!(field.name, format!("struct_{struct_index}"));
446            assert_eq!(field.children.len(), 2);
447            assert_eq!(field.children[0].id, parent_id + 1);
448            assert_eq!(field.children[0].name, format!("left_{struct_index}"));
449            assert_eq!(field.children[1].id, parent_id + 2);
450            assert_eq!(field.children[1].name, format!("right_{struct_index}"));
451        }
452    }
453
454    #[test]
455    fn test_reconstruct_deep_nested_schema() {
456        const DEPTH: usize = 1024;
457
458        let proto_fields = (0..DEPTH)
459            .map(|depth| {
460                proto_field(
461                    depth as i32,
462                    if depth == 0 { -1 } else { depth as i32 - 1 },
463                    format!("level_{depth}"),
464                    if depth + 1 == DEPTH {
465                        "int32"
466                    } else {
467                        "struct"
468                    },
469                )
470            })
471            .collect();
472
473        let fields = Fields(proto_fields);
474        let schema = Schema::try_from(&fields).unwrap();
475        assert_eq!(schema.fields.len(), 1);
476        let mut field = &schema.fields[0];
477        for depth in 0..DEPTH {
478            assert_eq!(field.id, depth as i32);
479            assert_eq!(field.name, format!("level_{depth}"));
480            if depth + 1 == DEPTH {
481                assert!(field.children.is_empty());
482            } else {
483                assert_eq!(field.children.len(), 1);
484                field = &field.children[0];
485            }
486        }
487    }
488
489    #[test]
490    fn test_reconstruct_schema_reports_missing_parent() {
491        let fields = Fields(vec![proto_field(7, 42, "child".to_owned(), "int32")]);
492
493        let error = Schema::try_from(&fields).unwrap_err();
494        assert!(matches!(&error, Error::Schema { .. }));
495        assert!(
496            error.to_string().contains(
497                "Field 'child' (id=7) references parent id 42, which must appear earlier"
498            )
499        );
500    }
501
502    #[test]
503    fn test_reconstruct_schema_preserves_legacy_duplicate_id_match() {
504        let fields = Fields(vec![
505            proto_field(1, -1, "root_a".to_owned(), "struct"),
506            proto_field(2, -1, "root_b".to_owned(), "struct"),
507            proto_field(2, 1, "nested_duplicate".to_owned(), "struct"),
508            proto_field(3, 2, "child".to_owned(), "int32"),
509        ]);
510
511        let schema = Schema::try_from(&fields).unwrap();
512        assert_eq!(schema.fields.len(), 2);
513        assert_eq!(schema.fields[0].name, "root_a");
514        assert_eq!(schema.fields[0].children.len(), 1);
515        assert_eq!(schema.fields[0].children[0].name, "nested_duplicate");
516        assert_eq!(schema.fields[0].children[0].children.len(), 1);
517        assert_eq!(schema.fields[0].children[0].children[0].name, "child");
518        assert_eq!(schema.fields[1].name, "root_b");
519        assert!(schema.fields[1].children.is_empty());
520    }
521
522    #[test]
523    fn test_clustering_key_roundtrip() {
524        let arrow_schema = ArrowSchema::new(vec![
525            ArrowField::new("region", DataType::Utf8, true).with_metadata(
526                vec![(
527                    "lance-schema:unenforced-clustering-key:position".to_owned(),
528                    "1".to_owned(),
529                )]
530                .into_iter()
531                .collect::<HashMap<_, _>>(),
532            ),
533            ArrowField::new("date", DataType::Int32, false).with_metadata(
534                vec![(
535                    "lance-schema:unenforced-clustering-key:position".to_owned(),
536                    "2".to_owned(),
537                )]
538                .into_iter()
539                .collect::<HashMap<_, _>>(),
540            ),
541            ArrowField::new("value", DataType::Float64, true),
542        ]);
543
544        let schema = Schema::try_from(&arrow_schema).unwrap();
545        let ck = schema.unenforced_clustering_key();
546        assert_eq!(ck.len(), 2);
547        assert_eq!(ck[0].name, "region");
548        assert_eq!(ck[1].name, "date");
549
550        // Round-trip through protobuf
551        let fields_with_meta: FieldsWithMeta = (&schema).into();
552        let restored = Schema::try_from(fields_with_meta).unwrap();
553
554        let ck2 = restored.unenforced_clustering_key();
555        assert_eq!(ck2.len(), 2);
556        assert_eq!(ck2[0].name, "region");
557        assert_eq!(ck2[1].name, "date");
558        assert_eq!(ck2[0].unenforced_clustering_key_position, Some(1));
559        assert_eq!(ck2[1].unenforced_clustering_key_position, Some(2));
560
561        // Non-clustering-key field should not have position
562        let value_field = restored.field("value").unwrap();
563        assert!(!value_field.is_unenforced_clustering_key());
564    }
565}