1use std::collections::{HashMap, HashSet};
19use std::sync::Arc;
20
21use async_trait::async_trait;
22use typed_builder::TypedBuilder;
23
24use crate::spec::{
25 ListType, Literal, MapType, NestedField, NestedFieldRef, SCHEMA_NAME_DELIMITER, Schema,
26 StructType, Type,
27};
28use crate::table::Table;
29use crate::transaction::action::{ActionCommit, TransactionAction};
30use crate::{Error, ErrorKind, Result, TableRequirement, TableUpdate};
31
32const DEFAULT_FIELD_ID: i32 = 0;
34
35#[derive(TypedBuilder)]
42pub struct AddColumn {
43 #[builder(default = None, setter(strip_option, into))]
44 parent: Option<String>,
45 #[builder(setter(into))]
46 name: String,
47 #[builder(default = false)]
48 required: bool,
49 field_type: Type,
50 #[builder(default = None, setter(strip_option, into))]
51 doc: Option<String>,
52 #[builder(default = None, setter(strip_option))]
53 initial_default: Option<Literal>,
54 #[builder(default = None, setter(strip_option))]
55 write_default: Option<Literal>,
56}
57
58impl AddColumn {
59 pub fn optional(name: impl ToString, field_type: Type) -> Self {
61 Self::builder()
62 .name(name.to_string())
63 .field_type(field_type)
64 .required(false)
65 .build()
66 }
67
68 pub fn required(name: impl ToString, field_type: Type, initial_default: Literal) -> Self {
70 Self::builder()
71 .name(name.to_string())
72 .field_type(field_type)
73 .required(true)
74 .initial_default(initial_default.clone())
75 .write_default(initial_default)
76 .build()
77 }
78
79 fn to_nested_field(&self) -> NestedFieldRef {
80 let mut field = NestedField::new(
81 DEFAULT_FIELD_ID,
82 self.name.clone(),
83 self.field_type.clone(),
84 self.required,
85 );
86
87 field.doc = self.doc.clone();
88 field.initial_default = self.initial_default.clone();
89 field.write_default = self.write_default.clone();
90 Arc::new(field)
91 }
92}
93
94pub struct UpdateSchemaAction {
117 additions: Vec<AddColumn>,
118 deletes: Vec<String>,
119}
120
121impl UpdateSchemaAction {
122 pub(crate) fn new() -> Self {
124 Self {
125 additions: Vec::new(),
126 deletes: Vec::new(),
127 }
128 }
129
130 pub fn add_column(mut self, add_column: AddColumn) -> Self {
138 self.additions.push(add_column);
139 self
140 }
141
142 pub fn delete_column(mut self, name: impl ToString) -> Self {
148 self.deletes.push(name.to_string());
149 self
150 }
151}
152
153fn assign_fresh_ids(field: &NestedField, next_id: &mut i32) -> NestedFieldRef {
165 *next_id += 1;
166 let new_id = *next_id;
167 let new_type = assign_fresh_ids_to_type(&field.field_type, next_id);
168
169 Arc::new(NestedField {
170 id: new_id,
171 name: field.name.clone(),
172 required: field.required,
173 field_type: Box::new(new_type),
174 doc: field.doc.clone(),
175 initial_default: field.initial_default.clone(),
176 write_default: field.write_default.clone(),
177 })
178}
179
180fn assign_fresh_ids_to_type(field_type: &Type, next_id: &mut i32) -> Type {
182 match field_type {
183 Type::Primitive(_) => field_type.clone(),
184 Type::Variant(v) => Type::Variant(*v),
187 Type::Struct(struct_type) => {
188 let new_fields: Vec<NestedFieldRef> = struct_type
189 .fields()
190 .iter()
191 .map(|f| assign_fresh_ids(f, next_id))
192 .collect();
193 Type::Struct(StructType::new(new_fields))
194 }
195 Type::List(list_type) => {
196 let new_element = assign_fresh_ids(&list_type.element_field, next_id);
197 Type::List(ListType {
198 element_field: new_element,
199 })
200 }
201 Type::Map(map_type) => {
202 let new_key = assign_fresh_ids(&map_type.key_field, next_id);
203 let new_value = assign_fresh_ids(&map_type.value_field, next_id);
204 Type::Map(MapType {
205 key_field: new_key,
206 value_field: new_value,
207 })
208 }
209 }
210}
211
212fn resolve_parent_target<'a>(
222 base_schema: &'a Schema,
223 parent: &str,
224) -> Result<(i32, &'a StructType)> {
225 base_schema
226 .field_by_name(parent)
227 .ok_or_else(|| {
228 Error::new(
229 ErrorKind::PreconditionFailed,
230 format!("Cannot add column: parent '{parent}' not found"),
231 )
232 })
233 .and_then(|parent_field| match parent_field.field_type.as_ref() {
234 Type::Struct(s) => Ok((parent_field.id, s)),
235 Type::Map(m) => match m.value_field.field_type.as_ref() {
236 Type::Struct(s) => Ok((m.value_field.id, s)),
237 _ => Err(Error::new(
238 ErrorKind::PreconditionFailed,
239 format!("Cannot add column: map value of '{parent}' is not a struct"),
240 )),
241 },
242 Type::List(l) => match l.element_field.field_type.as_ref() {
243 Type::Struct(s) => Ok((l.element_field.id, s)),
244 _ => Err(Error::new(
245 ErrorKind::PreconditionFailed,
246 format!("Cannot add column: list element of '{parent}' is not a struct"),
247 )),
248 },
249 _ => Err(Error::new(
250 ErrorKind::PreconditionFailed,
251 format!("Cannot add column: parent '{parent}' is not a struct, map, or list"),
252 )),
253 })
254}
255
256fn rebuild_fields(
263 fields: &[NestedFieldRef],
264 adds: &HashMap<Option<i32>, Vec<NestedFieldRef>>,
265 delete_ids: &HashSet<i32>,
266 parent_id: Option<i32>,
267) -> Vec<NestedFieldRef> {
268 fields
269 .iter()
270 .filter(|f| !delete_ids.contains(&f.id))
271 .map(|f| rebuild_field(f, adds, delete_ids))
272 .chain(adds.get(&parent_id).into_iter().flatten().cloned())
273 .collect()
274}
275
276fn rebuild_field(
280 field: &NestedFieldRef,
281 adds: &HashMap<Option<i32>, Vec<NestedFieldRef>>,
282 delete_ids: &HashSet<i32>,
283) -> NestedFieldRef {
284 match field.field_type.as_ref() {
285 Type::Primitive(_) | Type::Variant(_) => field.clone(),
286 Type::Struct(s) => {
287 let new_fields = rebuild_fields(s.fields(), adds, delete_ids, Some(field.id));
288 Arc::new(NestedField {
289 id: field.id,
290 name: field.name.clone(),
291 required: field.required,
292 field_type: Box::new(Type::Struct(StructType::new(new_fields))),
293 doc: field.doc.clone(),
294 initial_default: field.initial_default.clone(),
295 write_default: field.write_default.clone(),
296 })
297 }
298 Type::List(l) => {
299 let new_element = rebuild_field(&l.element_field, adds, delete_ids);
300 Arc::new(NestedField {
301 id: field.id,
302 name: field.name.clone(),
303 required: field.required,
304 field_type: Box::new(Type::List(ListType {
305 element_field: new_element,
306 })),
307 doc: field.doc.clone(),
308 initial_default: field.initial_default.clone(),
309 write_default: field.write_default.clone(),
310 })
311 }
312 Type::Map(m) => {
313 let new_key = rebuild_field(&m.key_field, adds, delete_ids);
314 let new_value = rebuild_field(&m.value_field, adds, delete_ids);
315 Arc::new(NestedField {
316 id: field.id,
317 name: field.name.clone(),
318 required: field.required,
319 field_type: Box::new(Type::Map(MapType {
320 key_field: new_key,
321 value_field: new_value,
322 })),
323 doc: field.doc.clone(),
324 initial_default: field.initial_default.clone(),
325 write_default: field.write_default.clone(),
326 })
327 }
328 }
329}
330
331#[async_trait]
336impl TransactionAction for UpdateSchemaAction {
337 async fn commit(self: Arc<Self>, table: &Table) -> Result<ActionCommit> {
338 let base_schema = table.metadata().current_schema();
339 let mut last_column_id = table.metadata().last_column_id();
340
341 let delete_ids = self
343 .deletes
344 .iter()
345 .map(|name: &String| {
346 base_schema
347 .field_by_name(name)
348 .ok_or_else(|| {
349 Error::new(
350 ErrorKind::PreconditionFailed,
351 format!("Cannot delete missing column: {name}"),
352 )
353 })
354 .and_then(|field| {
355 match base_schema
356 .identifier_field_ids()
357 .find(|id| *id == field.id)
358 {
359 Some(_) => Err(Error::new(
360 ErrorKind::PreconditionFailed,
361 format!("Cannot delete identifier field: {name}"),
362 )),
363 None => Ok(field.id),
364 }
365 })
366 })
367 .collect::<Result<HashSet<i32>>>()?;
368
369 let mut additions_by_parent: HashMap<Option<i32>, Vec<NestedFieldRef>> = HashMap::new();
373
374 for add in &self.additions {
375 let pending_field = add.to_nested_field();
376
377 if pending_field.name.contains(SCHEMA_NAME_DELIMITER) {
379 return Err(Error::new(
380 ErrorKind::PreconditionFailed,
381 format!(
382 "Cannot add column with ambiguous name: {}. Use `AddColumn::with_parent` to add a column to a nested struct.",
383 pending_field.name
384 ),
385 ));
386 }
387
388 if pending_field.required && pending_field.initial_default.is_none() {
390 return Err(Error::new(
391 ErrorKind::PreconditionFailed,
392 format!(
393 "Incompatible change: cannot add required column without an initial default: {}",
394 pending_field.name
395 ),
396 ));
397 }
398
399 let parent_id = match &add.parent {
400 None => {
401 if let Some(existing) = base_schema.field_by_name(&pending_field.name)
403 && !delete_ids.contains(&existing.id)
404 {
405 return Err(Error::new(
406 ErrorKind::PreconditionFailed,
407 format!(
408 "Cannot add column, name already exists: {}",
409 pending_field.name
410 ),
411 ));
412 }
413 None
414 }
415 Some(parent_path) => {
416 let (resolved_parent_id, parent_struct) =
418 resolve_parent_target(base_schema, parent_path)?;
419
420 if parent_struct.fields().iter().any(|f| {
421 f.name == pending_field.name
422 && !delete_ids.contains(&f.id)
423 && !delete_ids.contains(&resolved_parent_id)
424 }) {
425 return Err(Error::new(
426 ErrorKind::PreconditionFailed,
427 format!(
428 "Cannot add column, name already exists in '{}': {}",
429 parent_path, pending_field.name
430 ),
431 ));
432 }
433
434 Some(resolved_parent_id)
435 }
436 };
437
438 let field = assign_fresh_ids(&pending_field, &mut last_column_id);
440
441 additions_by_parent
442 .entry(parent_id)
443 .or_default()
444 .push(field);
445 }
446
447 let new_fields = rebuild_fields(
449 base_schema.as_struct().fields(),
450 &additions_by_parent,
451 &delete_ids,
452 None,
453 );
454
455 let schema = Schema::builder()
457 .with_fields(new_fields)
458 .with_identifier_field_ids(base_schema.identifier_field_ids())
459 .build()?;
460
461 let updates = vec![
462 TableUpdate::AddSchema { schema },
463 TableUpdate::SetCurrentSchema { schema_id: -1 },
464 ];
465
466 let requirements = vec![TableRequirement::CurrentSchemaIdMatch {
467 current_schema_id: base_schema.schema_id(),
468 }];
469
470 Ok(ActionCommit::new(updates, requirements))
471 }
472}
473
474#[cfg(test)]
475mod tests {
476 use std::io::BufReader;
477 use std::sync::Arc;
478
479 use as_any::Downcast;
480
481 use crate::spec::{
482 DEFAULT_SCHEMA_ID, Literal, NestedField, PrimitiveType, StructType, TableMetadata, Type,
483 VariantType,
484 };
485 use crate::table::Table;
486 use crate::transaction::Transaction;
487 use crate::transaction::action::{ApplyTransactionAction, TransactionAction};
488 use crate::transaction::tests::make_v2_table;
489 use crate::transaction::update_schema::{AddColumn, DEFAULT_FIELD_ID, UpdateSchemaAction};
490 use crate::{ErrorKind, TableIdent, TableRequirement, TableUpdate};
491
492 fn make_v2_table_with_nested() -> Table {
516 let json = r#"{
517 "format-version": 2,
518 "table-uuid": "9c12d441-03fe-4693-9a96-a0705ddf69c2",
519 "location": "s3://bucket/test/location",
520 "last-sequence-number": 0,
521 "last-updated-ms": 1602638573590,
522 "last-column-id": 14,
523 "current-schema-id": 0,
524 "schemas": [
525 {
526 "type": "struct",
527 "schema-id": 0,
528 "identifier-field-ids": [1, 2],
529 "fields": [
530 {"id": 1, "name": "x", "required": true, "type": "long"},
531 {"id": 2, "name": "y", "required": true, "type": "long"},
532 {"id": 3, "name": "z", "required": true, "type": "long"},
533 {"id": 4, "name": "person", "required": false, "type": {
534 "type": "struct",
535 "fields": [
536 {"id": 5, "name": "name", "required": false, "type": "string"},
537 {"id": 6, "name": "age", "required": true, "type": "int"}
538 ]
539 }},
540 {"id": 7, "name": "tags", "required": false, "type": {
541 "type": "list",
542 "element-id": 8,
543 "element": {
544 "type": "struct",
545 "fields": [
546 {"id": 9, "name": "key", "required": false, "type": "string"},
547 {"id": 10, "name": "value", "required": false, "type": "string"}
548 ]
549 },
550 "element-required": true
551 }},
552 {"id": 11, "name": "props", "required": false, "type": {
553 "type": "map",
554 "key-id": 12,
555 "key": "string",
556 "value-id": 13,
557 "value": {
558 "type": "struct",
559 "fields": [
560 {"id": 14, "name": "data", "required": false, "type": "string"}
561 ]
562 },
563 "value-required": true
564 }}
565 ]
566 }
567 ],
568 "default-spec-id": 0,
569 "partition-specs": [
570 {"spec-id": 0, "fields": []}
571 ],
572 "last-partition-id": 999,
573 "default-sort-order-id": 0,
574 "sort-orders": [
575 {"order-id": 0, "fields": []}
576 ],
577 "properties": {},
578 "current-snapshot-id": -1,
579 "snapshots": []
580 }"#;
581
582 let reader = BufReader::new(json.as_bytes());
583 let metadata = serde_json::from_reader::<_, TableMetadata>(reader).unwrap();
584
585 Table::builder()
586 .metadata(metadata)
587 .metadata_location("s3://bucket/test/location/metadata/v1.json".to_string())
588 .identifier(TableIdent::from_strs(["ns1", "test1"]).unwrap())
589 .file_io(crate::io::FileIO::new_with_memory())
590 .runtime(crate::test_utils::test_runtime())
591 .build()
592 .unwrap()
593 }
594
595 #[test]
600 fn test_assign_fresh_ids_variant() {
601 let mut next_id = 10;
604 let field = NestedField::optional(1, "data", Type::Variant(VariantType));
605 let assigned = super::assign_fresh_ids(&field, &mut next_id);
606
607 assert_eq!(assigned.id, 11);
608 assert_eq!(*assigned.field_type, Type::Variant(VariantType));
609 assert_eq!(next_id, 11);
610 }
611
612 #[tokio::test]
613 async fn test_add_column() {
614 let table = make_v2_table();
615 let tx = Transaction::new(&table);
616
617 let action = tx.update_schema().add_column(AddColumn::optional(
618 "new_col",
619 Type::Primitive(PrimitiveType::Int),
620 ));
621
622 let mut action_commit = Arc::new(action).commit(&table).await.unwrap();
623 let updates = action_commit.take_updates();
624 let requirements = action_commit.take_requirements();
625
626 assert_eq!(updates.len(), 2);
627
628 let new_schema = match &updates[0] {
630 TableUpdate::AddSchema { schema } => schema,
631 other => panic!("expected AddSchema, got {other:?}"),
632 };
633
634 let expected_schema = table
635 .metadata()
636 .current_schema()
637 .as_ref()
638 .clone()
639 .into_builder()
640 .with_schema_id(DEFAULT_SCHEMA_ID)
641 .with_fields([
642 NestedField::optional(4, "new_col", Type::Primitive(PrimitiveType::Int)).into(),
643 ])
644 .build()
645 .unwrap();
646 assert_eq!(new_schema, &expected_schema);
647
648 assert_eq!(updates[1], TableUpdate::SetCurrentSchema { schema_id: -1 });
649
650 assert_eq!(requirements.len(), 1);
652 assert_eq!(requirements[0], TableRequirement::CurrentSchemaIdMatch {
653 current_schema_id: table.metadata().current_schema().schema_id()
654 });
655 }
656
657 #[tokio::test]
658 async fn test_add_column_with_doc() {
659 let table = make_v2_table();
660 let tx = Transaction::new(&table);
661
662 let action = tx.update_schema().add_column(
663 AddColumn::builder()
664 .name("documented_col")
665 .field_type(Type::Primitive(PrimitiveType::String))
666 .doc("A documented column")
667 .build(),
668 );
669
670 let mut action_commit = Arc::new(action).commit(&table).await.unwrap();
671 let updates = action_commit.take_updates();
672
673 let new_schema = match &updates[0] {
674 TableUpdate::AddSchema { schema } => schema,
675 other => panic!("expected AddSchema, got {other:?}"),
676 };
677
678 let field = new_schema
679 .field_by_name("documented_col")
680 .expect("documented_col should exist");
681 assert_eq!(field.id, 4);
682 assert!(!field.required);
683 assert_eq!(field.doc.as_deref(), Some("A documented column"));
684 }
685
686 #[tokio::test]
687 async fn test_add_required_column_with_initial_default() {
688 let table = make_v2_table();
689 let tx = Transaction::new(&table);
690
691 let action = tx.update_schema().add_column(AddColumn::required(
692 "req_col",
693 Type::Primitive(PrimitiveType::Int),
694 Literal::int(0),
695 ));
696
697 let mut action_commit = Arc::new(action).commit(&table).await.unwrap();
698 let updates = action_commit.take_updates();
699
700 let new_schema = match &updates[0] {
701 TableUpdate::AddSchema { schema } => schema,
702 other => panic!("expected AddSchema, got {other:?}"),
703 };
704
705 let field = new_schema
706 .field_by_name("req_col")
707 .expect("req_col should exist");
708 assert_eq!(field.id, 4);
709 assert!(field.required);
710 assert_eq!(field.initial_default, Some(Literal::int(0)));
711 assert_eq!(field.write_default, Some(Literal::int(0)));
712 }
713
714 #[tokio::test]
715 async fn test_add_column_name_conflict_fails() {
716 let table = make_v2_table();
717 let tx = Transaction::new(&table);
718
719 let action = tx.update_schema().add_column(AddColumn::optional(
721 "x",
722 Type::Primitive(PrimitiveType::Int),
723 ));
724
725 let result = Arc::new(action).commit(&table).await;
726 let err = match result {
727 Err(e) => e,
728 Ok(_) => panic!("should reject adding a column with an existing name"),
729 };
730 assert_eq!(err.kind(), ErrorKind::PreconditionFailed);
731 assert!(
732 err.message().contains("already exists"),
733 "error should mention name conflict, got: {}",
734 err.message()
735 );
736 }
737
738 #[tokio::test]
739 async fn test_delete_column() {
740 let table = make_v2_table();
741 let tx = Transaction::new(&table);
742
743 let action = tx.update_schema().delete_column("z");
745
746 let mut action_commit = Arc::new(action).commit(&table).await.unwrap();
747 let updates = action_commit.take_updates();
748
749 let new_schema = match &updates[0] {
750 TableUpdate::AddSchema { schema } => schema,
751 other => panic!("expected AddSchema, got {other:?}"),
752 };
753
754 assert!(
755 new_schema.field_by_name("z").is_none(),
756 "z should be deleted"
757 );
758 assert!(new_schema.field_by_name("x").is_some());
759 assert!(new_schema.field_by_name("y").is_some());
760 }
761
762 #[tokio::test]
763 async fn test_delete_missing_column_fails() {
764 let table = make_v2_table();
765 let tx = Transaction::new(&table);
766
767 let action = tx.update_schema().delete_column("nonexistent");
768
769 let result = Arc::new(action).commit(&table).await;
770 let err = match result {
771 Err(e) => e,
772 Ok(_) => panic!("should reject deleting a non-existent column"),
773 };
774 assert_eq!(err.kind(), ErrorKind::PreconditionFailed);
775 assert!(
776 err.message().contains("nonexistent"),
777 "error should mention the missing column, got: {}",
778 err.message()
779 );
780 }
781
782 #[tokio::test]
783 async fn test_add_and_delete_combined() {
784 let table = make_v2_table();
785 let tx = Transaction::new(&table);
786
787 let action = tx
789 .update_schema()
790 .delete_column("z")
791 .add_column(AddColumn::optional(
792 "w",
793 Type::Primitive(PrimitiveType::Boolean),
794 ));
795
796 let mut action_commit = Arc::new(action).commit(&table).await.unwrap();
797 let updates = action_commit.take_updates();
798
799 let new_schema = match &updates[0] {
800 TableUpdate::AddSchema { schema } => schema,
801 other => panic!("expected AddSchema, got {other:?}"),
802 };
803
804 assert!(
805 new_schema.field_by_name("z").is_none(),
806 "z should be deleted"
807 );
808 let w = new_schema.field_by_name("w").expect("w should exist");
809 assert_eq!(w.id, 4);
810 assert!(!w.required);
811 }
812
813 #[tokio::test]
814 async fn test_delete_and_readd_same_name() {
815 let table = make_v2_table();
816 let tx = Transaction::new(&table);
817
818 let action = tx
820 .update_schema()
821 .delete_column("z")
822 .add_column(AddColumn::optional(
823 "z",
824 Type::Primitive(PrimitiveType::Boolean),
825 ));
826
827 let mut action_commit = Arc::new(action).commit(&table).await.unwrap();
828 let updates = action_commit.take_updates();
829
830 let new_schema = match &updates[0] {
831 TableUpdate::AddSchema { schema } => schema,
832 other => panic!("expected AddSchema, got {other:?}"),
833 };
834
835 let z = new_schema
836 .field_by_name("z")
837 .expect("z should exist with new type");
838 assert_eq!(z.id, 4); assert_eq!(*z.field_type, Type::Primitive(PrimitiveType::Boolean));
840 }
841
842 #[test]
843 fn test_apply() {
844 let table = make_v2_table();
845 let tx = Transaction::new(&table);
846
847 let tx = tx
848 .update_schema()
849 .add_column(AddColumn::optional(
850 "new_col",
851 Type::Primitive(PrimitiveType::Int),
852 ))
853 .apply(tx)
854 .unwrap();
855
856 assert_eq!(tx.actions.len(), 1);
857 (*tx.actions[0])
858 .downcast_ref::<UpdateSchemaAction>()
859 .expect("UpdateSchemaAction was not applied to Transaction!");
860 }
861
862 #[tokio::test]
867 async fn test_add_column_to_struct() {
868 let table = make_v2_table_with_nested();
869 let tx = Transaction::new(&table);
870
871 let action = tx.update_schema().add_column(
873 AddColumn::builder()
874 .name("email")
875 .field_type(Type::Primitive(PrimitiveType::String))
876 .parent("person")
877 .build(),
878 );
879
880 let mut action_commit = Arc::new(action).commit(&table).await.unwrap();
881 let updates = action_commit.take_updates();
882
883 let new_schema = match &updates[0] {
884 TableUpdate::AddSchema { schema } => schema,
885 other => panic!("expected AddSchema, got {other:?}"),
886 };
887
888 let email = new_schema
890 .field_by_name("person.email")
891 .expect("person.email should exist");
892 assert_eq!(email.id, 15);
893 assert!(!email.required);
894 assert_eq!(*email.field_type, Type::Primitive(PrimitiveType::String));
895
896 assert!(new_schema.field_by_name("person.name").is_some());
898 assert!(new_schema.field_by_name("person.age").is_some());
899 }
900
901 #[tokio::test]
902 async fn test_add_column_to_struct_with_doc() {
903 let table = make_v2_table_with_nested();
904 let tx = Transaction::new(&table);
905
906 let action = tx.update_schema().add_column(
907 AddColumn::builder()
908 .name("phone")
909 .field_type(Type::Primitive(PrimitiveType::String))
910 .parent("person")
911 .doc("Phone number")
912 .build(),
913 );
914
915 let mut action_commit = Arc::new(action).commit(&table).await.unwrap();
916 let updates = action_commit.take_updates();
917
918 let new_schema = match &updates[0] {
919 TableUpdate::AddSchema { schema } => schema,
920 other => panic!("expected AddSchema, got {other:?}"),
921 };
922
923 let phone = new_schema
924 .field_by_name("person.phone")
925 .expect("person.phone should exist");
926 assert_eq!(phone.id, 15);
927 assert_eq!(phone.doc.as_deref(), Some("Phone number"));
928 }
929
930 #[tokio::test]
931 async fn test_add_column_to_list_element_struct() {
932 let table = make_v2_table_with_nested();
933 let tx = Transaction::new(&table);
934
935 let action = tx.update_schema().add_column(
938 AddColumn::builder()
939 .name("score")
940 .field_type(Type::Primitive(PrimitiveType::Double))
941 .parent("tags")
942 .build(),
943 );
944
945 let mut action_commit = Arc::new(action).commit(&table).await.unwrap();
946 let updates = action_commit.take_updates();
947
948 let new_schema = match &updates[0] {
949 TableUpdate::AddSchema { schema } => schema,
950 other => panic!("expected AddSchema, got {other:?}"),
951 };
952
953 let score = new_schema
955 .field_by_name("tags.element.score")
956 .expect("tags.element.score should exist");
957 assert_eq!(score.id, 15);
958 assert!(!score.required);
959
960 assert!(new_schema.field_by_name("tags.element.key").is_some());
962 assert!(new_schema.field_by_name("tags.element.value").is_some());
963 }
964
965 #[tokio::test]
966 async fn test_add_column_to_map_value_struct() {
967 let table = make_v2_table_with_nested();
968 let tx = Transaction::new(&table);
969
970 let action = tx.update_schema().add_column(
973 AddColumn::builder()
974 .name("version")
975 .field_type(Type::Primitive(PrimitiveType::Int))
976 .parent("props")
977 .build(),
978 );
979
980 let mut action_commit = Arc::new(action).commit(&table).await.unwrap();
981 let updates = action_commit.take_updates();
982
983 let new_schema = match &updates[0] {
984 TableUpdate::AddSchema { schema } => schema,
985 other => panic!("expected AddSchema, got {other:?}"),
986 };
987
988 let version = new_schema
989 .field_by_name("props.value.version")
990 .expect("props.value.version should exist");
991 assert_eq!(version.id, 15);
992
993 assert!(new_schema.field_by_name("props.value.data").is_some());
995 }
996
997 #[tokio::test]
998 async fn test_add_column_to_nonexistent_parent_fails() {
999 let table = make_v2_table_with_nested();
1000 let tx = Transaction::new(&table);
1001
1002 let action = tx.update_schema().add_column(
1003 AddColumn::builder()
1004 .name("col")
1005 .field_type(Type::Primitive(PrimitiveType::Int))
1006 .parent("nonexistent")
1007 .build(),
1008 );
1009
1010 let err = match Arc::new(action).commit(&table).await {
1011 Err(e) => e,
1012 Ok(_) => panic!("should reject adding to a nonexistent parent"),
1013 };
1014 assert_eq!(err.kind(), ErrorKind::PreconditionFailed);
1015 assert!(
1016 err.message().contains("nonexistent"),
1017 "error should mention the missing parent, got: {}",
1018 err.message()
1019 );
1020 }
1021
1022 #[tokio::test]
1023 async fn test_add_column_to_primitive_parent_fails() {
1024 let table = make_v2_table_with_nested();
1025 let tx = Transaction::new(&table);
1026
1027 let action = tx.update_schema().add_column(
1029 AddColumn::builder()
1030 .name("col")
1031 .field_type(Type::Primitive(PrimitiveType::Int))
1032 .parent("x")
1033 .build(),
1034 );
1035
1036 let err = match Arc::new(action).commit(&table).await {
1037 Err(e) => e,
1038 Ok(_) => panic!("should reject adding to a primitive parent"),
1039 };
1040 assert_eq!(err.kind(), ErrorKind::PreconditionFailed);
1041 assert!(
1042 err.message().contains("not a struct"),
1043 "error should mention type mismatch, got: {}",
1044 err.message()
1045 );
1046 }
1047
1048 #[tokio::test]
1049 async fn test_add_column_to_nested_name_conflict_fails() {
1050 let table = make_v2_table_with_nested();
1051 let tx = Transaction::new(&table);
1052
1053 let action = tx.update_schema().add_column(
1055 AddColumn::builder()
1056 .name("name")
1057 .field_type(Type::Primitive(PrimitiveType::String))
1058 .parent("person")
1059 .build(),
1060 );
1061
1062 let err = match Arc::new(action).commit(&table).await {
1063 Err(e) => e,
1064 Ok(_) => panic!("should reject adding a column with conflicting name"),
1065 };
1066 assert_eq!(err.kind(), ErrorKind::PreconditionFailed);
1067 assert!(
1068 err.message().contains("already exists"),
1069 "error should mention name conflict, got: {}",
1070 err.message()
1071 );
1072 }
1073
1074 #[tokio::test]
1075 async fn test_root_and_nested_add_combined() {
1076 let table = make_v2_table_with_nested();
1077 let tx = Transaction::new(&table);
1078
1079 let action = tx
1081 .update_schema()
1082 .add_column(AddColumn::optional(
1083 "root_col",
1084 Type::Primitive(PrimitiveType::Boolean),
1085 ))
1086 .add_column(
1087 AddColumn::builder()
1088 .name("email")
1089 .field_type(Type::Primitive(PrimitiveType::String))
1090 .parent("person")
1091 .build(),
1092 );
1093
1094 let mut action_commit = Arc::new(action).commit(&table).await.unwrap();
1095 let updates = action_commit.take_updates();
1096
1097 let new_schema = match &updates[0] {
1098 TableUpdate::AddSchema { schema } => schema,
1099 other => panic!("expected AddSchema, got {other:?}"),
1100 };
1101
1102 let root_col = new_schema
1104 .field_by_name("root_col")
1105 .expect("root_col should exist");
1106 assert_eq!(root_col.id, 15);
1107
1108 let email = new_schema
1110 .field_by_name("person.email")
1111 .expect("person.email should exist");
1112 assert_eq!(email.id, 16);
1113 }
1114
1115 #[tokio::test]
1116 async fn test_add_nested_struct_type_with_fresh_ids() {
1117 let table = make_v2_table();
1120 let tx = Transaction::new(&table);
1121
1122 let action = tx.update_schema().add_column(AddColumn::optional(
1123 "address",
1124 Type::Struct(StructType::new(vec![
1125 NestedField::optional(
1126 DEFAULT_FIELD_ID,
1127 "street",
1128 Type::Primitive(PrimitiveType::String),
1129 )
1130 .into(),
1131 NestedField::optional(
1132 DEFAULT_FIELD_ID,
1133 "city",
1134 Type::Primitive(PrimitiveType::String),
1135 )
1136 .into(),
1137 ])),
1138 ));
1139
1140 let mut action_commit = Arc::new(action).commit(&table).await.unwrap();
1141 let updates = action_commit.take_updates();
1142
1143 let new_schema = match &updates[0] {
1144 TableUpdate::AddSchema { schema } => schema,
1145 other => panic!("expected AddSchema, got {other:?}"),
1146 };
1147
1148 let address = new_schema
1150 .field_by_name("address")
1151 .expect("address should exist");
1152 assert_eq!(address.id, 4);
1153
1154 let street = new_schema
1156 .field_by_name("address.street")
1157 .expect("address.street should exist");
1158 assert_eq!(street.id, 5);
1159
1160 let city = new_schema
1161 .field_by_name("address.city")
1162 .expect("address.city should exist");
1163 assert_eq!(city.id, 6);
1164 }
1165}