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