Skip to main content

lance_nd/
encode.rs

1//! Encode nd record batches as rows of a Lance table, and decode them.
2//!
3//! The table schema is the nd-arrow encoding: one `nd_arrow.array` column per
4//! variable and an optional `__global_attrs__` column. Each row is one
5//! [`NdRecordBatch`].
6
7use std::sync::Arc;
8
9use arrow::array::{Array, ArrayRef, AsArray, RecordBatch, RecordBatchOptions, StructArray};
10use arrow::datatypes::{Field, FieldRef, Schema};
11use nd_arrow_array::extension::{
12    GLOBAL_ATTRIBUTES_FIELD, global_attributes_field, is_global_attributes_field, is_nd_field,
13    logical_field,
14};
15use nd_arrow_array::{Attributes, NdColumn, NdColumnBuilder, NdRecordBatch};
16
17use crate::error::{Error, Result};
18use crate::options::without_encoding_metadata;
19use crate::schema::conform_batch;
20use crate::stats::{STATS_PREFIX, is_stats_name};
21
22/// Encode `rows` as one record batch, one row per nd record batch.
23///
24/// The columns are the union of the variables of all rows. A row without a
25/// variable has a null in its column. One variable must have one value type.
26pub fn encode_rows(rows: &[NdRecordBatch]) -> Result<RecordBatch> {
27    let mut variables: Vec<FieldRef> = Vec::new();
28    for field in rows.iter().flat_map(|r| r.schema().fields().iter()) {
29        if field.name() == GLOBAL_ATTRIBUTES_FIELD || is_stats_name(field.name()) {
30            return Err(Error::SchemaMismatch(format!(
31                "variable name '{}' is reserved, also each name that starts with {STATS_PREFIX}",
32                field.name()
33            )));
34        }
35        match variables.iter().find(|v| v.name() == field.name()) {
36            Some(existing) if existing.data_type() != field.data_type() => {
37                return Err(Error::SchemaMismatch(format!(
38                    "variable '{}' has value types {} and {}",
39                    field.name(),
40                    existing.data_type(),
41                    field.data_type()
42                )));
43            }
44            Some(_) => {}
45            None => variables.push(field.clone()),
46        }
47    }
48
49    let mut fields: Vec<Field> = Vec::with_capacity(variables.len() + 1);
50    let mut columns: Vec<ArrayRef> = Vec::with_capacity(variables.len() + 1);
51    for variable in &variables {
52        let mut builder = NdColumnBuilder::with_value_type(variable.data_type().clone());
53        for row in rows {
54            builder.append_option(row.column_by_name(variable.name()))?;
55        }
56        let column = builder.finish()?;
57        let field = column.to_field(variable.name());
58        // Keep the metadata of the logical field. The extension keys win.
59        let mut metadata = variable.metadata().clone();
60        metadata.extend(field.metadata().clone());
61        fields.push(field.with_metadata(metadata));
62        columns.push(column.into_array_ref());
63    }
64
65    if rows.iter().any(|r| !r.attributes().is_empty()) {
66        let global = Attributes::union_fields(rows.iter().map(NdRecordBatch::attributes))?;
67        let attrs: Vec<Option<&Attributes>> = rows.iter().map(|r| Some(r.attributes())).collect();
68        columns.push(Arc::new(Attributes::encode_rows(&attrs, &global)?));
69        // Nullable, so that Lance can add the column to an existing table.
70        fields.push(global_attributes_field(global).with_nullable(true));
71    }
72
73    let options = RecordBatchOptions::new().with_row_count(Some(rows.len()));
74    Ok(RecordBatch::try_new_with_options(
75        Arc::new(Schema::new(fields)),
76        columns,
77        &options,
78    )?)
79}
80
81/// The logical field of an nd column, without the Lance encoding keys.
82pub fn variable_field(encoded: &Field) -> Result<Field> {
83    Ok(logical_field(&without_encoding_metadata(encoded))?)
84}
85
86/// Decodes the rows of one record batch of a table, one row at a time.
87///
88/// A null variable is absent from its row. The grid of each row is inferred
89/// from its variables, as in the nd-arrow format.
90#[derive(Debug, Clone)]
91pub struct RowDecoder {
92    variables: Vec<(FieldRef, NdColumn)>,
93    global: Option<StructArray>,
94    num_rows: usize,
95}
96
97impl RowDecoder {
98    /// A decoder for `batch`. Each column must be an nd column, the global
99    /// attributes column or a stats column, which the decoder ignores.
100    /// The Lance encoding keys are removed.
101    pub fn try_new(batch: &RecordBatch) -> Result<Self> {
102        let fields: Vec<Field> = batch
103            .schema()
104            .fields()
105            .iter()
106            .filter(|field| !is_stats_name(field.name()))
107            .map(|field| without_encoding_metadata(field))
108            .collect();
109        let schema = Arc::new(Schema::new(fields));
110        let batch = &conform_batch(batch, &schema)?;
111        let mut variables = Vec::new();
112        let mut global = None;
113        for (field, column) in schema.fields().iter().zip(batch.columns()) {
114            if is_global_attributes_field(field) {
115                global = column.as_struct_opt().cloned();
116            } else if is_nd_field(field) {
117                let logical = Arc::new(logical_field(field)?);
118                variables.push((logical, NdColumn::try_from_array(column.as_ref())?));
119            } else {
120                return Err(Error::InvalidTable(format!(
121                    "column '{}' is not an nd column",
122                    field.name()
123                )));
124            }
125        }
126        Ok(Self {
127            variables,
128            global,
129            num_rows: batch.num_rows(),
130        })
131    }
132
133    /// The number of rows in the batch.
134    pub fn num_rows(&self) -> usize {
135        self.num_rows
136    }
137
138    /// Decode row `row`. Panics when `row` is out of bounds.
139    pub fn row(&self, row: usize) -> Result<NdRecordBatch> {
140        let mut fields = Vec::new();
141        let mut arrays = Vec::new();
142        for (field, column) in &self.variables {
143            if !column.is_null(row) {
144                fields.push(field.clone());
145                arrays.push(column.value(row));
146            }
147        }
148        let mut batch = NdRecordBatch::try_new_inferred(Arc::new(Schema::new(fields)), arrays)?;
149        if let Some(global) = self.global.as_ref().filter(|g| g.is_valid(row)) {
150            batch = batch.with_attributes(Attributes::from_struct_row(global, row));
151        }
152        Ok(batch)
153    }
154
155    /// Decode the rows one at a time, in order.
156    pub fn into_rows(self) -> impl Iterator<Item = Result<NdRecordBatch>> + Send + 'static {
157        (0..self.num_rows).map(move |row| self.row(row))
158    }
159}