diff --git a/vortex-array/src/expr/analysis/fallible.rs b/vortex-array/src/expr/analysis/fallible.rs index ff43d51603b..4b39f9c09b5 100644 --- a/vortex-array/src/expr/analysis/fallible.rs +++ b/vortex-array/src/expr/analysis/fallible.rs @@ -10,8 +10,11 @@ pub fn label_is_fallible(expr: &Expression) -> BooleanLabels<'_> { expr, |expr| match expr { Expression::Scalar { scalar_fn, .. } => scalar_fn.signature().is_fallible(), - // The scope itself cannot fail. - Expression::Root => false, + // These add no fallibility of their own. Note this is the *self* label: a lambda's + // body is one of its children, so the folded label at a lambda node is the body's + // fallibility. A higher-order function therefore picks the body up through the + // ordinary fold instead of walking it by hand. + Expression::Root | Expression::Variable(_) | Expression::Lambda(_) => false, }, |acc, &child| acc | child, ) @@ -82,3 +85,27 @@ mod tests { assert_eq!(labels.get(&expr), Some(&false)); } } + +#[cfg(test)] +mod lambda_tests { + use super::*; + use crate::expr::checked_add; + use crate::expr::lambda; + use crate::expr::lit; + use crate::expr::var; + + /// A lambda contributes no fallibility of its own, but its body is one of its children, so the + /// label at the lambda node is the body's. That is what lets a future higher-order function + /// pick the body up through the ordinary fold rather than walking it by hand. + #[test] + fn a_lambdas_label_is_its_bodys_fallibility() { + let fallible = Expression::from(lambda(["x"], checked_add(var("x"), lit(1i32)))); + assert_eq!(label_is_fallible(&fallible).get(&fallible), Some(&true)); + + let infallible = Expression::from(lambda(["x"], var("x"))); + assert_eq!( + label_is_fallible(&infallible).get(&infallible), + Some(&false) + ); + } +} diff --git a/vortex-array/src/expr/analysis/immediate_access.rs b/vortex-array/src/expr/analysis/immediate_access.rs index 6c2e4975a92..8f97c0e862a 100644 --- a/vortex-array/src/expr/analysis/immediate_access.rs +++ b/vortex-array/src/expr/analysis/immediate_access.rs @@ -67,7 +67,13 @@ pub fn make_bound_free_field_annotator( ) -> impl AnnotationFn { move |expr: &BoundExpression| { let Some(scalar_fn) = expr.as_scalar() else { - return scope.names().iter().cloned().collect(); + // Only the scope root reads every field. A variable resolves against a frame, so it + // reads none of them, and saying otherwise would defeat column pruning. + return if expr.is_root() { + scope.names().iter().cloned().collect() + } else { + vec![] + }; }; if let Some(selection) = scalar_fn.as_opt::