Skip to content

Commit b245acb

Browse files
committed
feat[vortex-expr]: analysis
Signed-off-by: Joe Isaacs <[email protected]>
1 parent aafe376 commit b245acb

File tree

8 files changed

+162
-9
lines changed

8 files changed

+162
-9
lines changed

vortex-array/src/arrays/struct_/vtable/reduce.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ use vortex_dtype::{FieldName, FieldNames};
1010
use vortex_error::{VortexExpect, VortexResult};
1111

1212
use crate::arrays::{ExprArray, StructArray};
13+
use crate::expr::analysis::annotate_scope_access;
1314
use crate::expr::session::ExprSession;
14-
use crate::expr::transform::immediate_access::annotate_scope_access;
1515
use crate::expr::transform::{
1616
ExprOptimizer, PartitionedExpr, partition, replace, replace_root_fields,
1717
};
File renamed without changes.

vortex-array/src/expr/transform/immediate_access.rs renamed to vortex-array/src/expr/analysis/immediate_access.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@ use vortex_error::VortexExpect;
66
use vortex_utils::aliases::hash_set::HashSet;
77

88
use crate::expr::Expression;
9+
use crate::expr::analysis::annotations::{AnnotationFn, Annotations, descendent_annotations};
910
use crate::expr::exprs::get_item::GetItem;
1011
use crate::expr::exprs::root::Root;
1112
use crate::expr::exprs::select::Select;
12-
use crate::expr::transform::annotations::{AnnotationFn, Annotations, descendent_annotations};
1313

1414
pub type FieldAccesses<'a> = Annotations<'a, FieldName>;
1515

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
// SPDX-License-Identifier: Apache-2.0
22
// SPDX-FileCopyrightText: Copyright the Vortex contributors
33

4+
pub mod annotations;
5+
pub mod immediate_access;
6+
mod null_sensitive;
7+
8+
pub use annotations::*;
9+
pub use immediate_access::*;
10+
pub use null_sensitive::*;
411
use vortex_dtype::FieldPath;
512

613
use crate::expr::Expression;
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// SPDX-FileCopyrightText: Copyright the Vortex contributors
3+
4+
use vortex_error::{VortexExpect, VortexResult};
5+
use vortex_utils::aliases::hash_map::HashMap;
6+
7+
use crate::expr::Expression;
8+
use crate::expr::exprs::is_null::IsNull;
9+
use crate::expr::traversal::{NodeExt, NodeVisitor, TraversalOrder};
10+
11+
/// Tracks whether an expression is null-sensitive.
12+
///
13+
/// An expression is null-sensitive if it or any of its children is an `is_null` operation.
14+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
15+
pub enum NullSensitive {
16+
/// The expression or one of its children contains an `is_null` operation.
17+
Yes,
18+
/// The expression and all of its children do not contain an `is_null` operation.
19+
No,
20+
}
21+
22+
impl NullSensitive {
23+
/// Combine two null sensitivity labels.
24+
///
25+
/// Returns `Yes` if either label is `Yes`, otherwise `No`.
26+
pub fn combine(self, other: Self) -> Self {
27+
match (self, other) {
28+
(NullSensitive::Yes, _) | (_, NullSensitive::Yes) => NullSensitive::Yes,
29+
(NullSensitive::No, NullSensitive::No) => NullSensitive::No,
30+
}
31+
}
32+
}
33+
34+
pub type NullSensitiveLabels<'a> = HashMap<&'a Expression, NullSensitive>;
35+
36+
/// Label each expression in the tree with whether it is null-sensitive.
37+
///
38+
/// An expression is null-sensitive if it or any of its descendants contain an `is_null` operation.
39+
pub fn label_null_sensitive(expr: &Expression) -> NullSensitiveLabels<'_> {
40+
let mut visitor = NullSensitiveVisitor {
41+
labels: Default::default(),
42+
};
43+
expr.accept(&mut visitor)
44+
.vortex_expect("NullSensitiveVisitor is infallible");
45+
visitor.labels
46+
}
47+
48+
struct NullSensitiveVisitor<'a> {
49+
labels: NullSensitiveLabels<'a>,
50+
}
51+
52+
impl<'a> NodeVisitor<'a> for NullSensitiveVisitor<'a> {
53+
type NodeTy = Expression;
54+
55+
fn visit_down(&mut self, _node: &'a Self::NodeTy) -> VortexResult<TraversalOrder> {
56+
// Continue traversing down
57+
Ok(TraversalOrder::Continue)
58+
}
59+
60+
fn visit_up(&mut self, node: &'a Expression) -> VortexResult<TraversalOrder> {
61+
// Check if this node is an is_null operation
62+
let is_null = node.is::<IsNull>();
63+
64+
// Combine labels from all children
65+
let children_sensitive = node
66+
.children()
67+
.iter()
68+
.filter_map(|child| self.labels.get(child))
69+
.any(|&label| label == NullSensitive::Yes);
70+
71+
// This node is sensitive if it's is_null or any child is sensitive
72+
let label = if is_null || children_sensitive {
73+
NullSensitive::Yes
74+
} else {
75+
NullSensitive::No
76+
};
77+
78+
self.labels.insert(node, label);
79+
80+
Ok(TraversalOrder::Continue)
81+
}
82+
}
83+
84+
#[cfg(test)]
85+
mod tests {
86+
use super::*;
87+
use crate::expr::exprs::binary::eq;
88+
use crate::expr::exprs::get_item::col;
89+
use crate::expr::exprs::is_null::is_null;
90+
use crate::expr::exprs::literal::lit;
91+
92+
#[test]
93+
fn test_null_sensitive_with_is_null() {
94+
// Expression: is_null($.col1)
95+
let expr = is_null(col("col1"));
96+
let labels = label_null_sensitive(&expr);
97+
98+
// The root expression should be null-sensitive
99+
assert_eq!(labels.get(&expr), Some(&NullSensitive::Yes));
100+
}
101+
102+
#[test]
103+
fn test_null_sensitive_without_is_null() {
104+
// Expression: $.col1 = 5
105+
let expr = eq(col("col1"), lit(5));
106+
let labels = label_null_sensitive(&expr);
107+
108+
// The root expression should not be null-sensitive
109+
assert_eq!(labels.get(&expr), Some(&NullSensitive::No));
110+
}
111+
112+
#[test]
113+
fn test_null_sensitive_nested() {
114+
// Expression: ($.col1 = 5) = is_null($.col2)
115+
let left = eq(col("col1"), lit(5));
116+
let right = is_null(col("col2"));
117+
let expr = eq(left.clone(), right.clone());
118+
119+
let labels = label_null_sensitive(&expr);
120+
121+
// The left side should not be sensitive
122+
assert_eq!(labels.get(&left), Some(&NullSensitive::No));
123+
124+
// The right side should be sensitive
125+
assert_eq!(labels.get(&right), Some(&NullSensitive::Yes));
126+
127+
// The root should be sensitive (because right child is sensitive)
128+
assert_eq!(labels.get(&expr), Some(&NullSensitive::Yes));
129+
}
130+
131+
#[test]
132+
fn test_combine() {
133+
assert_eq!(
134+
NullSensitive::Yes.combine(NullSensitive::Yes),
135+
NullSensitive::Yes
136+
);
137+
assert_eq!(
138+
NullSensitive::Yes.combine(NullSensitive::No),
139+
NullSensitive::Yes
140+
);
141+
assert_eq!(
142+
NullSensitive::No.combine(NullSensitive::Yes),
143+
NullSensitive::Yes
144+
);
145+
assert_eq!(
146+
NullSensitive::No.combine(NullSensitive::No),
147+
NullSensitive::No
148+
);
149+
}
150+
}

vortex-array/src/expr/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ use vortex_utils::aliases::hash_set::HashSet;
2121
use crate::expr::traversal::{NodeExt, ReferenceCollector};
2222

2323
pub mod aliases;
24-
mod analysis;
24+
pub mod analysis;
2525
#[cfg(feature = "arbitrary")]
2626
pub mod arbitrary;
2727
pub mod display;

vortex-array/src/expr/transform/mod.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22
// SPDX-FileCopyrightText: Copyright the Vortex contributors
33

44
//! A collection of transformations that can be applied to a [`crate::expr::Expression`].
5-
pub mod annotations;
6-
pub mod immediate_access;
75
pub(crate) mod match_between;
86
mod optimizer;
97
mod partition;

vortex-array/src/expr/transform/partition.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,11 @@ use vortex_error::{VortexExpect, VortexResult};
99
use vortex_utils::aliases::hash_map::HashMap;
1010

1111
use crate::expr::Expression;
12+
use crate::expr::analysis::{Annotation, AnnotationFn, Annotations, descendent_annotations};
1213
use crate::expr::exprs::get_item::get_item;
1314
use crate::expr::exprs::pack::pack;
1415
use crate::expr::exprs::root::root;
1516
use crate::expr::transform::ExprOptimizer;
16-
use crate::expr::transform::annotations::{
17-
Annotation, AnnotationFn, Annotations, descendent_annotations,
18-
};
1917
use crate::expr::traversal::{NodeExt, NodeRewriter, Transformed, TraversalOrder};
2018

2119
/// Partition an expression into sub-expressions that are uniquely associated with an annotation.
@@ -201,6 +199,7 @@ mod tests {
201199
use vortex_dtype::{DType, StructFields};
202200

203201
use super::*;
202+
use crate::expr::analysis::annotate_scope_access;
204203
use crate::expr::exprs::binary::and;
205204
use crate::expr::exprs::get_item::{col, get_item};
206205
use crate::expr::exprs::literal::lit;
@@ -209,7 +208,6 @@ mod tests {
209208
use crate::expr::exprs::root::root;
210209
use crate::expr::exprs::select::select;
211210
use crate::expr::session::ExprSession;
212-
use crate::expr::transform::immediate_access::annotate_scope_access;
213211
use crate::expr::transform::replace::replace_root_fields;
214212
use crate::expr::transform::simplify_typed::simplify_typed;
215213

0 commit comments

Comments
 (0)