Skip to content

Commit 9321138

Browse files
fix: Snowflake LATERAL FLATTEN handles SubqueryAlias between Unnest and Projection
When a table is accessed through a passthrough/virtual table mapping, DataFusion inserts a SubqueryAlias node between Unnest and its inner Projection. The FLATTEN rendering code assumed a direct Projection child and failed with "Unnest input is not a Projection: SubqueryAlias(...)". Peel through SubqueryAlias in three code paths that inspect unnest.input: try_unnest_to_lateral_flatten_sql, the inline-vs-table source check, and the general unnest recursion. Also fix a pre-existing collapsible_if clippy warning in check_unnest_placeholder_with_outer_ref. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent e701962 commit 9321138

2 files changed

Lines changed: 62 additions & 8 deletions

File tree

datafusion/sql/src/unparser/plan.rs

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -442,6 +442,14 @@ impl Unparser<'_> {
442442
// vs an inline array (EmptyRelation).
443443
let inner_projection = match unnest.input.as_ref() {
444444
LogicalPlan::Projection(proj) => proj,
445+
LogicalPlan::SubqueryAlias(alias) => match alias.input.as_ref() {
446+
LogicalPlan::Projection(proj) => proj,
447+
other => {
448+
return internal_err!(
449+
"Unnest input (through SubqueryAlias) is not a Projection: {other:?}"
450+
);
451+
}
452+
},
445453
other => {
446454
return internal_err!(
447455
"Unnest input is not a Projection: {other:?}"
@@ -1186,6 +1194,11 @@ impl Unparser<'_> {
11861194
if let LogicalPlan::Projection(p) = unnest.input.as_ref() {
11871195
// continue with projection input
11881196
self.select_to_sql_recursively(&p.input, query, select, relation)
1197+
} else if let LogicalPlan::SubqueryAlias(alias) = unnest.input.as_ref()
1198+
&& let LogicalPlan::Projection(p) = alias.input.as_ref()
1199+
{
1200+
// SubqueryAlias wraps the Projection (e.g. passthrough tables)
1201+
self.select_to_sql_recursively(&p.input, query, select, relation)
11891202
} else {
11901203
internal_err!("Unnest input is not a Projection: {unnest:?}")
11911204
}
@@ -1250,13 +1263,13 @@ impl Unparser<'_> {
12501263
}
12511264
_ => return None,
12521265
};
1253-
if let Expr::Column(Column { name, .. }) = inner {
1254-
if let Some(prefix) = name.strip_prefix(UNNEST_PLACEHOLDER) {
1255-
if prefix.starts_with(&format!("({OUTER_REFERENCE_COLUMN_PREFIX}(")) {
1256-
return Some(UnnestInputType::OuterReference);
1257-
}
1258-
return Some(UnnestInputType::Scalar);
1266+
if let Expr::Column(Column { name, .. }) = inner
1267+
&& let Some(prefix) = name.strip_prefix(UNNEST_PLACEHOLDER)
1268+
{
1269+
if prefix.starts_with(&format!("({OUTER_REFERENCE_COLUMN_PREFIX}(")) {
1270+
return Some(UnnestInputType::OuterReference);
12591271
}
1272+
return Some(UnnestInputType::Scalar);
12601273
}
12611274
None
12621275
}
@@ -1306,8 +1319,16 @@ impl Unparser<'_> {
13061319
&self,
13071320
unnest: &Unnest,
13081321
) -> Result<Option<FlattenRelationBuilder>> {
1309-
let LogicalPlan::Projection(projection) = unnest.input.as_ref() else {
1310-
return Ok(None);
1322+
let projection = match unnest.input.as_ref() {
1323+
LogicalPlan::Projection(p) => p,
1324+
LogicalPlan::SubqueryAlias(alias) => {
1325+
if let LogicalPlan::Projection(p) = alias.input.as_ref() {
1326+
p
1327+
} else {
1328+
return Ok(None);
1329+
}
1330+
}
1331+
_ => return Ok(None),
13111332
};
13121333

13131334
// For now, handle the simple case of a single expression to flatten.

datafusion/sql/tests/cases/plan_to_sql.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3183,3 +3183,36 @@ fn snowflake_flatten_unnest_udf_result() -> Result<(), DataFusionError> {
31833183
insta::assert_snapshot!(actual, @r#"SELECT _unnest."VALUE" FROM "j1" CROSS JOIN LATERAL FLATTEN(INPUT => json_get_array("j1"."j1_string")) AS _unnest LIMIT 5"#);
31843184
Ok(())
31853185
}
3186+
3187+
#[test]
3188+
fn snowflake_unnest_through_subquery_alias() -> Result<(), DataFusionError> {
3189+
// Build: Projection → Unnest → SubqueryAlias → Projection → TableScan
3190+
// This simulates the plan produced when a virtual/passthrough table
3191+
// wraps the source in a SubqueryAlias, which sits between the Unnest
3192+
// and its inner Projection.
3193+
3194+
let schema = Schema::new(vec![Field::new(
3195+
"items",
3196+
DataType::List(Arc::new(Field::new_list_field(DataType::Utf8, true))),
3197+
true,
3198+
)]);
3199+
3200+
let plan = table_scan(Some("source"), &schema, None)?
3201+
.project(vec![col("items").alias("__unnest_placeholder(items)")])?
3202+
.alias("t")? // SubqueryAlias — this is what breaks
3203+
.unnest_column("__unnest_placeholder(items)")?
3204+
.project(vec![col("__unnest_placeholder(items)").alias("item")])?
3205+
.build()?;
3206+
3207+
let snowflake = SnowflakeDialect::new();
3208+
let unparser = Unparser::new(&snowflake);
3209+
let result = unparser.plan_to_sql(&plan)?;
3210+
let sql_str = result.to_string();
3211+
3212+
// Should contain LATERAL FLATTEN, not error
3213+
assert!(
3214+
sql_str.contains("LATERAL FLATTEN"),
3215+
"Expected LATERAL FLATTEN in SQL, got: {sql_str}"
3216+
);
3217+
Ok(())
3218+
}

0 commit comments

Comments
 (0)