Summary
Some expression parser paths validate determinism after TypeChecker::resolve, but TypeChecker rewrites session/catalog/auth/settings-dependent sugar functions into literals
before the deterministic check runs.
As a result, expressions such as current_database(), current_user(), getvariable(...), last_query_id(...), and timezone() can become ConstantExpr/Expr::Constant
first, so later Expr::is_deterministic() no longer sees that the original expression depended on query/session context.
This is especially risky for persisted or storage-level expressions, such as computed columns, cluster keys, and pruning/filter expressions.
Evidence on main
In src/query/sql/src/planner/expression/expression_parser.rs:
parse_computed_expr_to_string resolves the AST first, then checks computed_expr.is_deterministic(...).
analyze_cluster_keys resolves the AST first, then checks expr.is_deterministic(...).
parse_computed_expr, parse_exprs, parse_to_filters, and parse_cluster_keys use full TypeChecker::try_create(...) and do not consistently enforce a context-
independent policy.
In src/query/sql/src/planner/semantic/type_check.rs:
resolve_function calls try_rewrite_sugar_function(...) before normal scalar function resolution.
- Context sugar functions are rewritten to literals, for example:
current_catalog() -> current catalog string
current_database() -> current database string
current_user() -> current user string
connection_id() / client_session_id() -> session values
timezone() -> setting value
In src/query/expression/src/expression.rs, Expr::is_deterministic() only checks function calls against FunctionProperty::non_deterministic. Once a context function has
already been rewritten to a constant, this check cannot distinguish it from a real literal.
Problem
There are two different concepts currently mixed together:
-
Function determinism inside common_expression::Expr
Example: rand() and now() are non-deterministic because they remain function calls with non_deterministic properties.
-
Context independence / persistability
Example: current_database() may be folded to a constant during binding, but the value depends on the binding session. It should not be accepted in persisted schema
expressions or storage-level expressions just because it becomes a literal before Expr::is_deterministic().
The current checks only reliably cover the first concept.
Examples of problematic shapes
These should be rejected for persisted/context-independent expression use, but the current architecture can lose the original dependency before validation:
- computed column expression using
current_database() or getvariable('x')
- cluster key expression like
concat(to_string(a), current_database())
- storage/pruning filter expression using context-dependent sugar
- any expression parser path that converts full SQL expressions into reusable
common_expression::Expr without preserving context dependencies
Expected behavior
Expression parser entry points should choose an explicit policy before resolving:
- Full query binding may allow context-dependent functions and fold them for the current query.
- Persisted/schema/storage expression parsing should reject:
- catalog/session/auth functions
- session variables
last_query_id
- settings-dependent values such as
timezone() if the expression is meant to be portable/persisted
- ordinary non-deterministic functions such as
rand(), now(), etc.
Pure sugar rewrites such as coalesce, decode, greatest/least, to_variant, and decode-string helpers can still be allowed if their arguments and resolved functions are
deterministic.
Suggested fix direction
Add a pre-resolution or lowering-level capability check for expression parser paths that require context-independent expressions.
The check should run before context-dependent sugar functions are folded into constants, or it should preserve dependency information through lowering. Relying only on
common_expression::Expr::is_deterministic() after type checking is not sufficient.
Possible implementation shape:
- introduce an expression policy such as
ContextIndependent / Persistable
- have
TypeChecker or its adapter reject context-dependent sugar/functions under that policy
- keep existing
Expr::is_deterministic() for normal function-property validation
- apply the stricter policy to computed columns, cluster keys, storage/pruning expressions, and similar expression_parser entry points
- add regression tests that assert these are rejected before being folded to constants:
- computed column:
current_database()
- computed column or cluster key:
getvariable('x')
- cluster key:
concat(to_string(a), current_database())
- storage filter parser:
a > rand()
Summary
Some expression parser paths validate determinism after
TypeChecker::resolve, butTypeCheckerrewrites session/catalog/auth/settings-dependent sugar functions into literalsbefore the deterministic check runs.
As a result, expressions such as
current_database(),current_user(),getvariable(...),last_query_id(...), andtimezone()can becomeConstantExpr/Expr::Constantfirst, so later
Expr::is_deterministic()no longer sees that the original expression depended on query/session context.This is especially risky for persisted or storage-level expressions, such as computed columns, cluster keys, and pruning/filter expressions.
Evidence on main
In
src/query/sql/src/planner/expression/expression_parser.rs:parse_computed_expr_to_stringresolves the AST first, then checkscomputed_expr.is_deterministic(...).analyze_cluster_keysresolves the AST first, then checksexpr.is_deterministic(...).parse_computed_expr,parse_exprs,parse_to_filters, andparse_cluster_keysuse fullTypeChecker::try_create(...)and do not consistently enforce a context-independent policy.
In
src/query/sql/src/planner/semantic/type_check.rs:resolve_functioncallstry_rewrite_sugar_function(...)before normal scalar function resolution.current_catalog()-> current catalog stringcurrent_database()-> current database stringcurrent_user()-> current user stringconnection_id()/client_session_id()-> session valuestimezone()-> setting valueIn
src/query/expression/src/expression.rs,Expr::is_deterministic()only checks function calls againstFunctionProperty::non_deterministic. Once a context function hasalready been rewritten to a constant, this check cannot distinguish it from a real literal.
Problem
There are two different concepts currently mixed together:
Function determinism inside
common_expression::ExprExample:
rand()andnow()are non-deterministic because they remain function calls withnon_deterministicproperties.Context independence / persistability
Example:
current_database()may be folded to a constant during binding, but the value depends on the binding session. It should not be accepted in persisted schemaexpressions or storage-level expressions just because it becomes a literal before
Expr::is_deterministic().The current checks only reliably cover the first concept.
Examples of problematic shapes
These should be rejected for persisted/context-independent expression use, but the current architecture can lose the original dependency before validation:
current_database()orgetvariable('x')concat(to_string(a), current_database())common_expression::Exprwithout preserving context dependenciesExpected behavior
Expression parser entry points should choose an explicit policy before resolving:
last_query_idtimezone()if the expression is meant to be portable/persistedrand(),now(), etc.Pure sugar rewrites such as
coalesce,decode,greatest/least,to_variant, and decode-string helpers can still be allowed if their arguments and resolved functions aredeterministic.
Suggested fix direction
Add a pre-resolution or lowering-level capability check for expression parser paths that require context-independent expressions.
The check should run before context-dependent sugar functions are folded into constants, or it should preserve dependency information through lowering. Relying only on
common_expression::Expr::is_deterministic()after type checking is not sufficient.Possible implementation shape:
ContextIndependent/PersistableTypeCheckeror its adapter reject context-dependent sugar/functions under that policyExpr::is_deterministic()for normal function-property validationcurrent_database()getvariable('x')concat(to_string(a), current_database())a > rand()