diff --git a/datafusion/common/src/dfschema.rs b/datafusion/common/src/dfschema.rs index de0aacf9e8bcd..50b632e8211de 100644 --- a/datafusion/common/src/dfschema.rs +++ b/datafusion/common/src/dfschema.rs @@ -1338,11 +1338,44 @@ impl SchemaExt for Schema { } } +/// Build a fully-qualified field name string. This is equivalent to +/// `format!("{q}.{name}")` when `qualifier` is `Some`, or just `name` when +/// `None`. We avoid going through the `fmt` machinery for performance reasons. pub fn qualified_name(qualifier: Option<&TableReference>, name: &str) -> String { - match qualifier { - Some(q) => format!("{q}.{name}"), - None => name.to_string(), - } + let qualifier = match qualifier { + None => return name.to_string(), + Some(q) => q, + }; + let (a, b, c) = match qualifier { + TableReference::Bare { table } => (table.as_ref(), None, None), + TableReference::Partial { schema, table } => { + (schema.as_ref(), Some(table.as_ref()), None) + } + TableReference::Full { + catalog, + schema, + table, + } => ( + catalog.as_ref(), + Some(schema.as_ref()), + Some(table.as_ref()), + ), + }; + + let extra = b.unwrap_or("").len() + c.unwrap_or("").len(); + let mut s = String::with_capacity(a.len() + extra + 3 + name.len()); + s.push_str(a); + if let Some(b) = b { + s.push('.'); + s.push_str(b); + } + if let Some(c) = c { + s.push('.'); + s.push_str(c); + } + s.push('.'); + s.push_str(name); + s } #[cfg(test)] @@ -1351,6 +1384,36 @@ mod tests { use super::*; + /// `qualified_name` doesn't use `TableReference::Display` for performance + /// reasons, but check that the output is consistent. + #[test] + fn qualified_name_agrees_with_display() { + let cases: &[(Option, &str)] = &[ + (None, "col"), + (Some(TableReference::bare("t")), "c0"), + (Some(TableReference::partial("s", "t")), "c0"), + (Some(TableReference::full("c", "s", "t")), "c0"), + (Some(TableReference::bare("mytable")), "some_column_name"), + // Empty segments must be preserved so that distinct qualified + // fields don't collide in `DFSchema::field_names()`. + (Some(TableReference::bare("")), "col"), + (Some(TableReference::partial("s", "")), "col"), + (Some(TableReference::partial("", "t")), "col"), + (Some(TableReference::full("c", "", "t")), "col"), + (Some(TableReference::full("", "s", "t")), "col"), + (Some(TableReference::full("c", "s", "")), "col"), + (Some(TableReference::full("", "", "")), "col"), + ]; + for (qualifier, name) in cases { + let actual = qualified_name(qualifier.as_ref(), name); + let expected = match qualifier { + Some(q) => format!("{q}.{name}"), + None => name.to_string(), + }; + assert_eq!(actual, expected, "qualifier={qualifier:?} name={name}"); + } + } + #[test] fn qualifier_in_name() -> Result<()> { let col = Column::from_name("t1.c0");