Skip to main content

vortex_array/scalar_fn/fns/
get_item.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4use std::fmt::Display;
5use std::fmt::Formatter;
6
7use prost::Message;
8use vortex_error::VortexResult;
9use vortex_error::vortex_err;
10use vortex_proto::expr as pb;
11use vortex_session::VortexSession;
12use vortex_session::registry::CachedId;
13
14use crate::ArrayRef;
15use crate::ExecutionCtx;
16use crate::arrays::ScalarFnArray;
17use crate::arrays::StructArray;
18use crate::arrays::struct_::StructArrayExt;
19use crate::builtins::ArrayBuiltins;
20use crate::builtins::ExprBuiltins;
21use crate::dtype::DType;
22use crate::dtype::FieldName;
23use crate::dtype::Nullability;
24use crate::expr::Expression;
25use crate::expr::display::ExprDisplay;
26use crate::expr::lit;
27use crate::scalar_fn::Arity;
28use crate::scalar_fn::ChildName;
29use crate::scalar_fn::EmptyOptions;
30use crate::scalar_fn::ExecutionArgs;
31use crate::scalar_fn::ReduceNode;
32use crate::scalar_fn::ScalarFnId;
33use crate::scalar_fn::ScalarFnVTable;
34use crate::scalar_fn::ScalarFnVTableExt;
35use crate::scalar_fn::fns::literal::Literal;
36use crate::scalar_fn::fns::mask::Mask;
37use crate::scalar_fn::fns::pack::Pack;
38
39#[derive(Clone)]
40pub struct GetItem;
41
42impl GetItem {
43    /// Creates a lazy projection of `field_name` from `input`.
44    ///
45    /// # Errors
46    ///
47    /// Returns an error if `input` is not a struct or does not contain `field_name`.
48    pub fn try_new(
49        input: ArrayRef,
50        field_name: impl Into<FieldName>,
51    ) -> VortexResult<ScalarFnArray> {
52        ScalarFnArray::try_new(GetItem.bind(field_name.into()), vec![input])
53    }
54}
55
56impl ScalarFnVTable for GetItem {
57    type Options = FieldName;
58
59    fn id(&self) -> ScalarFnId {
60        static ID: CachedId = CachedId::new("vortex.get_item");
61        *ID
62    }
63
64    fn serialize(&self, instance: &Self::Options) -> VortexResult<Option<Vec<u8>>> {
65        Ok(Some(
66            pb::GetItemOpts {
67                path: instance.to_string(),
68            }
69            .encode_to_vec(),
70        ))
71    }
72
73    fn deserialize(
74        &self,
75        _metadata: &[u8],
76        _session: &VortexSession,
77    ) -> VortexResult<Self::Options> {
78        let opts = pb::GetItemOpts::decode(_metadata)?;
79        Ok(FieldName::from(opts.path))
80    }
81
82    fn arity(&self, _field_name: &FieldName) -> Arity {
83        Arity::Exact(1)
84    }
85
86    fn child_name(&self, _instance: &Self::Options, child_idx: usize) -> ChildName {
87        match child_idx {
88            0 => ChildName::from("input"),
89            _ => unreachable!("Invalid child index {} for GetItem expression", child_idx),
90        }
91    }
92
93    fn fmt_sql(
94        &self,
95        field_name: &FieldName,
96        expr: &dyn ExprDisplay,
97        f: &mut Formatter<'_>,
98    ) -> std::fmt::Result {
99        Display::fmt(expr.display_child(0), f)?;
100        write!(f, ".{}", field_name)
101    }
102
103    fn return_dtype(&self, field_name: &FieldName, arg_dtypes: &[DType]) -> VortexResult<DType> {
104        let struct_dtype = &arg_dtypes[0];
105        let field_dtype = struct_dtype
106            .as_struct_fields_opt()
107            .and_then(|st| st.field(field_name))
108            .ok_or_else(|| {
109                vortex_err!("Couldn't find the {} field in the input scope", field_name)
110            })?;
111
112        // Match here to avoid cloning the dtype if nullability doesn't need to change
113        if matches!(
114            (struct_dtype.nullability(), field_dtype.nullability()),
115            (Nullability::Nullable, Nullability::NonNullable)
116        ) {
117            return Ok(field_dtype.with_nullability(Nullability::Nullable));
118        }
119
120        Ok(field_dtype)
121    }
122
123    fn execute(
124        &self,
125        field_name: &FieldName,
126        args: &dyn ExecutionArgs,
127        ctx: &mut ExecutionCtx,
128    ) -> VortexResult<ArrayRef> {
129        let input = args.get(0)?.execute::<StructArray>(ctx)?;
130        let field = input.unmasked_field_by_name(field_name).cloned()?;
131
132        match input.dtype().nullability() {
133            Nullability::NonNullable => Ok(field),
134            Nullability::Nullable => field.mask(input.validity()?.to_array(input.len())),
135        }
136    }
137
138    fn reduce<T: ReduceNode>(&self, field_name: &FieldName, node: &T) -> VortexResult<Option<T>> {
139        let child = node.child(0);
140        if let Some(child_fn) = child.scalar_fn()
141            && let Some(pack) = child_fn.as_opt::<Pack>()
142            && let Some(idx) = pack.names.find(field_name)
143        {
144            let mut field = child.child(idx);
145
146            // Possibly mask the field if the pack is nullable
147            if pack.nullability.is_nullable() {
148                field = node.new_node(
149                    Mask.bind(EmptyOptions),
150                    &[field, node.new_node(Literal.bind(true.into()), &[])?],
151                )?;
152            }
153
154            return Ok(Some(field));
155        }
156
157        Ok(None)
158    }
159
160    fn simplify_untyped(
161        &self,
162        field_name: &FieldName,
163        expr: &Expression,
164    ) -> VortexResult<Option<Expression>> {
165        let child = expr.child(0);
166
167        // If the child is a Pack expression, we can directly return the corresponding child.
168        if let Some(pack) = child.as_opt::<Pack>() {
169            let idx = pack
170                .names
171                .iter()
172                .position(|name| name == field_name)
173                .ok_or_else(|| {
174                    vortex_err!(
175                        "Cannot find field {} in pack fields {:?}",
176                        field_name,
177                        pack.names
178                    )
179                })?;
180
181            let mut field = child.child(idx).clone();
182
183            // It's useful to simplify this node without type info, but we need to make sure
184            // the nullability is correct. We cannot cast since we don't have the dtype info here,
185            // so instead we insert a Mask expression that we know converts a child's dtype to
186            // nullable.
187            if pack.nullability.is_nullable() {
188                // Mask with an all-true array to ensure the field DType is nullable.
189                field = field.mask(lit(true))?;
190            }
191
192            return Ok(Some(field));
193        }
194
195        Ok(None)
196    }
197
198    fn is_strict(&self, _field_name: &FieldName) -> bool {
199        true
200    }
201
202    fn is_infallible(&self, _field_name: &FieldName) -> bool {
203        // If this type-checks, it is infallible.
204        true
205    }
206}
207
208#[cfg(test)]
209mod tests {
210    use vortex_buffer::buffer;
211    use vortex_error::VortexResult;
212
213    use crate::IntoArray;
214    use crate::dtype::DType;
215    use crate::dtype::FieldNames;
216    use crate::dtype::Nullability;
217    use crate::dtype::Nullability::NonNullable;
218    use crate::dtype::PType;
219    use crate::dtype::StructFields;
220    use crate::expr::checked_add;
221    use crate::expr::get_item;
222    use crate::expr::lit;
223    use crate::expr::pack;
224    use crate::expr::root;
225    use crate::scalar_fn::fns::get_item::StructArray;
226    use crate::validity::Validity;
227
228    fn test_array() -> StructArray {
229        StructArray::from_fields(&[
230            ("a", buffer![0i32, 1, 2].into_array()),
231            ("b", buffer![4i64, 5, 6].into_array()),
232        ])
233        .unwrap()
234    }
235
236    #[test]
237    fn get_item_by_name() {
238        let st = test_array();
239        let get_item = get_item("a", root());
240        assert!(
241            get_item
242                .as_scalar()
243                .is_some_and(|f| f.signature().is_strict())
244        );
245        let item = st.into_array().apply(&get_item).unwrap();
246        assert_eq!(item.dtype(), &DType::from(PType::I32))
247    }
248
249    #[test]
250    fn get_item_by_name_none() {
251        let st = test_array();
252        let get_item = get_item("c", root());
253        assert!(st.into_array().apply(&get_item).is_err());
254    }
255
256    #[test]
257    fn get_nullable_field() {
258        let st = StructArray::try_new(
259            FieldNames::from(["a"]),
260            vec![buffer![1i32].into_array()],
261            1,
262            Validity::AllInvalid,
263        )
264        .unwrap()
265        .into_array();
266
267        let get_item_expr = get_item("a", root());
268        let item = st.apply(&get_item_expr).unwrap();
269        // The dtype should be nullable since it inherits struct validity
270        assert_eq!(
271            item.dtype(),
272            &DType::Primitive(PType::I32, Nullability::Nullable)
273        );
274    }
275
276    #[test]
277    fn get_non_nullable_field_from_all_valid_nullable_struct() -> VortexResult<()> {
278        let st = StructArray::try_new(
279            FieldNames::from(["a"]),
280            vec![buffer![1i32].into_array()],
281            1,
282            Validity::AllValid,
283        )?
284        .into_array();
285
286        let item = st.apply(&get_item("a", root()))?;
287        assert_eq!(
288            item.dtype(),
289            &DType::Primitive(PType::I32, Nullability::Nullable)
290        );
291        Ok(())
292    }
293
294    #[test]
295    fn test_pack_get_item_rule() {
296        // Create: pack(a: lit(1), b: lit(2)).get_item("b")
297        let pack_expr = pack([("a", lit(1)), ("b", lit(2))], NonNullable);
298        let get_item_expr = get_item("b", pack_expr);
299
300        let result = get_item_expr
301            .optimize_recursive(&DType::Struct(StructFields::empty(), NonNullable))
302            .unwrap();
303
304        assert_eq!(result, lit(2));
305    }
306
307    #[test]
308    fn test_multi_level_pack_get_item_simplify() {
309        let inner_pack = pack([("a", lit(1)), ("b", lit(2))], NonNullable);
310        let get_a = get_item("a", inner_pack);
311
312        let outer_pack = pack([("x", get_a), ("y", lit(3)), ("z", lit(4))], NonNullable);
313        let get_z = get_item("z", outer_pack);
314
315        let dtype = DType::Primitive(PType::I32, NonNullable);
316
317        let result = get_z.optimize_recursive(&dtype).unwrap();
318        assert_eq!(result, lit(4));
319    }
320
321    #[test]
322    fn test_deeply_nested_pack_get_item() {
323        let innermost = pack([("a", lit(42))], NonNullable);
324        let get_a = get_item("a", innermost);
325
326        let level2 = pack([("b", get_a)], NonNullable);
327        let get_b = get_item("b", level2);
328
329        let level3 = pack([("c", get_b)], NonNullable);
330        let get_c = get_item("c", level3);
331
332        let outermost = pack([("final", get_c)], NonNullable);
333        let get_final = get_item("final", outermost);
334
335        let dtype = DType::Primitive(PType::I32, NonNullable);
336
337        let result = get_final.optimize_recursive(&dtype).unwrap();
338        assert_eq!(result, lit(42));
339    }
340
341    #[test]
342    fn test_partial_pack_get_item_simplify() {
343        let inner_pack = pack([("x", lit(1)), ("y", lit(2))], NonNullable);
344        let get_x = get_item("x", inner_pack);
345        let add_expr = checked_add(get_x, lit(10));
346
347        let outer_pack = pack([("result", add_expr)], NonNullable);
348        let get_result = get_item("result", outer_pack);
349
350        let dtype = DType::Primitive(PType::I32, NonNullable);
351
352        let result = get_result.optimize_recursive(&dtype).unwrap();
353        let expected = checked_add(lit(1), lit(10));
354        assert_eq!(&result, &expected);
355    }
356
357    #[test]
358    fn get_item_filter_list_field() {
359        use vortex_mask::Mask;
360
361        use crate::arrays::BoolArray;
362        use crate::arrays::FilterArray;
363        use crate::arrays::ListArray;
364
365        let list = ListArray::try_new(
366            buffer![0f32, 1., 2., 3., 4., 5., 6., 7., 8., 9., 10., 11.].into_array(),
367            buffer![2u64, 4, 6, 8, 10, 12].into_array(),
368            Validity::Array(BoolArray::from_iter([true, true, false, true, true]).into_array()),
369        )
370        .unwrap();
371
372        let filtered = FilterArray::try_new(
373            list.into_array(),
374            Mask::from_iter([true, true, false, false, false]),
375        )
376        .unwrap();
377
378        let st = StructArray::try_new(
379            FieldNames::from(["data"]),
380            vec![filtered.into_array()],
381            2,
382            Validity::AllValid,
383        )
384        .unwrap();
385
386        st.into_array().apply(&get_item("data", root())).unwrap();
387    }
388}