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