Skip to main content

vortex_array/scalar_fn/fns/
list_length.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4use num_traits::AsPrimitive;
5use vortex_error::VortexResult;
6use vortex_error::vortex_bail;
7use vortex_session::VortexSession;
8use vortex_session::registry::CachedId;
9
10use crate::ArrayRef;
11use crate::ExecutionCtx;
12use crate::IntoArray;
13use crate::array::ArrayView;
14use crate::arrays::ConstantArray;
15use crate::arrays::FixedSizeList;
16use crate::arrays::List;
17use crate::arrays::ListView;
18use crate::arrays::fixed_size_list::FixedSizeListArrayExt;
19use crate::arrays::list::ListArrayExt;
20use crate::arrays::list::ListArraySlotsExt;
21use crate::arrays::listview::ListViewArrayExt;
22use crate::arrays::listview::ListViewArraySlotsExt;
23use crate::builtins::ArrayBuiltins;
24use crate::dtype::DType;
25use crate::dtype::Nullability;
26use crate::dtype::PType;
27use crate::matcher::Matcher;
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::ScalarFnId;
34use crate::scalar_fn::ScalarFnVTable;
35use crate::scalar_fn::fns::operators::Operator;
36
37/// Number of elements in each list of a `List` or `FixedSizeList` typed array.
38///
39/// This is computed purely from the list's offsets (`ListArray`), sizes (`ListViewArray`), or
40/// dtype (`FixedSizeListArray`) without reading the element *values*. Validity is carried over
41/// from the original array.
42#[derive(Clone)]
43pub struct ListLength;
44
45impl ScalarFnVTable for ListLength {
46    type Options = EmptyOptions;
47
48    fn id(&self) -> ScalarFnId {
49        static ID: CachedId = CachedId::new("vortex.list.length");
50        *ID
51    }
52
53    fn serialize(&self, _instance: &Self::Options) -> VortexResult<Option<Vec<u8>>> {
54        Ok(Some(vec![]))
55    }
56
57    fn deserialize(
58        &self,
59        _metadata: &[u8],
60        _session: &VortexSession,
61    ) -> VortexResult<Self::Options> {
62        Ok(EmptyOptions)
63    }
64
65    fn arity(&self, _options: &Self::Options) -> Arity {
66        Arity::Exact(1)
67    }
68
69    fn child_name(&self, _instance: &Self::Options, child_idx: usize) -> ChildName {
70        match child_idx {
71            0 => ChildName::from("input"),
72            _ => unreachable!("Invalid child index {child_idx} for list_length()"),
73        }
74    }
75
76    fn return_dtype(&self, _options: &Self::Options, arg_dtypes: &[DType]) -> VortexResult<DType> {
77        match &arg_dtypes[0] {
78            DType::List(_, nullable) | DType::FixedSizeList(_, _, nullable) => {
79                Ok(DType::Primitive(PType::U64, *nullable))
80            }
81            other => vortex_bail!("list_length() requires List or FixedSizeList, got {other}"),
82        }
83    }
84
85    fn execute(
86        &self,
87        _options: &Self::Options,
88        args: &dyn ExecutionArgs,
89        ctx: &mut ExecutionCtx,
90    ) -> VortexResult<ArrayRef> {
91        let input = args.get(0)?;
92        let nullability = input.dtype().nullability();
93
94        if let Some(scalar) = input.as_constant() {
95            let len_scalar = scalar_list_length(&scalar, nullability)?;
96            return Ok(ConstantArray::new(len_scalar, args.row_count()).into_array());
97        }
98
99        list_length(&input, nullability, ctx)
100    }
101
102    fn is_strict(&self, _options: &Self::Options) -> bool {
103        // A null list has a null length, and the length of a valid list is a non-null value
104        // determined by that list alone, with `return_dtype` carrying over the input nullability.
105        true
106    }
107
108    fn is_infallible(&self, _options: &Self::Options) -> bool {
109        true
110    }
111}
112
113fn scalar_list_length(scalar: &Scalar, nullability: Nullability) -> VortexResult<Scalar> {
114    if scalar.is_null() {
115        let dtype = DType::Primitive(PType::U64, Nullability::Nullable);
116        return Ok(Scalar::null(dtype));
117    }
118    let len: u64 = scalar.as_list().len().as_();
119    Ok(Scalar::primitive(len, nullability))
120}
121
122pub(crate) fn list_length(
123    array: &ArrayRef,
124    nullability: Nullability,
125    ctx: &mut ExecutionCtx,
126) -> VortexResult<ArrayRef> {
127    let any_list = array.clone().execute_until::<AnyList>(ctx)?;
128
129    let (lengths, validity) = if let Some(fsl) = any_list.as_opt::<FixedSizeList>() {
130        // The length of fixed-size list is constant, so just need to carry over validity
131        let size = fsl.list_size() as u64;
132        let lengths =
133            ConstantArray::new(Scalar::primitive(size, Nullability::NonNullable), fsl.len())
134                .into_array();
135        (lengths, fsl.validity()?)
136    } else if let Some(lv) = any_list.as_opt::<ListView>() {
137        // Length array is exactly the sizes child
138        (lv.sizes().clone(), lv.listview_validity())
139    } else if let Some(l) = any_list.as_opt::<List>() {
140        let lengths = list_length_from_offsets(l)?;
141        (lengths, l.list_validity())
142    } else {
143        let dtype = any_list.dtype();
144        vortex_bail!("list_length() requires List, ListView, or FixedSizeList but got {dtype}")
145    };
146
147    // Cast to `U64`
148    let len = lengths.len();
149    let lengths = lengths.cast(DType::Primitive(PType::U64, nullability))?;
150
151    // Carry over validity mask for nullable arrays
152    if matches!(nullability, Nullability::Nullable) {
153        lengths.mask(validity.to_array(len))
154    } else {
155        Ok(lengths)
156    }
157}
158
159/// Calculate the lengths of `ListArray` elements via the `offsets` child:
160/// `length[i] = offsets[i + 1] - offsets[i]`.
161fn list_length_from_offsets(list: ArrayView<'_, List>) -> VortexResult<ArrayRef> {
162    let offsets = list.offsets();
163    let n = offsets.len().saturating_sub(1);
164
165    offsets
166        .slice(1..offsets.len())?
167        .binary(offsets.slice(0..n)?, Operator::Sub)
168}
169
170/// Matches an `Array<List>`, `Array<ListView>`, or `Array<FixedSizeList>`
171struct AnyList;
172
173impl Matcher for AnyList {
174    type Match<'a> = ();
175
176    fn try_match(array: &ArrayRef) -> Option<Self::Match<'_>> {
177        (array.as_opt::<List>().is_some()
178            || array.as_opt::<ListView>().is_some()
179            || array.as_opt::<FixedSizeList>().is_some())
180        .then_some(())
181    }
182}
183
184#[cfg(test)]
185mod tests {
186    use std::sync::Arc;
187
188    use rstest::rstest;
189    use vortex_buffer::buffer;
190    use vortex_error::VortexResult;
191
192    use crate::ArrayRef;
193    use crate::IntoArray;
194    use crate::VortexSessionExecute;
195    use crate::array_session;
196    use crate::arrays::BoolArray;
197    use crate::arrays::ConstantArray;
198    use crate::arrays::FixedSizeListArray;
199    use crate::arrays::ListArray;
200    use crate::arrays::ListViewArray;
201    use crate::arrays::PrimitiveArray;
202    use crate::assert_arrays_eq;
203    use crate::dtype::DType;
204    use crate::dtype::Nullability;
205    use crate::dtype::PType;
206    use crate::expr::cast;
207    use crate::expr::list_length;
208    use crate::expr::root;
209    use crate::scalar::Scalar;
210    use crate::validity::Validity;
211
212    fn create_list_elements() -> ArrayRef {
213        PrimitiveArray::from_option_iter::<i32, _>([
214            Some(1),
215            Some(2),
216            Some(3),
217            Some(4),
218            Some(5),
219            Some(6),
220            None,
221        ])
222        .into_array()
223    }
224
225    #[rstest]
226    #[case(buffer![0u32, 2, 5, 5, 7].into_array())]
227    #[case(buffer![0u64, 2, 5, 5, 7].into_array())]
228    fn test_list_length(#[case] offsets: ArrayRef) -> VortexResult<()> {
229        let elements = create_list_elements();
230        let list = ListArray::try_new(elements, offsets, Validity::NonNullable)?.into_array();
231        let result = list.apply(&list_length(root()))?;
232        let mut ctx = array_session().create_execution_ctx();
233        assert_arrays_eq!(result, PrimitiveArray::from_iter([2u64, 3, 0, 2]), &mut ctx);
234        Ok(())
235    }
236
237    #[rstest]
238    #[case(buffer![0u32, 2, 5, 5, 7].into_array())]
239    #[case(buffer![0u64, 2, 5, 5, 7].into_array())]
240    fn test_nullable_list_length(#[case] offsets: ArrayRef) -> VortexResult<()> {
241        let elements = create_list_elements();
242        let list = ListArray::try_new(
243            elements,
244            offsets,
245            Validity::Array(BoolArray::from_iter([true, false, true, false]).into_array()),
246        )?
247        .into_array();
248        let result = list.apply(&list_length(root()))?;
249
250        let mut ctx = array_session().create_execution_ctx();
251        let result = result.execute::<PrimitiveArray>(&mut ctx)?;
252
253        let expected = PrimitiveArray::from_option_iter::<u64, _>([Some(2), None, Some(0), None]);
254
255        assert_arrays_eq!(result, expected, &mut ctx);
256
257        Ok(())
258    }
259
260    #[test]
261    fn test_null_scalar_list_length() -> VortexResult<()> {
262        let null_scalar = Scalar::null(DType::List(
263            Arc::new(DType::Primitive(PType::I32, Nullability::NonNullable)),
264            Nullability::Nullable,
265        ));
266        let array = ConstantArray::new(null_scalar, 2).into_array();
267        let result = array.apply(&list_length(root()))?;
268
269        let mut ctx = array_session().create_execution_ctx();
270        assert!(!result.is_valid(0, &mut ctx)?);
271        assert!(!result.is_valid(1, &mut ctx)?);
272        Ok(())
273    }
274
275    #[test]
276    fn test_listview_length() -> VortexResult<()> {
277        let elements = create_list_elements();
278        let lv = ListViewArray::new(
279            elements,
280            buffer![5u32, 0, 4, 1].into_array(),
281            buffer![2u32, 3, 0, 2].into_array(),
282            Validity::NonNullable,
283        )
284        .into_array();
285        let result = lv.apply(&list_length(root()))?;
286        let mut ctx = array_session().create_execution_ctx();
287        assert_arrays_eq!(result, PrimitiveArray::from_iter([2u64, 3, 0, 2]), &mut ctx);
288        Ok(())
289    }
290
291    #[test]
292    fn test_listview_length_nullable() -> VortexResult<()> {
293        let elements = create_list_elements();
294        let lv = ListViewArray::new(
295            elements,
296            buffer![5u32, 0, 4, 1].into_array(),
297            buffer![2u32, 3, 0, 2].into_array(),
298            Validity::Array(BoolArray::from_iter([true, false, true, false]).into_array()),
299        )
300        .into_array();
301        let result = lv.apply(&list_length(root()))?;
302
303        let mut ctx = array_session().create_execution_ctx();
304        let result = result.execute::<PrimitiveArray>(&mut ctx)?;
305
306        let expected = PrimitiveArray::from_option_iter::<u64, _>([Some(2), None, Some(0), None]);
307        assert_arrays_eq!(result, expected, &mut ctx);
308        Ok(())
309    }
310
311    #[test]
312    fn test_list_length_take() -> VortexResult<()> {
313        let elements = create_list_elements();
314        let list = ListArray::try_new(
315            elements,
316            buffer![0u32, 2, 5, 5, 7].into_array(),
317            Validity::NonNullable,
318        )?
319        .into_array();
320        let taken = list.take(buffer![3u64, 0, 2].into_array())?;
321
322        let result = taken.apply(&list_length(root()))?;
323        let mut ctx = array_session().create_execution_ctx();
324        assert_arrays_eq!(result, PrimitiveArray::from_iter([2u64, 2, 0]), &mut ctx);
325        Ok(())
326    }
327
328    fn create_fixed_size_list(validity: Validity) -> ArrayRef {
329        // 4 lists of size 2 over 8 primitive elements.
330        let elements = PrimitiveArray::from_iter([1i32, 2, 3, 4, 5, 6, 7, 8]).into_array();
331        FixedSizeListArray::new(elements, 2, validity, 4).into_array()
332    }
333
334    #[test]
335    fn test_fixed_size_list_length() -> VortexResult<()> {
336        let fsl = create_fixed_size_list(Validity::NonNullable);
337        let result = fsl.apply(&list_length(root()))?;
338
339        let mut ctx = array_session().create_execution_ctx();
340        assert_arrays_eq!(result, PrimitiveArray::from_iter([2u64, 2, 2, 2]), &mut ctx);
341        Ok(())
342    }
343
344    #[test]
345    fn test_fixed_size_list_length_nullable() -> VortexResult<()> {
346        let fsl = create_fixed_size_list(Validity::Array(
347            BoolArray::from_iter([true, false, true, false]).into_array(),
348        ));
349        let result = fsl.apply(&list_length(root()))?;
350
351        let mut ctx = array_session().create_execution_ctx();
352        let result = result.execute::<PrimitiveArray>(&mut ctx)?;
353
354        let expected = PrimitiveArray::from_option_iter::<u64, _>([Some(2), None, Some(2), None]);
355        assert_arrays_eq!(result, expected, &mut ctx);
356        Ok(())
357    }
358
359    #[test]
360    fn test_fallible_child_expression_fails() -> VortexResult<()> {
361        let fsl = create_fixed_size_list(Validity::Array(
362            BoolArray::from_iter([true, false, true, false]).into_array(),
363        ));
364        let failing_cast_dtype = DType::FixedSizeList(
365            Arc::new(DType::Primitive(PType::I32, Nullability::NonNullable)),
366            2,
367            Nullability::NonNullable,
368        );
369
370        let lengths = fsl.apply(&list_length(cast(root(), failing_cast_dtype)))?;
371
372        let mut ctx = array_session().create_execution_ctx();
373        let result = lengths.execute::<ArrayRef>(&mut ctx);
374
375        assert!(result.is_err());
376
377        let err_message = result.unwrap_err().to_string();
378
379        assert!(
380            err_message.contains("Cannot cast array with invalid values to non-nullable type.")
381        );
382
383        Ok(())
384    }
385
386    #[test]
387    fn test_display() {
388        let expr = list_length(root());
389        assert_eq!(expr.to_string(), "vortex.list.length($)");
390    }
391}