use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use async_trait::async_trait;
use typed_builder::TypedBuilder;
use crate::spec::{
ListType, Literal, MapType, NestedField, NestedFieldRef, SCHEMA_NAME_DELIMITER, Schema,
StructType, Type,
};
use crate::table::Table;
use crate::transaction::action::{ActionCommit, TransactionAction};
use crate::{Error, ErrorKind, Result, TableRequirement, TableUpdate};
const DEFAULT_FIELD_ID: i32 = 0;
#[derive(TypedBuilder)]
pub struct AddColumn {
#[builder(default = None, setter(strip_option, into))]
parent: Option<String>,
#[builder(setter(into))]
name: String,
#[builder(default = false)]
required: bool,
field_type: Type,
#[builder(default = None, setter(strip_option, into))]
doc: Option<String>,
#[builder(default = None, setter(strip_option))]
initial_default: Option<Literal>,
#[builder(default = None, setter(strip_option))]
write_default: Option<Literal>,
}
impl AddColumn {
pub fn optional(name: impl ToString, field_type: Type) -> Self {
Self::builder()
.name(name.to_string())
.field_type(field_type)
.required(false)
.build()
}
pub fn required(name: impl ToString, field_type: Type, initial_default: Literal) -> Self {
Self::builder()
.name(name.to_string())
.field_type(field_type)
.required(true)
.initial_default(initial_default.clone())
.write_default(initial_default)
.build()
}
fn to_nested_field(&self) -> NestedFieldRef {
let mut field = NestedField::new(
DEFAULT_FIELD_ID,
self.name.clone(),
self.field_type.clone(),
self.required,
);
field.doc = self.doc.clone();
field.initial_default = self.initial_default.clone();
field.write_default = self.write_default.clone();
Arc::new(field)
}
}
pub struct UpdateSchemaAction {
additions: Vec<AddColumn>,
deletes: Vec<String>,
}
impl UpdateSchemaAction {
pub(crate) fn new() -> Self {
Self {
additions: Vec::new(),
deletes: Vec::new(),
}
}
pub fn add_column(mut self, add_column: AddColumn) -> Self {
self.additions.push(add_column);
self
}
pub fn delete_column(mut self, name: impl ToString) -> Self {
self.deletes.push(name.to_string());
self
}
}
fn assign_fresh_ids(field: &NestedField, next_id: &mut i32) -> NestedFieldRef {
*next_id += 1;
let new_id = *next_id;
let new_type = assign_fresh_ids_to_type(&field.field_type, next_id);
Arc::new(NestedField {
id: new_id,
name: field.name.clone(),
required: field.required,
field_type: Box::new(new_type),
doc: field.doc.clone(),
initial_default: field.initial_default.clone(),
write_default: field.write_default.clone(),
})
}
fn assign_fresh_ids_to_type(field_type: &Type, next_id: &mut i32) -> Type {
match field_type {
Type::Primitive(_) => field_type.clone(),
Type::Struct(struct_type) => {
let new_fields: Vec<NestedFieldRef> = struct_type
.fields()
.iter()
.map(|f| assign_fresh_ids(f, next_id))
.collect();
Type::Struct(StructType::new(new_fields))
}
Type::List(list_type) => {
let new_element = assign_fresh_ids(&list_type.element_field, next_id);
Type::List(ListType {
element_field: new_element,
})
}
Type::Map(map_type) => {
let new_key = assign_fresh_ids(&map_type.key_field, next_id);
let new_value = assign_fresh_ids(&map_type.value_field, next_id);
Type::Map(MapType {
key_field: new_key,
value_field: new_value,
})
}
}
}
fn resolve_parent_target<'a>(
base_schema: &'a Schema,
parent: &str,
) -> Result<(i32, &'a StructType)> {
base_schema
.field_by_name(parent)
.ok_or_else(|| {
Error::new(
ErrorKind::PreconditionFailed,
format!("Cannot add column: parent '{parent}' not found"),
)
})
.and_then(|parent_field| match parent_field.field_type.as_ref() {
Type::Struct(s) => Ok((parent_field.id, s)),
Type::Map(m) => match m.value_field.field_type.as_ref() {
Type::Struct(s) => Ok((m.value_field.id, s)),
_ => Err(Error::new(
ErrorKind::PreconditionFailed,
format!("Cannot add column: map value of '{parent}' is not a struct"),
)),
},
Type::List(l) => match l.element_field.field_type.as_ref() {
Type::Struct(s) => Ok((l.element_field.id, s)),
_ => Err(Error::new(
ErrorKind::PreconditionFailed,
format!("Cannot add column: list element of '{parent}' is not a struct"),
)),
},
_ => Err(Error::new(
ErrorKind::PreconditionFailed,
format!("Cannot add column: parent '{parent}' is not a struct, map, or list"),
)),
})
}
fn rebuild_fields(
fields: &[NestedFieldRef],
adds: &HashMap<Option<i32>, Vec<NestedFieldRef>>,
delete_ids: &HashSet<i32>,
parent_id: Option<i32>,
) -> Vec<NestedFieldRef> {
fields
.iter()
.filter(|f| !delete_ids.contains(&f.id))
.map(|f| rebuild_field(f, adds, delete_ids))
.chain(adds.get(&parent_id).into_iter().flatten().cloned())
.collect()
}
fn rebuild_field(
field: &NestedFieldRef,
adds: &HashMap<Option<i32>, Vec<NestedFieldRef>>,
delete_ids: &HashSet<i32>,
) -> NestedFieldRef {
match field.field_type.as_ref() {
Type::Primitive(_) => field.clone(),
Type::Struct(s) => {
let new_fields = rebuild_fields(s.fields(), adds, delete_ids, Some(field.id));
Arc::new(NestedField {
id: field.id,
name: field.name.clone(),
required: field.required,
field_type: Box::new(Type::Struct(StructType::new(new_fields))),
doc: field.doc.clone(),
initial_default: field.initial_default.clone(),
write_default: field.write_default.clone(),
})
}
Type::List(l) => {
let new_element = rebuild_field(&l.element_field, adds, delete_ids);
Arc::new(NestedField {
id: field.id,
name: field.name.clone(),
required: field.required,
field_type: Box::new(Type::List(ListType {
element_field: new_element,
})),
doc: field.doc.clone(),
initial_default: field.initial_default.clone(),
write_default: field.write_default.clone(),
})
}
Type::Map(m) => {
let new_key = rebuild_field(&m.key_field, adds, delete_ids);
let new_value = rebuild_field(&m.value_field, adds, delete_ids);
Arc::new(NestedField {
id: field.id,
name: field.name.clone(),
required: field.required,
field_type: Box::new(Type::Map(MapType {
key_field: new_key,
value_field: new_value,
})),
doc: field.doc.clone(),
initial_default: field.initial_default.clone(),
write_default: field.write_default.clone(),
})
}
}
}
#[async_trait]
impl TransactionAction for UpdateSchemaAction {
async fn commit(self: Arc<Self>, table: &Table) -> Result<ActionCommit> {
let base_schema = table.metadata().current_schema();
let mut last_column_id = table.metadata().last_column_id();
let delete_ids = self
.deletes
.iter()
.map(|name: &String| {
base_schema
.field_by_name(name)
.ok_or_else(|| {
Error::new(
ErrorKind::PreconditionFailed,
format!("Cannot delete missing column: {name}"),
)
})
.and_then(|field| {
match base_schema
.identifier_field_ids()
.find(|id| *id == field.id)
{
Some(_) => Err(Error::new(
ErrorKind::PreconditionFailed,
format!("Cannot delete identifier field: {name}"),
)),
None => Ok(field.id),
}
})
})
.collect::<Result<HashSet<i32>>>()?;
let mut additions_by_parent: HashMap<Option<i32>, Vec<NestedFieldRef>> = HashMap::new();
for add in &self.additions {
let pending_field = add.to_nested_field();
if pending_field.name.contains(SCHEMA_NAME_DELIMITER) {
return Err(Error::new(
ErrorKind::PreconditionFailed,
format!(
"Cannot add column with ambiguous name: {}. Use `AddColumn::with_parent` to add a column to a nested struct.",
pending_field.name
),
));
}
if pending_field.required && pending_field.initial_default.is_none() {
return Err(Error::new(
ErrorKind::PreconditionFailed,
format!(
"Incompatible change: cannot add required column without an initial default: {}",
pending_field.name
),
));
}
let parent_id = match &add.parent {
None => {
if let Some(existing) = base_schema.field_by_name(&pending_field.name)
&& !delete_ids.contains(&existing.id)
{
return Err(Error::new(
ErrorKind::PreconditionFailed,
format!(
"Cannot add column, name already exists: {}",
pending_field.name
),
));
}
None
}
Some(parent_path) => {
let (resolved_parent_id, parent_struct) =
resolve_parent_target(base_schema, parent_path)?;
if parent_struct.fields().iter().any(|f| {
f.name == pending_field.name
&& !delete_ids.contains(&f.id)
&& !delete_ids.contains(&resolved_parent_id)
}) {
return Err(Error::new(
ErrorKind::PreconditionFailed,
format!(
"Cannot add column, name already exists in '{}': {}",
parent_path, pending_field.name
),
));
}
Some(resolved_parent_id)
}
};
let field = assign_fresh_ids(&pending_field, &mut last_column_id);
additions_by_parent
.entry(parent_id)
.or_default()
.push(field);
}
let new_fields = rebuild_fields(
base_schema.as_struct().fields(),
&additions_by_parent,
&delete_ids,
None,
);
let schema = Schema::builder()
.with_fields(new_fields)
.with_identifier_field_ids(base_schema.identifier_field_ids())
.build()?;
let updates = vec![
TableUpdate::AddSchema { schema },
TableUpdate::SetCurrentSchema { schema_id: -1 },
];
let requirements = vec![TableRequirement::CurrentSchemaIdMatch {
current_schema_id: base_schema.schema_id(),
}];
Ok(ActionCommit::new(updates, requirements))
}
}
#[cfg(test)]
mod tests {
use std::io::BufReader;
use std::sync::Arc;
use as_any::Downcast;
use crate::spec::{
DEFAULT_SCHEMA_ID, Literal, NestedField, PrimitiveType, StructType, TableMetadata, Type,
};
use crate::table::Table;
use crate::transaction::Transaction;
use crate::transaction::action::{ApplyTransactionAction, TransactionAction};
use crate::transaction::tests::make_v2_table;
use crate::transaction::update_schema::{AddColumn, DEFAULT_FIELD_ID, UpdateSchemaAction};
use crate::{ErrorKind, TableIdent, TableRequirement, TableUpdate};
fn make_v2_table_with_nested() -> Table {
let json = r#"{
"format-version": 2,
"table-uuid": "9c12d441-03fe-4693-9a96-a0705ddf69c2",
"location": "s3://bucket/test/location",
"last-sequence-number": 0,
"last-updated-ms": 1602638573590,
"last-column-id": 14,
"current-schema-id": 0,
"schemas": [
{
"type": "struct",
"schema-id": 0,
"identifier-field-ids": [1, 2],
"fields": [
{"id": 1, "name": "x", "required": true, "type": "long"},
{"id": 2, "name": "y", "required": true, "type": "long"},
{"id": 3, "name": "z", "required": true, "type": "long"},
{"id": 4, "name": "person", "required": false, "type": {
"type": "struct",
"fields": [
{"id": 5, "name": "name", "required": false, "type": "string"},
{"id": 6, "name": "age", "required": true, "type": "int"}
]
}},
{"id": 7, "name": "tags", "required": false, "type": {
"type": "list",
"element-id": 8,
"element": {
"type": "struct",
"fields": [
{"id": 9, "name": "key", "required": false, "type": "string"},
{"id": 10, "name": "value", "required": false, "type": "string"}
]
},
"element-required": true
}},
{"id": 11, "name": "props", "required": false, "type": {
"type": "map",
"key-id": 12,
"key": "string",
"value-id": 13,
"value": {
"type": "struct",
"fields": [
{"id": 14, "name": "data", "required": false, "type": "string"}
]
},
"value-required": true
}}
]
}
],
"default-spec-id": 0,
"partition-specs": [
{"spec-id": 0, "fields": []}
],
"last-partition-id": 999,
"default-sort-order-id": 0,
"sort-orders": [
{"order-id": 0, "fields": []}
],
"properties": {},
"current-snapshot-id": -1,
"snapshots": []
}"#;
let reader = BufReader::new(json.as_bytes());
let metadata = serde_json::from_reader::<_, TableMetadata>(reader).unwrap();
Table::builder()
.metadata(metadata)
.metadata_location("s3://bucket/test/location/metadata/v1.json".to_string())
.identifier(TableIdent::from_strs(["ns1", "test1"]).unwrap())
.file_io(crate::io::FileIO::new_with_memory())
.runtime(crate::test_utils::test_runtime())
.build()
.unwrap()
}
#[tokio::test]
async fn test_add_column() {
let table = make_v2_table();
let tx = Transaction::new(&table);
let action = tx.update_schema().add_column(AddColumn::optional(
"new_col",
Type::Primitive(PrimitiveType::Int),
));
let mut action_commit = Arc::new(action).commit(&table).await.unwrap();
let updates = action_commit.take_updates();
let requirements = action_commit.take_requirements();
assert_eq!(updates.len(), 2);
let new_schema = match &updates[0] {
TableUpdate::AddSchema { schema } => schema,
other => panic!("expected AddSchema, got {other:?}"),
};
let expected_schema = table
.metadata()
.current_schema()
.as_ref()
.clone()
.into_builder()
.with_schema_id(DEFAULT_SCHEMA_ID)
.with_fields([
NestedField::optional(4, "new_col", Type::Primitive(PrimitiveType::Int)).into(),
])
.build()
.unwrap();
assert_eq!(new_schema, &expected_schema);
assert_eq!(updates[1], TableUpdate::SetCurrentSchema { schema_id: -1 });
assert_eq!(requirements.len(), 1);
assert_eq!(requirements[0], TableRequirement::CurrentSchemaIdMatch {
current_schema_id: table.metadata().current_schema().schema_id()
});
}
#[tokio::test]
async fn test_add_column_with_doc() {
let table = make_v2_table();
let tx = Transaction::new(&table);
let action = tx.update_schema().add_column(
AddColumn::builder()
.name("documented_col")
.field_type(Type::Primitive(PrimitiveType::String))
.doc("A documented column")
.build(),
);
let mut action_commit = Arc::new(action).commit(&table).await.unwrap();
let updates = action_commit.take_updates();
let new_schema = match &updates[0] {
TableUpdate::AddSchema { schema } => schema,
other => panic!("expected AddSchema, got {other:?}"),
};
let field = new_schema
.field_by_name("documented_col")
.expect("documented_col should exist");
assert_eq!(field.id, 4);
assert!(!field.required);
assert_eq!(field.doc.as_deref(), Some("A documented column"));
}
#[tokio::test]
async fn test_add_required_column_with_initial_default() {
let table = make_v2_table();
let tx = Transaction::new(&table);
let action = tx.update_schema().add_column(AddColumn::required(
"req_col",
Type::Primitive(PrimitiveType::Int),
Literal::int(0),
));
let mut action_commit = Arc::new(action).commit(&table).await.unwrap();
let updates = action_commit.take_updates();
let new_schema = match &updates[0] {
TableUpdate::AddSchema { schema } => schema,
other => panic!("expected AddSchema, got {other:?}"),
};
let field = new_schema
.field_by_name("req_col")
.expect("req_col should exist");
assert_eq!(field.id, 4);
assert!(field.required);
assert_eq!(field.initial_default, Some(Literal::int(0)));
assert_eq!(field.write_default, Some(Literal::int(0)));
}
#[tokio::test]
async fn test_add_column_name_conflict_fails() {
let table = make_v2_table();
let tx = Transaction::new(&table);
let action = tx.update_schema().add_column(AddColumn::optional(
"x",
Type::Primitive(PrimitiveType::Int),
));
let result = Arc::new(action).commit(&table).await;
let err = match result {
Err(e) => e,
Ok(_) => panic!("should reject adding a column with an existing name"),
};
assert_eq!(err.kind(), ErrorKind::PreconditionFailed);
assert!(
err.message().contains("already exists"),
"error should mention name conflict, got: {}",
err.message()
);
}
#[tokio::test]
async fn test_delete_column() {
let table = make_v2_table();
let tx = Transaction::new(&table);
let action = tx.update_schema().delete_column("z");
let mut action_commit = Arc::new(action).commit(&table).await.unwrap();
let updates = action_commit.take_updates();
let new_schema = match &updates[0] {
TableUpdate::AddSchema { schema } => schema,
other => panic!("expected AddSchema, got {other:?}"),
};
assert!(
new_schema.field_by_name("z").is_none(),
"z should be deleted"
);
assert!(new_schema.field_by_name("x").is_some());
assert!(new_schema.field_by_name("y").is_some());
}
#[tokio::test]
async fn test_delete_missing_column_fails() {
let table = make_v2_table();
let tx = Transaction::new(&table);
let action = tx.update_schema().delete_column("nonexistent");
let result = Arc::new(action).commit(&table).await;
let err = match result {
Err(e) => e,
Ok(_) => panic!("should reject deleting a non-existent column"),
};
assert_eq!(err.kind(), ErrorKind::PreconditionFailed);
assert!(
err.message().contains("nonexistent"),
"error should mention the missing column, got: {}",
err.message()
);
}
#[tokio::test]
async fn test_add_and_delete_combined() {
let table = make_v2_table();
let tx = Transaction::new(&table);
let action = tx
.update_schema()
.delete_column("z")
.add_column(AddColumn::optional(
"w",
Type::Primitive(PrimitiveType::Boolean),
));
let mut action_commit = Arc::new(action).commit(&table).await.unwrap();
let updates = action_commit.take_updates();
let new_schema = match &updates[0] {
TableUpdate::AddSchema { schema } => schema,
other => panic!("expected AddSchema, got {other:?}"),
};
assert!(
new_schema.field_by_name("z").is_none(),
"z should be deleted"
);
let w = new_schema.field_by_name("w").expect("w should exist");
assert_eq!(w.id, 4);
assert!(!w.required);
}
#[tokio::test]
async fn test_delete_and_readd_same_name() {
let table = make_v2_table();
let tx = Transaction::new(&table);
let action = tx
.update_schema()
.delete_column("z")
.add_column(AddColumn::optional(
"z",
Type::Primitive(PrimitiveType::Boolean),
));
let mut action_commit = Arc::new(action).commit(&table).await.unwrap();
let updates = action_commit.take_updates();
let new_schema = match &updates[0] {
TableUpdate::AddSchema { schema } => schema,
other => panic!("expected AddSchema, got {other:?}"),
};
let z = new_schema
.field_by_name("z")
.expect("z should exist with new type");
assert_eq!(z.id, 4); assert_eq!(*z.field_type, Type::Primitive(PrimitiveType::Boolean));
}
#[test]
fn test_apply() {
let table = make_v2_table();
let tx = Transaction::new(&table);
let tx = tx
.update_schema()
.add_column(AddColumn::optional(
"new_col",
Type::Primitive(PrimitiveType::Int),
))
.apply(tx)
.unwrap();
assert_eq!(tx.actions.len(), 1);
(*tx.actions[0])
.downcast_ref::<UpdateSchemaAction>()
.expect("UpdateSchemaAction was not applied to Transaction!");
}
#[tokio::test]
async fn test_add_column_to_struct() {
let table = make_v2_table_with_nested();
let tx = Transaction::new(&table);
let action = tx.update_schema().add_column(
AddColumn::builder()
.name("email")
.field_type(Type::Primitive(PrimitiveType::String))
.parent("person")
.build(),
);
let mut action_commit = Arc::new(action).commit(&table).await.unwrap();
let updates = action_commit.take_updates();
let new_schema = match &updates[0] {
TableUpdate::AddSchema { schema } => schema,
other => panic!("expected AddSchema, got {other:?}"),
};
let email = new_schema
.field_by_name("person.email")
.expect("person.email should exist");
assert_eq!(email.id, 15);
assert!(!email.required);
assert_eq!(*email.field_type, Type::Primitive(PrimitiveType::String));
assert!(new_schema.field_by_name("person.name").is_some());
assert!(new_schema.field_by_name("person.age").is_some());
}
#[tokio::test]
async fn test_add_column_to_struct_with_doc() {
let table = make_v2_table_with_nested();
let tx = Transaction::new(&table);
let action = tx.update_schema().add_column(
AddColumn::builder()
.name("phone")
.field_type(Type::Primitive(PrimitiveType::String))
.parent("person")
.doc("Phone number")
.build(),
);
let mut action_commit = Arc::new(action).commit(&table).await.unwrap();
let updates = action_commit.take_updates();
let new_schema = match &updates[0] {
TableUpdate::AddSchema { schema } => schema,
other => panic!("expected AddSchema, got {other:?}"),
};
let phone = new_schema
.field_by_name("person.phone")
.expect("person.phone should exist");
assert_eq!(phone.id, 15);
assert_eq!(phone.doc.as_deref(), Some("Phone number"));
}
#[tokio::test]
async fn test_add_column_to_list_element_struct() {
let table = make_v2_table_with_nested();
let tx = Transaction::new(&table);
let action = tx.update_schema().add_column(
AddColumn::builder()
.name("score")
.field_type(Type::Primitive(PrimitiveType::Double))
.parent("tags")
.build(),
);
let mut action_commit = Arc::new(action).commit(&table).await.unwrap();
let updates = action_commit.take_updates();
let new_schema = match &updates[0] {
TableUpdate::AddSchema { schema } => schema,
other => panic!("expected AddSchema, got {other:?}"),
};
let score = new_schema
.field_by_name("tags.element.score")
.expect("tags.element.score should exist");
assert_eq!(score.id, 15);
assert!(!score.required);
assert!(new_schema.field_by_name("tags.element.key").is_some());
assert!(new_schema.field_by_name("tags.element.value").is_some());
}
#[tokio::test]
async fn test_add_column_to_map_value_struct() {
let table = make_v2_table_with_nested();
let tx = Transaction::new(&table);
let action = tx.update_schema().add_column(
AddColumn::builder()
.name("version")
.field_type(Type::Primitive(PrimitiveType::Int))
.parent("props")
.build(),
);
let mut action_commit = Arc::new(action).commit(&table).await.unwrap();
let updates = action_commit.take_updates();
let new_schema = match &updates[0] {
TableUpdate::AddSchema { schema } => schema,
other => panic!("expected AddSchema, got {other:?}"),
};
let version = new_schema
.field_by_name("props.value.version")
.expect("props.value.version should exist");
assert_eq!(version.id, 15);
assert!(new_schema.field_by_name("props.value.data").is_some());
}
#[tokio::test]
async fn test_add_column_to_nonexistent_parent_fails() {
let table = make_v2_table_with_nested();
let tx = Transaction::new(&table);
let action = tx.update_schema().add_column(
AddColumn::builder()
.name("col")
.field_type(Type::Primitive(PrimitiveType::Int))
.parent("nonexistent")
.build(),
);
let err = match Arc::new(action).commit(&table).await {
Err(e) => e,
Ok(_) => panic!("should reject adding to a nonexistent parent"),
};
assert_eq!(err.kind(), ErrorKind::PreconditionFailed);
assert!(
err.message().contains("nonexistent"),
"error should mention the missing parent, got: {}",
err.message()
);
}
#[tokio::test]
async fn test_add_column_to_primitive_parent_fails() {
let table = make_v2_table_with_nested();
let tx = Transaction::new(&table);
let action = tx.update_schema().add_column(
AddColumn::builder()
.name("col")
.field_type(Type::Primitive(PrimitiveType::Int))
.parent("x")
.build(),
);
let err = match Arc::new(action).commit(&table).await {
Err(e) => e,
Ok(_) => panic!("should reject adding to a primitive parent"),
};
assert_eq!(err.kind(), ErrorKind::PreconditionFailed);
assert!(
err.message().contains("not a struct"),
"error should mention type mismatch, got: {}",
err.message()
);
}
#[tokio::test]
async fn test_add_column_to_nested_name_conflict_fails() {
let table = make_v2_table_with_nested();
let tx = Transaction::new(&table);
let action = tx.update_schema().add_column(
AddColumn::builder()
.name("name")
.field_type(Type::Primitive(PrimitiveType::String))
.parent("person")
.build(),
);
let err = match Arc::new(action).commit(&table).await {
Err(e) => e,
Ok(_) => panic!("should reject adding a column with conflicting name"),
};
assert_eq!(err.kind(), ErrorKind::PreconditionFailed);
assert!(
err.message().contains("already exists"),
"error should mention name conflict, got: {}",
err.message()
);
}
#[tokio::test]
async fn test_root_and_nested_add_combined() {
let table = make_v2_table_with_nested();
let tx = Transaction::new(&table);
let action = tx
.update_schema()
.add_column(AddColumn::optional(
"root_col",
Type::Primitive(PrimitiveType::Boolean),
))
.add_column(
AddColumn::builder()
.name("email")
.field_type(Type::Primitive(PrimitiveType::String))
.parent("person")
.build(),
);
let mut action_commit = Arc::new(action).commit(&table).await.unwrap();
let updates = action_commit.take_updates();
let new_schema = match &updates[0] {
TableUpdate::AddSchema { schema } => schema,
other => panic!("expected AddSchema, got {other:?}"),
};
let root_col = new_schema
.field_by_name("root_col")
.expect("root_col should exist");
assert_eq!(root_col.id, 15);
let email = new_schema
.field_by_name("person.email")
.expect("person.email should exist");
assert_eq!(email.id, 16);
}
#[tokio::test]
async fn test_add_nested_struct_type_with_fresh_ids() {
let table = make_v2_table();
let tx = Transaction::new(&table);
let action = tx.update_schema().add_column(AddColumn::optional(
"address",
Type::Struct(StructType::new(vec![
NestedField::optional(
DEFAULT_FIELD_ID,
"street",
Type::Primitive(PrimitiveType::String),
)
.into(),
NestedField::optional(
DEFAULT_FIELD_ID,
"city",
Type::Primitive(PrimitiveType::String),
)
.into(),
])),
));
let mut action_commit = Arc::new(action).commit(&table).await.unwrap();
let updates = action_commit.take_updates();
let new_schema = match &updates[0] {
TableUpdate::AddSchema { schema } => schema,
other => panic!("expected AddSchema, got {other:?}"),
};
let address = new_schema
.field_by_name("address")
.expect("address should exist");
assert_eq!(address.id, 4);
let street = new_schema
.field_by_name("address.street")
.expect("address.street should exist");
assert_eq!(street.id, 5);
let city = new_schema
.field_by_name("address.city")
.expect("address.city should exist");
assert_eq!(city.id, 6);
}
}