fix: reject build-dependent functions the volatility label misses

DataFusion marks version() immutable, but it returns the build string:
identical-across-refreshes has to hold across upgrades too, and an
incremental view would mix values from different builds. The immutability
guard now also excludes known build-dependent functions by name.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Wyatt Alt
2026-08-13 23:42:37 -07:00
co-authored by Claude Fable 5
parent 9b6f6719b0
commit e26fbd3dcc
+10 -3
View File
@@ -285,11 +285,18 @@ 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"];
let mut offending: Option<String> = None;
expr.apply(|node| {
if let datafusion_expr::Expr::ScalarFunction(function) = node {
if function.func.signature().volatility != Volatility::Immutable {
offending = Some(function.func.name().to_string());
let name = function.func.name();
if function.func.signature().volatility != Volatility::Immutable
|| BUILD_DEPENDENT.contains(&name)
{
offending = Some(name.to_string());
return Ok(TreeNodeRecursion::Stop);
}
}
@@ -1014,7 +1021,7 @@ mod tests {
#[tokio::test]
async fn test_volatile_and_unstable_expressions_are_rejected() {
let conn = people_db().await;
for expression in ["random()", "now()"] {
for expression in ["random()", "now()", "version()"] {
let err = conn
.create_materialized_view("bad", "people")
.select([("x", expression)])