Skip to content
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
22 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
8a4d073
Generate unique LATERAL FLATTEN aliases per query
yonatan-sevenai Apr 17, 2026
5f64085
Rewrite ORDER BY placeholder columns to FLATTEN alias.VALUE
yonatan-sevenai Apr 20, 2026
83e51e0
Merge datafusion/main into feature/snowflake_unparser
yonatan-sevenai Apr 20, 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
99 changes: 98 additions & 1 deletion datafusion/sql/src/unparser/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -432,11 +432,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 +458,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 +479,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 +492,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 +697,94 @@ impl Default for UnnestRelationBuilder {
}
}

/// Default table alias for FLATTEN table factors.
/// Snowflake requires an alias to reference output columns (e.g. `_unnest.VALUE`).
pub const FLATTEN_DEFAULT_ALIAS: &str = "_unnest";
Comment thread
yonatan-sevenai marked this conversation as resolved.
Outdated

/// 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(),
})
}

/// Returns the alias name for this FLATTEN relation.
/// Used to build qualified column references like `alias.VALUE`.
pub fn alias_name(&self) -> &str {
self.alias
.as_ref()
.map(|a| a.name.value.as_str())
.unwrap_or(FLATTEN_DEFAULT_ALIAS)
}

fn create_empty() -> Self {
Self {
alias: Some(ast::TableAlias {
name: ast::Ident::with_quote('"', FLATTEN_DEFAULT_ALIAS),
columns: vec![],
explicit: true,
}),
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