From e6716d74ddeaa023e9889ea3e4965737f69e05c3 Mon Sep 17 00:00:00 2001 From: buraksenn Date: Thu, 5 Mar 2026 15:12:55 +0300 Subject: [PATCH 1/5] attach span to cte and diagnostic --- datafusion/sql/src/cte.rs | 21 ++++-- datafusion/sql/src/planner.rs | 26 ++++++-- datafusion/sql/src/select.rs | 84 ++++++++++++++++++++---- datafusion/sql/tests/cases/diagnostic.rs | 54 +++++++++++++++ 4 files changed, 165 insertions(+), 20 deletions(-) diff --git a/datafusion/sql/src/cte.rs b/datafusion/sql/src/cte.rs index 18766d7056355..bbf76dd69bdb6 100644 --- a/datafusion/sql/src/cte.rs +++ b/datafusion/sql/src/cte.rs @@ -20,7 +20,7 @@ use std::sync::Arc; use crate::planner::{ContextProvider, PlannerContext, SqlToRel}; use datafusion_common::{ - Result, not_impl_err, plan_err, + Diagnostic, Result, Span, not_impl_err, plan_err, tree_node::{TreeNode, TreeNodeRecursion}, }; use datafusion_expr::{LogicalPlan, LogicalPlanBuilder, TableSource}; @@ -37,10 +37,24 @@ impl SqlToRel<'_, S> { for cte in with.cte_tables { // A `WITH` block can't use the same name more than once let cte_name = self.ident_normalizer.normalize(cte.alias.name.clone()); + let cte_name_span = + Span::try_from_sqlparser_span(cte.alias.name.span); if planner_context.contains_cte(&cte_name) { + let first_span = planner_context.get_cte_span(&cte_name); + let mut diagnostic = Diagnostic::new_error( + format!( + "WITH query name {cte_name:?} specified more than once" + ), + cte_name_span, + ); + if let Some(first_span) = first_span { + diagnostic = + diagnostic.with_note("previously defined here", Some(first_span)); + } return plan_err!( "WITH query name {cte_name:?} specified more than once" - ); + ) + .map_err(|e| e.with_diagnostic(diagnostic)); } // Create a logical plan for the CTE @@ -53,8 +67,7 @@ impl SqlToRel<'_, S> { // Each `WITH` block can change the column names in the last // projection (e.g. "WITH table(t1, t2) AS SELECT 1, 2"). let final_plan = self.apply_table_alias(cte_plan, cte.alias)?; - // Export the CTE to the outer query - planner_context.insert_cte(cte_name, final_plan); + planner_context.insert_cte_with_span(cte_name, final_plan, cte_name_span); } Ok(()) } diff --git a/datafusion/sql/src/planner.rs b/datafusion/sql/src/planner.rs index 307f28e8ff9ad..6c813073994d1 100644 --- a/datafusion/sql/src/planner.rs +++ b/datafusion/sql/src/planner.rs @@ -27,7 +27,7 @@ use datafusion_common::TableReference; use datafusion_common::config::SqlParserOptions; use datafusion_common::datatype::{DataTypeExt, FieldExt}; use datafusion_common::error::add_possible_columns_to_diag; -use datafusion_common::{DFSchema, DataFusionError, Result, not_impl_err, plan_err}; +use datafusion_common::{DFSchema, DataFusionError, Result, Span, not_impl_err, plan_err}; use datafusion_common::{ DFSchemaRef, Diagnostic, SchemaError, field_not_found, internal_err, plan_datafusion_err, @@ -258,9 +258,9 @@ pub struct PlannerContext { /// Data types for numbered parameters ($1, $2, etc), if supplied /// in `PREPARE` statement prepare_param_data_types: Arc>, - /// Map of CTE name to logical plan of the WITH clause. + /// Map of CTE name to logical plan of the WITH clause and optional span. /// Use `Arc` to allow cheap cloning - ctes: HashMap>, + ctes: HashMap, Option)>, /// The queries schemas of outer query relations, used to resolve the outer referenced /// columns in subquery (recursive aware) @@ -387,19 +387,35 @@ impl PlannerContext { /// Subquery for the specified name pub fn insert_cte(&mut self, cte_name: impl Into, plan: LogicalPlan) { let cte_name = cte_name.into(); - self.ctes.insert(cte_name, Arc::new(plan)); + self.ctes.insert(cte_name, (Arc::new(plan), None)); + } + + /// Inserts a LogicalPlan with an optional span for the CTE + pub(super) fn insert_cte_with_span( + &mut self, + cte_name: impl Into, + plan: LogicalPlan, + span: Option, + ) { + let cte_name = cte_name.into(); + self.ctes.insert(cte_name, (Arc::new(plan), span)); } /// Return a plan for the Common Table Expression (CTE) / Subquery for the /// specified name pub fn get_cte(&self, cte_name: &str) -> Option<&LogicalPlan> { - self.ctes.get(cte_name).map(|cte| cte.as_ref()) + self.ctes.get(cte_name).map(|(cte, _)| cte.as_ref()) } /// Remove the plan of CTE / Subquery for the specified name pub(super) fn remove_cte(&mut self, cte_name: &str) { self.ctes.remove(cte_name); } + + /// Get the span of a previously defined CTE name + pub(super) fn get_cte_span(&self, name: &str) -> Option { + self.ctes.get(name).and_then(|(_, span)| *span) + } } /// SQL query planner and binder diff --git a/datafusion/sql/src/select.rs b/datafusion/sql/src/select.rs index edf4b9ef79e83..b7074a7ae554c 100644 --- a/datafusion/sql/src/select.rs +++ b/datafusion/sql/src/select.rs @@ -15,7 +15,7 @@ // specific language governing permissions and limitations // under the License. -use std::collections::HashSet; +use std::collections::{HashMap, HashSet}; use std::ops::ControlFlow; use std::sync::Arc; @@ -29,7 +29,7 @@ use crate::utils::{ use datafusion_common::error::DataFusionErrorBuilder; use datafusion_common::tree_node::{TreeNode, TreeNodeRecursion}; -use datafusion_common::{Column, DFSchema, Result, not_impl_err, plan_err}; +use datafusion_common::{Column, DFSchema, Diagnostic, Result, Span, not_impl_err, plan_err}; use datafusion_common::{RecursionUnnestOption, UnnestOptions}; use datafusion_expr::expr::{Alias, PlannedReplaceSelectItem, WildcardOptions}; use datafusion_expr::expr_rewriter::{ @@ -50,7 +50,9 @@ use sqlparser::ast::{ SelectItemQualifiedWildcardKind, WildcardAdditionalOptions, WindowType, visit_expressions_mut, }; -use sqlparser::ast::{NamedWindowDefinition, Select, SelectItem, TableWithJoins}; +use sqlparser::ast::{ + NamedWindowDefinition, Select, SelectItem, Spanned, TableFactor, TableWithJoins, +}; /// Result of the `aggregate` function, containing the aggregate plan and /// rewritten expressions that reference the aggregate output columns. @@ -690,21 +692,81 @@ impl SqlToRel<'_, S> { self.plan_table_with_joins(input, planner_context) } _ => { + let extract_table_name = + |t: &TableWithJoins| -> Option<(String, Option)> { + let span = + Span::try_from_sqlparser_span(t.relation.span()); + match &t.relation { + TableFactor::Table { alias: Some(a), .. } => { + let name = self + .ident_normalizer + .normalize(a.name.clone()); + Some((name, span)) + } + TableFactor::Table { name, alias: None, .. } => { + let table_name = name + .0 + .iter() + .filter_map(|p| p.as_ident()) + .map(|id| { + self.ident_normalizer + .normalize(id.clone()) + }) + .last()?; + Some((table_name, span)) + } + _ => None, + } + }; + + let mut alias_spans: HashMap> = + HashMap::new(); + let mut from = from.into_iter(); + let first = from.next().unwrap(); + + if let Some((name, span)) = extract_table_name(&first) { + alias_spans.entry(name).or_insert(span); + } + + let mut left = LogicalPlanBuilder::from( + self.plan_table_with_joins(first, planner_context)?, + ); - let mut left = LogicalPlanBuilder::from({ - let input = from.next().unwrap(); - self.plan_table_with_joins(input, planner_context)? - }); let old_outer_from_schema = { let left_schema = Some(Arc::clone(left.schema())); planner_context.set_outer_from_schema(left_schema) }; for input in from { - // Join `input` with the current result (`left`). - let right = self.plan_table_with_joins(input, planner_context)?; - left = left.cross_join(right)?; - // Update the outer FROM schema. + let current_span = + Span::try_from_sqlparser_span(input.relation.span()); + let current_name = extract_table_name(&input); + + if let Some((ref name, _)) = current_name { + alias_spans.entry(name.clone()).or_insert(current_span); + } + + let right = + self.plan_table_with_joins(input, planner_context)?; + + left = left.cross_join(right).map_err(|e| { + if let Some((ref name, _)) = current_name { + if let Some(prior_span) = + alias_spans.get(name).copied().flatten() + { + let mut diagnostic = Diagnostic::new_error( + "duplicate table alias in FROM clause", + current_span, + ); + diagnostic = diagnostic.with_note( + "first defined here", + Some(prior_span), + ); + return e.with_diagnostic(diagnostic); + } + } + e + })?; let left_schema = Some(Arc::clone(left.schema())); planner_context.set_outer_from_schema(left_schema); } diff --git a/datafusion/sql/tests/cases/diagnostic.rs b/datafusion/sql/tests/cases/diagnostic.rs index 7a729739469d3..96a3b18824422 100644 --- a/datafusion/sql/tests/cases/diagnostic.rs +++ b/datafusion/sql/tests/cases/diagnostic.rs @@ -390,3 +390,57 @@ fn test_syntax_error() -> Result<()> { }, } } + +#[test] +fn test_duplicate_cte_name() -> Result<()> { + let query = + "WITH /*a*/cte/*a*/ AS (SELECT 1 AS col), /*b*/cte/*b*/ AS (SELECT 2 AS col) SELECT 1"; + let spans = get_spans(query); + let diag = do_query(query); + assert_snapshot!(diag.message, @r#"WITH query name "cte" specified more than once"#); + assert_eq!(diag.span, Some(spans["b"])); + assert_eq!(diag.notes.len(), 1); + assert_snapshot!(diag.notes[0].message, @"previously defined here"); + assert_eq!(diag.notes[0].span, Some(spans["a"])); + Ok(()) +} + +#[test] +fn test_duplicate_table_alias() -> Result<()> { + let query = "SELECT * FROM /*a*/person a/*a*/, /*b*/person a/*b*/"; + let spans = get_spans(query); + let diag = do_query(query); + assert_snapshot!(diag.message, @"duplicate table alias in FROM clause"); + assert_eq!(diag.span, Some(spans["b"])); + assert_eq!(diag.notes.len(), 1); + assert_snapshot!(diag.notes[0].message, @"first defined here"); + assert_eq!(diag.notes[0].span, Some(spans["a"])); + Ok(()) +} + +#[test] +fn test_duplicate_table_alias_not_first() -> Result<()> { + let query = + "SELECT * FROM person a, /*b*/test_decimal b/*b*/, /*c*/person b/*c*/"; + let spans = get_spans(query); + let diag = do_query(query); + assert_snapshot!(diag.message, @"duplicate table alias in FROM clause"); + assert_eq!(diag.span, Some(spans["c"])); + assert_eq!(diag.notes.len(), 1); + assert_snapshot!(diag.notes[0].message, @"first defined here"); + assert_eq!(diag.notes[0].span, Some(spans["b"])); + Ok(()) +} + +#[test] +fn test_duplicate_bare_table_in_from() -> Result<()> { + let query = "SELECT * FROM /*a*/person/*a*/, /*b*/person/*b*/"; + let spans = get_spans(query); + let diag = do_query(query); + assert_snapshot!(diag.message, @"duplicate table alias in FROM clause"); + assert_eq!(diag.span, Some(spans["b"])); + assert_eq!(diag.notes.len(), 1); + assert_snapshot!(diag.notes[0].message, @"first defined here"); + assert_eq!(diag.notes[0].span, Some(spans["a"])); + Ok(()) +} From fbdbc6173b723722689da4a83fa04424511125e4 Mon Sep 17 00:00:00 2001 From: buraksenn Date: Thu, 5 Mar 2026 15:18:15 +0300 Subject: [PATCH 2/5] refactor: small adjustments --- datafusion/sql/src/cte.rs | 18 +++++++----------- datafusion/sql/src/select.rs | 16 +++++++--------- 2 files changed, 14 insertions(+), 20 deletions(-) diff --git a/datafusion/sql/src/cte.rs b/datafusion/sql/src/cte.rs index bbf76dd69bdb6..d7dc7b5fcc8ba 100644 --- a/datafusion/sql/src/cte.rs +++ b/datafusion/sql/src/cte.rs @@ -40,21 +40,17 @@ impl SqlToRel<'_, S> { let cte_name_span = Span::try_from_sqlparser_span(cte.alias.name.span); if planner_context.contains_cte(&cte_name) { - let first_span = planner_context.get_cte_span(&cte_name); - let mut diagnostic = Diagnostic::new_error( - format!( - "WITH query name {cte_name:?} specified more than once" - ), - cte_name_span, + let msg = format!( + "WITH query name {cte_name:?} specified more than once" ); - if let Some(first_span) = first_span { + let mut diagnostic = + Diagnostic::new_error(&msg, cte_name_span); + if let Some(first_span) = planner_context.get_cte_span(&cte_name) { diagnostic = diagnostic.with_note("previously defined here", Some(first_span)); } - return plan_err!( - "WITH query name {cte_name:?} specified more than once" - ) - .map_err(|e| e.with_diagnostic(diagnostic)); + return plan_err!("{msg}") + .map_err(|e| e.with_diagnostic(diagnostic)); } // Create a logical plan for the CTE diff --git a/datafusion/sql/src/select.rs b/datafusion/sql/src/select.rs index b7074a7ae554c..ba4c1f4080f12 100644 --- a/datafusion/sql/src/select.rs +++ b/datafusion/sql/src/select.rs @@ -738,27 +738,25 @@ impl SqlToRel<'_, S> { planner_context.set_outer_from_schema(left_schema) }; for input in from { - let current_span = - Span::try_from_sqlparser_span(input.relation.span()); let current_name = extract_table_name(&input); - if let Some((ref name, _)) = current_name { - alias_spans.entry(name.clone()).or_insert(current_span); + if let Some((ref name, ref span)) = current_name { + alias_spans.entry(name.clone()).or_insert(*span); } let right = self.plan_table_with_joins(input, planner_context)?; left = left.cross_join(right).map_err(|e| { - if let Some((ref name, _)) = current_name { + if let Some((ref name, ref current_span)) = current_name { if let Some(prior_span) = alias_spans.get(name).copied().flatten() { - let mut diagnostic = Diagnostic::new_error( + let diagnostic = Diagnostic::new_error( "duplicate table alias in FROM clause", - current_span, - ); - diagnostic = diagnostic.with_note( + *current_span, + ) + .with_note( "first defined here", Some(prior_span), ); From 10f69467fba0d95b21e1f4370a8322ac0202aabf Mon Sep 17 00:00:00 2001 From: buraksenn Date: Thu, 5 Mar 2026 15:55:49 +0300 Subject: [PATCH 3/5] cargo fmt --- datafusion/sql/src/cte.rs | 14 ++++------- datafusion/sql/src/planner.rs | 4 ++- datafusion/sql/src/select.rs | 32 ++++++++++-------------- datafusion/sql/tests/cases/diagnostic.rs | 6 ++--- 4 files changed, 23 insertions(+), 33 deletions(-) diff --git a/datafusion/sql/src/cte.rs b/datafusion/sql/src/cte.rs index d7dc7b5fcc8ba..63f0ba20cf7bd 100644 --- a/datafusion/sql/src/cte.rs +++ b/datafusion/sql/src/cte.rs @@ -37,20 +37,16 @@ impl SqlToRel<'_, S> { for cte in with.cte_tables { // A `WITH` block can't use the same name more than once let cte_name = self.ident_normalizer.normalize(cte.alias.name.clone()); - let cte_name_span = - Span::try_from_sqlparser_span(cte.alias.name.span); + let cte_name_span = Span::try_from_sqlparser_span(cte.alias.name.span); if planner_context.contains_cte(&cte_name) { - let msg = format!( - "WITH query name {cte_name:?} specified more than once" - ); - let mut diagnostic = - Diagnostic::new_error(&msg, cte_name_span); + let msg = + format!("WITH query name {cte_name:?} specified more than once"); + let mut diagnostic = Diagnostic::new_error(&msg, cte_name_span); if let Some(first_span) = planner_context.get_cte_span(&cte_name) { diagnostic = diagnostic.with_note("previously defined here", Some(first_span)); } - return plan_err!("{msg}") - .map_err(|e| e.with_diagnostic(diagnostic)); + return plan_err!("{msg}").map_err(|e| e.with_diagnostic(diagnostic)); } // Create a logical plan for the CTE diff --git a/datafusion/sql/src/planner.rs b/datafusion/sql/src/planner.rs index 6c813073994d1..b816aa6f9e69c 100644 --- a/datafusion/sql/src/planner.rs +++ b/datafusion/sql/src/planner.rs @@ -27,7 +27,9 @@ use datafusion_common::TableReference; use datafusion_common::config::SqlParserOptions; use datafusion_common::datatype::{DataTypeExt, FieldExt}; use datafusion_common::error::add_possible_columns_to_diag; -use datafusion_common::{DFSchema, DataFusionError, Result, Span, not_impl_err, plan_err}; +use datafusion_common::{ + DFSchema, DataFusionError, Result, Span, not_impl_err, plan_err, +}; use datafusion_common::{ DFSchemaRef, Diagnostic, SchemaError, field_not_found, internal_err, plan_datafusion_err, diff --git a/datafusion/sql/src/select.rs b/datafusion/sql/src/select.rs index ba4c1f4080f12..468532a7fb964 100644 --- a/datafusion/sql/src/select.rs +++ b/datafusion/sql/src/select.rs @@ -29,7 +29,9 @@ use crate::utils::{ use datafusion_common::error::DataFusionErrorBuilder; use datafusion_common::tree_node::{TreeNode, TreeNodeRecursion}; -use datafusion_common::{Column, DFSchema, Diagnostic, Result, Span, not_impl_err, plan_err}; +use datafusion_common::{ + Column, DFSchema, Diagnostic, Result, Span, not_impl_err, plan_err, +}; use datafusion_common::{RecursionUnnestOption, UnnestOptions}; use datafusion_expr::expr::{Alias, PlannedReplaceSelectItem, WildcardOptions}; use datafusion_expr::expr_rewriter::{ @@ -694,24 +696,21 @@ impl SqlToRel<'_, S> { _ => { let extract_table_name = |t: &TableWithJoins| -> Option<(String, Option)> { - let span = - Span::try_from_sqlparser_span(t.relation.span()); + let span = Span::try_from_sqlparser_span(t.relation.span()); match &t.relation { TableFactor::Table { alias: Some(a), .. } => { - let name = self - .ident_normalizer - .normalize(a.name.clone()); + let name = + self.ident_normalizer.normalize(a.name.clone()); Some((name, span)) } - TableFactor::Table { name, alias: None, .. } => { + TableFactor::Table { + name, alias: None, .. + } => { let table_name = name .0 .iter() .filter_map(|p| p.as_ident()) - .map(|id| { - self.ident_normalizer - .normalize(id.clone()) - }) + .map(|id| self.ident_normalizer.normalize(id.clone())) .last()?; Some((table_name, span)) } @@ -719,8 +718,7 @@ impl SqlToRel<'_, S> { } }; - let mut alias_spans: HashMap> = - HashMap::new(); + let mut alias_spans: HashMap> = HashMap::new(); let mut from = from.into_iter(); let first = from.next().unwrap(); @@ -744,8 +742,7 @@ impl SqlToRel<'_, S> { alias_spans.entry(name.clone()).or_insert(*span); } - let right = - self.plan_table_with_joins(input, planner_context)?; + let right = self.plan_table_with_joins(input, planner_context)?; left = left.cross_join(right).map_err(|e| { if let Some((ref name, ref current_span)) = current_name { @@ -756,10 +753,7 @@ impl SqlToRel<'_, S> { "duplicate table alias in FROM clause", *current_span, ) - .with_note( - "first defined here", - Some(prior_span), - ); + .with_note("first defined here", Some(prior_span)); return e.with_diagnostic(diagnostic); } } diff --git a/datafusion/sql/tests/cases/diagnostic.rs b/datafusion/sql/tests/cases/diagnostic.rs index 96a3b18824422..f3c455d446f29 100644 --- a/datafusion/sql/tests/cases/diagnostic.rs +++ b/datafusion/sql/tests/cases/diagnostic.rs @@ -393,8 +393,7 @@ fn test_syntax_error() -> Result<()> { #[test] fn test_duplicate_cte_name() -> Result<()> { - let query = - "WITH /*a*/cte/*a*/ AS (SELECT 1 AS col), /*b*/cte/*b*/ AS (SELECT 2 AS col) SELECT 1"; + let query = "WITH /*a*/cte/*a*/ AS (SELECT 1 AS col), /*b*/cte/*b*/ AS (SELECT 2 AS col) SELECT 1"; let spans = get_spans(query); let diag = do_query(query); assert_snapshot!(diag.message, @r#"WITH query name "cte" specified more than once"#); @@ -420,8 +419,7 @@ fn test_duplicate_table_alias() -> Result<()> { #[test] fn test_duplicate_table_alias_not_first() -> Result<()> { - let query = - "SELECT * FROM person a, /*b*/test_decimal b/*b*/, /*c*/person b/*c*/"; + let query = "SELECT * FROM person a, /*b*/test_decimal b/*b*/, /*c*/person b/*c*/"; let spans = get_spans(query); let diag = do_query(query); assert_snapshot!(diag.message, @"duplicate table alias in FROM clause"); From 17868149511fe5389234cc8f521c7ccc86873540 Mon Sep 17 00:00:00 2001 From: buraksenn Date: Thu, 5 Mar 2026 16:04:07 +0300 Subject: [PATCH 4/5] chore: address clippy --- datafusion/sql/src/select.rs | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/datafusion/sql/src/select.rs b/datafusion/sql/src/select.rs index 468532a7fb964..e0206a9175f36 100644 --- a/datafusion/sql/src/select.rs +++ b/datafusion/sql/src/select.rs @@ -711,7 +711,7 @@ impl SqlToRel<'_, S> { .iter() .filter_map(|p| p.as_ident()) .map(|id| self.ident_normalizer.normalize(id.clone())) - .last()?; + .next_back()?; Some((table_name, span)) } _ => None, @@ -745,17 +745,16 @@ impl SqlToRel<'_, S> { let right = self.plan_table_with_joins(input, planner_context)?; left = left.cross_join(right).map_err(|e| { - if let Some((ref name, ref current_span)) = current_name { - if let Some(prior_span) = + if let Some((ref name, ref current_span)) = current_name + && let Some(prior_span) = alias_spans.get(name).copied().flatten() - { - let diagnostic = Diagnostic::new_error( - "duplicate table alias in FROM clause", - *current_span, - ) - .with_note("first defined here", Some(prior_span)); - return e.with_diagnostic(diagnostic); - } + { + let diagnostic = Diagnostic::new_error( + "duplicate table alias in FROM clause", + *current_span, + ) + .with_note("first defined here", Some(prior_span)); + return e.with_diagnostic(diagnostic); } e })?; From 308765676e0657d5a052eb6d628b55b86dcfaca8 Mon Sep 17 00:00:00 2001 From: buraksenn Date: Fri, 6 Mar 2026 16:23:03 +0300 Subject: [PATCH 5/5] address issue in the review --- datafusion/sql/src/select.rs | 31 ++++++++++++------------ datafusion/sql/tests/cases/diagnostic.rs | 26 ++++++++++++++++++++ 2 files changed, 41 insertions(+), 16 deletions(-) diff --git a/datafusion/sql/src/select.rs b/datafusion/sql/src/select.rs index e0206a9175f36..a4192411ba735 100644 --- a/datafusion/sql/src/select.rs +++ b/datafusion/sql/src/select.rs @@ -738,26 +738,25 @@ impl SqlToRel<'_, S> { for input in from { let current_name = extract_table_name(&input); - if let Some((ref name, ref span)) = current_name { - alias_spans.entry(name.clone()).or_insert(*span); + if let Some((ref name, current_span)) = current_name { + if let Some(prior_span) = alias_spans.get(name) { + let mut diagnostic = Diagnostic::new_error( + "duplicate table alias in FROM clause", + current_span, + ); + if let Some(span) = *prior_span { + diagnostic = diagnostic + .with_note("first defined here", Some(span)); + } + return plan_err!("duplicate table alias in FROM clause") + .map_err(|e| e.with_diagnostic(diagnostic)); + } + alias_spans.insert(name.clone(), current_span); } let right = self.plan_table_with_joins(input, planner_context)?; - left = left.cross_join(right).map_err(|e| { - if let Some((ref name, ref current_span)) = current_name - && let Some(prior_span) = - alias_spans.get(name).copied().flatten() - { - let diagnostic = Diagnostic::new_error( - "duplicate table alias in FROM clause", - *current_span, - ) - .with_note("first defined here", Some(prior_span)); - return e.with_diagnostic(diagnostic); - } - e - })?; + left = left.cross_join(right)?; let left_schema = Some(Arc::clone(left.schema())); planner_context.set_outer_from_schema(left_schema); } diff --git a/datafusion/sql/tests/cases/diagnostic.rs b/datafusion/sql/tests/cases/diagnostic.rs index f3c455d446f29..6858a5709a82d 100644 --- a/datafusion/sql/tests/cases/diagnostic.rs +++ b/datafusion/sql/tests/cases/diagnostic.rs @@ -442,3 +442,29 @@ fn test_duplicate_bare_table_in_from() -> Result<()> { assert_eq!(diag.notes[0].span, Some(spans["a"])); Ok(()) } + +#[test] +fn test_duplicate_alias_non_overlapping_columns() -> Result<()> { + let query = "SELECT * FROM /*a*/j1 AS t/*a*/, /*b*/j2 AS t/*b*/"; + let spans = get_spans(query); + let diag = do_query(query); + assert_snapshot!(diag.message, @"duplicate table alias in FROM clause"); + assert_eq!(diag.span, Some(spans["b"])); + assert_eq!(diag.notes.len(), 1); + assert_snapshot!(diag.notes[0].message, @"first defined here"); + assert_eq!(diag.notes[0].span, Some(spans["a"])); + Ok(()) +} + +#[test] +fn test_duplicate_alias_non_overlapping_three_tables() -> Result<()> { + let query = "SELECT * FROM j1 AS x, /*a*/j2 AS t/*a*/, j3 AS y, /*b*/j1 AS t/*b*/"; + let spans = get_spans(query); + let diag = do_query(query); + assert_snapshot!(diag.message, @"duplicate table alias in FROM clause"); + assert_eq!(diag.span, Some(spans["b"])); + assert_eq!(diag.notes.len(), 1); + assert_snapshot!(diag.notes[0].message, @"first defined here"); + assert_eq!(diag.notes[0].span, Some(spans["a"])); + Ok(()) +}