fix: also reject schema-dependent immutable functions

arrow_typeof, arrow_field and arrow_metadata are labeled immutable but
compute from schema state -- name, type, nullability, field metadata --
which can change without any data change a refresh could observe, mixing
evaluations across an incremental refresh. The guard's criterion is now
stated as it is: a definition may only call functions whose result is
determined by row values alone.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Wyatt Alt
2026-08-14 00:20:35 -07:00
co-authored by Claude Fable 5
parent e26fbd3dcc
commit a84325242e
+14 -5
View File
@@ -285,16 +285,19 @@ fn ensure_immutable(expr: &datafusion_expr::Expr, error: impl Fn(String) -> Erro
use datafusion_common::tree_node::{TreeNode, TreeNodeRecursion};
use datafusion_expr::Volatility;
// Labeled immutable but actually build-dependent: version() returns the
// DataFusion build string, so its value changes across upgrades.
const BUILD_DEPENDENT: &[&str] = &["version"];
// Labeled immutable but not determined by row values alone: version()
// depends on the build, and the arrow_* introspectors on schema state
// (name, type, nullability, field metadata) that may change without any
// data change a refresh could observe.
const NOT_VALUE_DETERMINED: &[&str] =
&["version", "arrow_typeof", "arrow_field", "arrow_metadata"];
let mut offending: Option<String> = None;
expr.apply(|node| {
if let datafusion_expr::Expr::ScalarFunction(function) = node {
let name = function.func.name();
if function.func.signature().volatility != Volatility::Immutable
|| BUILD_DEPENDENT.contains(&name)
|| NOT_VALUE_DETERMINED.contains(&name)
{
offending = Some(name.to_string());
return Ok(TreeNodeRecursion::Stop);
@@ -1021,7 +1024,13 @@ mod tests {
#[tokio::test]
async fn test_volatile_and_unstable_expressions_are_rejected() {
let conn = people_db().await;
for expression in ["random()", "now()", "version()"] {
for expression in [
"random()",
"now()",
"version()",
"arrow_typeof(age)",
"arrow_metadata(age, 'k')",
] {
let err = conn
.create_materialized_view("bad", "people")
.select([("x", expression)])