Skip to content

Feature: add is_deterministic() to ScalarExpr to handle AsyncFunctionCall correctly #19451

@dantengsky

Description

@dantengsky

Summary

ScalarExpr::as_expr() lowers AsyncFunctionCall (e.g. nextval) to a dummy ColumnRef, which causes Expr::is_deterministic() to return true — a false positive. This is a lossy abstraction boundary: callers of as_expr() silently lose async function semantics.

Concrete bug this caused

In interpreter_table_modify_column.rs, the default-only modify path used matches!(&scalar_expr, ScalarExpr::AsyncFunctionCall(_)) to detect nextval before falling through to as_expr().is_deterministic(). But parse_and_bind() wraps type-mismatched results in CastExpr, so nextval(seq) (returns UInt64) on an INT column becomes CastExpr(AsyncFunctionCall) — the top-level matches! misses it entirely.

The workaround is to use default_value_evaluable() which recursively walks the tree. But this is still fragile.

Proposed improvements

  1. ScalarExpr::is_deterministic(): Add a native method that handles AsyncFunctionCall without relying on the lossy as_expr() lowering:
impl ScalarExpr {
    fn is_deterministic(&self, registry: &FunctionRegistry) -> bool {
        match self {
            ScalarExpr::AsyncFunctionCall(_) => false,
            _ => self.as_expr()
                     .map(|e| e.is_deterministic(registry))
                     .unwrap_or(false),
        }
    }
}
  1. Rename default_value_evaluable(): The current name is misleading — it's a general ScalarExpr method, not specific to default values. The return type (bool, bool) lacks self-documentation. Consider:
    • Splitting into contains_nextval() and is_compile_time_evaluable()
    • Or returning a named struct instead of a tuple

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions