@@ -50,6 +50,110 @@ pub use datafusion_functions_aggregate_common::order::AggregateOrderSensitivity;
5050/// `COUNT(<constant>)` expressions
5151pub use datafusion_common:: utils:: expr:: COUNT_STAR_EXPANSION ;
5252
53+ /// Tracks expression names and generates aliases that do not conflict with
54+ /// names already in an output schema.
55+ ///
56+ /// In addition to duplicate schema names, this detects ambiguity between a
57+ /// qualified field such as `t.a` and an unqualified field named `a`.
58+ #[ derive( Default ) ]
59+ pub struct NameTracker {
60+ /// Tracks seen schema names (from expr.schema_name()).
61+ /// Used to detect duplicates that would fail validate_unique_names.
62+ seen_schema_names : HashSet < String > ,
63+ /// Tracks column names that have been seen with a qualifier.
64+ /// Used to detect ambiguous references (qualified + unqualified with same name).
65+ qualified_names : HashSet < String > ,
66+ /// Tracks column names that have been seen without a qualifier.
67+ /// Used to detect ambiguous references.
68+ unqualified_names : HashSet < String > ,
69+ }
70+
71+ impl NameTracker {
72+ pub fn new ( ) -> Self {
73+ Self :: default ( )
74+ }
75+
76+ /// Reserve the names of `exprs` without changing the expressions.
77+ ///
78+ /// This is useful when existing output expressions must retain their names
79+ /// and subsequently generated expressions need to avoid them.
80+ pub fn reserve ( & mut self , exprs : & [ Expr ] ) {
81+ for expr in exprs {
82+ self . insert ( expr) ;
83+ }
84+ }
85+
86+ fn would_conflict ( & self , expr : & Expr ) -> bool {
87+ let ( qualifier, name) = expr. qualified_name ( ) ;
88+ let schema_name = expr. schema_name ( ) . to_string ( ) ;
89+ self . would_conflict_inner ( ( qualifier, & name) , & schema_name)
90+ }
91+
92+ fn would_conflict_inner (
93+ & self ,
94+ qualified_name : ( Option < TableReference > , & str ) ,
95+ schema_name : & str ,
96+ ) -> bool {
97+ // Check for duplicate schema_name (would fail validate_unique_names)
98+ if self . seen_schema_names . contains ( schema_name) {
99+ return true ;
100+ }
101+
102+ // Check for ambiguous reference (would fail DFSchema::check_names)
103+ // This happens when a qualified field and unqualified field have the same name
104+ let ( qualifier, name) = qualified_name;
105+ match qualifier {
106+ Some ( _) => {
107+ // Adding a qualified name - conflicts if unqualified version exists
108+ self . unqualified_names . contains ( name)
109+ }
110+ None => {
111+ // Adding an unqualified name - conflicts if qualified version exists
112+ self . qualified_names . contains ( name)
113+ }
114+ }
115+ }
116+
117+ fn insert ( & mut self , expr : & Expr ) {
118+ let schema_name = expr. schema_name ( ) . to_string ( ) ;
119+ self . seen_schema_names . insert ( schema_name) ;
120+
121+ let ( qualifier, name) = expr. qualified_name ( ) ;
122+ match qualifier {
123+ Some ( _) => {
124+ self . qualified_names . insert ( name) ;
125+ }
126+ None => {
127+ self . unqualified_names . insert ( name) ;
128+ }
129+ }
130+ }
131+
132+ /// Return `expr` unchanged if its name is available, or alias it to a
133+ /// unique name otherwise.
134+ pub fn get_uniquely_named_expr ( & mut self , expr : Expr ) -> Result < Expr > {
135+ if !self . would_conflict ( & expr) {
136+ self . insert ( & expr) ;
137+ return Ok ( expr) ;
138+ }
139+
140+ // Name collision - need to generate a unique alias
141+ let schema_name = expr. schema_name ( ) . to_string ( ) ;
142+ let mut counter = 0 ;
143+ let candidate_name = loop {
144+ let candidate_name = format ! ( "{schema_name}__temp__{counter}" ) ;
145+ // .alias always produces an unqualified name so check for conflicts accordingly.
146+ if !self . would_conflict_inner ( ( None , & candidate_name) , & candidate_name) {
147+ break candidate_name;
148+ }
149+ counter += 1 ;
150+ } ;
151+ let candidate_expr = expr. alias ( & candidate_name) ;
152+ self . insert ( & candidate_expr) ;
153+ Ok ( candidate_expr)
154+ }
155+ }
156+
53157/// Count the number of distinct exprs in a list of group by expressions. If the
54158/// first element is a `GroupingSet` expression then it must be the only expr.
55159pub fn grouping_set_expr_count ( group_expr : & [ Expr ] ) -> Result < usize > {
@@ -1515,6 +1619,131 @@ mod tests {
15151619 use arrow:: datatypes:: { UnionFields , UnionMode } ;
15161620 use datafusion_expr_common:: signature:: Volatility ;
15171621
1622+ #[ test]
1623+ fn name_tracker_unique_names_pass_through ( ) -> Result < ( ) > {
1624+ let mut tracker = NameTracker :: new ( ) ;
1625+
1626+ // First expression should pass through unchanged
1627+ let expr1 = col ( "a" ) ;
1628+ let result1 = tracker. get_uniquely_named_expr ( expr1. clone ( ) ) ?;
1629+ assert_eq ! ( result1, col( "a" ) ) ;
1630+
1631+ // Different name should also pass through unchanged
1632+ let expr2 = col ( "b" ) ;
1633+ let result2 = tracker. get_uniquely_named_expr ( expr2) ?;
1634+ assert_eq ! ( result2, col( "b" ) ) ;
1635+
1636+ Ok ( ( ) )
1637+ }
1638+
1639+ #[ test]
1640+ fn name_tracker_duplicate_schema_name_gets_alias ( ) -> Result < ( ) > {
1641+ let mut tracker = NameTracker :: new ( ) ;
1642+
1643+ // First expression with name "a"
1644+ let expr1 = col ( "a" ) ;
1645+ let result1 = tracker. get_uniquely_named_expr ( expr1) ?;
1646+ assert_eq ! ( result1, col( "a" ) ) ;
1647+
1648+ // Second expression with same name "a" should get aliased
1649+ let expr2 = col ( "a" ) ;
1650+ let result2 = tracker. get_uniquely_named_expr ( expr2) ?;
1651+ assert_eq ! ( result2, col( "a" ) . alias( "a__temp__0" ) ) ;
1652+
1653+ // Third expression with same name "a" should get a different alias
1654+ let expr3 = col ( "a" ) ;
1655+ let result3 = tracker. get_uniquely_named_expr ( expr3) ?;
1656+ assert_eq ! ( result3, col( "a" ) . alias( "a__temp__1" ) ) ;
1657+
1658+ Ok ( ( ) )
1659+ }
1660+
1661+ #[ test]
1662+ fn name_tracker_qualified_then_unqualified_conflicts ( ) -> Result < ( ) > {
1663+ let mut tracker = NameTracker :: new ( ) ;
1664+
1665+ // First: qualified column "table.a"
1666+ let qualified_col = Expr :: Column ( Column :: new ( Some ( "table" ) , "a" ) ) ;
1667+ let result1 = tracker. get_uniquely_named_expr ( qualified_col) ?;
1668+ assert_eq ! ( result1, Expr :: Column ( Column :: new( Some ( "table" ) , "a" ) ) ) ;
1669+
1670+ // Second: unqualified column "a" - should conflict (ambiguous reference)
1671+ let unqualified_col = col ( "a" ) ;
1672+ let result2 = tracker. get_uniquely_named_expr ( unqualified_col) ?;
1673+ // Should be aliased to avoid ambiguous reference
1674+ assert_eq ! ( result2, col( "a" ) . alias( "a__temp__0" ) ) ;
1675+
1676+ Ok ( ( ) )
1677+ }
1678+
1679+ #[ test]
1680+ fn name_tracker_unqualified_then_qualified_conflicts ( ) -> Result < ( ) > {
1681+ let mut tracker = NameTracker :: new ( ) ;
1682+
1683+ // First: unqualified column "a"
1684+ let unqualified_col = col ( "a" ) ;
1685+ let result1 = tracker. get_uniquely_named_expr ( unqualified_col) ?;
1686+ assert_eq ! ( result1, col( "a" ) ) ;
1687+
1688+ // Second: qualified column "table.a" - should conflict (ambiguous reference)
1689+ let qualified_col = Expr :: Column ( Column :: new ( Some ( "table" ) , "a" ) ) ;
1690+ let result2 = tracker. get_uniquely_named_expr ( qualified_col) ?;
1691+ // Should be aliased to avoid ambiguous reference
1692+ assert_eq ! (
1693+ result2,
1694+ Expr :: Column ( Column :: new( Some ( "table" ) , "a" ) ) . alias( "table.a__temp__0" )
1695+ ) ;
1696+
1697+ Ok ( ( ) )
1698+ }
1699+
1700+ #[ test]
1701+ fn name_tracker_different_qualifiers_no_conflict ( ) -> Result < ( ) > {
1702+ let mut tracker = NameTracker :: new ( ) ;
1703+
1704+ // First: qualified column "table1.a"
1705+ let col1 = Expr :: Column ( Column :: new ( Some ( "table1" ) , "a" ) ) ;
1706+ let result1 = tracker. get_uniquely_named_expr ( col1. clone ( ) ) ?;
1707+ assert_eq ! ( result1, col1) ;
1708+
1709+ // Second: qualified column "table2.a" - different qualifier, different schema_name
1710+ // so should NOT conflict
1711+ let col2 = Expr :: Column ( Column :: new ( Some ( "table2" ) , "a" ) ) ;
1712+ let result2 = tracker. get_uniquely_named_expr ( col2. clone ( ) ) ?;
1713+ assert_eq ! ( result2, col2) ;
1714+
1715+ Ok ( ( ) )
1716+ }
1717+
1718+ #[ test]
1719+ fn name_tracker_aliased_expressions ( ) -> Result < ( ) > {
1720+ let mut tracker = NameTracker :: new ( ) ;
1721+
1722+ // First: col("x").alias("result")
1723+ let expr1 = col ( "x" ) . alias ( "result" ) ;
1724+ let result1 = tracker. get_uniquely_named_expr ( expr1. clone ( ) ) ?;
1725+ assert_eq ! ( result1, col( "x" ) . alias( "result" ) ) ;
1726+
1727+ // Second: col("y").alias("result") - same alias name, should conflict
1728+ let expr2 = col ( "y" ) . alias ( "result" ) ;
1729+ let result2 = tracker. get_uniquely_named_expr ( expr2) ?;
1730+ assert_eq ! ( result2, col( "y" ) . alias( "result" ) . alias( "result__temp__0" ) ) ;
1731+
1732+ Ok ( ( ) )
1733+ }
1734+
1735+ #[ test]
1736+ fn name_tracker_avoids_reserved_qualified_name ( ) -> Result < ( ) > {
1737+ let mut tracker = NameTracker :: new ( ) ;
1738+ tracker. reserve ( & [ Expr :: Column ( Column :: new ( Some ( "t" ) , "a" ) ) ] ) ;
1739+
1740+ assert_eq ! (
1741+ tracker. get_uniquely_named_expr( col( "a" ) ) ?,
1742+ col( "a" ) . alias( "a__temp__0" )
1743+ ) ;
1744+ Ok ( ( ) )
1745+ }
1746+
15181747 #[ test]
15191748 fn test_group_window_expr_by_sort_keys_empty_case ( ) -> Result < ( ) > {
15201749 let result = group_window_expr_by_sort_keys ( vec ! [ ] ) ?;
0 commit comments