vortex_array/expr/analysis/
referenced_field_paths.rs1use vortex_error::VortexResult;
5use vortex_error::vortex_err;
6
7use crate::dtype::Field;
8use crate::dtype::FieldPath;
9use crate::dtype::FieldPathSet;
10use crate::expr::BoundExpression;
11use crate::expr::traversal::FoldDownContext;
12use crate::expr::traversal::FoldUp;
13use crate::expr::traversal::NodeExt;
14use crate::expr::traversal::NodeFolderContext;
15use crate::scalar_fn::fns::get_item::GetItem;
16use crate::scalar_fn::fns::select::Select;
17
18pub fn referenced_field_paths(expr: &BoundExpression) -> VortexResult<FieldPathSet> {
26 let mut collector = ReferencedFieldPaths {
27 field_paths: FieldPathSet::default(),
28 };
29 expr.clone()
30 .fold_context(&vec![FieldPath::root()], &mut collector)?;
31 Ok(collector.field_paths)
32}
33
34struct ReferencedFieldPaths {
46 field_paths: FieldPathSet,
47}
48
49impl NodeFolderContext for ReferencedFieldPaths {
50 type NodeTy = BoundExpression;
51 type Result = ();
52 type Context = Vec<FieldPath>;
53
54 fn visit_down(
55 &mut self,
56 requested: &Self::Context,
57 node: &BoundExpression,
58 ) -> VortexResult<FoldDownContext<Self::Context, ()>> {
59 if node.is_root() {
60 self.field_paths.extend(
61 requested
62 .iter()
63 .map(|path| FieldPath::from_iter(path.parts().iter().rev().cloned())),
64 );
65 return Ok(FoldDownContext::Skip(()));
66 }
67
68 if let Some(field_name) = node
69 .as_scalar()
70 .and_then(|scalar_fn| scalar_fn.as_opt::<GetItem>())
71 {
72 let appended = requested
73 .iter()
74 .map(|path| path.clone().push(Field::Name(field_name.clone())))
75 .collect();
76 return Ok(FoldDownContext::Continue(appended));
77 }
78
79 if let Some(selection) = node
82 .as_scalar()
83 .and_then(|scalar_fn| scalar_fn.as_opt::<Select>())
84 {
85 let child_fields = node.children()[0]
86 .dtype()
87 .as_struct_fields_opt()
88 .ok_or_else(|| vortex_err!("Select child is not a struct"))?;
89 let included_fields = selection.normalize_to_included_fields(child_fields.names())?;
90
91 let mut narrowed = Vec::with_capacity(requested.len());
92 for path in requested {
93 if path.is_root() {
94 narrowed.extend(included_fields.iter().cloned().map(FieldPath::from_name));
95 } else if let Some(Field::Name(field_name)) = path.parts().last()
96 && included_fields
97 .iter()
98 .any(|included| included == field_name)
99 {
100 narrowed.push(path.clone());
101 }
102 }
103
104 if narrowed.is_empty() {
107 return Ok(FoldDownContext::Skip(()));
108 }
109 return Ok(FoldDownContext::Continue(narrowed));
110 }
111
112 Ok(FoldDownContext::Continue(vec![FieldPath::root()]))
114 }
115
116 fn visit_up(
117 &mut self,
118 _node: BoundExpression,
119 _requested: &Self::Context,
120 _children: Vec<()>,
121 ) -> VortexResult<FoldUp<()>> {
122 Ok(FoldUp::Continue(()))
123 }
124}
125
126#[cfg(test)]
127mod tests {
128 use vortex_utils::aliases::hash_set::HashSet;
129
130 use super::*;
131 use crate::dtype::DType;
132 use crate::dtype::Nullability::NonNullable;
133 use crate::dtype::PType::I32;
134 use crate::dtype::StructFields;
135 use crate::expr::Expression;
136 use crate::expr::get_item;
137 use crate::expr::pack;
138 use crate::expr::root;
139 use crate::expr::select;
140 use crate::expr::select_exclude;
141
142 fn scope() -> DType {
143 DType::Struct(
144 StructFields::from_iter([(
145 "a",
146 DType::Struct(
147 StructFields::from_iter([("x", I32), ("y", I32)]),
148 NonNullable,
149 ),
150 )]),
151 NonNullable,
152 )
153 }
154
155 fn referenced(expr: &Expression) -> VortexResult<HashSet<FieldPath>> {
157 Ok(referenced_field_paths(&expr.bind(&scope())?)?
158 .into_iter()
159 .collect())
160 }
161
162 #[test]
163 fn nested_select_preserves_field_path() -> VortexResult<()> {
164 let expr = select(["x"], get_item("a", root()));
165
166 assert_eq!(
167 referenced(&expr)?,
168 HashSet::from_iter([FieldPath::from_name("a").push("x")])
169 );
170 Ok(())
171 }
172
173 #[test]
174 fn get_item_after_select_only_references_requested_field() -> VortexResult<()> {
175 let expr = get_item("x", select(["x", "y"], get_item("a", root())));
176
177 assert_eq!(
178 referenced(&expr)?,
179 HashSet::from_iter([FieldPath::from_name("a").push("x")])
180 );
181 Ok(())
182 }
183
184 #[test]
185 fn select_exclude_references_included_fields() -> VortexResult<()> {
186 let expr = select_exclude(["y"], get_item("a", root()));
187
188 assert_eq!(
189 referenced(&expr)?,
190 HashSet::from_iter([FieldPath::from_name("a").push("x")])
191 );
192 Ok(())
193 }
194
195 #[test]
196 fn ancestor_path_subsumes_descendant() -> VortexResult<()> {
197 let expr = pack(
198 [
199 ("a", get_item("a", root())),
200 ("x", get_item("x", get_item("a", root()))),
201 ],
202 NonNullable,
203 );
204
205 assert_eq!(
206 referenced(&expr)?,
207 HashSet::from_iter([FieldPath::from_name("a")])
208 );
209 Ok(())
210 }
211
212 #[test]
213 fn get_item_through_opaque_fn_references_all_fields() -> VortexResult<()> {
214 let expr = get_item("x", pack([("x", root())], NonNullable));
217
218 assert_eq!(referenced(&expr)?, HashSet::from_iter([FieldPath::root()]));
219 Ok(())
220 }
221
222 #[test]
223 fn root_references_all_fields() -> VortexResult<()> {
224 assert_eq!(
225 referenced(&root())?,
226 HashSet::from_iter([FieldPath::root()])
227 );
228 Ok(())
229 }
230
231 #[test]
232 fn invalid_get_item_path_returns_error() {
233 let result = get_item("missing", root())
234 .bind(&scope())
235 .and_then(|expr| referenced_field_paths(&expr));
236 assert!(result.is_err());
237 }
238}