Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 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
30 changes: 30 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,34 @@ mod tests {
"
)
}

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

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.

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(())
}

#[test]
fn test_evaluate_stable_expressions_disabled() -> Result<()> {

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.

nit: I don't think this test is particularly useful, can probably just remove it

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.

agreed removed

// When evaluate_stable_expressions is false, stable functions should NOT be simplified
use datafusion_common::config::ConfigOptions;

let mut config_options = ConfigOptions::default();
config_options.optimizer.evaluate_stable_expressions = false;

// Verify the config is set correctly
assert!(
!config_options.optimizer.evaluate_stable_expressions,
"evaluate_stable_expressions should be false when explicitly disabled"
);
Ok(())
}
}
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
Loading