Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 3 additions & 2 deletions datafusion-examples/examples/query_planning/expr_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -598,9 +598,10 @@ fn type_coercion_demo() -> Result<()> {
if let Expr::Column(ref col_expr) = *e.left {
let field = df_schema.field_with_name(None, col_expr.name())?;
let cast_to_type = field.data_type();
let coerced_right = e.right.cast_to(cast_to_type, &df_schema)?;
let coerced_right =
(*e.right.into_inner()).cast_to(cast_to_type, &df_schema)?;
Ok(Transformed::yes(Expr::BinaryExpr(BinaryExpr::new(
e.left,
e.left.into_inner(),
e.op,
Box::new(coerced_right),
))))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ impl MyOptimizerRule {
let BinaryExpr { left, op: _, right } = binary_expr;
// rewrite to `my_eq(left, right)`
let udf = ScalarUDF::new_from_impl(MyEq::new());
let call = udf.call(vec![*left, *right]);
let call = udf.call(vec![*left.into_inner(), *right.into_inner()]);
Ok(Transformed::yes(call))
}
_ => Ok(Transformed::no(expr)),
Expand Down
20 changes: 10 additions & 10 deletions datafusion/core/tests/optimizer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -321,19 +321,19 @@ fn test_inequalities_non_null_bounded() {
(col("x").not_between(lit(0), lit(5)), false),
(col("x").not_between(lit(5), lit(10)), true),
(
Expr::BinaryExpr(BinaryExpr {
left: Box::new(col("x")),
op: Operator::IsDistinctFrom,
right: Box::new(lit(ScalarValue::Null)),
}),
Expr::BinaryExpr(BinaryExpr::new(
Box::new(col("x")),
Operator::IsDistinctFrom,
Box::new(lit(ScalarValue::Null)),
)),
true,
),
(
Expr::BinaryExpr(BinaryExpr {
left: Box::new(col("x")),
op: Operator::IsDistinctFrom,
right: Box::new(lit(5)),
}),
Expr::BinaryExpr(BinaryExpr::new(
Box::new(col("x")),
Operator::IsDistinctFrom,
Box::new(lit(5)),
)),
true,
),
];
Expand Down
20 changes: 10 additions & 10 deletions datafusion/core/tests/user_defined/expr_planner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,18 +41,18 @@ impl ExprPlanner for MyCustomPlanner {
) -> Result<PlannerResult<RawBinaryExpr>> {
match &expr.op {
BinaryOperator::Arrow => {
Ok(PlannerResult::Planned(Expr::BinaryExpr(BinaryExpr {
left: Box::new(expr.left.clone()),
right: Box::new(expr.right.clone()),
op: Operator::StringConcat,
})))
Ok(PlannerResult::Planned(Expr::BinaryExpr(BinaryExpr::new(
Box::new(expr.left.clone()),
Operator::StringConcat,
Box::new(expr.right.clone()),
))))
}
BinaryOperator::LongArrow => {
Ok(PlannerResult::Planned(Expr::BinaryExpr(BinaryExpr {
left: Box::new(expr.left.clone()),
right: Box::new(expr.right.clone()),
op: Operator::Plus,
})))
Ok(PlannerResult::Planned(Expr::BinaryExpr(BinaryExpr::new(
Box::new(expr.left.clone()),
Operator::Plus,
Box::new(expr.right.clone()),
))))
}
BinaryOperator::Question => {
Ok(PlannerResult::Planned(Expr::Alias(Alias::new(
Expand Down
131 changes: 125 additions & 6 deletions datafusion/expr/src/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ use std::collections::HashSet;
use std::fmt::{self, Display, Formatter, Write};
use std::hash::{Hash, Hasher};
use std::mem;
use std::ops::{Deref, DerefMut};
use std::sync::Arc;

use crate::expr_fn::binary_expr;
Expand Down Expand Up @@ -788,21 +789,123 @@ impl Alias {
}
}

/// A `Box<Expr>` for the recursive children of [`BinaryExpr`]. A long binary-operator chain
/// (e.g. `a OR b OR c OR ...`) builds an `Expr` as deep as the chain, and dropping it
/// recursively would overflow the stack. This wrapper's `Drop` tears the chain down iteratively.

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.

I feel like this is treating the symptom (overflow on drop) rather than the root cause (in general processing deeply nested expressions using recursive functions)

If we are going to change the representation of BinaryExpr and cause a bunch of downstream churn, I think we should consider more drastic measures -- like changing it to BinaryExpr { .. ops:Vec<Expr> }

I will file a ticket about this

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks @alamb. Agreed the real fix is the representation change; ops: Vec<Expr> reshapes every consumer, so I'd rather it land through #23264. On the drop-without-API-change idea: once Expr or BinaryExpr implements Drop, moving fields out of them is rejected (E0509), and by-value destructures like BinaryExpr { left, op, right } are everywhere in rewrites. The newtype on the two recursive edges was the smallest shape I found that keeps those moves compiling; documented on BoxedExpr in 024f059. The same commit removes the hot-path cost @xudong963 raised: a leaf-only binary now drops with no writes and no Vec, and into_inner hands the same box through rewrites instead of allocating a placeholder. I will close this PR for now, as it seems the consensus is on the Vec representation.

///
/// A newtype, rather than `impl Drop` on [`Expr`] or [`BinaryExpr`], keeps both of those types
/// free of `Drop`, so they can still be destructured by value (E0509). Pattern matches like
/// `BinaryExpr { left, op, right }` moving the fields out are pervasive in rewrites.
///
/// Only `BinaryExpr` carries it: a flat `WHERE x = 1 OR x = 2 OR ...` grows the tree one level
/// per predicate, so the binary chain is the shape that gets deep from ordinary query text.
#[derive(Clone, PartialEq, Eq, PartialOrd, Hash)]
pub struct BoxedExpr(Box<Expr>);

impl fmt::Debug for BoxedExpr {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
// Stay transparent like the `Box<Expr>` this replaced, so a `BinaryExpr`'s debug
// output keeps showing its children directly rather than wrapped.
fmt::Debug::fmt(&self.0, f)
}
}

impl BoxedExpr {
pub fn new(expr: Expr) -> Self {
Self(Box::new(expr))
}

/// Take the inner `Box<Expr>` out without running the iterative `Drop`.
///
/// Rewrites call this for every `BinaryExpr` they visit, so it must not allocate a
/// placeholder box; the same heap allocation flows through the rewrite untouched.
pub fn into_inner(self) -> Box<Expr> {
let this = mem::ManuallyDrop::new(self);
// SAFETY: `this.0` is read exactly once, and suppressing the destructor means
// the original is never dropped, so the box is not freed twice.
unsafe { std::ptr::read(&this.0) }
}
}

impl From<Box<Expr>> for BoxedExpr {
fn from(boxed: Box<Expr>) -> Self {
Self(boxed)
}
}

impl From<Expr> for BoxedExpr {
fn from(expr: Expr) -> Self {
Self(Box::new(expr))
}
}

impl Deref for BoxedExpr {
type Target = Expr;
fn deref(&self) -> &Expr {
&self.0
}
}

impl DerefMut for BoxedExpr {
fn deref_mut(&mut self) -> &mut Expr {
&mut self.0
}
}

impl AsRef<Expr> for BoxedExpr {
fn as_ref(&self) -> &Expr {
&self.0
}
}

impl Drop for BoxedExpr {
fn drop(&mut self) {
// Detach a `BinaryExpr`'s binary children into a heap stack before the node drops, so
// an arbitrarily long chain is torn down iteratively instead of recursing. Non-binary
// children stay in place: they cannot extend the binary spine, and a chain nested
// deeper inside them restarts this loop from its own `BoxedExpr`. A binary expression
// over two leaves (the common case) takes neither the writes nor the allocation.
fn detach(expr: &mut Expr, stack: &mut Vec<Expr>) {
if let Expr::BinaryExpr(b) = expr {
for child in [&mut b.left, &mut b.right] {
if matches!(child.0.as_ref(), Expr::BinaryExpr(_)) {
stack.push(mem::replace(
child.0.as_mut(),
Expr::Literal(ScalarValue::Null, None),
));
}
}
}
}

let mut stack: Vec<Expr> = Vec::new();
detach(self.0.as_mut(), &mut stack);
while let Some(mut expr) = stack.pop() {
detach(&mut expr, &mut stack);
// `expr` drops here; its binary children were detached, so the drop is shallow.
}
}
}

/// Binary expression for [`Expr::BinaryExpr`]
#[derive(Clone, PartialEq, Eq, PartialOrd, Hash, Debug)]
pub struct BinaryExpr {
/// Left-hand side of the expression
pub left: Box<Expr>,
pub left: BoxedExpr,
/// The comparison operator
pub op: Operator,
/// Right-hand side of the expression
pub right: Box<Expr>,
pub right: BoxedExpr,
}

impl BinaryExpr {
/// Create a new binary expression
pub fn new(left: Box<Expr>, op: Operator, right: Box<Expr>) -> Self {
Self { left, op, right }
Self {
left: BoxedExpr(left),
op,
right: BoxedExpr(right),
}
}
}

Expand Down Expand Up @@ -2186,8 +2289,8 @@ impl Expr {
match &mut expr {
// Default to assuming the arguments are the same type
Expr::BinaryExpr(BinaryExpr { left, op: _, right }) => {
rewrite_placeholder(left.as_mut(), right.as_ref(), schema)?;
rewrite_placeholder(right.as_mut(), left.as_ref(), schema)?;
rewrite_placeholder(left, right.as_ref(), schema)?;
rewrite_placeholder(right, left.as_ref(), schema)?;
}
Expr::Between(Between {
expr,
Expand Down Expand Up @@ -3839,6 +3942,22 @@ mod test {
use sqlparser::ast;
use sqlparser::ast::{Ident, IdentWithAlias};

#[test]
fn deep_binary_expr_chain_drops_without_overflowing() {
// Build a left-deep `OR` chain far deeper than a recursive drop could unwind, then let it
// drop on the normal test stack. The iterative `Drop` on `BoxedExpr` must keep it from
// overflowing.
let mut expr = Expr::Literal(ScalarValue::Null, None);
for _ in 0..200_000 {
expr = Expr::BinaryExpr(BinaryExpr::new(
Box::new(expr),
Operator::Or,
Box::new(Expr::Literal(ScalarValue::Null, None)),
));
}
drop(expr);
}

#[test]
fn infer_placeholder_in_clause() {
// SELECT * FROM employees WHERE department_id IN ($1, $2, $3);
Expand Down Expand Up @@ -4167,7 +4286,7 @@ mod test {

let (inferred_expr, _) = expr.infer_placeholder_types(&df_schema).unwrap();
match inferred_expr {
Expr::BinaryExpr(BinaryExpr { right, .. }) => match *right {
Expr::BinaryExpr(BinaryExpr { right, .. }) => match *right.into_inner() {
Expr::Placeholder(placeholder) => {
assert_eq!(
placeholder.field.as_ref().unwrap().data_type(),
Expand Down
50 changes: 25 additions & 25 deletions datafusion/expr/src/expr_rewriter/guarantees.rs
Original file line number Diff line number Diff line change
Expand Up @@ -489,19 +489,19 @@ mod tests {
true,
),
(
Expr::BinaryExpr(BinaryExpr {
left: Box::new(col("x")),
op: Operator::IsDistinctFrom,
right: Box::new(lit(ScalarValue::Null)),
}),
Expr::BinaryExpr(BinaryExpr::new(
Box::new(col("x")),
Operator::IsDistinctFrom,
Box::new(lit(ScalarValue::Null)),
)),
true,
),
(
Expr::BinaryExpr(BinaryExpr {
left: Box::new(col("x")),
op: Operator::IsDistinctFrom,
right: Box::new(lit(ScalarValue::Date32(Some(17000)))),
}),
Expr::BinaryExpr(BinaryExpr::new(
Box::new(col("x")),
Operator::IsDistinctFrom,
Box::new(lit(ScalarValue::Date32(Some(17000)))),
)),
true,
),
];
Expand Down Expand Up @@ -547,19 +547,19 @@ mod tests {
// (original_expr, expected_simplification)
let simplified_cases = &[
(
Expr::BinaryExpr(BinaryExpr {
left: Box::new(col("x")),
op: Operator::IsDistinctFrom,
right: Box::new(lit("z")),
}),
Expr::BinaryExpr(BinaryExpr::new(
Box::new(col("x")),
Operator::IsDistinctFrom,
Box::new(lit("z")),
)),
true,
),
(
Expr::BinaryExpr(BinaryExpr {
left: Box::new(col("x")),
op: Operator::IsNotDistinctFrom,
right: Box::new(lit("z")),
}),
Expr::BinaryExpr(BinaryExpr::new(
Box::new(col("x")),
Operator::IsNotDistinctFrom,
Box::new(lit("z")),
)),
false,
),
];
Expand All @@ -575,11 +575,11 @@ mod tests {
col("x").not_eq(lit("a")),
col("x").between(lit("a"), lit("z")),
col("x").not_between(lit("a"), lit("z")),
Expr::BinaryExpr(BinaryExpr {
left: Box::new(col("x")),
op: Operator::IsDistinctFrom,
right: Box::new(lit(ScalarValue::Null)),
}),
Expr::BinaryExpr(BinaryExpr::new(
Box::new(col("x")),
Operator::IsDistinctFrom,
Box::new(lit(ScalarValue::Null)),
)),
];

validate_unchanged_cases(&guarantees, unchanged_cases);
Expand Down
14 changes: 8 additions & 6 deletions datafusion/expr/src/tree_node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ impl TreeNode for Expr {
| Expr::Placeholder(_)
| Expr::LambdaVariable(_) => Ok(TreeNodeRecursion::Continue),
Expr::BinaryExpr(BinaryExpr { left, right, .. }) => {
(left, right).apply_ref_elements(f)
(left.as_ref(), right.as_ref()).apply_ref_elements(f)
}
Expr::Like(Like { expr, pattern, .. })
| Expr::SimilarTo(Like { expr, pattern, .. }) => {
Expand Down Expand Up @@ -173,11 +173,13 @@ impl TreeNode for Expr {
}) => expr.map_elements(f)?.update_data(|be| {
Expr::InSubquery(InSubquery::new(be, subquery, negated))
}),
Expr::BinaryExpr(BinaryExpr { left, op, right }) => (left, right)
.map_elements(f)?
.update_data(|(new_left, new_right)| {
Expr::BinaryExpr(BinaryExpr::new(new_left, op, new_right))
}),
Expr::BinaryExpr(BinaryExpr { left, op, right }) => {
(left.into_inner(), right.into_inner())
.map_elements(f)?
.update_data(|(new_left, new_right)| {
Expr::BinaryExpr(BinaryExpr::new(new_left, op, new_right))
})
}
Expr::Like(Like {
negated,
expr,
Expand Down
8 changes: 4 additions & 4 deletions datafusion/expr/src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1175,8 +1175,8 @@ pub fn iter_conjunction_owned(expr: Expr) -> impl Iterator<Item = Expr> {
op: Operator::And,
left,
}) => {
stack.push(*right);
stack.push(*left);
stack.push(*right.into_inner());
stack.push(*left.into_inner());
}
Expr::Alias(Alias { expr, .. }) => stack.push(*expr),
other => return Some(other),
Expand Down Expand Up @@ -1238,8 +1238,8 @@ fn split_binary_owned_impl(
) -> Vec<Expr> {
match expr {
Expr::BinaryExpr(BinaryExpr { right, op, left }) if op == operator => {
let exprs = split_binary_owned_impl(*left, operator, exprs);
split_binary_owned_impl(*right, operator, exprs)
let exprs = split_binary_owned_impl(*left.into_inner(), operator, exprs);
split_binary_owned_impl(*right.into_inner(), operator, exprs)
}
Expr::Alias(Alias { expr, .. }) => {
split_binary_owned_impl(*expr, operator, exprs)
Expand Down
Loading
Loading