Skip to content
Open
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
11 changes: 11 additions & 0 deletions datafusion/common/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1122,6 +1122,17 @@ config_namespace! {
///
/// Default: true
pub enable_sort_pushdown: bool, default = true

/// When set to true (default), the optimizer will evaluate stable functions
/// (like `now()`, `current_date()`, `current_time()`) during query planning,
/// converting them to literal values. When set to false, stable functions
/// are preserved in the plan and evaluated at execution time.
///
/// Setting this to false is useful when performing query rewrites that need
/// to preserve stable function calls, or when you want the function to be
/// re-evaluated for each execution of a prepared statement rather than
/// being fixed at planning time.
pub evaluate_stable_expressions: bool, default = true
}
}

Expand Down
12 changes: 12 additions & 0 deletions datafusion/expr/src/simplify.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,18 @@ pub trait SimplifyInfo {

/// Returns data type of this expr needed for determining optimized int type of a value
fn get_data_type(&self, expr: &Expr) -> Result<DataType>;

/// Returns true if stable expressions (like `now()`) should be evaluated
/// during simplification. Defaults to true for backward compatibility.
///
/// When false, stable functions are preserved in the expression tree
/// rather than being converted to literal values.
fn evaluate_stable_expressions(&self) -> bool {
self.execution_props()
.config_options
.as_ref()
.is_none_or(|opts| opts.optimizer.evaluate_stable_expressions)
}
}

/// Provides simplification information based on DFSchema and
Expand Down
10 changes: 8 additions & 2 deletions datafusion/functions/src/datetime/current_date.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,15 +99,21 @@ impl ScalarUDFImpl for CurrentDateFunc {

fn simplify(
&self,
_args: Vec<Expr>,
args: Vec<Expr>,
info: &dyn SimplifyInfo,
) -> Result<ExprSimplifyResult> {
// Check if stable expression evaluation is disabled
if !info.evaluate_stable_expressions() {
return Ok(ExprSimplifyResult::Original(args));
}

let now_ts = info.execution_props().query_execution_start_time;

// Get timezone from config and convert to local time
let days = info
.execution_props()
.config_options()
.config_options
.as_ref()
.and_then(|config| {
config
.execution
Expand Down
10 changes: 8 additions & 2 deletions datafusion/functions/src/datetime/current_time.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,15 +95,21 @@ impl ScalarUDFImpl for CurrentTimeFunc {

fn simplify(
&self,
_args: Vec<Expr>,
args: Vec<Expr>,
info: &dyn SimplifyInfo,
) -> Result<ExprSimplifyResult> {
// Check if stable expression evaluation is disabled
if !info.evaluate_stable_expressions() {
return Ok(ExprSimplifyResult::Original(args));
}

let now_ts = info.execution_props().query_execution_start_time;

// Try to get timezone from config and convert to local time
let nano = info
.execution_props()
.config_options()
.config_options
.as_ref()
.and_then(|config| {
config
.execution
Expand Down
7 changes: 6 additions & 1 deletion datafusion/functions/src/datetime/now.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,9 +121,14 @@ impl ScalarUDFImpl for NowFunc {

fn simplify(
&self,
_args: Vec<Expr>,
args: Vec<Expr>,
info: &dyn SimplifyInfo,
) -> Result<ExprSimplifyResult> {
// Check if stable expression evaluation is disabled
if !info.evaluate_stable_expressions() {
return Ok(ExprSimplifyResult::Original(args));
}

let now_ts = info
.execution_props()
.query_execution_start_time
Expand Down
16 changes: 11 additions & 5 deletions datafusion/optimizer/src/simplify_expressions/expr_simplifier.rs
Original file line number Diff line number Diff line change
Expand Up @@ -541,7 +541,7 @@ impl TreeNodeRewriter for ConstEvaluator<'_> {
// stack as not ok (as all parents have at least one child or
// descendant that can not be evaluated

if !Self::can_evaluate(&expr) {
if !self.can_evaluate(&expr) {
// walk back up stack, marking first parent that is not mutable
let parent_iter = self.can_evaluate.iter_mut().rev();
for p in parent_iter {
Expand Down Expand Up @@ -620,18 +620,24 @@ impl<'a> ConstEvaluator<'a> {
}

/// Can a function of the specified volatility be evaluated?
fn volatility_ok(volatility: Volatility) -> bool {
fn volatility_ok(&self, volatility: Volatility) -> bool {
match volatility {
Volatility::Immutable => true,
// Values for functions such as now() are taken from ExecutionProps
Volatility::Stable => true,
Volatility::Stable => {
// Check if stable expression evaluation is enabled in config
self.execution_props
.config_options
.as_ref()
.is_none_or(|opts| opts.optimizer.evaluate_stable_expressions)
}
Volatility::Volatile => false,
}
}

/// Can the expression be evaluated at plan time, (assuming all of
/// its children can also be evaluated)?
fn can_evaluate(expr: &Expr) -> bool {
fn can_evaluate(&self, expr: &Expr) -> bool {
// check for reasons we can't evaluate this node
//
// NOTE all expr types are listed here so when new ones are
Expand All @@ -652,7 +658,7 @@ impl<'a> ConstEvaluator<'a> {
| Expr::Wildcard { .. }
| Expr::Placeholder(_) => false,
Expr::ScalarFunction(ScalarFunction { func, .. }) => {
Self::volatility_ok(func.signature().volatility)
self.volatility_ok(func.signature().volatility)
}
Expr::Literal(_, _)
| Expr::Alias(..)
Expand Down
14 changes: 14 additions & 0 deletions datafusion/optimizer/src/simplify_expressions/simplify_exprs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1073,4 +1073,18 @@ mod tests {
"
)
}

#[test]
fn test_evaluate_stable_expressions_enabled_by_default() -> Result<()> {
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: Not sure, but I think better place for these test cases should be, datafusion/core/tests/expr_api/simplification.rs

// By default, stable expressions like now() should be simplified to literals
let time = Utc::now();

// With default config, evaluate_stable_expressions should be true
let config = OptimizerContext::new().with_query_execution_start_time(time);
assert!(
config.options().optimizer.evaluate_stable_expressions,
"evaluate_stable_expressions should be true by default"
);
Ok(())
}
}
2 changes: 2 additions & 0 deletions datafusion/sqllogictest/test_files/information_schema.slt
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,7 @@ datafusion.optimizer.enable_sort_pushdown true
datafusion.optimizer.enable_topk_aggregation true
datafusion.optimizer.enable_topk_dynamic_filter_pushdown true
datafusion.optimizer.enable_window_limits true
datafusion.optimizer.evaluate_stable_expressions true
datafusion.optimizer.expand_views_at_output false
datafusion.optimizer.filter_null_join_keys false
datafusion.optimizer.hash_join_inlist_pushdown_max_distinct_values 150
Expand Down Expand Up @@ -436,6 +437,7 @@ datafusion.optimizer.enable_sort_pushdown true Enable sort pushdown optimization
datafusion.optimizer.enable_topk_aggregation true When set to true, the optimizer will attempt to perform limit operations during aggregations, if possible
datafusion.optimizer.enable_topk_dynamic_filter_pushdown true When set to true, the optimizer will attempt to push down TopK dynamic filters into the file scan phase.
datafusion.optimizer.enable_window_limits true When set to true, the optimizer will attempt to push limit operations past window functions, if possible
datafusion.optimizer.evaluate_stable_expressions true When set to true (default), the optimizer will evaluate stable functions (like `now()`, `current_date()`, `current_time()`) during query planning, converting them to literal values. When set to false, stable functions are preserved in the plan and evaluated at execution time. Setting this to false is useful when performing query rewrites that need to preserve stable function calls, or when you want the function to be re-evaluated for each execution of a prepared statement rather than being fixed at planning time.
datafusion.optimizer.expand_views_at_output false When set to true, if the returned type is a view type then the output will be coerced to a non-view. Coerces `Utf8View` to `LargeUtf8`, and `BinaryView` to `LargeBinary`.
datafusion.optimizer.filter_null_join_keys false When set to true, the optimizer will insert filters before a join between a nullable and non-nullable column to filter out nulls on the nullable side. This filter can add additional overhead when the file format does not fully support predicate push down.
datafusion.optimizer.hash_join_inlist_pushdown_max_distinct_values 150 Maximum number of distinct values (rows) in the build side of a hash join to be pushed down as an InList expression for dynamic filtering. Build sides with more rows than this will use hash table lookups instead. Set to 0 to always use hash table lookups. This provides an additional limit beyond `hash_join_inlist_pushdown_max_size` to prevent very large IN lists that might not provide much benefit over hash table lookups. This uses the deduplicated row count once the build side has been evaluated. The default is 150 values per partition. This is inspired by Trino's `max-filter-keys-per-column` setting. See: <https://trino.io/docs/current/admin/dynamic-filtering.html#dynamic-filter-collection-thresholds>
Expand Down
24 changes: 24 additions & 0 deletions datafusion/sqllogictest/test_files/options.slt
Original file line number Diff line number Diff line change
Expand Up @@ -282,3 +282,27 @@ select 1e40 + 1e40, arrow_typeof(1e40 + 1e40),
# Restore option to default value
statement ok
set datafusion.sql_parser.parse_float_as_decimal = false;

##
# test_evaluate_stable_expressions
##

# By default, now() should be simplified to a literal in the plan
# Disable stable expression evaluation
statement ok
set datafusion.optimizer.evaluate_stable_expressions = false;

# With config disabled, now() should remain as a function call in the plan
query TT
explain select now();
----
logical_plan
01)Projection: now()
02)--EmptyRelation: rows=1
physical_plan
01)ProjectionExec: expr=[now()]
02)--PlaceholderRowExec

# Restore default
statement ok
set datafusion.optimizer.evaluate_stable_expressions = true;
1 change: 1 addition & 0 deletions docs/source/user-guide/configs.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ The following configuration settings are available:
| datafusion.optimizer.prefer_existing_union | false | When set to true, the optimizer will not attempt to convert Union to Interleave |
| datafusion.optimizer.expand_views_at_output | false | When set to true, if the returned type is a view type then the output will be coerced to a non-view. Coerces `Utf8View` to `LargeUtf8`, and `BinaryView` to `LargeBinary`. |
| datafusion.optimizer.enable_sort_pushdown | true | Enable sort pushdown optimization. When enabled, attempts to push sort requirements down to data sources that can natively handle them (e.g., by reversing file/row group read order). Returns **inexact ordering**: Sort operator is kept for correctness, but optimized input enables early termination for TopK queries (ORDER BY ... LIMIT N), providing significant speedup. Memory: No additional overhead (only changes read order). Future: Will add option to detect perfectly sorted data and eliminate Sort completely. Default: true |
| datafusion.optimizer.evaluate_stable_expressions | true | When set to true (default), the optimizer will evaluate stable functions (like `now()`, `current_date()`, `current_time()`) during query planning, converting them to literal values. When set to false, stable functions are preserved in the plan and evaluated at execution time. Setting this to false is useful when performing query rewrites that need to preserve stable function calls, or when you want the function to be re-evaluated for each execution of a prepared statement rather than being fixed at planning time. |
| datafusion.explain.logical_plan_only | false | When set to true, the explain statement will only print logical plans |
| datafusion.explain.physical_plan_only | false | When set to true, the explain statement will only print physical plans |
| datafusion.explain.show_statistics | false | When set to true, the explain statement will print operator statistics for physical plans |
Expand Down