Skip to main content

cobble_table/
evolution.rs

1use crate::logical_type::{assign_fresh_field_ids, assign_fresh_type_ids};
2use crate::metadata::TableMetadata;
3use crate::transform::compile_table_transform;
4use crate::{
5    DataField, FieldId, LogicalType, LogicalTypeKind, Result, TableError, TableSchema, Value,
6    ValueCodec,
7};
8use cobble::{ColumnEvolution, TransformSpec};
9use serde::{Deserialize, Serialize};
10use std::collections::{HashMap, HashSet};
11
12/// A top-level table schema edit addressed by its current field name.
13#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
14pub enum SchemaChange {
15    /// Append a nullable top-level value field.
16    AddField {
17        name: String,
18        logical_type: LogicalType,
19    },
20    /// Rename a top-level field while retaining its stable field id.
21    RenameField {
22        field_name: String,
23        new_name: String,
24    },
25    /// Drop a non-key top-level field.
26    DropField { field_name: String },
27    /// Losslessly widen one existing non-key field's logical type.
28    ///
29    /// The catalog derives and persists the built-in transform from the field's
30    /// actual current type and this target type.
31    AlterFieldType {
32        field_name: String,
33        logical_type: LogicalType,
34    },
35    /// Transform one existing non-key field, optionally changing its logical type.
36    ///
37    /// The field name is resolved when this change is applied. Catalog storage
38    /// retains the resulting stable field id with the opaque transform spec.
39    TransformField {
40        field_name: String,
41        logical_type: LogicalType,
42        transform: TransformSpec,
43    },
44}
45
46/// One persisted field transform for a single catalog schema version.
47#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
48pub(crate) struct FieldTransform {
49    pub(crate) field_id: FieldId,
50    pub(crate) transform: TransformSpec,
51}
52
53/// Apply sequential schema edits while retaining every historical field id.
54///
55/// `used_field_ids` must include both active and retired ids from every prior
56/// version of the table schema.
57pub(crate) fn apply_schema_changes(
58    mut schema: TableSchema,
59    changes: Vec<SchemaChange>,
60    mut used_field_ids: HashSet<FieldId>,
61) -> Result<(TableSchema, HashSet<FieldId>, Vec<FieldTransform>)> {
62    if changes.is_empty() {
63        return Err(TableError::InvalidSchema(
64            "schema changes must not be empty".to_string(),
65        ));
66    }
67    let key_ids = schema
68        .primary_key
69        .iter()
70        .chain(&schema.bucket_key)
71        .copied()
72        .collect::<HashSet<_>>();
73
74    let mut added_fields = HashSet::new();
75    let mut field_transforms = HashMap::new();
76    for change in changes {
77        match change {
78            SchemaChange::AddField { name, logical_type } => {
79                if schema.fields.iter().any(|field| field.name == name) {
80                    return Err(TableError::InvalidSchema(format!(
81                        "duplicate top-level field name: {name}"
82                    )));
83                }
84                if !logical_type.nullable {
85                    return Err(TableError::InvalidSchema(format!(
86                        "added field '{name}' must be nullable"
87                    )));
88                }
89                let mut added = DataField {
90                    id: FieldId(0),
91                    name,
92                    logical_type,
93                };
94                let mut next_id = next_field_id(&used_field_ids)?;
95                assign_fresh_field_ids(std::slice::from_mut(&mut added), &mut next_id)?;
96                collect_field_ids(&added, &mut used_field_ids);
97                added_fields.insert(added.id);
98                schema.fields.push(added);
99            }
100            SchemaChange::RenameField {
101                field_name,
102                new_name,
103            } => {
104                let index = field_index(&schema, &field_name)?;
105                if new_name != field_name
106                    && schema.fields.iter().any(|field| field.name == new_name)
107                {
108                    return Err(TableError::InvalidSchema(format!(
109                        "duplicate top-level field name: {new_name}"
110                    )));
111                }
112                schema.fields[index].name = new_name;
113            }
114            SchemaChange::DropField { field_name } => {
115                let index = field_index(&schema, &field_name)?;
116                if key_ids.contains(&schema.fields[index].id) {
117                    return Err(TableError::InvalidSchema(format!(
118                        "key field '{}' cannot be dropped",
119                        field_name
120                    )));
121                }
122                let field = schema.fields.remove(index);
123                // A later drop makes an earlier same-version transform unreachable.
124                field_transforms.remove(&field.id);
125            }
126            SchemaChange::AlterFieldType {
127                field_name,
128                logical_type,
129            } => {
130                let index = transform_field_index(
131                    &schema,
132                    &key_ids,
133                    &added_fields,
134                    &field_transforms,
135                    &field_name,
136                )?;
137                let source = schema.fields[index].logical_type.clone();
138                let transform = compile_table_transform(&source, &logical_type)?;
139                schema.fields[index].logical_type = logical_type;
140                if let Some(transform) = transform {
141                    field_transforms.insert(schema.fields[index].id, transform);
142                }
143            }
144            SchemaChange::TransformField {
145                field_name,
146                mut logical_type,
147                transform,
148            } => {
149                if transform.transform_type.trim().is_empty() {
150                    return Err(TableError::InvalidSchema(
151                        "field transform type must not be empty".to_string(),
152                    ));
153                }
154                let index = transform_field_index(
155                    &schema,
156                    &key_ids,
157                    &added_fields,
158                    &field_transforms,
159                    &field_name,
160                )?;
161                let field = &mut schema.fields[index];
162                if logical_type != field.logical_type {
163                    let mut new_nested_ids = HashSet::new();
164                    collect_type_field_ids(&logical_type, &mut new_nested_ids);
165                    if !new_nested_ids.is_empty() {
166                        let mut next_id = next_field_id(&used_field_ids)?;
167                        assign_fresh_type_ids(&mut logical_type, &mut next_id)?;
168                        new_nested_ids.clear();
169                        collect_type_field_ids(&logical_type, &mut new_nested_ids);
170                        used_field_ids.extend(new_nested_ids);
171                    }
172                }
173                field.logical_type = logical_type;
174                field_transforms.insert(field.id, transform);
175            }
176        }
177    }
178    let schema = TableSchema::new(schema.fields, schema.primary_key, schema.bucket_key)?;
179    let mut field_transforms = field_transforms
180        .into_iter()
181        .map(|(field_id, transform)| FieldTransform {
182            field_id,
183            transform,
184        })
185        .collect::<Vec<_>>();
186    field_transforms.sort_by_key(|transform| transform.field_id);
187    Ok((schema, used_field_ids, field_transforms))
188}
189
190/// Compile core column remapping for two validated table metadata versions.
191pub(crate) fn compile_column_evolution(
192    existing: &TableMetadata,
193    target: &TableMetadata,
194    field_transforms: &[FieldTransform],
195) -> Result<Vec<ColumnEvolution>> {
196    if existing.layout.key_fields != target.layout.key_fields
197        || existing.layout.bucket_fields != target.layout.bucket_fields
198    {
199        return Err(TableError::InvalidSchema(
200            "schema evolution changed the table key".to_string(),
201        ));
202    }
203    let existing_fields = existing
204        .schema
205        .fields
206        .iter()
207        .map(|field| (field.id, field))
208        .collect::<HashMap<_, _>>();
209    let transforms = field_transforms
210        .iter()
211        .map(|transform| (transform.field_id, &transform.transform))
212        .collect::<HashMap<_, _>>();
213    if transforms.len() != field_transforms.len() {
214        return Err(TableError::InvalidSchema(
215            "catalog schema has duplicate field transforms".to_string(),
216        ));
217    }
218    for field in &target.schema.fields {
219        if let Some(previous) = existing_fields.get(&field.id)
220            && previous.logical_type != field.logical_type
221            && !transforms.contains_key(&field.id)
222        {
223            return Err(TableError::InvalidSchema(format!(
224                "schema evolution changed the type of field {} without a transform",
225                field.id.0
226            )));
227        }
228    }
229
230    if target.layout.value_columns.is_empty() {
231        return Ok(vec![ColumnEvolution::Default {
232            value: Vec::new().into(),
233        }]);
234    }
235    let existing_columns = existing
236        .layout
237        .value_columns
238        .iter()
239        .map(|column| (column.field_id, usize::from(column.column_index)))
240        .collect::<HashMap<_, _>>();
241    let target_fields = target
242        .schema
243        .fields
244        .iter()
245        .map(|field| (field.id, &field.logical_type))
246        .collect::<HashMap<_, _>>();
247    target
248        .layout
249        .value_columns
250        .iter()
251        .map(|column| {
252            if let Some(source) = existing_columns.get(&column.field_id) {
253                return Ok(ColumnEvolution::Source {
254                    source_index: *source,
255                    transform: transforms.get(&column.field_id).map(|spec| (*spec).clone()),
256                });
257            }
258            if transforms.contains_key(&column.field_id) {
259                return Err(TableError::InvalidSchema(format!(
260                    "catalog transform cannot target added field {}",
261                    column.field_id.0
262                )));
263            }
264            Ok(ColumnEvolution::Default {
265                value: ValueCodec::encode_validated(target_fields[&column.field_id], &Value::Null)?
266                    .into(),
267            })
268        })
269        .collect()
270}
271
272pub(crate) fn schema_field_ids(schema: &TableSchema) -> HashSet<FieldId> {
273    let mut field_ids = HashSet::new();
274    for field in &schema.fields {
275        collect_field_ids(field, &mut field_ids);
276    }
277    field_ids
278}
279
280fn field_index(schema: &TableSchema, field_name: &str) -> Result<usize> {
281    schema
282        .fields
283        .iter()
284        .position(|field| field.name == field_name)
285        .ok_or_else(|| TableError::InvalidSchema(format!("field '{field_name}' does not exist")))
286}
287
288fn transform_field_index(
289    schema: &TableSchema,
290    key_ids: &HashSet<FieldId>,
291    added_fields: &HashSet<FieldId>,
292    transforms: &HashMap<FieldId, TransformSpec>,
293    field_name: &str,
294) -> Result<usize> {
295    let index = field_index(schema, field_name)?;
296    let field = &schema.fields[index];
297    if key_ids.contains(&field.id) {
298        return Err(TableError::InvalidSchema(format!(
299            "key field '{field_name}' cannot be transformed"
300        )));
301    }
302    if added_fields.contains(&field.id) {
303        return Err(TableError::InvalidSchema(format!(
304            "added field '{field_name}' cannot be transformed in the same schema version"
305        )));
306    }
307    if transforms.contains_key(&field.id) {
308        return Err(TableError::InvalidSchema(format!(
309            "field '{field_name}' has multiple transforms in one schema version"
310        )));
311    }
312    Ok(index)
313}
314
315fn next_field_id(used_field_ids: &HashSet<FieldId>) -> Result<u32> {
316    used_field_ids
317        .iter()
318        .map(|field_id| field_id.0)
319        .max()
320        .map_or(Ok(0), |field_id| {
321            field_id
322                .checked_add(1)
323                .ok_or_else(|| TableError::InvalidSchema("field id space exhausted".to_string()))
324        })
325}
326
327fn collect_field_ids(field: &DataField, field_ids: &mut HashSet<FieldId>) {
328    field_ids.insert(field.id);
329    collect_type_field_ids(&field.logical_type, field_ids);
330}
331
332fn collect_type_field_ids(logical_type: &LogicalType, field_ids: &mut HashSet<FieldId>) {
333    match &logical_type.kind {
334        LogicalTypeKind::List { element_type } => collect_type_field_ids(element_type, field_ids),
335        LogicalTypeKind::Map {
336            key_type,
337            value_type,
338        } => {
339            collect_type_field_ids(key_type, field_ids);
340            collect_type_field_ids(value_type, field_ids);
341        }
342        LogicalTypeKind::Struct { fields } => {
343            for field in fields {
344                collect_field_ids(field, field_ids);
345            }
346        }
347        LogicalTypeKind::Extension { extension } => {
348            collect_type_field_ids(&extension.physical_type, field_ids);
349        }
350        _ => {}
351    }
352}