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