1use arrow::datatypes::{DataType as ArrowDataType, Field, Fields, Schema};
55use thiserror::Error;
56
57use crate::export::arrow_convert::cql_type_to_arrow_data_type;
58use crate::schema::{CqlType, TableSchema};
59
60#[derive(Debug, Error)]
69pub enum DeltaSchemaError {
70 #[error(
76 "Column '{column}' collides with envelope reserved name '{reserved}'. \
77 Use DeltaSchemaOpts::envelope_prefix to choose a different prefix \
78 (e.g. envelope_prefix = \"_cqlite_\" gives \"_cqlite_op\", \"_cqlite_ts\", etc.)."
79 )]
80 ColumnCollision {
81 column: String,
83 reserved: String,
85 },
86
87 #[error(
93 "Counter tables cannot be projected to the delta envelope. \
94 Table '{keyspace}.{table}' contains counter column(s): {columns}. \
95 Counter semantics (distributed add/subtract) are incompatible with \
96 the per-cell writetime delta model."
97 )]
98 CounterTable {
99 keyspace: String,
101 table: String,
103 columns: String,
105 },
106
107 #[error("CQL type parse error for column '{column}': {source}")]
109 CqlTypeParse {
110 column: String,
112 #[source]
114 source: crate::error::Error,
115 },
116}
117
118#[derive(Debug, Clone)]
126pub struct DeltaSchemaOpts {
127 pub envelope_prefix: String,
134}
135
136impl Default for DeltaSchemaOpts {
137 fn default() -> Self {
138 Self {
139 envelope_prefix: "__".to_string(),
140 }
141 }
142}
143
144impl DeltaSchemaOpts {
145 pub fn with_prefix(prefix: impl Into<String>) -> Self {
147 Self {
148 envelope_prefix: prefix.into(),
149 }
150 }
151
152 pub fn op_col(&self) -> String {
154 format!("{}op", self.envelope_prefix)
155 }
156
157 pub fn ts_col(&self) -> String {
159 format!("{}ts", self.envelope_prefix)
160 }
161
162 pub fn range_start_col(&self) -> String {
164 format!("{}range_start", self.envelope_prefix)
165 }
166
167 pub fn range_end_col(&self) -> String {
169 format!("{}range_end", self.envelope_prefix)
170 }
171
172 fn reserved_names(&self) -> [String; 4] {
174 [
175 self.op_col(),
176 self.ts_col(),
177 self.range_start_col(),
178 self.range_end_col(),
179 ]
180 }
181}
182
183pub fn derive_delta_schema(
212 table: &TableSchema,
213 opts: &DeltaSchemaOpts,
214) -> Result<Schema, DeltaSchemaError> {
215 let counter_cols: Vec<String> = table
219 .columns
220 .iter()
221 .filter(|col| {
222 CqlType::parse(&col.data_type)
224 .map(|t| is_counter_type(&t))
225 .unwrap_or(false)
226 })
227 .map(|col| col.name.clone())
228 .collect();
229
230 if !counter_cols.is_empty() {
231 return Err(DeltaSchemaError::CounterTable {
232 keyspace: table.keyspace.clone(),
233 table: table.table.clone(),
234 columns: counter_cols.join(", "),
235 });
236 }
237
238 let reserved = opts.reserved_names();
248
249 let all_column_names = table
251 .partition_keys
252 .iter()
253 .map(|k| &k.name)
254 .chain(table.clustering_keys.iter().map(|k| &k.name))
255 .chain(table.columns.iter().map(|c| &c.name));
256
257 for col_name in all_column_names {
258 for res in &reserved {
259 if col_name == res {
260 return Err(DeltaSchemaError::ColumnCollision {
261 column: col_name.clone(),
262 reserved: res.clone(),
263 });
264 }
265 }
266 }
267
268 let mut fields: Vec<Field> = Vec::new();
272
273 let ordered_pk = table.ordered_partition_keys();
275 for key_col in &ordered_pk {
276 let cql_type =
277 CqlType::parse(&key_col.data_type).map_err(|e| DeltaSchemaError::CqlTypeParse {
278 column: key_col.name.clone(),
279 source: e,
280 })?;
281 let arrow_type = cql_type_to_arrow_data_type(&cql_type);
282 fields.push(Field::new(&key_col.name, arrow_type, false));
283 }
284
285 let ordered_ck = table.ordered_clustering_keys();
288 for ck_col in &ordered_ck {
289 let cql_type =
290 CqlType::parse(&ck_col.data_type).map_err(|e| DeltaSchemaError::CqlTypeParse {
291 column: ck_col.name.clone(),
292 source: e,
293 })?;
294 let arrow_type = cql_type_to_arrow_data_type(&cql_type);
295 fields.push(Field::new(&ck_col.name, arrow_type, true));
296 }
297
298 let pk_names: std::collections::HashSet<&str> =
302 ordered_pk.iter().map(|k| k.name.as_str()).collect();
303 let ck_names: std::collections::HashSet<&str> =
304 ordered_ck.iter().map(|k| k.name.as_str()).collect();
305
306 for col in &table.columns {
307 if pk_names.contains(col.name.as_str()) || ck_names.contains(col.name.as_str()) {
308 continue;
310 }
311
312 let cql_type =
313 CqlType::parse(&col.data_type).map_err(|e| DeltaSchemaError::CqlTypeParse {
314 column: col.name.clone(),
315 source: e,
316 })?;
317
318 let cell_field = build_cell_struct_field(&col.name, &cql_type);
319 fields.push(cell_field);
320 }
321
322 fields.push(build_op_field(&opts.op_col()));
324 fields.push(Field::new(opts.ts_col(), ArrowDataType::Int64, true));
325 fields.push(build_range_bound_field(
326 &opts.range_start_col(),
327 &ordered_ck,
328 )?);
329 fields.push(build_range_bound_field(&opts.range_end_col(), &ordered_ck)?);
330
331 Ok(Schema::new(fields))
332}
333
334fn is_counter_type(cql_type: &CqlType) -> bool {
340 match cql_type {
341 CqlType::Counter => true,
342 CqlType::Frozen(inner) => is_counter_type(inner),
343 _ => false,
344 }
345}
346
347fn is_collection_type(cql_type: &CqlType) -> bool {
351 match cql_type {
352 CqlType::List(_) | CqlType::Set(_) | CqlType::Map(_, _) => true,
353 _ => false,
355 }
356}
357
358fn build_cell_struct_field(col_name: &str, cql_type: &CqlType) -> Field {
371 let value_arrow_type = cql_type_to_arrow_data_type(cql_type);
372 let is_collection = is_collection_type(cql_type);
373
374 let mut struct_fields = vec![
375 Field::new("value", value_arrow_type, true),
377 Field::new("writetime", ArrowDataType::Int64, false),
379 Field::new("expires_at", ArrowDataType::Int64, true),
381 ];
382
383 if is_collection {
384 struct_fields.push(Field::new("replaced", ArrowDataType::Boolean, false));
386 }
387
388 Field::new(
390 col_name,
391 ArrowDataType::Struct(Fields::from(struct_fields)),
392 true, )
394}
395
396fn build_op_field(col_name: &str) -> Field {
402 Field::new(
403 col_name,
404 ArrowDataType::Dictionary(Box::new(ArrowDataType::Int8), Box::new(ArrowDataType::Utf8)),
405 false,
406 )
407}
408
409fn build_range_bound_field(
427 col_name: &str,
428 clustering_keys: &[&crate::schema::ClusteringColumn],
429) -> Result<Field, DeltaSchemaError> {
430 let mut struct_fields: Vec<Field> = Vec::with_capacity(clustering_keys.len() + 1);
431
432 for ck_col in clustering_keys {
433 let cql_type =
434 CqlType::parse(&ck_col.data_type).map_err(|e| DeltaSchemaError::CqlTypeParse {
435 column: ck_col.name.clone(),
436 source: e,
437 })?;
438 let arrow_type = cql_type_to_arrow_data_type(&cql_type);
439 struct_fields.push(Field::new(&ck_col.name, arrow_type, true));
441 }
442
443 struct_fields.push(Field::new("inclusive", ArrowDataType::Boolean, false));
444
445 Ok(Field::new(
446 col_name,
447 ArrowDataType::Struct(Fields::from(struct_fields)),
448 true, ))
450}
451
452#[cfg(test)]
457mod tests {
458 use super::*;
459 use crate::schema::{ClusteringColumn, ClusteringOrder, Column, KeyColumn, TableSchema};
460 use std::collections::HashMap;
461
462 fn example_table() -> TableSchema {
467 TableSchema {
468 keyspace: "example_ks".to_string(),
469 table: "t".to_string(),
470 partition_keys: vec![KeyColumn {
471 name: "pk".to_string(),
472 data_type: "int".to_string(),
473 position: 0,
474 }],
475 clustering_keys: vec![ClusteringColumn {
476 name: "ck".to_string(),
477 data_type: "text".to_string(),
478 position: 0,
479 order: ClusteringOrder::Asc,
480 }],
481 columns: vec![
482 Column {
483 name: "pk".to_string(),
484 data_type: "int".to_string(),
485 nullable: false,
486 default: None,
487 is_static: false,
488 },
489 Column {
490 name: "ck".to_string(),
491 data_type: "text".to_string(),
492 nullable: true,
493 default: None,
494 is_static: false,
495 },
496 Column {
497 name: "val".to_string(),
498 data_type: "text".to_string(),
499 nullable: true,
500 default: None,
501 is_static: false,
502 },
503 Column {
504 name: "st".to_string(),
505 data_type: "text".to_string(),
506 nullable: true,
507 default: None,
508 is_static: true,
509 },
510 ],
511 comments: HashMap::new(),
512 dropped_columns: HashMap::new(),
513 }
514 }
515
516 #[test]
521 fn snapshot_example_table_schema() {
522 let table = example_table();
523 let opts = DeltaSchemaOpts::default();
524 let schema = derive_delta_schema(&table, &opts).expect("derive_delta_schema failed");
525
526 assert_eq!(
529 schema.fields().len(),
530 8,
531 "expected 8 fields, got {}",
532 schema.fields().len()
533 );
534
535 let pk = schema.field_with_name("pk").expect("no pk field");
537 assert_eq!(*pk.data_type(), ArrowDataType::Int32, "pk should be Int32");
538 assert!(!pk.is_nullable(), "pk should be non-nullable");
539
540 let ck = schema.field_with_name("ck").expect("no ck field");
542 assert_eq!(*ck.data_type(), ArrowDataType::Utf8, "ck should be Utf8");
543 assert!(ck.is_nullable(), "ck should be nullable");
544
545 let val = schema.field_with_name("val").expect("no val field");
548 assert!(val.is_nullable(), "val cell struct should be nullable");
549 if let ArrowDataType::Struct(val_fields) = val.data_type() {
550 assert_eq!(
551 val_fields.len(),
552 3,
553 "val struct should have 3 fields (no replaced)"
554 );
555 let vf = val_fields
556 .find("value")
557 .expect("no value field in val struct");
558 assert_eq!(*vf.1.data_type(), ArrowDataType::Utf8);
559 assert!(
560 vf.1.is_nullable(),
561 "value should be nullable (cell tombstone = null)"
562 );
563 let wt = val_fields.find("writetime").expect("no writetime field");
564 assert_eq!(*wt.1.data_type(), ArrowDataType::Int64);
565 assert!(!wt.1.is_nullable(), "writetime should be non-nullable");
566 let ea = val_fields.find("expires_at").expect("no expires_at field");
567 assert_eq!(*ea.1.data_type(), ArrowDataType::Int64);
568 assert!(ea.1.is_nullable(), "expires_at should be nullable");
569 } else {
570 panic!("val should be a Struct, got {:?}", val.data_type());
571 }
572
573 let st = schema.field_with_name("st").expect("no st field");
575 assert!(st.is_nullable(), "st cell struct should be nullable");
576 if let ArrowDataType::Struct(st_fields) = st.data_type() {
577 assert_eq!(
578 st_fields.len(),
579 3,
580 "st struct should have 3 fields (no replaced)"
581 );
582 } else {
583 panic!("st should be a Struct");
584 }
585
586 let op = schema.field_with_name("__op").expect("no __op field");
588 assert!(!op.is_nullable(), "__op should be non-nullable");
589 assert!(
590 matches!(op.data_type(), ArrowDataType::Dictionary(key, val)
591 if **key == ArrowDataType::Int8 && **val == ArrowDataType::Utf8),
592 "__op should be Dictionary(Int8, Utf8), got {:?}",
593 op.data_type()
594 );
595
596 let ts = schema.field_with_name("__ts").expect("no __ts field");
598 assert_eq!(*ts.data_type(), ArrowDataType::Int64);
599 assert!(ts.is_nullable(), "__ts should be nullable");
600
601 let rs = schema
603 .field_with_name("__range_start")
604 .expect("no __range_start field");
605 assert!(rs.is_nullable(), "__range_start should be nullable");
606 if let ArrowDataType::Struct(rs_fields) = rs.data_type() {
607 assert_eq!(
608 rs_fields.len(),
609 2,
610 "__range_start struct should have 2 fields (ck + inclusive)"
611 );
612 let ck_f = rs_fields.find("ck").expect("no ck in __range_start");
613 assert_eq!(*ck_f.1.data_type(), ArrowDataType::Utf8);
614 assert!(
615 ck_f.1.is_nullable(),
616 "ck in range bound should be nullable (prefix)"
617 );
618 let inc_f = rs_fields
619 .find("inclusive")
620 .expect("no inclusive in __range_start");
621 assert_eq!(*inc_f.1.data_type(), ArrowDataType::Boolean);
622 assert!(!inc_f.1.is_nullable(), "inclusive should be non-nullable");
623 } else {
624 panic!("__range_start should be a Struct");
625 }
626
627 let re = schema
629 .field_with_name("__range_end")
630 .expect("no __range_end field");
631 assert!(re.is_nullable(), "__range_end should be nullable");
632 if let ArrowDataType::Struct(re_fields) = re.data_type() {
633 assert_eq!(
634 re_fields.len(),
635 2,
636 "__range_end struct should have 2 fields"
637 );
638 } else {
639 panic!("__range_end should be a Struct");
640 }
641 }
642
643 #[test]
648 fn collection_column_has_replaced_field() {
649 let table = TableSchema {
650 keyspace: "ks".to_string(),
651 table: "with_collection".to_string(),
652 partition_keys: vec![KeyColumn {
653 name: "pk".to_string(),
654 data_type: "int".to_string(),
655 position: 0,
656 }],
657 clustering_keys: vec![],
658 columns: vec![
659 Column {
660 name: "pk".to_string(),
661 data_type: "int".to_string(),
662 nullable: false,
663 default: None,
664 is_static: false,
665 },
666 Column {
667 name: "tags".to_string(),
668 data_type: "set<text>".to_string(),
669 nullable: true,
670 default: None,
671 is_static: false,
672 },
673 Column {
674 name: "name".to_string(),
675 data_type: "text".to_string(),
676 nullable: true,
677 default: None,
678 is_static: false,
679 },
680 ],
681 comments: HashMap::new(),
682 dropped_columns: HashMap::new(),
683 };
684
685 let schema =
686 derive_delta_schema(&table, &DeltaSchemaOpts::default()).expect("derive failed");
687
688 let tags = schema.field_with_name("tags").expect("no tags field");
690 if let ArrowDataType::Struct(fields) = tags.data_type() {
691 assert_eq!(
692 fields.len(),
693 4,
694 "set column struct should have 4 fields (incl. replaced)"
695 );
696 assert!(
697 fields.find("replaced").is_some(),
698 "set column should have `replaced` field"
699 );
700 } else {
701 panic!("tags should be Struct");
702 }
703
704 let name = schema.field_with_name("name").expect("no name field");
706 if let ArrowDataType::Struct(fields) = name.data_type() {
707 assert_eq!(
708 fields.len(),
709 3,
710 "scalar column struct should have 3 fields (no replaced)"
711 );
712 assert!(
713 fields.find("replaced").is_none(),
714 "scalar column should NOT have `replaced`"
715 );
716 } else {
717 panic!("name should be Struct");
718 }
719 }
720
721 #[test]
726 fn frozen_collection_is_scalar_no_replaced() {
727 let table = TableSchema {
728 keyspace: "ks".to_string(),
729 table: "frozen_test".to_string(),
730 partition_keys: vec![KeyColumn {
731 name: "pk".to_string(),
732 data_type: "int".to_string(),
733 position: 0,
734 }],
735 clustering_keys: vec![],
736 columns: vec![
737 Column {
738 name: "pk".to_string(),
739 data_type: "int".to_string(),
740 nullable: false,
741 default: None,
742 is_static: false,
743 },
744 Column {
745 name: "frozen_list".to_string(),
746 data_type: "frozen<list<text>>".to_string(),
747 nullable: true,
748 default: None,
749 is_static: false,
750 },
751 ],
752 comments: HashMap::new(),
753 dropped_columns: HashMap::new(),
754 };
755
756 let schema =
757 derive_delta_schema(&table, &DeltaSchemaOpts::default()).expect("derive failed");
758
759 let frozen_col = schema
760 .field_with_name("frozen_list")
761 .expect("no frozen_list field");
762 if let ArrowDataType::Struct(fields) = frozen_col.data_type() {
763 assert_eq!(
764 fields.len(),
765 3,
766 "frozen<list> should be treated as scalar: 3 fields"
767 );
768 assert!(
769 fields.find("replaced").is_none(),
770 "frozen<list> should NOT have `replaced`"
771 );
772 } else {
773 panic!("frozen_list should be Struct");
774 }
775 }
776
777 #[test]
782 fn column_collision_default_prefix() {
783 let table = TableSchema {
784 keyspace: "ks".to_string(),
785 table: "bad".to_string(),
786 partition_keys: vec![KeyColumn {
787 name: "pk".to_string(),
788 data_type: "int".to_string(),
789 position: 0,
790 }],
791 clustering_keys: vec![],
792 columns: vec![
793 Column {
794 name: "pk".to_string(),
795 data_type: "int".to_string(),
796 nullable: false,
797 default: None,
798 is_static: false,
799 },
800 Column {
802 name: "__op".to_string(),
803 data_type: "text".to_string(),
804 nullable: true,
805 default: None,
806 is_static: false,
807 },
808 ],
809 comments: HashMap::new(),
810 dropped_columns: HashMap::new(),
811 };
812
813 let err = derive_delta_schema(&table, &DeltaSchemaOpts::default())
814 .expect_err("expected collision error");
815
816 match err {
817 DeltaSchemaError::ColumnCollision { column, reserved } => {
818 assert_eq!(column, "__op");
819 assert_eq!(reserved, "__op");
820 }
821 other => panic!("expected ColumnCollision, got {:?}", other),
822 }
823 }
824
825 #[test]
826 fn column_collision_ts_column() {
827 let table = TableSchema {
828 keyspace: "ks".to_string(),
829 table: "bad_ts".to_string(),
830 partition_keys: vec![KeyColumn {
831 name: "pk".to_string(),
832 data_type: "int".to_string(),
833 position: 0,
834 }],
835 clustering_keys: vec![],
836 columns: vec![
837 Column {
838 name: "pk".to_string(),
839 data_type: "int".to_string(),
840 nullable: false,
841 default: None,
842 is_static: false,
843 },
844 Column {
845 name: "__ts".to_string(),
846 data_type: "bigint".to_string(),
847 nullable: true,
848 default: None,
849 is_static: false,
850 },
851 ],
852 comments: HashMap::new(),
853 dropped_columns: HashMap::new(),
854 };
855
856 let err = derive_delta_schema(&table, &DeltaSchemaOpts::default())
857 .expect_err("expected collision error");
858
859 assert!(
860 matches!(err, DeltaSchemaError::ColumnCollision { .. }),
861 "expected ColumnCollision"
862 );
863 }
864
865 #[test]
875 fn partition_key_collision_default_prefix() {
876 let table = TableSchema {
877 keyspace: "ks".to_string(),
878 table: "pk_collision".to_string(),
879 partition_keys: vec![KeyColumn {
880 name: "__op".to_string(),
882 data_type: "int".to_string(),
883 position: 0,
884 }],
885 clustering_keys: vec![],
886 columns: vec![Column {
891 name: "__op".to_string(),
892 data_type: "int".to_string(),
893 nullable: false,
894 default: None,
895 is_static: false,
896 }],
897 comments: HashMap::new(),
898 dropped_columns: HashMap::new(),
899 };
900
901 let err = derive_delta_schema(&table, &DeltaSchemaOpts::default())
902 .expect_err("expected ColumnCollision for partition key named __op");
903
904 match err {
905 DeltaSchemaError::ColumnCollision { column, reserved } => {
906 assert_eq!(column, "__op");
907 assert_eq!(reserved, "__op");
908 }
909 other => panic!("expected ColumnCollision, got {:?}", other),
910 }
911 }
912
913 #[test]
915 fn clustering_key_collision_default_prefix() {
916 let table = TableSchema {
917 keyspace: "ks".to_string(),
918 table: "ck_collision".to_string(),
919 partition_keys: vec![KeyColumn {
920 name: "pk".to_string(),
921 data_type: "int".to_string(),
922 position: 0,
923 }],
924 clustering_keys: vec![ClusteringColumn {
925 name: "__ts".to_string(),
927 data_type: "text".to_string(),
928 position: 0,
929 order: ClusteringOrder::Asc,
930 }],
931 columns: vec![
932 Column {
933 name: "pk".to_string(),
934 data_type: "int".to_string(),
935 nullable: false,
936 default: None,
937 is_static: false,
938 },
939 Column {
940 name: "__ts".to_string(),
941 data_type: "text".to_string(),
942 nullable: true,
943 default: None,
944 is_static: false,
945 },
946 ],
947 comments: HashMap::new(),
948 dropped_columns: HashMap::new(),
949 };
950
951 let err = derive_delta_schema(&table, &DeltaSchemaOpts::default())
952 .expect_err("expected ColumnCollision for clustering key named __ts");
953
954 match err {
955 DeltaSchemaError::ColumnCollision { column, reserved } => {
956 assert_eq!(column, "__ts");
957 assert_eq!(reserved, "__ts");
958 }
959 other => panic!("expected ColumnCollision, got {:?}", other),
960 }
961 }
962
963 #[test]
968 fn custom_prefix_avoids_collision() {
969 let table = TableSchema {
970 keyspace: "ks".to_string(),
971 table: "custom_prefix".to_string(),
972 partition_keys: vec![KeyColumn {
973 name: "pk".to_string(),
974 data_type: "int".to_string(),
975 position: 0,
976 }],
977 clustering_keys: vec![],
978 columns: vec![
979 Column {
980 name: "pk".to_string(),
981 data_type: "int".to_string(),
982 nullable: false,
983 default: None,
984 is_static: false,
985 },
986 Column {
989 name: "__op".to_string(),
990 data_type: "text".to_string(),
991 nullable: true,
992 default: None,
993 is_static: false,
994 },
995 ],
996 comments: HashMap::new(),
997 dropped_columns: HashMap::new(),
998 };
999
1000 assert!(derive_delta_schema(&table, &DeltaSchemaOpts::default()).is_err());
1002
1003 let opts = DeltaSchemaOpts::with_prefix("_cqlite_");
1005 let schema = derive_delta_schema(&table, &opts).expect("should succeed with custom prefix");
1006
1007 let op_col = schema.field_with_name("__op").expect("no __op field");
1009 assert!(
1010 matches!(op_col.data_type(), ArrowDataType::Struct(_)),
1011 "__op should be a cell Struct under custom prefix"
1012 );
1013
1014 let cqlite_op = schema
1016 .field_with_name("_cqlite_op")
1017 .expect("no _cqlite_op field");
1018 assert!(
1019 matches!(cqlite_op.data_type(), ArrowDataType::Dictionary(..)),
1020 "_cqlite_op should be Dictionary-encoded"
1021 );
1022 }
1023
1024 #[test]
1029 fn custom_prefix_collision_error_names_custom_reserved() {
1030 let table = TableSchema {
1031 keyspace: "ks".to_string(),
1032 table: "t".to_string(),
1033 partition_keys: vec![KeyColumn {
1034 name: "pk".to_string(),
1035 data_type: "int".to_string(),
1036 position: 0,
1037 }],
1038 clustering_keys: vec![],
1039 columns: vec![
1040 Column {
1041 name: "pk".to_string(),
1042 data_type: "int".to_string(),
1043 nullable: false,
1044 default: None,
1045 is_static: false,
1046 },
1047 Column {
1049 name: "_cqlite_op".to_string(),
1050 data_type: "text".to_string(),
1051 nullable: true,
1052 default: None,
1053 is_static: false,
1054 },
1055 ],
1056 comments: HashMap::new(),
1057 dropped_columns: HashMap::new(),
1058 };
1059
1060 let opts = DeltaSchemaOpts::with_prefix("_cqlite_");
1061 let err = derive_delta_schema(&table, &opts).expect_err("expected collision");
1062 match err {
1063 DeltaSchemaError::ColumnCollision { column, reserved } => {
1064 assert_eq!(column, "_cqlite_op");
1065 assert_eq!(reserved, "_cqlite_op");
1066 }
1067 other => panic!("expected ColumnCollision, got {:?}", other),
1068 }
1069 }
1070
1071 #[test]
1076 fn counter_table_rejected() {
1077 let table = TableSchema {
1078 keyspace: "ks".to_string(),
1079 table: "counters".to_string(),
1080 partition_keys: vec![KeyColumn {
1081 name: "pk".to_string(),
1082 data_type: "int".to_string(),
1083 position: 0,
1084 }],
1085 clustering_keys: vec![],
1086 columns: vec![
1087 Column {
1088 name: "pk".to_string(),
1089 data_type: "int".to_string(),
1090 nullable: false,
1091 default: None,
1092 is_static: false,
1093 },
1094 Column {
1095 name: "views".to_string(),
1096 data_type: "counter".to_string(),
1097 nullable: true,
1098 default: None,
1099 is_static: false,
1100 },
1101 ],
1102 comments: HashMap::new(),
1103 dropped_columns: HashMap::new(),
1104 };
1105
1106 let err = derive_delta_schema(&table, &DeltaSchemaOpts::default())
1107 .expect_err("expected counter error");
1108
1109 match &err {
1110 DeltaSchemaError::CounterTable {
1111 keyspace,
1112 table: tbl,
1113 columns,
1114 } => {
1115 assert_eq!(keyspace, "ks");
1116 assert_eq!(tbl, "counters");
1117 assert!(columns.contains("views"), "error should name 'views'");
1118 }
1119 other => panic!("expected CounterTable, got {:?}", other),
1120 }
1121
1122 let msg = err.to_string();
1124 assert!(
1125 msg.contains("counter"),
1126 "message should mention counter: {}",
1127 msg
1128 );
1129 assert!(
1130 msg.contains("ks.counters"),
1131 "message should name the table: {}",
1132 msg
1133 );
1134 }
1135
1136 #[test]
1141 fn value_types_from_673_mapping() {
1142 let types_under_test = vec![
1147 ("bigint", CqlType::BigInt),
1148 ("boolean", CqlType::Boolean),
1149 ("float", CqlType::Float),
1150 ("double", CqlType::Double),
1151 ("uuid", CqlType::Uuid),
1152 ("timeuuid", CqlType::TimeUuid),
1153 ("timestamp", CqlType::Timestamp),
1154 ("date", CqlType::Date),
1155 ("time", CqlType::Time),
1156 ("blob", CqlType::Blob),
1157 ("inet", CqlType::Inet),
1158 ("decimal", CqlType::Decimal),
1159 ("varint", CqlType::Varint),
1160 ];
1161
1162 for (type_str, expected_cql_type) in types_under_test {
1163 let table = TableSchema {
1164 keyspace: "ks".to_string(),
1165 table: "t".to_string(),
1166 partition_keys: vec![KeyColumn {
1167 name: "pk".to_string(),
1168 data_type: "int".to_string(),
1169 position: 0,
1170 }],
1171 clustering_keys: vec![],
1172 columns: vec![
1173 Column {
1174 name: "pk".to_string(),
1175 data_type: "int".to_string(),
1176 nullable: false,
1177 default: None,
1178 is_static: false,
1179 },
1180 Column {
1181 name: "col".to_string(),
1182 data_type: type_str.to_string(),
1183 nullable: true,
1184 default: None,
1185 is_static: false,
1186 },
1187 ],
1188 comments: HashMap::new(),
1189 dropped_columns: HashMap::new(),
1190 };
1191
1192 let schema = derive_delta_schema(&table, &DeltaSchemaOpts::default())
1193 .unwrap_or_else(|e| panic!("derive failed for {}: {:?}", type_str, e));
1194
1195 let col_field = schema.field_with_name("col").expect("no col field");
1196 if let ArrowDataType::Struct(struct_fields) = col_field.data_type() {
1197 let value_field = struct_fields
1198 .find("value")
1199 .expect("no value field in struct");
1200 let expected_arrow_type = cql_type_to_arrow_data_type(&expected_cql_type);
1201 assert_eq!(
1202 *value_field.1.data_type(),
1203 expected_arrow_type,
1204 "value Arrow type mismatch for CQL type '{}': expected {:?}, got {:?}",
1205 type_str,
1206 expected_arrow_type,
1207 value_field.1.data_type()
1208 );
1209 } else {
1210 panic!("col should be Struct for type {}", type_str);
1211 }
1212 }
1213 }
1214
1215 #[test]
1220 fn no_clustering_keys_range_bound_has_only_inclusive() {
1221 let table = TableSchema {
1222 keyspace: "ks".to_string(),
1223 table: "no_ck".to_string(),
1224 partition_keys: vec![KeyColumn {
1225 name: "pk".to_string(),
1226 data_type: "int".to_string(),
1227 position: 0,
1228 }],
1229 clustering_keys: vec![],
1230 columns: vec![Column {
1231 name: "pk".to_string(),
1232 data_type: "int".to_string(),
1233 nullable: false,
1234 default: None,
1235 is_static: false,
1236 }],
1237 comments: HashMap::new(),
1238 dropped_columns: HashMap::new(),
1239 };
1240
1241 let schema =
1242 derive_delta_schema(&table, &DeltaSchemaOpts::default()).expect("derive failed");
1243
1244 let rs = schema
1245 .field_with_name("__range_start")
1246 .expect("no __range_start");
1247 if let ArrowDataType::Struct(fields) = rs.data_type() {
1248 assert_eq!(
1249 fields.len(),
1250 1,
1251 "no-CK range bound should only have `inclusive`"
1252 );
1253 assert!(fields.find("inclusive").is_some());
1254 } else {
1255 panic!("__range_start should be Struct");
1256 }
1257 }
1258
1259 #[test]
1264 fn multi_ck_range_bound_has_all_ck_fields() {
1265 let table = TableSchema {
1266 keyspace: "ks".to_string(),
1267 table: "multi_ck".to_string(),
1268 partition_keys: vec![KeyColumn {
1269 name: "pk".to_string(),
1270 data_type: "int".to_string(),
1271 position: 0,
1272 }],
1273 clustering_keys: vec![
1274 ClusteringColumn {
1275 name: "year".to_string(),
1276 data_type: "int".to_string(),
1277 position: 0,
1278 order: ClusteringOrder::Asc,
1279 },
1280 ClusteringColumn {
1281 name: "month".to_string(),
1282 data_type: "int".to_string(),
1283 position: 1,
1284 order: ClusteringOrder::Asc,
1285 },
1286 ],
1287 columns: vec![
1288 Column {
1289 name: "pk".to_string(),
1290 data_type: "int".to_string(),
1291 nullable: false,
1292 default: None,
1293 is_static: false,
1294 },
1295 Column {
1296 name: "year".to_string(),
1297 data_type: "int".to_string(),
1298 nullable: true,
1299 default: None,
1300 is_static: false,
1301 },
1302 Column {
1303 name: "month".to_string(),
1304 data_type: "int".to_string(),
1305 nullable: true,
1306 default: None,
1307 is_static: false,
1308 },
1309 ],
1310 comments: HashMap::new(),
1311 dropped_columns: HashMap::new(),
1312 };
1313
1314 let schema =
1315 derive_delta_schema(&table, &DeltaSchemaOpts::default()).expect("derive failed");
1316
1317 let rs = schema
1318 .field_with_name("__range_start")
1319 .expect("no __range_start");
1320 if let ArrowDataType::Struct(fields) = rs.data_type() {
1321 assert_eq!(fields.len(), 3);
1323 assert!(fields.find("year").is_some());
1324 assert!(fields.find("month").is_some());
1325 assert!(fields.find("inclusive").is_some());
1326 let year_f = fields.find("year").unwrap();
1328 assert!(
1329 year_f.1.is_nullable(),
1330 "year in range bound should be nullable"
1331 );
1332 } else {
1333 panic!("__range_start should be Struct");
1334 }
1335 }
1336
1337 #[test]
1342 fn map_column_has_replaced_field() {
1343 let table = TableSchema {
1344 keyspace: "ks".to_string(),
1345 table: "with_map".to_string(),
1346 partition_keys: vec![KeyColumn {
1347 name: "pk".to_string(),
1348 data_type: "int".to_string(),
1349 position: 0,
1350 }],
1351 clustering_keys: vec![],
1352 columns: vec![
1353 Column {
1354 name: "pk".to_string(),
1355 data_type: "int".to_string(),
1356 nullable: false,
1357 default: None,
1358 is_static: false,
1359 },
1360 Column {
1361 name: "attrs".to_string(),
1362 data_type: "map<text, text>".to_string(),
1363 nullable: true,
1364 default: None,
1365 is_static: false,
1366 },
1367 ],
1368 comments: HashMap::new(),
1369 dropped_columns: HashMap::new(),
1370 };
1371
1372 let schema =
1373 derive_delta_schema(&table, &DeltaSchemaOpts::default()).expect("derive failed");
1374 let attrs = schema.field_with_name("attrs").expect("no attrs field");
1375 if let ArrowDataType::Struct(fields) = attrs.data_type() {
1376 assert_eq!(
1377 fields.len(),
1378 4,
1379 "map column should have 4 fields (incl. replaced)"
1380 );
1381 assert!(fields.find("replaced").is_some());
1382 } else {
1383 panic!("attrs should be Struct");
1384 }
1385 }
1386
1387 #[test]
1392 fn list_column_has_replaced_field() {
1393 let table = TableSchema {
1394 keyspace: "ks".to_string(),
1395 table: "with_list".to_string(),
1396 partition_keys: vec![KeyColumn {
1397 name: "pk".to_string(),
1398 data_type: "int".to_string(),
1399 position: 0,
1400 }],
1401 clustering_keys: vec![],
1402 columns: vec![
1403 Column {
1404 name: "pk".to_string(),
1405 data_type: "int".to_string(),
1406 nullable: false,
1407 default: None,
1408 is_static: false,
1409 },
1410 Column {
1411 name: "events".to_string(),
1412 data_type: "list<bigint>".to_string(),
1413 nullable: true,
1414 default: None,
1415 is_static: false,
1416 },
1417 ],
1418 comments: HashMap::new(),
1419 dropped_columns: HashMap::new(),
1420 };
1421
1422 let schema =
1423 derive_delta_schema(&table, &DeltaSchemaOpts::default()).expect("derive failed");
1424 let events = schema.field_with_name("events").expect("no events field");
1425 if let ArrowDataType::Struct(fields) = events.data_type() {
1426 assert_eq!(
1427 fields.len(),
1428 4,
1429 "list column should have 4 fields (incl. replaced)"
1430 );
1431 assert!(fields.find("replaced").is_some());
1432 } else {
1433 panic!("events should be Struct");
1434 }
1435 }
1436}