Skip to content

Commit 07b65db

Browse files
authored
Validate that ConnectionSchema fields are not duplicated (#1120)
1 parent 078def1 commit 07b65db

1 file changed

Lines changed: 75 additions & 1 deletion

File tree

crates/arroyo-rpc/src/api_types/connections.rs

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@ use crate::MetadataField;
22
use crate::df::{ArroyoSchema, ArroyoSchemaRef};
33
use crate::formats::{BadData, Format, Framing};
44
use ahash::HashSet;
5-
use anyhow::bail;
5+
use anyhow::{anyhow, bail};
66
use arrow_schema::{DataType, Field, Fields, TimeUnit};
77
use arroyo_types::ArroyoExtensionType;
8+
use datafusion::common::DFSchema;
89
use serde::{Deserialize, Serialize, Serializer};
910
use std::collections::{BTreeMap, HashMap};
1011
use 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")]
425453
pub 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

Comments
 (0)