Skip to content

Commit 8e83e17

Browse files
Generate unique LATERAL FLATTEN aliases per query
Replace the hardcoded FLATTEN_DEFAULT_ALIAS ("_unnest") with a per-SelectBuilder counter that generates unique aliases (_unnest_1, _unnest_2, …). This prevents alias collisions when multiple unnests appear in the same query. - Add flatten_alias_counter to SelectBuilder with next/current accessor methods, scoped to one SELECT so subqueries get independent counters - Remove FLATTEN_DEFAULT_ALIAS constant, the dead alias_name() method, and the default alias from FlattenRelationBuilder - All three FLATTEN code paths (placeholder projection, display-name projection, and Unnest handler) now coordinate through the SelectBuilder to ensure SELECT items and FROM clause use the same alias - Use internal_datafusion_err! macro for FLATTEN error handling - Migrate unnest tests from partial .contains() assertions to insta::assert_snapshot! for full SQL verification
1 parent 5f1d805 commit 8e83e17

3 files changed

Lines changed: 174 additions & 184 deletions

File tree

datafusion/sql/src/unparser/ast.rs

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -162,9 +162,28 @@ pub struct SelectBuilder {
162162
qualify: Option<ast::Expr>,
163163
value_table_mode: Option<ast::ValueTableMode>,
164164
flavor: Option<SelectFlavor>,
165+
/// Counter for generating unique LATERAL FLATTEN aliases within this SELECT.
166+
flatten_alias_counter: usize,
165167
}
166168

167169
impl SelectBuilder {
170+
/// Generate a unique alias for a LATERAL FLATTEN relation
171+
/// (`_unnest_1`, `_unnest_2`, …). Each call returns a fresh name.
172+
pub fn next_flatten_alias(&mut self) -> String {
173+
self.flatten_alias_counter += 1;
174+
format!("_unnest_{}", self.flatten_alias_counter)
175+
}
176+
177+
/// Returns the most recently generated flatten alias, or `None` if
178+
/// `next_flatten_alias` has not been called yet.
179+
pub fn current_flatten_alias(&self) -> Option<String> {
180+
if self.flatten_alias_counter > 0 {
181+
Some(format!("_unnest_{}", self.flatten_alias_counter))
182+
} else {
183+
None
184+
}
185+
}
186+
168187
pub fn distinct(&mut self, value: Option<ast::Distinct>) -> &mut Self {
169188
self.distinct = value;
170189
self
@@ -371,6 +390,7 @@ impl SelectBuilder {
371390
qualify: Default::default(),
372391
value_table_mode: Default::default(),
373392
flavor: Some(SelectFlavor::Standard),
393+
flatten_alias_counter: 0,
374394
}
375395
}
376396
}
@@ -697,10 +717,6 @@ impl Default for UnnestRelationBuilder {
697717
}
698718
}
699719

700-
/// Default table alias for FLATTEN table factors.
701-
/// Snowflake requires an alias to reference output columns (e.g. `_unnest.VALUE`).
702-
pub const FLATTEN_DEFAULT_ALIAS: &str = "_unnest";
703-
704720
/// Builds a `LATERAL FLATTEN(INPUT => expr, OUTER => bool)` table factor
705721
/// for Snowflake-style unnesting.
706722
#[derive(Clone)]
@@ -757,22 +773,9 @@ impl FlattenRelationBuilder {
757773
})
758774
}
759775

760-
/// Returns the alias name for this FLATTEN relation.
761-
/// Used to build qualified column references like `alias.VALUE`.
762-
pub fn alias_name(&self) -> &str {
763-
self.alias
764-
.as_ref()
765-
.map(|a| a.name.value.as_str())
766-
.unwrap_or(FLATTEN_DEFAULT_ALIAS)
767-
}
768-
769776
fn create_empty() -> Self {
770777
Self {
771-
alias: Some(ast::TableAlias {
772-
name: ast::Ident::with_quote('"', FLATTEN_DEFAULT_ALIAS),
773-
columns: vec![],
774-
explicit: true,
775-
}),
778+
alias: None,
776779
input_expr: None,
777780
outer: false,
778781
}

datafusion/sql/src/unparser/plan.rs

Lines changed: 137 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,12 @@ use crate::unparser::extension_unparser::{
3838
};
3939
use crate::unparser::utils::{find_unnest_node_until_relation, unproject_agg_exprs};
4040
use crate::unparser::{
41-
ast::FLATTEN_DEFAULT_ALIAS, ast::FlattenRelationBuilder, ast::UnnestRelationBuilder,
42-
rewrite::rewrite_qualify,
41+
ast::FlattenRelationBuilder, ast::UnnestRelationBuilder, rewrite::rewrite_qualify,
4342
};
4443
use crate::utils::UNNEST_PLACEHOLDER;
4544
use datafusion_common::{
4645
Column, DataFusionError, Result, ScalarValue, TableReference, assert_or_internal_err,
47-
internal_err, not_impl_err,
46+
internal_datafusion_err, internal_err, not_impl_err,
4847
tree_node::{TransformedResult, TreeNode, TreeNodeRecursion},
4948
};
5049
use datafusion_expr::expr::{OUTER_REFERENCE_COLUMN_PREFIX, UNNEST_COLUMN_PREFIX};
@@ -239,17 +238,12 @@ impl Unparser<'_> {
239238
let mut exprs = p.expr.clone();
240239

241240
// If an Unnest node is found within the select, find and unproject the unnest column
241+
let flatten_alias = select.current_flatten_alias();
242242
if let Some(unnest) = find_unnest_node_within_select(plan) {
243-
if self.dialect.unnest_as_lateral_flatten() {
243+
if let Some(ref alias) = flatten_alias {
244244
exprs = exprs
245245
.into_iter()
246-
.map(|e| {
247-
unproject_unnest_expr_as_flatten_value(
248-
e,
249-
unnest,
250-
FLATTEN_DEFAULT_ALIAS,
251-
)
252-
})
246+
.map(|e| unproject_unnest_expr_as_flatten_value(e, unnest, alias))
253247
.collect::<Result<Vec<_>>>()?;
254248
} else {
255249
exprs = exprs
@@ -294,19 +288,17 @@ impl Unparser<'_> {
294288
select.projection(items);
295289
}
296290
_ => {
297-
let use_flatten = self.dialect.unnest_as_lateral_flatten();
298291
let items = exprs
299292
.iter()
300293
.map(|e| {
301294
// After unproject_unnest_expr_as_flatten_value, an
302295
// internal UNNEST display-name alias may still wrap
303296
// the rewritten _unnest.VALUE column. Replace it
304297
// with the bare FLATTEN VALUE select item.
305-
if use_flatten && Self::has_internal_unnest_alias(e) {
306-
return Ok(self.build_flatten_value_select_item(
307-
FLATTEN_DEFAULT_ALIAS,
308-
None,
309-
));
298+
if let Some(ref alias) = flatten_alias
299+
&& Self::has_internal_unnest_alias(e)
300+
{
301+
return Ok(self.build_flatten_value_select_item(alias, None));
310302
}
311303
self.select_item_to_sql(e)
312304
})
@@ -360,6 +352,105 @@ impl Unparser<'_> {
360352
}
361353
}
362354

355+
/// Projection unparsing when [`super::dialect::Dialect::unnest_as_lateral_flatten`] is enabled:
356+
/// Snowflake-style `LATERAL FLATTEN` for unnest (not other dialect spellings).
357+
///
358+
/// [`Self::peel_to_unnest_with_modifiers`] walks through any intermediate
359+
/// Limit/Sort nodes (the optimizer can insert these between the Projection
360+
/// and the Unnest), applies their modifiers to the query, and returns the
361+
/// Unnest plus the [`LogicalPlan`] ref to recurse into. This bypasses the
362+
/// normal Limit/Sort handlers which would wrap the subtree in a derived
363+
/// subquery.
364+
///
365+
/// SELECT rendering is delegated to [`Self::reconstruct_select_statement`],
366+
/// which rewrites placeholder columns to `alias."VALUE"` via
367+
/// [`unproject_unnest_expr_as_flatten_value`].
368+
///
369+
/// Returns `Ok(true)` when this path fully handled the projection.
370+
fn try_projection_unnest_as_lateral_flatten(
371+
&self,
372+
plan: &LogicalPlan,
373+
p: &Projection,
374+
query: &mut Option<QueryBuilder>,
375+
select: &mut SelectBuilder,
376+
relation: &mut RelationBuilder,
377+
unnest_input_type: Option<&UnnestInputType>,
378+
) -> Result<bool> {
379+
// unnest_as_lateral_flatten: Snowflake LATERAL FLATTEN
380+
if self.dialect.unnest_as_lateral_flatten()
381+
&& unnest_input_type.is_some()
382+
&& let Some((unnest, unnest_plan)) =
383+
self.peel_to_unnest_with_modifiers(p.input.as_ref(), query)?
384+
&& let Some(mut flatten) = self.try_unnest_to_lateral_flatten_sql(unnest)?
385+
{
386+
let inner_projection = Self::peel_to_inner_projection(unnest.input.as_ref())
387+
.ok_or_else(|| {
388+
internal_datafusion_err!(
389+
"Unnest input is not a Projection: {:?}",
390+
unnest.input
391+
)
392+
})?;
393+
394+
// Generate a unique alias for this FLATTEN so that
395+
// multiple unnests in the same query don't collide.
396+
// When the SELECT was already built by an outer Projection
397+
// (already_projected), it already called
398+
// next_flatten_alias(), so we reuse that alias.
399+
if !select.already_projected() {
400+
let flatten_alias_name = select.next_flatten_alias();
401+
flatten.alias(Some(ast::TableAlias {
402+
name: Ident::with_quote('"', &flatten_alias_name),
403+
columns: vec![],
404+
explicit: true,
405+
}));
406+
self.reconstruct_select_statement(plan, p, select)?;
407+
} else if let Some(alias) = select.current_flatten_alias() {
408+
flatten.alias(Some(ast::TableAlias {
409+
name: Ident::with_quote('"', &alias),
410+
columns: vec![],
411+
explicit: true,
412+
}));
413+
}
414+
415+
if matches!(
416+
inner_projection.input.as_ref(),
417+
LogicalPlan::EmptyRelation(_)
418+
) {
419+
// Inline array (e.g. UNNEST([1,2,3])):
420+
// FLATTEN is the sole FROM source.
421+
relation.flatten(flatten);
422+
self.select_to_sql_recursively(unnest_plan, query, select, relation)?;
423+
return Ok(true);
424+
}
425+
426+
// Non-empty source (table, subquery, etc.):
427+
// recurse to set the primary FROM, then attach FLATTEN
428+
// as a CROSS JOIN.
429+
self.select_to_sql_recursively(unnest_plan, query, select, relation)?;
430+
431+
let flatten_factor = flatten
432+
.build()
433+
.map_err(|e| internal_datafusion_err!("Failed to build FLATTEN: {e}"))?;
434+
let cross_join = ast::Join {
435+
relation: flatten_factor,
436+
global: false,
437+
join_operator: ast::JoinOperator::CrossJoin(ast::JoinConstraint::None),
438+
};
439+
if let Some(mut from) = select.pop_from() {
440+
from.push_join(cross_join);
441+
select.push_from(from);
442+
} else {
443+
let mut twj = TableWithJoinsBuilder::default();
444+
twj.push_join(cross_join);
445+
select.push_from(twj);
446+
}
447+
448+
return Ok(true);
449+
}
450+
451+
Ok(false)
452+
}
453+
363454
#[cfg_attr(feature = "recursive_protection", recursive::recursive)]
364455
fn select_to_sql_recursively(
365456
&self,
@@ -435,80 +526,14 @@ impl Unparser<'_> {
435526
);
436527
}
437528

438-
// --- Snowflake LATERAL FLATTEN path ---
439-
// `peel_to_unnest_with_modifiers` walks through any
440-
// intermediate Limit/Sort nodes (the optimizer can insert
441-
// these between the Projection and the Unnest), applies
442-
// their modifiers to the query, and returns the Unnest +
443-
// the LogicalPlan ref to recurse into. This bypasses the
444-
// normal Limit/Sort handlers which would wrap the subtree
445-
// in a derived subquery.
446-
// SELECT rendering is delegated to
447-
// `reconstruct_select_statement`, which rewrites
448-
// placeholder columns to `"_unnest"."VALUE"` via
449-
// `unproject_unnest_expr_as_flatten_value` — this works
450-
// for bare, wrapped, and multi-expression projections.
451-
if self.dialect.unnest_as_lateral_flatten()
452-
&& unnest_input_type.is_some()
453-
&& let Some((unnest, unnest_plan)) =
454-
self.peel_to_unnest_with_modifiers(p.input.as_ref(), query)?
455-
&& let Some(flatten) =
456-
self.try_unnest_to_lateral_flatten_sql(unnest)?
457-
{
458-
let inner_projection =
459-
Self::peel_to_inner_projection(unnest.input.as_ref())
460-
.ok_or_else(|| {
461-
DataFusionError::Internal(format!(
462-
"Unnest input is not a Projection: {:?}",
463-
unnest.input
464-
))
465-
})?;
466-
467-
// An outer plan (e.g. a wrapping Projection) may have
468-
// already set SELECT columns; only set them once.
469-
if !select.already_projected() {
470-
self.reconstruct_select_statement(plan, p, select)?;
471-
}
472-
473-
if matches!(
474-
inner_projection.input.as_ref(),
475-
LogicalPlan::EmptyRelation(_)
476-
) {
477-
// Inline array (e.g. UNNEST([1,2,3])):
478-
// FLATTEN is the sole FROM source.
479-
relation.flatten(flatten);
480-
return self.select_to_sql_recursively(
481-
unnest_plan,
482-
query,
483-
select,
484-
relation,
485-
);
486-
}
487-
488-
// Non-empty source (table, subquery, etc.):
489-
// recurse to set the primary FROM, then attach FLATTEN
490-
// as a CROSS JOIN.
491-
self.select_to_sql_recursively(unnest_plan, query, select, relation)?;
492-
493-
let flatten_factor = flatten.build().map_err(|e| {
494-
DataFusionError::Internal(format!("Failed to build FLATTEN: {e}"))
495-
})?;
496-
let cross_join = ast::Join {
497-
relation: flatten_factor,
498-
global: false,
499-
join_operator: ast::JoinOperator::CrossJoin(
500-
ast::JoinConstraint::None,
501-
),
502-
};
503-
if let Some(mut from) = select.pop_from() {
504-
from.push_join(cross_join);
505-
select.push_from(from);
506-
} else {
507-
let mut twj = TableWithJoinsBuilder::default();
508-
twj.push_join(cross_join);
509-
select.push_from(twj);
510-
}
511-
529+
if self.try_projection_unnest_as_lateral_flatten(
530+
plan,
531+
p,
532+
query,
533+
select,
534+
relation,
535+
unnest_input_type.as_ref(),
536+
)? {
512537
return Ok(());
513538
}
514539

@@ -536,6 +561,16 @@ impl Unparser<'_> {
536561
columns,
537562
);
538563
}
564+
// For Snowflake FLATTEN: when the outer Projection has
565+
// UNNEST(...) display-name columns (from SELECT * / SELECT
566+
// UNNEST(...)), generate a flatten alias now so that
567+
// reconstruct_select_statement and the downstream Unnest
568+
// handler both use the same alias.
569+
if self.dialect.unnest_as_lateral_flatten()
570+
&& p.expr.iter().any(Self::has_internal_unnest_alias)
571+
{
572+
select.next_flatten_alias();
573+
}
539574
self.reconstruct_select_statement(plan, p, select)?;
540575
self.select_to_sql_recursively(p.input.as_ref(), query, select, relation)
541576
}
@@ -1135,9 +1170,19 @@ impl Unparser<'_> {
11351170
// relation here so the FROM clause is emitted.
11361171
if self.dialect.unnest_as_lateral_flatten()
11371172
&& !relation.has_relation()
1138-
&& let Some(flatten_relation) =
1173+
&& let Some(mut flatten_relation) =
11391174
self.try_unnest_to_lateral_flatten_sql(unnest)?
11401175
{
1176+
// Use the alias already generated by the Projection
1177+
// handler so SELECT items and the FLATTEN relation
1178+
// reference the same name.
1179+
if let Some(alias) = select.current_flatten_alias() {
1180+
flatten_relation.alias(Some(ast::TableAlias {
1181+
name: Ident::with_quote('"', &alias),
1182+
columns: vec![],
1183+
explicit: true,
1184+
}));
1185+
}
11411186
relation.flatten(flatten_relation);
11421187
}
11431188

0 commit comments

Comments
 (0)