From 6db789e0dad9cf3a49037e54add492bcfc006a7c Mon Sep 17 00:00:00 2001 From: Wyatt Alt Date: Fri, 14 Aug 2026 00:20:35 -0700 Subject: [PATCH] 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 --- rust/lancedb/src/materialized_view.rs | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/rust/lancedb/src/materialized_view.rs b/rust/lancedb/src/materialized_view.rs index 977c98ea4..6f2a8157a 100644 --- a/rust/lancedb/src/materialized_view.rs +++ b/rust/lancedb/src/materialized_view.rs @@ -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 = 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)])