Skip to content

Commit 5e03e56

Browse files
xudong963claude
andauthored
Refactor: expose predicate constant inference from physical-expr (#44)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent befeec7 commit 5e03e56

2 files changed

Lines changed: 98 additions & 56 deletions

File tree

datafusion/physical-expr/src/utils/mod.rs

Lines changed: 91 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,11 @@ pub use guarantee::{Guarantee, LiteralGuarantee};
2121
use std::borrow::Borrow;
2222
use std::sync::Arc;
2323

24-
use crate::PhysicalExpr;
25-
use crate::PhysicalSortExpr;
26-
use crate::expressions::{BinaryExpr, Column};
24+
use crate::expressions::{BinaryExpr, Column, Literal};
2725
use crate::tree_node::ExprContext;
26+
use crate::{
27+
AcrossPartitions, ConstExpr, EquivalenceProperties, PhysicalExpr, PhysicalSortExpr,
28+
};
2829

2930
use arrow::datatypes::Schema;
3031
use datafusion_common::tree_node::{
@@ -45,6 +46,66 @@ pub fn split_conjunction(
4546
split_impl(Operator::And, predicate, vec![])
4647
}
4748

49+
impl ConstExpr {
50+
/// Collects predicate-derived constants from equality conjunctions.
51+
///
52+
/// For each equality predicate of the form `lhs = rhs`, if either side is
53+
/// already known constant according to `input_eqs`, or is a literal, then
54+
/// the other side is also constant and will be returned as a [`ConstExpr`].
55+
///
56+
/// Literals are treated as uniform constants across partitions, so
57+
/// `col = literal` produces a constant for `col` with the literal value.
58+
///
59+
/// For example, given predicate `a = 5 AND b = c` where `c` is already
60+
/// known constant, this returns constants for both `a` (Uniform with value
61+
/// 5) and `b` (propagating `c`'s across-partitions value).
62+
pub fn collect_predicate_constants(
63+
input_eqs: &EquivalenceProperties,
64+
predicate: &Arc<dyn PhysicalExpr>,
65+
) -> Vec<ConstExpr> {
66+
/// Returns the `AcrossPartitions` value for `expr` if it is constant:
67+
/// either already known constant in `input_eqs`, or a `Literal`
68+
/// (which is inherently constant across all partitions).
69+
fn expr_constant_or_literal(
70+
expr: &Arc<dyn PhysicalExpr>,
71+
input_eqs: &EquivalenceProperties,
72+
) -> Option<AcrossPartitions> {
73+
input_eqs.is_expr_constant(expr).or_else(|| {
74+
expr.as_any()
75+
.downcast_ref::<Literal>()
76+
.map(|l| AcrossPartitions::Uniform(Some(l.value().clone())))
77+
})
78+
}
79+
80+
let mut constants = Vec::new();
81+
for conjunction in split_conjunction(predicate) {
82+
if let Some(binary) = conjunction.as_any().downcast_ref::<BinaryExpr>()
83+
&& binary.op() == &Operator::Eq
84+
{
85+
// Check if either side is constant — either already known
86+
// constant from the input equivalence properties, or a literal
87+
// value (which is inherently constant across all partitions).
88+
let left_const = expr_constant_or_literal(binary.left(), input_eqs);
89+
let right_const = expr_constant_or_literal(binary.right(), input_eqs);
90+
91+
if let Some(left_across) = left_const {
92+
// LEFT is constant, so RIGHT must also be constant.
93+
// Use RIGHT's known across value if available, otherwise
94+
// propagate LEFT's (e.g. Uniform from a literal).
95+
let across = right_const.unwrap_or(left_across);
96+
constants.push(ConstExpr::new(Arc::clone(binary.right()), across));
97+
} else if let Some(right_across) = right_const {
98+
// RIGHT is constant, so LEFT must also be constant.
99+
constants
100+
.push(ConstExpr::new(Arc::clone(binary.left()), right_across));
101+
}
102+
}
103+
}
104+
105+
constants
106+
}
107+
}
108+
48109
/// Create a conjunction of the given predicates.
49110
/// If the input is empty, return a literal true.
50111
/// If the input contains a single predicate, return the predicate.
@@ -562,4 +623,31 @@ pub(crate) mod tests {
562623
assert_eq!(collect_columns(&expr3), expected);
563624
Ok(())
564625
}
626+
627+
#[test]
628+
fn test_collect_predicate_constants_propagates_uniform_literal_value() -> Result<()> {
629+
let schema = Arc::new(Schema::new(vec![Field::new(
630+
"ticker",
631+
DataType::Utf8,
632+
false,
633+
)]));
634+
let predicate = binary(
635+
col("ticker", schema.as_ref())?,
636+
Operator::Eq,
637+
lit(ScalarValue::Utf8(Some("NGJ26".to_string()))),
638+
schema.as_ref(),
639+
)?;
640+
let eq_properties = EquivalenceProperties::new(schema);
641+
642+
let constants =
643+
ConstExpr::collect_predicate_constants(&eq_properties, &predicate);
644+
645+
assert_eq!(constants.len(), 1);
646+
assert_eq!(
647+
constants[0].across_partitions,
648+
AcrossPartitions::Uniform(Some(ScalarValue::Utf8(Some("NGJ26".to_string()))))
649+
);
650+
651+
Ok(())
652+
}
565653
}

datafusion/physical-plan/src/filter.rs

Lines changed: 7 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -55,12 +55,12 @@ use datafusion_common::{
5555
use datafusion_execution::TaskContext;
5656
use datafusion_expr::Operator;
5757
use datafusion_physical_expr::equivalence::ProjectionMapping;
58-
use datafusion_physical_expr::expressions::{BinaryExpr, Column, Literal, lit};
58+
use datafusion_physical_expr::expressions::{BinaryExpr, Column, lit};
5959
use datafusion_physical_expr::intervals::utils::check_support;
6060
use datafusion_physical_expr::utils::{collect_columns, reassign_expr_columns};
6161
use datafusion_physical_expr::{
62-
AcrossPartitions, AnalysisContext, ConstExpr, EquivalenceProperties, ExprBoundaries,
63-
PhysicalExpr, analyze, conjunction, split_conjunction,
62+
AcrossPartitions, AnalysisContext, ConstExpr, ExprBoundaries, PhysicalExpr, analyze,
63+
conjunction, split_conjunction,
6464
};
6565

6666
use datafusion_physical_expr_common::physical_expr::fmt_sql;
@@ -243,55 +243,6 @@ impl FilterExec {
243243
})
244244
}
245245

246-
/// Returns the `AcrossPartitions` value for `expr` if it is constant:
247-
/// either already known constant in `input_eqs`, or a `Literal`
248-
/// (which is inherently constant across all partitions).
249-
fn expr_constant_or_literal(
250-
expr: &Arc<dyn PhysicalExpr>,
251-
input_eqs: &EquivalenceProperties,
252-
) -> Option<AcrossPartitions> {
253-
input_eqs.is_expr_constant(expr).or_else(|| {
254-
expr.as_any()
255-
.downcast_ref::<Literal>()
256-
.map(|l| AcrossPartitions::Uniform(Some(l.value().clone())))
257-
})
258-
}
259-
260-
fn extend_constants(
261-
input: &Arc<dyn ExecutionPlan>,
262-
predicate: &Arc<dyn PhysicalExpr>,
263-
) -> Vec<ConstExpr> {
264-
let mut res_constants = Vec::new();
265-
let input_eqs = input.equivalence_properties();
266-
267-
let conjunctions = split_conjunction(predicate);
268-
for conjunction in conjunctions {
269-
if let Some(binary) = conjunction.as_any().downcast_ref::<BinaryExpr>()
270-
&& binary.op() == &Operator::Eq
271-
{
272-
// Check if either side is constant — either already known
273-
// constant from the input equivalence properties, or a literal
274-
// value (which is inherently constant across all partitions).
275-
let left_const = Self::expr_constant_or_literal(binary.left(), input_eqs);
276-
let right_const =
277-
Self::expr_constant_or_literal(binary.right(), input_eqs);
278-
279-
if let Some(left_across) = left_const {
280-
// LEFT is constant, so RIGHT must also be constant.
281-
// Use RIGHT's known across value if available, otherwise
282-
// propagate LEFT's (e.g. Uniform from a literal).
283-
let across = right_const.unwrap_or(left_across);
284-
res_constants
285-
.push(ConstExpr::new(Arc::clone(binary.right()), across));
286-
} else if let Some(right_across) = right_const {
287-
// RIGHT is constant, so LEFT must also be constant.
288-
res_constants
289-
.push(ConstExpr::new(Arc::clone(binary.left()), right_across));
290-
}
291-
}
292-
}
293-
res_constants
294-
}
295246
/// This function creates the cache object that stores the plan properties such as schema, equivalence properties, ordering, partitioning, etc.
296247
fn compute_properties(
297248
input: &Arc<dyn ExecutionPlan>,
@@ -329,7 +280,10 @@ impl FilterExec {
329280
eq_properties.add_constants(constants)?;
330281
// This is for logical constant (for example: a = '1', then a could be marked as a constant)
331282
// to do: how to deal with multiple situation to represent = (for example c1 between 0 and 0)
332-
eq_properties.add_constants(Self::extend_constants(input, predicate))?;
283+
eq_properties.add_constants(ConstExpr::collect_predicate_constants(
284+
input.equivalence_properties(),
285+
predicate,
286+
))?;
333287

334288
let mut output_partitioning = input.output_partitioning().clone();
335289
// If contains projection, update the PlanProperties.

0 commit comments

Comments
 (0)