Skip to main content

vortex_array/scalar/
scalar_impl.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4//! Core [`Scalar`] type definition.
5
6use std::cmp::Ordering;
7use std::hash::Hash;
8use std::hash::Hasher;
9
10use vortex_error::VortexResult;
11use vortex_error::vortex_ensure_eq;
12use vortex_error::vortex_panic;
13
14use crate::dtype::DType;
15use crate::dtype::NativeDType;
16use crate::dtype::PType;
17use crate::dtype::StructFields;
18use crate::scalar::Scalar;
19use crate::scalar::ScalarValue;
20
21impl Scalar {
22    // Constructors for null scalars.
23
24    /// Creates a new null [`Scalar`] with the given [`DType`].
25    ///
26    /// # Panics
27    ///
28    /// Panics if the given [`DType`] is non-nullable.
29    pub fn null(dtype: DType) -> Self {
30        assert!(
31            dtype.is_nullable(),
32            "Cannot create null scalar with non-nullable dtype {dtype}"
33        );
34
35        Self { dtype, value: None }
36    }
37
38    // TODO(connor): This method arguably shouldn't exist...
39    /// Creates a new null [`Scalar`] for the given scalar type.
40    ///
41    /// The resulting scalar will have a nullable version of the type's data type.
42    pub fn null_native<T: NativeDType>() -> Self {
43        Self {
44            dtype: T::dtype().as_nullable(),
45            value: None,
46        }
47    }
48
49    // Constructors for potentially null scalars.
50
51    /// Creates a new [`Scalar`] with the given [`DType`] and potentially null [`ScalarValue`].
52    ///
53    /// This is just a helper function for tests.
54    ///
55    /// # Panics
56    ///
57    /// Panics if the given [`DType`] and [`ScalarValue`] are incompatible.
58    #[cfg(test)]
59    pub fn new(dtype: DType, value: Option<ScalarValue>) -> Self {
60        use vortex_error::VortexExpect;
61
62        Self::try_new(dtype, value).vortex_expect("Failed to create Scalar")
63    }
64
65    /// Attempts to create a new [`Scalar`] with the given [`DType`] and potentially null
66    /// [`ScalarValue`].
67    ///
68    /// # Errors
69    ///
70    /// Returns an error if the given [`DType`] and [`ScalarValue`] are incompatible.
71    pub fn try_new(dtype: DType, value: Option<ScalarValue>) -> VortexResult<Self> {
72        Self::validate(&dtype, value.as_ref())?;
73
74        Ok(Self { dtype, value })
75    }
76
77    /// Creates a new [`Scalar`] with the given [`DType`] and potentially null [`ScalarValue`]
78    /// without checking compatibility.
79    ///
80    /// # Safety
81    ///
82    /// The caller must ensure that the given [`DType`] and [`ScalarValue`] are compatible per the
83    /// rules defined in [`Self::validate`].
84    pub unsafe fn new_unchecked(dtype: DType, value: Option<ScalarValue>) -> Self {
85        #[cfg(debug_assertions)]
86        {
87            use vortex_error::VortexExpect;
88
89            Self::validate(&dtype, value.as_ref())
90                .vortex_expect("Scalar::new_unchecked called with incompatible dtype and value");
91        }
92
93        Self { dtype, value }
94    }
95
96    /// Returns a default value for the given [`DType`].
97    ///
98    /// For nullable types, this returns a null scalar. For non-nullable and non-nested types, this
99    /// returns the zero value for the type.
100    ///
101    /// See [`Scalar::zero_value`] for more details about "zero" values.
102    ///
103    /// For non-nullable nested types, this function recursively creates valid default children.
104    /// For a union specifically:
105    ///
106    /// - A nullable union defaults to an **outer null**. It has no selected variant or type ID.
107    /// - A non-nullable union selects its first variant. That selected child may itself default to
108    ///   an **inner null** when its dtype is nullable; the enclosing union remains non-null and
109    ///   retains the first variant's type ID.
110    ///
111    /// # Panics
112    ///
113    /// Panics if `dtype` has no default value.
114    pub fn default_value(dtype: &DType) -> Self {
115        Self::try_default_value(dtype)
116            .unwrap_or_else(|| vortex_panic!("{dtype} has no default value"))
117    }
118
119    /// Returns a valid default scalar, or [`None`] if `dtype` has no default.
120    pub(crate) fn try_default_value(dtype: &DType) -> Option<Self> {
121        let value = ScalarValue::try_default_value(dtype)?;
122        Self::try_new(dtype.clone(), value).ok()
123    }
124
125    /// Returns a non-null zero / identity value for the given [`DType`].
126    ///
127    /// # Zero Values
128    ///
129    /// Here is the list of non-null zero values for each [`DType`], regardless of its nullability:
130    ///
131    /// - `Null`: Does not have a "zero" value
132    /// - `Bool`: `false`
133    /// - `Primitive`: `0`
134    /// - `Decimal`: `0`
135    /// - `Utf8`: `""`
136    /// - `Binary`: An empty buffer
137    /// - `List`: An empty list
138    /// - `Map`: An empty map
139    /// - `FixedSizeList`: A list (with correct size) of zero values, which is determined by the
140    ///   element [`DType`]
141    /// - `Struct`: A struct where each field has a zero value, which is determined by the field
142    ///   [`DType`]
143    /// - `Union`: A non-null union selecting the first variant with a non-null child zero value
144    /// - `Extension`: The zero value of the storage [`DType`]
145    /// # Panics
146    ///
147    /// Panics if the dtype has no non-null zero value, such as `Null`, or if a nested dtype needed
148    /// to construct the zero value has no non-null zero value.
149    ///
150    /// Unlike [`Scalar::default_value`], this never uses either an outer or inner null for a union:
151    /// even a nullable union's zero value is non-null and contains a non-null selected child.
152    pub fn zero_value(dtype: &DType) -> Self {
153        let value = ScalarValue::zero_value(dtype);
154
155        // SAFETY: `zero_value` creates a valid `ScalarValue` for the `DType`.
156        unsafe { Self::new_unchecked(dtype.clone(), Some(value)) }
157    }
158
159    // Other methods.
160
161    /// Check if two scalars are equal, ignoring nullability of the [`DType`].
162    pub fn eq_ignore_nullability(&self, other: &Self) -> bool {
163        self.dtype.eq_ignore_nullability(&other.dtype) && self.value == other.value
164    }
165
166    /// Returns the parts of the [`Scalar`].
167    pub fn into_parts(self) -> (DType, Option<ScalarValue>) {
168        (self.dtype, self.value)
169    }
170
171    /// Returns the [`DType`] of the [`Scalar`].
172    pub fn dtype(&self) -> &DType {
173        &self.dtype
174    }
175
176    /// Returns an optional [`ScalarValue`] of the [`Scalar`], where `None` means the value is null.
177    pub fn value(&self) -> Option<&ScalarValue> {
178        self.value.as_ref()
179    }
180
181    /// Returns the internal optional [`ScalarValue`], where `None` means the value is null,
182    /// consuming the [`Scalar`].
183    pub fn into_value(self) -> Option<ScalarValue> {
184        self.value
185    }
186
187    /// Returns `true` if the [`Scalar`] has a non-null value.
188    pub fn is_valid(&self) -> bool {
189        self.value.is_some()
190    }
191
192    /// Returns `true` if the [`Scalar`] is null.
193    pub fn is_null(&self) -> bool {
194        self.value.is_none()
195    }
196
197    /// Returns `true` if the [`Scalar`] has a non-null zero value.
198    ///
199    /// Returns `None` if the scalar is null or its zero-ness is undefined. Otherwise, returns
200    /// `Some(true)` if the value is zero and `Some(false)` if it is not.
201    ///
202    /// A union that selects a variant other than the first returns `Some(false)`. A union selecting
203    /// its first variant delegates to that child's [`Scalar::is_zero`], so an inner null child
204    /// returns `None` and a non-null zero child returns `Some(true)`.
205    pub fn is_zero(&self) -> Option<bool> {
206        let value = self.value()?;
207
208        let is_zero = match self.dtype() {
209            DType::Null => vortex_panic!("non-null value somehow had `DType::Null`"),
210            DType::Bool(_) => !value.as_bool(),
211            DType::Primitive(..) => value.as_primitive().is_zero(),
212            DType::Decimal(..) => value.as_decimal().is_zero(),
213            DType::Utf8(_) => value.as_utf8().is_empty(),
214            DType::Binary(_) => value.as_binary().is_empty(),
215            DType::List(..) => value.as_list().is_empty(),
216            DType::Map(..) => self.as_map().is_empty(),
217            // A fixed-size list is zero only if it has the expected number of elements and every
218            // element is itself a non-null zero value.1
219            DType::FixedSizeList(_, list_size, _) => {
220                let list = self.as_list();
221                list.len() == *list_size as usize
222                    && (0..list.len())
223                        .all(|i| list.element(i).is_some_and(|e| e.is_zero() == Some(true)))
224            }
225            // A struct is zero only if every one of its fields is itself a non-null zero value.
226            DType::Struct(..) => self
227                .as_struct()
228                .fields_iter()
229                .is_some_and(|mut fields| fields.all(|f| f.is_zero() == Some(true))),
230            // Only the first variant of unions can be zero. Its child determines whether the
231            // zero-ness is true, false, or undefined.
232            DType::Union(..) => {
233                let union = self.as_union();
234                if union.child_index() != Some(0) {
235                    false
236                } else {
237                    union.child().and_then(|child| child.is_zero())?
238                }
239            }
240            DType::Variant(_) => self.as_variant().is_zero()?,
241            DType::Extension(_) => self.as_extension().to_storage_scalar().is_zero()?,
242        };
243
244        Some(is_zero)
245    }
246
247    /// Reinterprets the bytes of this scalar as a different primitive type.
248    ///
249    /// # Errors
250    ///
251    /// Panics if the scalar is not a primitive type or if the types have different byte widths.
252    pub fn primitive_reinterpret_cast(&self, ptype: PType) -> VortexResult<Self> {
253        let primitive = self.as_primitive();
254        if primitive.ptype() == ptype {
255            return Ok(self.clone());
256        }
257
258        vortex_ensure_eq!(
259            primitive.ptype().byte_width(),
260            ptype.byte_width(),
261            "can't reinterpret cast between integers of two different widths"
262        );
263
264        Scalar::try_new(
265            DType::Primitive(ptype, self.dtype().nullability()),
266            primitive
267                .pvalue()
268                .map(|p| p.reinterpret_cast(ptype))
269                .map(ScalarValue::Primitive),
270        )
271    }
272
273    /// Returns an **ESTIMATE** of the size of the scalar in bytes, uncompressed.
274    ///
275    /// Note that the protobuf serialization of scalars will likely have a different (but roughly
276    /// similar) length.
277    pub fn approx_nbytes(&self) -> usize {
278        use crate::dtype::NativeDecimalType;
279        use crate::dtype::i256;
280
281        match self.dtype() {
282            DType::Null => 0,
283            DType::Bool(_) => 1,
284            DType::Primitive(ptype, _) => ptype.byte_width(),
285            DType::Decimal(dt, _) => {
286                if dt.precision() <= i128::MAX_PRECISION {
287                    size_of::<i128>()
288                } else {
289                    size_of::<i256>()
290                }
291            }
292            DType::Utf8(_) => self
293                .value()
294                .map_or_else(|| 0, |value| value.as_utf8().len()),
295            DType::Binary(_) => self
296                .value()
297                .map_or_else(|| 0, |value| value.as_binary().len()),
298            DType::List(..) | DType::FixedSizeList(..) => self
299                .as_list()
300                .elements()
301                .map(|fields| fields.into_iter().map(|f| f.approx_nbytes()).sum::<usize>())
302                .unwrap_or_default(),
303            DType::Map(..) => self
304                .as_map()
305                .entries()
306                .map(|(key, value)| key.approx_nbytes() + value.approx_nbytes())
307                .sum(),
308            DType::Struct(..) => self
309                .as_struct()
310                .fields_iter()
311                .map(|fields| fields.into_iter().map(|f| f.approx_nbytes()).sum::<usize>())
312                .unwrap_or_default(),
313            DType::Union(..) => self
314                .as_union()
315                .child()
316                .map_or(0, |value| 1 + value.approx_nbytes()),
317            DType::Variant(_) => self.as_variant().value().map_or(0, Scalar::approx_nbytes),
318            DType::Extension(_) => self.as_extension().to_storage_scalar().approx_nbytes(),
319        }
320    }
321}
322
323/// We implement `Hash` manually to be consistent with `PartialEq`. Since we ignore nullability in
324/// equality comparisons, we must also ignore it when hashing to maintain the invariant that equal
325/// values have equal hashes.
326impl Hash for Scalar {
327    fn hash<H: Hasher>(&self, state: &mut H) {
328        self.dtype.hash_ignore_nullability(state);
329        self.value.hash(state);
330    }
331}
332
333/// We implement `PartialEq` manually because we want to ignore nullability when comparing scalars.
334/// Two scalars with the same value but different nullability should be considered equal.
335///
336/// Note that this has **different** behavior than the [`PartialOrd`] implementation since the
337/// [`PartialOrd`] returns `None` if the types are different, whereas this `PartialEq`
338/// implementation simply returns `false`.
339impl PartialEq for Scalar {
340    fn eq(&self, other: &Self) -> bool {
341        self.dtype.eq_ignore_nullability(&other.dtype) && self.value == other.value
342    }
343}
344
345impl PartialOrd for Scalar {
346    /// Compares two scalar values for ordering.
347    ///
348    /// # Returns
349    /// - `Some(Ordering)` if both scalars have the same data type (ignoring nullability)
350    /// - `None` if the scalars have different data types
351    ///
352    /// # Ordering Rules
353    /// When types match, the ordering follows these rules:
354    /// - Null values are considered less than all non-null values
355    /// - Non-null values are compared according to their natural ordering
356    ///
357    /// # Examples
358    ///
359    /// ```
360    /// use std::cmp::Ordering;
361    /// use vortex_array::dtype::DType;
362    /// use vortex_array::dtype::Nullability;
363    /// use vortex_array::dtype::PType;
364    /// use vortex_array::scalar::Scalar;
365    ///
366    /// // Same types compare successfully
367    /// let a = Scalar::primitive(10i32, Nullability::NonNullable);
368    /// let b = Scalar::primitive(20i32, Nullability::NonNullable);
369    /// assert_eq!(a.partial_cmp(&b), Some(Ordering::Less));
370    ///
371    /// // Different types return None
372    /// let int_scalar = Scalar::primitive(10i32, Nullability::NonNullable);
373    /// let str_scalar = Scalar::utf8("hello", Nullability::NonNullable);
374    /// assert_eq!(int_scalar.partial_cmp(&str_scalar), None);
375    ///
376    /// // Nulls are less than non-nulls
377    /// let null = Scalar::null(DType::Primitive(PType::I32, Nullability::Nullable));
378    /// let value = Scalar::primitive(0i32, Nullability::Nullable);
379    /// assert_eq!(null.partial_cmp(&value), Some(Ordering::Less));
380    /// ```
381    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
382        if !self.dtype().eq_ignore_nullability(other.dtype()) {
383            return None;
384        }
385
386        partial_cmp_scalar_values(self.dtype(), self.value(), other.value())
387    }
388}
389
390/// Compare two optional scalar values using `dtype` for nested tuple interpretation.
391fn partial_cmp_scalar_values(
392    dtype: &DType,
393    lhs: Option<&ScalarValue>,
394    rhs: Option<&ScalarValue>,
395) -> Option<Ordering> {
396    match (lhs, rhs) {
397        (None, None) => Some(Ordering::Equal),
398        (None, Some(_)) => Some(Ordering::Less),
399        (Some(_), None) => Some(Ordering::Greater),
400        (Some(lhs), Some(rhs)) => partial_cmp_non_null_scalar_values(dtype, lhs, rhs),
401    }
402}
403
404/// Compare two non-null scalar values, consulting `dtype` only for tuple-backed values.
405fn partial_cmp_non_null_scalar_values(
406    dtype: &DType,
407    lhs: &ScalarValue,
408    rhs: &ScalarValue,
409) -> Option<Ordering> {
410    // `Scalar::validate` guarantees that a scalar's value matches its dtype. Most of the scalar
411    // value variants have only 1 method of comparison, regardless of the dtype.
412    match (lhs, rhs) {
413        (ScalarValue::Bool(lhs), ScalarValue::Bool(rhs)) => lhs.partial_cmp(rhs),
414        (ScalarValue::Primitive(lhs), ScalarValue::Primitive(rhs)) => lhs.partial_cmp(rhs),
415        (ScalarValue::Decimal(lhs), ScalarValue::Decimal(rhs)) => lhs.partial_cmp(rhs),
416        (ScalarValue::Utf8(lhs), ScalarValue::Utf8(rhs)) => lhs.partial_cmp(rhs),
417        (ScalarValue::Binary(lhs), ScalarValue::Binary(rhs)) => lhs.partial_cmp(rhs),
418        // `Tuple` is the exception here. Since it backs lists, fixed-size lists, and structs, we
419        // need the dtype to know whether children share one element dtype or use per-field dtypes.
420        (ScalarValue::Tuple(lhs), ScalarValue::Tuple(rhs)) => {
421            partial_cmp_tuple_values(dtype, lhs, rhs)
422        }
423        (ScalarValue::Union(lhs), ScalarValue::Union(rhs)) => {
424            if lhs.type_id() != rhs.type_id() {
425                return None;
426            }
427
428            let DType::Union(variants, _) = dtype else {
429                return None;
430            };
431            let child_index = variants.tag_to_child_index(lhs.type_id())?;
432            let child_dtype = variants.variant_by_index(child_index)?;
433
434            partial_cmp_scalar_values(&child_dtype, lhs.child_value(), rhs.child_value())
435        }
436        // Variant values can have a different dtype in each row, so it doesn't make sense to
437        // compare them.
438        (ScalarValue::Variant(_), ScalarValue::Variant(_)) => None,
439        _ => None,
440    }
441}
442
443/// Compare tuple values according to the list, fixed-size list, or struct dtype layout.
444fn partial_cmp_tuple_values(
445    dtype: &DType,
446    lhs: &[Option<ScalarValue>],
447    rhs: &[Option<ScalarValue>],
448) -> Option<Ordering> {
449    match dtype {
450        DType::List(element_dtype, _) | DType::FixedSizeList(element_dtype, ..) => {
451            partial_cmp_list_values(element_dtype, lhs, rhs)
452        }
453        DType::Struct(fields, _) => partial_cmp_struct_values(fields, lhs, rhs),
454        DType::Map(..) => None,
455        DType::Extension(ext_dtype) => {
456            partial_cmp_tuple_values(ext_dtype.storage_dtype(), lhs, rhs)
457        }
458        _ => None,
459    }
460}
461
462/// Compare list tuple values using the shared element dtype for each element.
463fn partial_cmp_list_values(
464    element_dtype: &DType,
465    lhs: &[Option<ScalarValue>],
466    rhs: &[Option<ScalarValue>],
467) -> Option<Ordering> {
468    for (lhs, rhs) in lhs.iter().zip(rhs.iter()) {
469        match partial_cmp_scalar_values(element_dtype, lhs.as_ref(), rhs.as_ref())? {
470            Ordering::Equal => continue,
471            ordering => return Some(ordering),
472        }
473    }
474
475    Some(lhs.len().cmp(&rhs.len()))
476}
477
478/// Compare struct tuple values using each field's dtype in field order.
479fn partial_cmp_struct_values(
480    fields: &StructFields,
481    lhs: &[Option<ScalarValue>],
482    rhs: &[Option<ScalarValue>],
483) -> Option<Ordering> {
484    if lhs.len() != fields.nfields() || rhs.len() != fields.nfields() {
485        return None;
486    }
487
488    for ((field_dtype, lhs), rhs) in fields.fields().zip(lhs.iter()).zip(rhs.iter()) {
489        match partial_cmp_scalar_values(&field_dtype, lhs.as_ref(), rhs.as_ref())? {
490            Ordering::Equal => continue,
491            ordering => return Some(ordering),
492        }
493    }
494
495    Some(Ordering::Equal)
496}
497
498#[cfg(test)]
499mod tests {
500    use std::sync::Arc;
501
502    use rstest::rstest;
503
504    use crate::dtype::DType;
505    use crate::dtype::Nullability;
506    use crate::dtype::PType;
507    use crate::dtype::StructFields;
508    use crate::scalar::Scalar;
509
510    fn i32_scalar(value: i32) -> Scalar {
511        Scalar::primitive::<i32>(value, Nullability::NonNullable)
512    }
513
514    fn nullable_i32(value: Option<i32>) -> Scalar {
515        match value {
516            Some(value) => Scalar::primitive::<i32>(value, Nullability::Nullable),
517            None => Scalar::null(DType::Primitive(PType::I32, Nullability::Nullable)),
518        }
519    }
520
521    fn ab_struct_dtype(nullability: Nullability) -> DType {
522        DType::Struct(
523            StructFields::new(
524                ["a", "b"].into(),
525                vec![
526                    DType::Primitive(PType::I32, Nullability::NonNullable),
527                    DType::Utf8(Nullability::NonNullable),
528                ],
529            ),
530            nullability,
531        )
532    }
533
534    #[rstest]
535    // A fixed-size list of all-zero elements is itself zero.
536    #[case(vec![0, 0], Some(true))]
537    #[case(vec![0], Some(true))]
538    // A single non-zero element makes the whole list non-zero. On `develop` these incorrectly
539    // returned `Some(true)` because only the element count was checked.
540    #[case(vec![0, 5], Some(false))]
541    #[case(vec![5, 0], Some(false))]
542    #[case(vec![1, 2], Some(false))]
543    fn fixed_size_list_is_zero(#[case] values: Vec<i32>, #[case] expected: Option<bool>) {
544        let element_dtype = DType::Primitive(PType::I32, Nullability::NonNullable);
545        let children: Vec<Scalar> = values.into_iter().map(i32_scalar).collect();
546        let scalar = Scalar::fixed_size_list(element_dtype, children, Nullability::NonNullable);
547        assert_eq!(scalar.is_zero(), expected);
548    }
549
550    #[test]
551    fn null_fixed_size_list_is_zero_is_none() {
552        let element_dtype = Arc::new(DType::Primitive(PType::I32, Nullability::NonNullable));
553        let scalar = Scalar::null(DType::FixedSizeList(
554            element_dtype,
555            2,
556            Nullability::Nullable,
557        ));
558        assert_eq!(scalar.is_zero(), None);
559    }
560
561    #[test]
562    fn fixed_size_list_with_null_element_is_not_zero() {
563        // A non-null fixed-size list containing a null element is not a zero value. On `develop`
564        // this incorrectly returned `Some(true)`.
565        let element_dtype = DType::Primitive(PType::I32, Nullability::Nullable);
566        let children = vec![nullable_i32(Some(0)), nullable_i32(None)];
567        let scalar = Scalar::fixed_size_list(element_dtype, children, Nullability::NonNullable);
568        assert_eq!(scalar.is_zero(), Some(false));
569    }
570
571    #[test]
572    fn struct_with_all_zero_fields_is_zero() {
573        let scalar = Scalar::struct_(
574            ab_struct_dtype(Nullability::NonNullable),
575            vec![i32_scalar(0), Scalar::utf8("", Nullability::NonNullable)],
576        );
577        assert_eq!(scalar.is_zero(), Some(true));
578    }
579
580    #[rstest]
581    // A non-zero primitive field, a non-empty string field, or both, make the struct non-zero. On
582    // `develop` all of these incorrectly returned `Some(true)`.
583    #[case(5, "")]
584    #[case(0, "x")]
585    #[case(7, "y")]
586    fn struct_with_non_zero_field_is_not_zero(#[case] a: i32, #[case] b: &str) {
587        let scalar = Scalar::struct_(
588            ab_struct_dtype(Nullability::NonNullable),
589            vec![i32_scalar(a), Scalar::utf8(b, Nullability::NonNullable)],
590        );
591        assert_eq!(scalar.is_zero(), Some(false));
592    }
593
594    #[test]
595    fn null_struct_is_zero_is_none() {
596        let scalar = Scalar::null(ab_struct_dtype(Nullability::Nullable));
597        assert_eq!(scalar.is_zero(), None);
598    }
599
600    #[test]
601    fn struct_with_null_field_is_not_zero() {
602        // A non-null struct with a null field is not a zero value. On `develop` this incorrectly
603        // returned `Some(true)`.
604        let dtype = DType::Struct(
605            StructFields::new(
606                ["a", "b"].into(),
607                vec![
608                    DType::Primitive(PType::I32, Nullability::Nullable),
609                    DType::Primitive(PType::I32, Nullability::Nullable),
610                ],
611            ),
612            Nullability::NonNullable,
613        );
614        let scalar = Scalar::struct_(dtype, vec![nullable_i32(Some(0)), nullable_i32(None)]);
615        assert_eq!(scalar.is_zero(), Some(false));
616    }
617
618    #[test]
619    fn nested_struct_of_fixed_size_list_recurses() {
620        // Zero-checking must recurse through both structs and fixed-size lists. On `develop` the
621        // non-zero case incorrectly returned `Some(true)`.
622        let element_dtype = Arc::new(DType::Primitive(PType::I32, Nullability::NonNullable));
623        let fsl_dtype =
624            DType::FixedSizeList(Arc::clone(&element_dtype), 2, Nullability::NonNullable);
625        let struct_dtype = DType::Struct(
626            StructFields::new(["fsl"].into(), vec![fsl_dtype]),
627            Nullability::NonNullable,
628        );
629
630        let all_zero = Scalar::struct_(
631            struct_dtype.clone(),
632            vec![Scalar::fixed_size_list(
633                Arc::clone(&element_dtype),
634                vec![i32_scalar(0), i32_scalar(0)],
635                Nullability::NonNullable,
636            )],
637        );
638        assert_eq!(all_zero.is_zero(), Some(true));
639
640        let with_non_zero = Scalar::struct_(
641            struct_dtype,
642            vec![Scalar::fixed_size_list(
643                element_dtype,
644                vec![i32_scalar(0), i32_scalar(9)],
645                Nullability::NonNullable,
646            )],
647        );
648        assert_eq!(with_non_zero.is_zero(), Some(false));
649    }
650}