Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
75cd4d7
fix: preserve subquery structure when unparsing SubqueryAlias over Ag…
yonatan-sevenai Mar 22, 2026
2f8f667
Merge branch 'main' into main
yonatan-sevenai Mar 27, 2026
0d223f9
fix: preserve subquery structure when unparsing SubqueryAlias over Ag…
yonatan-sevenai Mar 27, 2026
42f7f64
Fixes in PR
yonatan-sevenai Apr 4, 2026
ae2fdcf
Merge branch 'main' into main
yonatan-sevenai Apr 4, 2026
1667252
Merge branch 'main' into main
yonatan-sevenai Apr 7, 2026
ab8acf9
Merge branch 'apache:main' into main
yonatan-sevenai Apr 9, 2026
adfec24
Merge remote-tracking branch 'datafusion/main'
yonatan-sevenai Apr 10, 2026
8d95d48
test: define correct expected output for Snowflake FLATTEN unparsing
yonatan-sevenai Apr 11, 2026
6acaa9c
Working on unnest support for snowflake
yonatan-sevenai Apr 11, 2026
e701962
Snowflake Dialect
yonatan-sevenai Apr 11, 2026
9321138
fix: Snowflake LATERAL FLATTEN handles SubqueryAlias between Unnest a…
yonatan-sevenai Apr 11, 2026
4cc97c8
Merge remote-tracking branch 'datafusion/main' into feature/snowflake…
yonatan-sevenai Apr 11, 2026
065e111
Snowflake Dialect
yonatan-sevenai Apr 12, 2026
909d8c6
More snowflake dialect fixes for unnest
yonatan-sevenai Apr 13, 2026
54d8a1d
More snowflake dialect fixes for unnes
yonatan-sevenai Apr 13, 2026
c083c6c
Quoting fix
yonatan-sevenai Apr 13, 2026
455fc83
Unnest fixes for multi-arguments
yonatan-sevenai Apr 13, 2026
5f1d805
Few more fixes
yonatan-sevenai Apr 13, 2026
8e83e17
Generate unique LATERAL FLATTEN aliases per query
yonatan-sevenai Apr 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 101 additions & 1 deletion datafusion/sql/src/unparser/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -162,9 +162,28 @@ pub struct SelectBuilder {
qualify: Option<ast::Expr>,
value_table_mode: Option<ast::ValueTableMode>,
flavor: Option<SelectFlavor>,
/// Counter for generating unique LATERAL FLATTEN aliases within this SELECT.
flatten_alias_counter: usize,
}

impl SelectBuilder {
/// Generate a unique alias for a LATERAL FLATTEN relation
/// (`_unnest_1`, `_unnest_2`, …). Each call returns a fresh name.
pub fn next_flatten_alias(&mut self) -> String {
self.flatten_alias_counter += 1;
format!("_unnest_{}", self.flatten_alias_counter)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's better to use a constant to present the name. Something like

pub const UNNEST_PREFIX: &str = "__unnest_";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add a test for the multiple unnest case?

}

/// Returns the most recently generated flatten alias, or `None` if
/// `next_flatten_alias` has not been called yet.
pub fn current_flatten_alias(&self) -> Option<String> {
if self.flatten_alias_counter > 0 {
Some(format!("_unnest_{}", self.flatten_alias_counter))
} else {
None
}
}

pub fn distinct(&mut self, value: Option<ast::Distinct>) -> &mut Self {
self.distinct = value;
self
Expand Down Expand Up @@ -371,6 +390,7 @@ impl SelectBuilder {
qualify: Default::default(),
value_table_mode: Default::default(),
flavor: Some(SelectFlavor::Standard),
flatten_alias_counter: 0,
}
}
}
Expand Down Expand Up @@ -432,11 +452,11 @@ pub struct RelationBuilder {
}

#[derive(Clone)]
#[expect(clippy::large_enum_variant)]
enum TableFactorBuilder {
Table(TableRelationBuilder),
Derived(DerivedRelationBuilder),
Unnest(UnnestRelationBuilder),
Flatten(FlattenRelationBuilder),
Empty,
}

Expand All @@ -458,6 +478,11 @@ impl RelationBuilder {
self
}

pub fn flatten(&mut self, value: FlattenRelationBuilder) -> &mut Self {
self.relation = Some(TableFactorBuilder::Flatten(value));
self
}

pub fn empty(&mut self) -> &mut Self {
self.relation = Some(TableFactorBuilder::Empty);
self
Expand All @@ -474,6 +499,9 @@ impl RelationBuilder {
Some(TableFactorBuilder::Unnest(ref mut rel_builder)) => {
rel_builder.alias = value;
}
Some(TableFactorBuilder::Flatten(ref mut rel_builder)) => {
rel_builder.alias = value;
}
Some(TableFactorBuilder::Empty) => (),
None => (),
}
Expand All @@ -484,6 +512,7 @@ impl RelationBuilder {
Some(TableFactorBuilder::Table(ref value)) => Some(value.build()?),
Some(TableFactorBuilder::Derived(ref value)) => Some(value.build()?),
Some(TableFactorBuilder::Unnest(ref value)) => Some(value.build()?),
Some(TableFactorBuilder::Flatten(ref value)) => Some(value.build()?),
Some(TableFactorBuilder::Empty) => None,
None => return Err(Into::into(UninitializedFieldError::from("relation"))),
})
Expand Down Expand Up @@ -688,6 +717,77 @@ impl Default for UnnestRelationBuilder {
}
}

/// Builds a `LATERAL FLATTEN(INPUT => expr, OUTER => bool)` table factor
/// for Snowflake-style unnesting.
#[derive(Clone)]
pub struct FlattenRelationBuilder {
pub alias: Option<ast::TableAlias>,
/// The input expression to flatten (e.g. a column reference).
pub input_expr: Option<ast::Expr>,
/// Whether to preserve rows for NULL/empty inputs (Snowflake `OUTER` param).
pub outer: bool,
}

impl FlattenRelationBuilder {
pub fn alias(&mut self, value: Option<ast::TableAlias>) -> &mut Self {
self.alias = value;
self
}

pub fn input_expr(&mut self, value: ast::Expr) -> &mut Self {
self.input_expr = Some(value);
self
}

pub fn outer(&mut self, value: bool) -> &mut Self {
self.outer = value;
self
}

pub fn build(&self) -> Result<ast::TableFactor, BuilderError> {
let input = self.input_expr.clone().ok_or_else(|| {
BuilderError::from(UninitializedFieldError::from("input_expr"))
})?;

let mut args = vec![ast::FunctionArg::Named {
name: ast::Ident::new("INPUT"),
arg: ast::FunctionArgExpr::Expr(input),
operator: ast::FunctionArgOperator::RightArrow,
}];

if self.outer {
args.push(ast::FunctionArg::Named {
name: ast::Ident::new("OUTER"),
arg: ast::FunctionArgExpr::Expr(ast::Expr::Value(
ast::Value::Boolean(true).into(),
)),
operator: ast::FunctionArgOperator::RightArrow,
});
}

Ok(ast::TableFactor::Function {
lateral: true,
name: ast::ObjectName::from(vec![ast::Ident::new("FLATTEN")]),
args,
alias: self.alias.clone(),
})
}

fn create_empty() -> Self {
Self {
alias: None,
input_expr: None,
outer: false,
}
}
}

impl Default for FlattenRelationBuilder {
fn default() -> Self {
Self::create_empty()
}
}

/// Runtime error when a `build()` method is called and one or more required fields
/// do not have a value.
#[derive(Debug, Clone)]
Expand Down
79 changes: 79 additions & 0 deletions datafusion/sql/src/unparser/dialect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,15 @@ pub trait Dialect: Send + Sync {
false
}

/// Unparse the unnest plan as `LATERAL FLATTEN(INPUT => expr, ...)`.
///
/// Snowflake uses FLATTEN as a table function instead of the SQL-standard UNNEST.
/// When this returns `true`, the unparser emits
/// `LATERAL FLATTEN(INPUT => <col>, OUTER => <bool>)` in the FROM clause.
fn unnest_as_lateral_flatten(&self) -> bool {
false
}
Comment thread
goldmedal marked this conversation as resolved.

/// Allows the dialect to override column alias unparsing if the dialect has specific rules.
/// Returns None if the default unparsing should be used, or Some(String) if there is
/// a custom implementation for the alias.
Expand Down Expand Up @@ -664,6 +673,59 @@ impl BigQueryDialect {
}
}

/// Dialect for Snowflake SQL.
///
/// Key differences from the default dialect:
/// - Uses double-quote identifier quoting
/// - Supports `NULLS FIRST`/`NULLS LAST` in `ORDER BY`
/// - Does not support empty select lists (`SELECT FROM t`)
/// - Does not support column aliases in table alias definitions
/// (Snowflake accepts the syntax but silently ignores the renames in join contexts)
/// - Unparses `UNNEST` plans as `LATERAL FLATTEN(INPUT => expr, ...)`
pub struct SnowflakeDialect {}

#[expect(clippy::new_without_default)]
impl SnowflakeDialect {
#[must_use]
pub fn new() -> Self {
Self {}
}
}

impl Dialect for SnowflakeDialect {
fn identifier_quote_style(&self, _: &str) -> Option<char> {
Some('"')
}

fn supports_nulls_first_in_sort(&self) -> bool {
true
}

fn supports_empty_select_list(&self) -> bool {
false
}

fn supports_column_alias_in_table_alias(&self) -> bool {
false
}

fn timestamp_cast_dtype(
&self,
_time_unit: &TimeUnit,
tz: &Option<Arc<str>>,
) -> ast::DataType {
if tz.is_some() {
ast::DataType::Timestamp(None, TimezoneInfo::WithTimeZone)
} else {
ast::DataType::Timestamp(None, TimezoneInfo::None)
}
}

fn unnest_as_lateral_flatten(&self) -> bool {
true
}
}

pub struct CustomDialect {
identifier_quote_style: Option<char>,
supports_nulls_first_in_sort: bool,
Expand All @@ -686,6 +748,7 @@ pub struct CustomDialect {
window_func_support_window_frame: bool,
full_qualified_col: bool,
unnest_as_table_factor: bool,
unnest_as_lateral_flatten: bool,
}

impl Default for CustomDialect {
Expand Down Expand Up @@ -715,6 +778,7 @@ impl Default for CustomDialect {
window_func_support_window_frame: true,
full_qualified_col: false,
unnest_as_table_factor: false,
unnest_as_lateral_flatten: false,
}
}
}
Expand Down Expand Up @@ -829,6 +893,10 @@ impl Dialect for CustomDialect {
fn unnest_as_table_factor(&self) -> bool {
self.unnest_as_table_factor
}

fn unnest_as_lateral_flatten(&self) -> bool {
self.unnest_as_lateral_flatten
}
}

/// `CustomDialectBuilder` to build `CustomDialect` using builder pattern
Expand Down Expand Up @@ -867,6 +935,7 @@ pub struct CustomDialectBuilder {
window_func_support_window_frame: bool,
full_qualified_col: bool,
unnest_as_table_factor: bool,
unnest_as_lateral_flatten: bool,
}

impl Default for CustomDialectBuilder {
Expand Down Expand Up @@ -902,6 +971,7 @@ impl CustomDialectBuilder {
window_func_support_window_frame: true,
full_qualified_col: false,
unnest_as_table_factor: false,
unnest_as_lateral_flatten: false,
}
}

Expand Down Expand Up @@ -929,6 +999,7 @@ impl CustomDialectBuilder {
window_func_support_window_frame: self.window_func_support_window_frame,
full_qualified_col: self.full_qualified_col,
unnest_as_table_factor: self.unnest_as_table_factor,
unnest_as_lateral_flatten: self.unnest_as_lateral_flatten,
}
}

Expand Down Expand Up @@ -1075,4 +1146,12 @@ impl CustomDialectBuilder {
self.unnest_as_table_factor = unnest_as_table_factor;
self
}

pub fn with_unnest_as_lateral_flatten(
mut self,
unnest_as_lateral_flatten: bool,
) -> Self {
self.unnest_as_lateral_flatten = unnest_as_lateral_flatten;
self
}
}
Loading
Loading