Skip to content

Commit 3bdff2d

Browse files
committed
Narrow the derived scope to shapes it can render faithfully
Reconstructing the enclosing SELECT list from the limited subtree's schema is only sound when the derived query names its output columns the same way: a projection can emit an unaliased expression the derived query never names, or two columns differing only by a qualifier that SQL cannot carry across the boundary. Only a scan under clauses that rename nothing is accepted now. A `Sort` is excluded even below the limit — SQL does not carry a derived table's row order out to the query selecting from it, so wrapping a sort would repair the row set and lose the ordering the plan promises. Two further refusals: a dialect spelling columns in full leaves the predicate naming a path no single-identifier alias can keep in scope, and a scan projecting no columns has no column list to name the derived output with. The HAVING/QUALIFY refusal now fires only when the grouping it filters is below this limit. Join inputs are walked with one shared `SelectBuilder`, so a predicate on it may belong to a sibling input, and one input's HAVING must not make the other input's limit unrenderable.
1 parent ab4f98e commit 3bdff2d

2 files changed

Lines changed: 246 additions & 53 deletions

File tree

datafusion/sql/src/unparser/plan.rs

Lines changed: 90 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -451,56 +451,92 @@ impl Unparser<'_> {
451451
/// Whether the subtree carrying a row limit needs a `SELECT` of its own.
452452
///
453453
/// The walk is top-down, so any predicate already on `select` came from a
454-
/// node *above* the one being unparsed. `WHERE`, `HAVING` and `QUALIFY`
455-
/// are all evaluated before `LIMIT`/`OFFSET`, while the plan says the
456-
/// opposite: the limit runs first and the predicate filters what it
457-
/// produced. Keeping both in one `SELECT` therefore states the reverse of
458-
/// the plan, and can return rows the plan excludes.
454+
/// node visited earlier. `WHERE`, `HAVING` and `QUALIFY` are all evaluated
455+
/// before `LIMIT`/`OFFSET`, while a plan that puts a filter above a limit
456+
/// says the opposite: the limit runs first and the predicate filters what
457+
/// it produced. Keeping both in one `SELECT` therefore states the reverse
458+
/// of the plan, and can return rows the plan excludes.
459459
///
460-
/// A `WHERE` can be left behind in an enclosing query while the limited
461-
/// subtree moves into a derived table. `HAVING` and `QUALIFY` cannot: they
462-
/// reference an aggregate or window expression that only the `SELECT`
463-
/// computing it can name. Refuse those rather than emit either the
464-
/// reversed form or a predicate with nothing to bind to.
465-
fn row_limit_needs_own_scope(select: &SelectBuilder) -> Result<bool> {
466-
if select.has_grouped_predicate() {
460+
/// A `WHERE` can stay in the enclosing query while the limited subtree
461+
/// moves into a derived table. `HAVING` and `QUALIFY` cannot: they name an
462+
/// aggregate or window expression that only the `SELECT` computing it can
463+
/// name. Refuse those rather than emit the reversed form — but only when
464+
/// the grouping they filter is in fact below this limit. Join inputs are
465+
/// walked with one shared `SelectBuilder`, so a predicate on it may have
466+
/// come from a sibling input rather than from an ancestor of this node.
467+
fn row_limit_needs_own_scope(
468+
plan: &LogicalPlan,
469+
select: &SelectBuilder,
470+
) -> Result<bool> {
471+
if select.has_grouped_predicate()
472+
&& (find_agg_node_within_select(plan, select.already_projected()).is_some()
473+
|| find_window_nodes_within_select(
474+
plan,
475+
None,
476+
select.already_projected(),
477+
)
478+
.is_some())
479+
{
467480
return not_impl_err!(
468481
"Unparsing a HAVING or QUALIFY predicate that is applied after a row limit is not supported"
469482
);
470483
}
471484
Ok(select.has_selection())
472485
}
473486

474-
/// Unparses `plan` — a `Limit`, or a `Sort` carrying a `fetch` — as a
475-
/// derived table, so the predicate already on the enclosing `SELECT`
476-
/// applies to the limited rows rather than to the rows feeding the limit.
487+
/// Unparses a `Limit` as a derived table, so the `WHERE` already on the
488+
/// enclosing `SELECT` applies to the limited rows rather than to the rows
489+
/// feeding the limit.
490+
///
491+
/// The derived table takes the name of the relation it reads, because the
492+
/// predicate staying outside is still qualified by that name, and its
493+
/// columns are listed explicitly: a wildcard would expand to every
494+
/// relation in the enclosing `FROM`, not to this one's contribution.
477495
///
478-
/// The derived table takes the name of the relation the subtree reads,
479-
/// because the predicate staying outside is still qualified by it. A
480-
/// subtree reading more than one relation has no such name to take: every
481-
/// qualifier the predicate could carry would be gone from scope, so the
482-
/// query is refused rather than rendered unbindable.
496+
/// Both of those need the derived table's output columns to be exactly the
497+
/// relation's own columns, under their own names — which is why only a
498+
/// scan, and the clauses that can wrap one without renaming anything, are
499+
/// accepted here. A projection may emit a column the derived query never
500+
/// names (an unaliased expression) or two columns that differ only by a
501+
/// qualifier SQL cannot carry across the boundary; a join, union or
502+
/// aggregate has no single name for the alias to take. Those are refused,
503+
/// which costs the pushdown but never the rows.
483504
fn derive_row_limited_scope(
484505
&self,
485506
plan: &LogicalPlan,
486507
select: &mut SelectBuilder,
487508
relation: &mut RelationBuilder,
488509
) -> Result<()> {
489-
let Some(table_ref) = Self::sole_relation_of(plan) else {
510+
let Some(table_ref) = Self::scanned_relation_of(plan) else {
490511
return not_impl_err!(
491-
"Unparsing a filter applied after a row limit is not supported when the limited input reads more than one relation"
512+
"Unparsing a filter applied after a row limit is only supported when the limited input is a single table scan"
492513
);
493514
};
494515

516+
// Only the last component survives as an alias, so a predicate spelled
517+
// with the full path would be left pointing at a name that is gone.
518+
if self.dialect.full_qualified_col() && table_ref.to_vec().len() > 1 {
519+
return not_impl_err!(
520+
"Unparsing a filter applied after a row limit is not supported for a qualified table name on a dialect that spells columns in full"
521+
);
522+
}
523+
524+
// A scan can project no columns at all, which every other empty
525+
// projection in this unparser renders as `SELECT 1`. There is no
526+
// column list to name a derived table's output with here, so refuse.
527+
// (Two columns of one name cannot arrive: `DFSchema` rejects a scan
528+
// with a duplicate qualified field.)
529+
let fields = plan.schema().fields();
530+
if fields.is_empty() {
531+
return not_impl_err!(
532+
"Unparsing a filter applied after a row limit is not supported for an input projecting no columns"
533+
);
534+
}
535+
495536
// The subtree moves into a statement of its own, so this `SELECT` no
496-
// longer receives a projection from the nodes below. Name the derived
497-
// table's columns explicitly, as the scan underneath would have: a
498-
// wildcard here would expand to every relation in the enclosing `FROM`
499-
// — every column of a join, not this side's contribution to it.
537+
// longer receives a projection from the nodes below it.
500538
if !select.already_projected() {
501-
let items = plan
502-
.schema()
503-
.fields()
539+
let items = fields
504540
.iter()
505541
.map(|field| {
506542
self.select_item_to_sql(&Expr::Column(Column::new(
@@ -520,22 +556,24 @@ impl Unparser<'_> {
520556
)
521557
}
522558

523-
/// The single relation a subtree reads from, if it reads exactly one.
559+
/// The relation a subtree scans, when the subtree is one scan under
560+
/// clauses that neither rename nor add columns and neither reorder nor
561+
/// combine rows from elsewhere.
524562
///
525-
/// Only the shapes that can sit between a limit and its source are walked
526-
/// through; anything else (a join, a union, a set operation) has no single
527-
/// name to answer with, and neither does a subtree reading two tables.
528-
fn sole_relation_of(plan: &LogicalPlan) -> Option<TableReference> {
563+
/// A `Sort` is deliberately not walked through. It is the one such clause
564+
/// whose effect does not survive being wrapped: SQL does not carry a
565+
/// derived table's row order into the query selecting from it.
566+
fn scanned_relation_of(plan: &LogicalPlan) -> Option<TableReference> {
529567
match plan {
530568
LogicalPlan::TableScan(scan) => Some(scan.table_name.clone()),
531-
LogicalPlan::SubqueryAlias(alias) => Some(alias.alias.clone()),
532-
LogicalPlan::Limit(limit) => Self::sole_relation_of(limit.input.as_ref()),
533-
LogicalPlan::Filter(filter) => Self::sole_relation_of(filter.input.as_ref()),
534-
LogicalPlan::Sort(sort) => Self::sole_relation_of(sort.input.as_ref()),
535-
LogicalPlan::Projection(projection) => {
536-
Self::sole_relation_of(projection.input.as_ref())
569+
LogicalPlan::SubqueryAlias(alias) => {
570+
Self::scanned_relation_of(alias.input.as_ref())
571+
.map(|_| alias.alias.clone())
572+
}
573+
LogicalPlan::Limit(limit) => Self::scanned_relation_of(limit.input.as_ref()),
574+
LogicalPlan::Filter(filter) => {
575+
Self::scanned_relation_of(filter.input.as_ref())
537576
}
538-
LogicalPlan::Distinct(distinct) => Self::sole_relation_of(distinct.input()),
539577
_ => None,
540578
}
541579
}
@@ -894,7 +932,7 @@ impl Unparser<'_> {
894932
);
895933
}
896934
if (limit.fetch.is_some() || limit.skip.is_some())
897-
&& Self::row_limit_needs_own_scope(select)?
935+
&& Self::row_limit_needs_own_scope(plan, select)?
898936
{
899937
return self.derive_row_limited_scope(plan, select, relation);
900938
}
@@ -940,10 +978,16 @@ impl Unparser<'_> {
940978
}
941979
// A `Sort` carrying a `fetch` renders that fetch as this
942980
// query's `LIMIT`, so it reorders against a predicate above it
943-
// exactly as a `Limit` node does. A sort without one does not:
944-
// `ORDER BY` is evaluated after `WHERE` either way.
945-
if sort.fetch.is_some() && Self::row_limit_needs_own_scope(select)? {
946-
return self.derive_row_limited_scope(plan, select, relation);
981+
// exactly as a `Limit` node does — but it cannot be moved into
982+
// a derived table the way a `Limit` can, because SQL does not
983+
// carry a derived table's row order out to the query selecting
984+
// from it. (A sort without a fetch reorders nothing: `ORDER BY`
985+
// is evaluated after `WHERE` either way.)
986+
if sort.fetch.is_some() && Self::row_limit_needs_own_scope(plan, select)?
987+
{
988+
return not_impl_err!(
989+
"Unparsing a filter applied after a sort's fetch is not supported"
990+
);
947991
}
948992

949993
let Some(query_ref) = query else {

datafusion/sql/tests/cases/plan_to_sql.rs

Lines changed: 156 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4307,16 +4307,28 @@ fn test_filter_above_limit_gets_its_own_scope() -> Result<()> {
43074307
@r#"SELECT t.id, t."name" FROM (SELECT * FROM t OFFSET 3) AS t WHERE (t.id = 'a')"#
43084308
);
43094309

4310-
// A `Sort` carrying a `fetch` renders that fetch as the query's `LIMIT`,
4311-
// so it needs the same treatment as a `Limit` node.
4310+
Ok(())
4311+
}
4312+
4313+
/// A `Sort` carrying a `fetch` renders that fetch as the query's `LIMIT`, so a
4314+
/// predicate above it reorders in the same way. It cannot take the same fix: a
4315+
/// derived table's row order is not carried out to the query selecting from
4316+
/// it, so moving the sort inside one would repair the row set and lose the
4317+
/// ordering the plan promises.
4318+
#[test]
4319+
fn test_filter_above_a_sort_fetch_is_refused() -> Result<()> {
4320+
let schema = Schema::new(vec![
4321+
Field::new("id", DataType::Utf8, false),
4322+
Field::new("name", DataType::Utf8, false),
4323+
]);
4324+
43124325
let plan = table_scan(Some("t"), &schema, None)?
43134326
.sort_with_limit(vec![col("id").sort(true, false)], Some(5))?
43144327
.filter(col("id").eq(lit("a")))?
43154328
.build()?;
4316-
assert_snapshot!(
4317-
plan_to_sql(&plan)?,
4318-
@r#"SELECT t.id, t."name" FROM (SELECT * FROM t ORDER BY t.id ASC NULLS LAST LIMIT 5) AS t WHERE (t.id = 'a')"#
4319-
);
4329+
let error =
4330+
plan_to_sql(&plan).expect_err("a filter above a sort fetch cannot be unparsed");
4331+
assert_contains!(error.to_string(), "after a sort\'s fetch");
43204332

43214333
Ok(())
43224334
}
@@ -4494,7 +4506,7 @@ fn test_filter_above_limit_over_a_join_is_refused() -> Result<()> {
44944506
.build()?;
44954507
let error = plan_to_sql(&plan)
44964508
.expect_err("a filter above a limit over a join cannot be unparsed");
4497-
assert_contains!(error.to_string(), "reads more than one relation");
4509+
assert_contains!(error.to_string(), "limited input is a single table scan");
44984510

44994511
Ok(())
45004512
}
@@ -4555,3 +4567,140 @@ fn test_filter_above_a_limited_join_input_keeps_the_join_schema() -> Result<()>
45554567

45564568
Ok(())
45574569
}
4570+
4571+
/// The derived table's columns are named by the enclosing query, which only
4572+
/// works if the derived query names them the same way. A projection breaks
4573+
/// that: an unaliased expression is a column the derived query never names,
4574+
/// and two aliases differing only by a qualifier collapse onto one SQL name.
4575+
#[test]
4576+
fn test_filter_above_limit_over_a_projection_is_refused() -> Result<()> {
4577+
let schema = Schema::new(vec![
4578+
Field::new("a", DataType::Int32, false),
4579+
Field::new("b", DataType::Int32, false),
4580+
]);
4581+
4582+
let plan = table_scan(Some("t"), &schema, None)?
4583+
.project(vec![col("a").add(col("b"))])?
4584+
.limit(0, Some(5))?
4585+
.filter(col("t.a + t.b").gt(lit(1i32)))?
4586+
.build()?;
4587+
let error = plan_to_sql(&plan)
4588+
.expect_err("a filter above a limited projection cannot be unparsed");
4589+
assert_contains!(error.to_string(), "limited input is a single table scan");
4590+
4591+
Ok(())
4592+
}
4593+
4594+
/// A sort below the limit orders what the plan returns just as surely as one
4595+
/// above it, and a derived table does not carry its row order out. It is the
4596+
/// one clause that may wrap a scan and still not be safe to move inside.
4597+
#[test]
4598+
fn test_filter_above_a_limited_sort_is_refused() -> Result<()> {
4599+
let schema = Schema::new(vec![
4600+
Field::new("id", DataType::Utf8, false),
4601+
Field::new("name", DataType::Utf8, false),
4602+
]);
4603+
4604+
let plan = table_scan(Some("t"), &schema, None)?
4605+
.sort(vec![col("id").sort(true, false)])?
4606+
.limit(0, Some(5))?
4607+
.filter(col("id").eq(lit("a")))?
4608+
.build()?;
4609+
let error =
4610+
plan_to_sql(&plan).expect_err("a filter above a limited sort cannot be unparsed");
4611+
assert_contains!(error.to_string(), "limited input is a single table scan");
4612+
4613+
Ok(())
4614+
}
4615+
4616+
/// A dialect that spells columns in full leaves the outer predicate qualified
4617+
/// by every part of the table's path, while a derived table can only be
4618+
/// aliased by one identifier. The predicate would be left naming a path that
4619+
/// is no longer in scope.
4620+
#[test]
4621+
fn test_filter_above_limit_is_refused_for_a_fully_qualified_column() -> Result<()> {
4622+
let schema = Schema::new(vec![
4623+
Field::new("id", DataType::Utf8, false),
4624+
Field::new("name", DataType::Utf8, false),
4625+
]);
4626+
let dialect = CustomDialectBuilder::default()
4627+
.with_full_qualified_col(true)
4628+
.with_identifier_quote_style('"')
4629+
.build();
4630+
4631+
let plan = table_scan(Some("catalog.schema.t"), &schema, None)?
4632+
.limit(0, Some(5))?
4633+
.filter(col("id").eq(lit("a")))?
4634+
.build()?;
4635+
let error = Unparser::new(&dialect)
4636+
.plan_to_sql(&plan)
4637+
.expect_err("a fully qualified predicate cannot survive the alias");
4638+
assert_contains!(error.to_string(), "dialect that spells columns in full");
4639+
4640+
// A bare table name has nothing to lose, so the same dialect renders it.
4641+
let plan = table_scan(Some("t"), &schema, None)?
4642+
.limit(0, Some(5))?
4643+
.filter(col("id").eq(lit("a")))?
4644+
.build()?;
4645+
assert_snapshot!(
4646+
Unparser::new(&dialect).plan_to_sql(&plan)?,
4647+
@r#"SELECT "t"."id", "t"."name" FROM (SELECT * FROM "t" LIMIT 5) AS "t" WHERE ("t"."id" = 'a')"#
4648+
);
4649+
4650+
Ok(())
4651+
}
4652+
4653+
/// Join inputs are walked with one shared `SelectBuilder`, so a predicate on
4654+
/// it need not be an ancestor of the node being unparsed. A `HAVING` from one
4655+
/// input must not make the other input's limit unrenderable.
4656+
#[test]
4657+
fn test_a_sibling_having_does_not_refuse_the_other_inputs_limit() -> Result<()> {
4658+
let left = Schema::new(vec![
4659+
Field::new("id", DataType::Utf8, false),
4660+
Field::new("name", DataType::Utf8, false),
4661+
]);
4662+
let right = Schema::new(vec![
4663+
Field::new("id", DataType::Utf8, false),
4664+
Field::new("age", DataType::Int32, false),
4665+
]);
4666+
4667+
let grouped_left = table_scan(Some("a"), &left, None)?
4668+
.aggregate(vec![col("id")], vec![count(col("name"))])?
4669+
.filter(col("COUNT(a.name)").gt(lit(1i64)))?
4670+
.build()?;
4671+
let limited_right = table_scan(Some("b"), &right, Some(vec![0, 1]))?
4672+
.limit(0, Some(5))?
4673+
.build()?;
4674+
let plan = LogicalPlanBuilder::from(grouped_left)
4675+
.join(
4676+
limited_right,
4677+
datafusion_expr::JoinType::Inner,
4678+
(vec!["a.id"], vec!["b.id"]),
4679+
None,
4680+
)?
4681+
.build()?;
4682+
4683+
plan_to_sql(&plan).expect("a sibling's HAVING is not this limit's predicate");
4684+
4685+
Ok(())
4686+
}
4687+
4688+
/// A scan can project no columns, and then there is no column list to name
4689+
/// the derived table's output with.
4690+
#[test]
4691+
fn test_filter_above_limit_over_an_empty_projection_is_refused() -> Result<()> {
4692+
let schema = Schema::new(vec![
4693+
Field::new("id", DataType::Utf8, false),
4694+
Field::new("name", DataType::Utf8, false),
4695+
]);
4696+
4697+
let plan = table_scan(Some("t"), &schema, Some(vec![]))?
4698+
.limit(0, Some(5))?
4699+
.filter(lit(true))?
4700+
.build()?;
4701+
let error = plan_to_sql(&plan)
4702+
.expect_err("a limited scan with no columns cannot be unparsed");
4703+
assert_contains!(error.to_string(), "projecting no columns");
4704+
4705+
Ok(())
4706+
}

0 commit comments

Comments
 (0)