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