@@ -2,9 +2,10 @@ use crate::MetadataField;
22use crate :: df:: { ArroyoSchema , ArroyoSchemaRef } ;
33use crate :: formats:: { BadData , Format , Framing } ;
44use ahash:: HashSet ;
5- use anyhow:: bail;
5+ use anyhow:: { anyhow , bail} ;
66use arrow_schema:: { DataType , Field , Fields , TimeUnit } ;
77use arroyo_types:: ArroyoExtensionType ;
8+ use datafusion:: common:: DFSchema ;
89use serde:: { Deserialize , Serialize , Serializer } ;
910use std:: collections:: { BTreeMap , HashMap } ;
1011use std:: fmt:: { Display , Formatter } ;
@@ -420,6 +421,33 @@ impl TryFrom<Field> for SourceField {
420421 }
421422}
422423
424+ fn validate_unique_field_names ( fields : & [ SourceField ] , path : & str ) -> anyhow:: Result < ( ) > {
425+ let arrow_fields = fields
426+ . iter ( )
427+ . cloned ( )
428+ . map ( Field :: from)
429+ . collect :: < Vec < _ > > ( )
430+ . into ( ) ;
431+ DFSchema :: from_unqualified_fields ( arrow_fields, HashMap :: new ( ) )
432+ . map_err ( |e| anyhow ! ( "invalid field names in {path}: {e}" ) ) ?;
433+
434+ for field in fields {
435+ validate_unique_nested_field_names ( & field. field_type , & format ! ( "{path}.{}" , field. name) ) ?;
436+ }
437+
438+ Ok ( ( ) )
439+ }
440+
441+ fn validate_unique_nested_field_names ( field_type : & FieldType , path : & str ) -> anyhow:: Result < ( ) > {
442+ match field_type {
443+ FieldType :: Struct ( StructField { fields } ) => validate_unique_field_names ( fields, path) ,
444+ FieldType :: List ( ListField { items } ) => {
445+ validate_unique_nested_field_names ( & items. field_type , & format ! ( "{path}[]" ) )
446+ }
447+ _ => Ok ( ( ) ) ,
448+ }
449+ }
450+
423451#[ derive( Serialize , Deserialize , Clone , Debug , ToSchema , PartialEq ) ]
424452#[ serde( rename_all = "snake_case" , tag = "type" ) ]
425453pub enum SchemaDefinition {
@@ -478,6 +506,8 @@ impl ConnectionSchema {
478506 }
479507
480508 pub fn validate ( self ) -> anyhow:: Result < Self > {
509+ validate_unique_field_names ( & self . fields , "schema" ) ?;
510+
481511 let non_metadata_fields: Vec < _ > = self
482512 . fields
483513 . iter ( )
@@ -635,6 +665,50 @@ mod tests {
635665 ArrowField :: new ( name, dt, nullable)
636666 }
637667
668+ fn source_field ( name : & str , field_type : FieldType ) -> SourceField {
669+ SourceField {
670+ name : name. to_string ( ) ,
671+ field_type,
672+ required : false ,
673+ sql_name : None ,
674+ metadata_key : None ,
675+ }
676+ }
677+
678+ #[ test]
679+ fn connection_schema_rejects_duplicate_field_names_in_list_structs ( ) {
680+ let schema = ConnectionSchema {
681+ format : None ,
682+ bad_data : None ,
683+ framing : None ,
684+ fields : vec ! [ source_field(
685+ "events" ,
686+ FieldType :: List ( ListField {
687+ items: Box :: new( ListFieldItem {
688+ name: "item" . to_string( ) ,
689+ field_type: FieldType :: Struct ( StructField {
690+ fields: vec![
691+ source_field( "id" , FieldType :: Int64 ) ,
692+ source_field( "id" , FieldType :: String ) ,
693+ ] ,
694+ } ) ,
695+ required: false ,
696+ sql_name: None ,
697+ } ) ,
698+ } ) ,
699+ ) ] ,
700+ definition : None ,
701+ inferred : None ,
702+ primary_keys : HashSet :: default ( ) ,
703+ } ;
704+
705+ let err = schema. validate ( ) . unwrap_err ( ) ;
706+ assert_eq ! (
707+ err. to_string( ) ,
708+ "invalid field names in schema.events[]: Schema error: Schema contains duplicate unqualified field name id"
709+ ) ;
710+ }
711+
638712 #[ test]
639713 fn sql_type_struct_and_list ( ) {
640714 let st = FieldType :: Struct ( StructField {
0 commit comments