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