Skip to main content

vortex_array/arrays/constant/vtable/
mod.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4use std::fmt::Debug;
5use std::hash::Hash;
6use std::hash::Hasher;
7
8use itertools::Itertools;
9use vortex_error::VortexExpect;
10use vortex_error::VortexResult;
11use vortex_error::vortex_ensure_eq;
12use vortex_error::vortex_panic;
13use vortex_session::VortexSession;
14use vortex_session::registry::CachedId;
15
16use crate::ArrayEq;
17use crate::ArrayHash;
18use crate::ArrayParts;
19use crate::ArrayRef;
20use crate::ArraySlots;
21use crate::EqMode;
22use crate::ExecutionCtx;
23use crate::ExecutionResult;
24use crate::IntoArray;
25use crate::array::Array;
26use crate::array::ArrayId;
27use crate::array::ArrayView;
28use crate::array::VTable;
29use crate::array::unsupported_buffer_replacement;
30use crate::arrays::ExtensionArray;
31use crate::arrays::constant::ConstantData;
32use crate::arrays::constant::compute::rules::PARENT_RULES;
33use crate::arrays::constant::vtable::canonical::constant_canonicalize;
34use crate::buffer::BufferHandle;
35use crate::builders::ArrayBuilder;
36use crate::builders::BoolBuilder;
37use crate::builders::DecimalBuilder;
38use crate::builders::FixedSizeListBuilder;
39use crate::builders::ListViewBuilder;
40use crate::builders::NullBuilder;
41use crate::builders::PrimitiveBuilder;
42use crate::builders::VarBinViewBuilder;
43use crate::builders::builder_with_capacity_in;
44use crate::canonical::Canonical;
45use crate::dtype::DType;
46use crate::dtype::OffsetBuilderPType;
47use crate::match_each_decimal_value;
48use crate::match_each_listview_builder;
49use crate::match_each_native_ptype;
50use crate::match_each_varbin_builder;
51use crate::scalar::DecimalValue;
52use crate::scalar::ListScalar;
53use crate::scalar::Scalar;
54use crate::scalar::ScalarValue;
55use crate::serde::ArrayChildren;
56pub(crate) mod canonical;
57mod operations;
58mod validity;
59
60/// A [`Constant`]-encoded Vortex array.
61pub type ConstantArray = Array<Constant>;
62
63#[derive(Clone, Debug)]
64pub struct Constant;
65
66impl ArrayHash for ConstantData {
67    fn array_hash<H: Hasher>(&self, state: &mut H, _accuracy: EqMode) {
68        self.scalar.hash(state);
69    }
70}
71
72impl ArrayEq for ConstantData {
73    fn array_eq(&self, other: &Self, _accuracy: EqMode) -> bool {
74        self.scalar == other.scalar
75    }
76}
77
78impl VTable for Constant {
79    type TypedArrayData = ConstantData;
80
81    type OperationsVTable = Self;
82    type ValidityVTable = Self;
83
84    fn id(&self) -> ArrayId {
85        static ID: CachedId = CachedId::new("vortex.constant");
86        *ID
87    }
88
89    fn validate(
90        &self,
91        data: &ConstantData,
92        dtype: &DType,
93        _len: usize,
94        _slots: &[Option<ArrayRef>],
95    ) -> VortexResult<()> {
96        vortex_ensure_eq!(
97            data.scalar.dtype(),
98            dtype,
99            "ConstantArray scalar dtype does not match outer dtype",
100        );
101        Ok(())
102    }
103
104    fn nbuffers(_array: ArrayView<'_, Self>) -> usize {
105        1
106    }
107
108    fn buffer(array: ArrayView<'_, Self>, idx: usize) -> BufferHandle {
109        match idx {
110            0 => BufferHandle::new_host(
111                ScalarValue::to_proto_bytes::<Vec<u8>>(array.scalar.value()).into(),
112            ),
113            _ => vortex_panic!("ConstantArray buffer index {idx} out of bounds"),
114        }
115    }
116
117    fn buffer_name(_array: ArrayView<'_, Self>, idx: usize) -> Option<String> {
118        match idx {
119            0 => Some("scalar".to_string()),
120            _ => None,
121        }
122    }
123
124    fn with_buffers(
125        &self,
126        array: ArrayView<'_, Self>,
127        buffers: &[BufferHandle],
128    ) -> VortexResult<ArrayParts<Self>> {
129        unsupported_buffer_replacement(array, buffers)
130    }
131
132    fn slot_name(_array: ArrayView<'_, Self>, idx: usize) -> String {
133        vortex_panic!("ConstantArray slot_name index {idx} out of bounds")
134    }
135
136    fn serialize(
137        _array: ArrayView<'_, Self>,
138        _session: &VortexSession,
139    ) -> VortexResult<Option<Vec<u8>>> {
140        // HACK: Because the scalar is stored in the buffers, we do not need to serialize the
141        // metadata at all.
142        Ok(Some(vec![]))
143    }
144
145    fn deserialize(
146        &self,
147        dtype: &DType,
148        len: usize,
149        _metadata: &[u8],
150
151        buffers: &[BufferHandle],
152        _children: &dyn ArrayChildren,
153        session: &VortexSession,
154    ) -> VortexResult<ArrayParts<Self>> {
155        vortex_ensure_eq!(buffers.len(), 1);
156
157        let buffer = buffers[0].clone().try_to_host_sync()?;
158        let bytes: &[u8] = buffer.as_ref();
159
160        let scalar_value = ScalarValue::from_proto_bytes(bytes, dtype, session)?;
161        let scalar = Scalar::try_new(dtype.clone(), scalar_value)?;
162
163        Ok(ArrayParts::new(
164            self.clone(),
165            dtype.clone(),
166            len,
167            ConstantData::new(scalar),
168            ArraySlots::new(),
169        ))
170    }
171
172    fn reduce_parent(
173        array: ArrayView<'_, Self>,
174        parent: &ArrayRef,
175        child_idx: usize,
176    ) -> VortexResult<Option<ArrayRef>> {
177        PARENT_RULES.evaluate(array, parent, child_idx)
178    }
179
180    fn execute(array: Array<Self>, ctx: &mut ExecutionCtx) -> VortexResult<ExecutionResult> {
181        Ok(ExecutionResult::done(constant_canonicalize(
182            array.as_view(),
183            ctx,
184        )?))
185    }
186
187    fn append_to_builder(
188        array: ArrayView<'_, Self>,
189        builder: &mut dyn ArrayBuilder,
190        ctx: &mut ExecutionCtx,
191    ) -> VortexResult<()> {
192        let n = array.len();
193        let scalar = array.scalar();
194
195        match array.dtype() {
196            DType::Null => append_value_or_nulls::<NullBuilder>(builder, true, n, |_| {}),
197            DType::Bool(_) => {
198                append_value_or_nulls::<BoolBuilder>(builder, scalar.is_null(), n, |b| {
199                    b.append_values(
200                        scalar
201                            .as_bool()
202                            .value()
203                            .vortex_expect("non-null bool scalar must have a value"),
204                        n,
205                    );
206                })
207            }
208            DType::Primitive(ptype, _) => {
209                match_each_native_ptype!(ptype, |P| {
210                    append_value_or_nulls::<PrimitiveBuilder<P>>(
211                        builder,
212                        scalar.is_null(),
213                        n,
214                        |b| {
215                            let value = P::try_from(scalar)
216                                .vortex_expect("Couldn't unwrap constant scalar to primitive");
217                            b.append_n_values(value, n);
218                        },
219                    );
220                });
221            }
222            DType::Decimal(..) => {
223                append_value_or_nulls::<DecimalBuilder>(builder, scalar.is_null(), n, |b| {
224                    let value = scalar
225                        .as_decimal()
226                        .decimal_value()
227                        .vortex_expect("non-null decimal scalar must have a value");
228                    match_each_decimal_value!(value, |v| { b.append_n_values(v, n) });
229                });
230            }
231            DType::Utf8(_) => {
232                if let Some(result) = match_each_varbin_builder!(builder, |builder| {
233                    builder.append_scalar_repeated(scalar, n)
234                }) {
235                    result?;
236                } else {
237                    append_value_or_nulls::<VarBinViewBuilder>(builder, scalar.is_null(), n, |b| {
238                        let value = scalar
239                            .as_utf8()
240                            .value()
241                            .vortex_expect("non-null utf8 scalar must have a value");
242                        b.append_n_values(value.as_bytes(), n);
243                    });
244                }
245            }
246            DType::Binary(_) => {
247                if let Some(result) = match_each_varbin_builder!(builder, |builder| {
248                    builder.append_scalar_repeated(scalar, n)
249                }) {
250                    result?;
251                } else {
252                    append_value_or_nulls::<VarBinViewBuilder>(builder, scalar.is_null(), n, |b| {
253                        let value = scalar
254                            .as_binary()
255                            .value()
256                            .vortex_expect("non-null binary scalar must have a value");
257                        b.append_n_values(value, n);
258                    });
259                }
260            }
261            DType::List(..) => append_constant_list_run(array, n, builder, ctx)?,
262            DType::Extension(ext_dtype) => {
263                // An extension array is its storage wearing a dtype, so a run of identical values
264                // is a constant storage array, which stays constant-encoded in the builder.
265                // Canonicalizing instead would materialize the storage: see the note in
266                // `constant_canonicalize` about `ExtensionConstantRule`.
267                let storage = ConstantArray::new(scalar.as_extension().to_storage_scalar(), n);
268                ExtensionArray::new(ext_dtype.clone(), storage.into_array())
269                    .into_array()
270                    .append_to_builder(builder, ctx)?
271            }
272            DType::FixedSizeList(..) => {
273                append_constant_fixed_size_list_run(array, n, builder, ctx)?
274            }
275            // The remaining dtypes canonicalize cheaply: a constant struct canonicalizes to
276            // constant fields, and a constant map to views sharing one copy of the entries, so
277            // appending the canonical array preserves the run's economy.
278            // TODO: add a fast path for DType::Union once it has a builder.
279            _ => append_via_canonical(array, builder, ctx)?,
280        }
281
282        Ok(())
283    }
284}
285
286/// Appends the constant list `array` as one run sharing a single copy of its elements.
287///
288/// The list's elements materialize once, and
289/// [`ListViewBuilder::append_array_as_repeated_list`] points the run's `n` views at that one
290/// copy. Only a list-view builder has a layout that can share elements; any other builder for a
291/// list dtype - a [`ListBuilder`](crate::builders::ListBuilder), whose offsets can only describe
292/// contiguous lists - appends the canonical run instead.
293fn append_constant_list_run(
294    array: ArrayView<'_, Constant>,
295    n: usize,
296    builder: &mut dyn ArrayBuilder,
297    ctx: &mut ExecutionCtx,
298) -> VortexResult<()> {
299    let scalar = array.scalar();
300    match match_each_listview_builder!(builder, |b| append_repeated_list_run(
301        b,
302        scalar.as_list(),
303        n,
304        ctx
305    )) {
306        Some(result) => result,
307        None => append_via_canonical(array, builder, ctx),
308    }
309}
310
311/// Appends the list `scalar` to a [`ListViewBuilder`] `n` times, storing its elements once.
312fn append_repeated_list_run<O: OffsetBuilderPType, S: OffsetBuilderPType>(
313    builder: &mut ListViewBuilder<O, S>,
314    scalar: ListScalar,
315    n: usize,
316    ctx: &mut ExecutionCtx,
317) -> VortexResult<()> {
318    if n == 0 {
319        return Ok(());
320    }
321
322    let Some(elements) = scalar.elements() else {
323        // A null run stores no elements at all.
324        builder.append_nulls(n);
325        return Ok(());
326    };
327
328    let mut elements_builder =
329        builder_with_capacity_in(scalar.element_dtype(), elements.len(), ctx.allocator());
330    for element in &elements {
331        elements_builder.append_scalar(element)?;
332    }
333
334    builder.append_array_as_repeated_list(&elements_builder.finish(), n, ctx)
335}
336
337/// Appends the constant fixed-size-list `array` as its list's elements tiled `n` times.
338///
339/// The list's elements materialize into a tile once - a single [`ConstantArray`] when they are
340/// all the same scalar, so that the tiling costs nothing - and
341/// [`FixedSizeListBuilder::append_array_as_repeated_list`] shares that one tile across the run.
342fn append_constant_fixed_size_list_run(
343    array: ArrayView<'_, Constant>,
344    n: usize,
345    builder: &mut dyn ArrayBuilder,
346    ctx: &mut ExecutionCtx,
347) -> VortexResult<()> {
348    let Some(builder) = builder.as_any_mut().downcast_mut::<FixedSizeListBuilder>() else {
349        return append_via_canonical(array, builder, ctx);
350    };
351
352    if n == 0 {
353        return Ok(());
354    }
355
356    let scalar = array.scalar().as_list();
357    let Some(elements) = scalar.elements() else {
358        // A null run stores no elements of its own, only the placeholders the builder writes.
359        builder.append_nulls(n);
360        return Ok(());
361    };
362
363    let tile = match elements.iter().all_equal_value() {
364        Ok(uniform) => ConstantArray::new(uniform.clone(), elements.len()).into_array(),
365        Err(_) => {
366            let mut tile_builder =
367                builder_with_capacity_in(builder.element_dtype(), elements.len(), ctx.allocator());
368            for element in &elements {
369                tile_builder.append_scalar(element)?;
370            }
371            tile_builder.finish()
372        }
373    };
374
375    builder.append_array_as_repeated_list(&tile, n, ctx)
376}
377
378/// Appends `array` by canonicalizing it first, for the dtypes with no fast path of their own.
379fn append_via_canonical(
380    array: ArrayView<'_, Constant>,
381    builder: &mut dyn ArrayBuilder,
382    ctx: &mut ExecutionCtx,
383) -> VortexResult<()> {
384    let canonical = array
385        .array()
386        .clone()
387        .execute::<Canonical>(ctx)?
388        .into_array();
389    canonical.append_to_builder(builder, ctx)
390}
391
392/// Downcasts `builder` to `B`, then either appends `n` nulls or calls `fill` with the typed
393/// builder depending on `is_null`.
394///
395/// `is_null` must only be `true` when the builder is nullable.
396fn append_value_or_nulls<B: ArrayBuilder + 'static>(
397    builder: &mut dyn ArrayBuilder,
398    is_null: bool,
399    n: usize,
400    fill: impl FnOnce(&mut B),
401) {
402    let b = builder
403        .as_any_mut()
404        .downcast_mut::<B>()
405        .vortex_expect("builder dtype must match array dtype");
406    if is_null {
407        // SAFETY: is_null=true only when the scalar (and thus the builder) is nullable.
408        unsafe { b.append_nulls_unchecked(n) };
409    } else {
410        fill(b);
411    }
412}
413
414#[cfg(test)]
415mod tests {
416    use std::sync::Arc;
417
418    use rstest::rstest;
419    use vortex_error::VortexResult;
420
421    use crate::ArrayRef;
422    use crate::ExecutionCtx;
423    use crate::IntoArray;
424    use crate::VortexSessionExecute;
425    use crate::arrays::Chunked;
426    use crate::arrays::Constant;
427    use crate::arrays::ConstantArray;
428    use crate::arrays::Extension;
429    use crate::arrays::FixedSizeList;
430    use crate::arrays::ListView;
431    use crate::arrays::Struct;
432    use crate::arrays::chunked::ChunkedArrayExt;
433    use crate::arrays::constant::vtable::canonical::constant_canonicalize;
434    use crate::arrays::extension::ExtensionArrayExt;
435    use crate::arrays::fixed_size_list::FixedSizeListArraySlotsExt;
436    use crate::arrays::listview::ListViewArraySlotsExt;
437    use crate::arrays::struct_::StructArrayExt;
438    use crate::assert_arrays_eq;
439    use crate::builders::ArrayBuilder;
440    use crate::builders::ListBuilder;
441    use crate::builders::builder_with_capacity_in;
442    use crate::dtype::DType;
443    use crate::dtype::Nullability;
444    use crate::dtype::PType;
445    use crate::dtype::StructFields;
446    use crate::extension::datetime::Date;
447    use crate::extension::datetime::TimeUnit;
448    use crate::scalar::Scalar;
449
450    fn assert_constant_values(
451        array: &ArrayRef,
452        scalar: &Scalar,
453        len: usize,
454        ctx: &mut ExecutionCtx,
455    ) -> VortexResult<()> {
456        assert_eq!(array.dtype(), scalar.dtype());
457        assert_eq!(array.len(), len);
458        for index in 0..len {
459            assert_eq!(&array.execute_scalar(index, ctx)?, scalar);
460        }
461        Ok(())
462    }
463
464    /// Checks append against both the fixture scalar and the canonicalization path.
465    fn assert_append_matches_canonical(array: ConstantArray) -> VortexResult<()> {
466        let mut ctx = crate::array_session().create_execution_ctx();
467
468        let scalar = array.scalar().clone();
469        let len = array.len();
470        let expected = constant_canonicalize(array.as_view(), &mut ctx)?.into_array();
471        let mut builder = builder_with_capacity_in(
472            array.dtype(),
473            array.len(),
474            vortex_buffer::BufferAllocatorRef::static_ref(),
475        );
476        array
477            .into_array()
478            .append_to_builder(builder.as_mut(), &mut ctx)?;
479        let result = builder.finish();
480        assert_arrays_eq!(&result, &expected, &mut ctx);
481        assert_constant_values(&result, &scalar, len, &mut ctx)?;
482        Ok(())
483    }
484
485    #[test]
486    fn test_null_constant_append() -> VortexResult<()> {
487        assert_append_matches_canonical(ConstantArray::new(Scalar::null(DType::Null), 5))
488    }
489
490    #[test]
491    fn test_with_buffers_rejects_serialized_scalar_buffer() {
492        let array =
493            ConstantArray::new(Scalar::primitive(42i32, Nullability::NonNullable), 3).into_array();
494        let buffers = array.buffer_handles();
495
496        // SAFETY: the replacement buffers are the array's existing buffers, so the logical values
497        // would be unchanged if the encoding supported buffer replacement.
498        let Err(err) = (unsafe { array.with_buffers(buffers) }) else {
499            panic!("ConstantArray should reject replacing its serialized scalar buffer");
500        };
501        assert!(
502            err.to_string()
503                .contains("does not support in-memory buffer replacement")
504        );
505    }
506
507    #[rstest]
508    #[case::bool_true(true, 5)]
509    #[case::bool_false(false, 3)]
510    fn test_bool_constant_append(#[case] value: bool, #[case] n: usize) -> VortexResult<()> {
511        assert_append_matches_canonical(ConstantArray::new(
512            Scalar::bool(value, Nullability::NonNullable),
513            n,
514        ))
515    }
516
517    #[test]
518    fn test_bool_null_constant_append() -> VortexResult<()> {
519        assert_append_matches_canonical(ConstantArray::new(
520            Scalar::null(DType::Bool(Nullability::Nullable)),
521            4,
522        ))
523    }
524
525    #[rstest]
526    #[case::i32(Scalar::primitive(42i32, Nullability::NonNullable), 5)]
527    #[case::u8(Scalar::primitive(7u8, Nullability::NonNullable), 3)]
528    #[case::f64(Scalar::primitive(1.5f64, Nullability::NonNullable), 4)]
529    #[case::i32_null(Scalar::null(DType::Primitive(PType::I32, Nullability::Nullable)), 3)]
530    fn test_primitive_constant_append(
531        #[case] scalar: Scalar,
532        #[case] n: usize,
533    ) -> VortexResult<()> {
534        assert_append_matches_canonical(ConstantArray::new(scalar, n))
535    }
536
537    #[rstest]
538    #[case::utf8_inline("hi", 5)] // ≤12 bytes: inlined in BinaryView
539    #[case::utf8_noninline("hello world!!", 5)] // >12 bytes: requires buffer block
540    #[case::utf8_empty("", 3)]
541    #[case::utf8_n_zero("hello world!!", 0)] // n=0 with non-inline: must not write orphaned bytes
542    fn test_utf8_constant_append(#[case] value: &str, #[case] n: usize) -> VortexResult<()> {
543        assert_append_matches_canonical(ConstantArray::new(
544            Scalar::utf8(value, Nullability::NonNullable),
545            n,
546        ))
547    }
548
549    #[test]
550    fn test_utf8_null_constant_append() -> VortexResult<()> {
551        assert_append_matches_canonical(ConstantArray::new(
552            Scalar::null(DType::Utf8(Nullability::Nullable)),
553            4,
554        ))
555    }
556
557    #[rstest]
558    #[case::binary_inline(vec![1u8, 2, 3], 5)] // ≤12 bytes: inlined
559    #[case::binary_noninline(vec![0u8; 13], 5)] // >12 bytes: buffer block
560    fn test_binary_constant_append(#[case] value: Vec<u8>, #[case] n: usize) -> VortexResult<()> {
561        assert_append_matches_canonical(ConstantArray::new(
562            Scalar::binary(value, Nullability::NonNullable),
563            n,
564        ))
565    }
566
567    #[test]
568    fn test_binary_null_constant_append() -> VortexResult<()> {
569        assert_append_matches_canonical(ConstantArray::new(
570            Scalar::null(DType::Binary(Nullability::Nullable)),
571            4,
572        ))
573    }
574
575    #[rstest]
576    #[case::non_empty(vec![Scalar::from(1i32), Scalar::from(2i32)], 4)]
577    #[case::empty(vec![], 3)]
578    #[case::n_zero(vec![Scalar::from(1i32)], 0)]
579    fn test_list_constant_append(
580        #[case] elements: Vec<Scalar>,
581        #[case] n: usize,
582    ) -> VortexResult<()> {
583        let scalar = Scalar::list(
584            Arc::new(DType::Primitive(PType::I32, Nullability::NonNullable)),
585            elements,
586            Nullability::NonNullable,
587        );
588        assert_append_matches_canonical(ConstantArray::new(scalar, n))
589    }
590
591    #[test]
592    fn test_null_list_constant_append() -> VortexResult<()> {
593        let dtype = DType::List(
594            Arc::new(DType::Primitive(PType::I32, Nullability::NonNullable)),
595            Nullability::Nullable,
596        );
597        assert_append_matches_canonical(ConstantArray::new(Scalar::null(dtype), 3))
598    }
599
600    /// A run of identical lists appended into a list-view builder shares one copy of its elements
601    /// across the whole run.
602    #[test]
603    fn test_list_constant_append_keeps_one_copy_of_the_elements() -> VortexResult<()> {
604        let mut ctx = crate::array_session().create_execution_ctx();
605        let scalar = Scalar::list(
606            Arc::new(DType::Primitive(PType::I32, Nullability::NonNullable)),
607            vec![Scalar::from(1i32), Scalar::from(2i32), Scalar::from(3i32)],
608            Nullability::NonNullable,
609        );
610        let array = ConstantArray::new(scalar.clone(), 1_000);
611
612        let mut builder = builder_with_capacity_in(
613            array.dtype(),
614            array.len(),
615            vortex_buffer::BufferAllocatorRef::static_ref(),
616        );
617        array
618            .into_array()
619            .append_to_builder(builder.as_mut(), &mut ctx)?;
620        let result = builder.finish();
621        assert_constant_values(&result, &scalar, 1_000, &mut ctx)?;
622
623        assert_eq!(
624            result.as_::<ListView>().elements().len(),
625            3,
626            "the run's elements should be stored once, not once per row",
627        );
628        Ok(())
629    }
630
631    /// A `ListBuilder`'s offsets can only describe contiguous lists, so a constant run cannot
632    /// share its elements there and takes the canonical path instead.
633    #[test]
634    fn test_list_constant_append_into_list_builder() -> VortexResult<()> {
635        let mut ctx = crate::array_session().create_execution_ctx();
636        let element_dtype: Arc<DType> =
637            Arc::new(DType::Primitive(PType::I32, Nullability::NonNullable));
638        let scalar = Scalar::list(
639            Arc::clone(&element_dtype),
640            vec![Scalar::from(1i32), Scalar::from(2i32)],
641            Nullability::NonNullable,
642        );
643        let array = ConstantArray::new(scalar, 4).into_array();
644
645        let mut builder = ListBuilder::<u32>::with_capacity_in(
646            element_dtype,
647            Nullability::NonNullable,
648            0,
649            0,
650            vortex_buffer::BufferAllocatorRef::static_ref(),
651        );
652        array.append_to_builder(&mut builder, &mut ctx)?;
653
654        assert_arrays_eq!(&builder.finish(), &array, &mut ctx);
655        Ok(())
656    }
657
658    #[test]
659    fn test_struct_constant_append() -> VortexResult<()> {
660        let fields = StructFields::new(
661            ["x", "y"].into(),
662            vec![
663                DType::Primitive(PType::I32, Nullability::NonNullable),
664                DType::Utf8(Nullability::NonNullable),
665            ],
666        );
667        let scalar = Scalar::struct_(
668            DType::Struct(fields, Nullability::NonNullable),
669            [
670                Scalar::primitive(42i32, Nullability::NonNullable),
671                Scalar::utf8("hi", Nullability::NonNullable),
672            ],
673        );
674        assert_append_matches_canonical(ConstantArray::new(scalar, 3))
675    }
676
677    #[test]
678    fn test_null_struct_constant_append() -> VortexResult<()> {
679        let fields = StructFields::new(
680            ["x"].into(),
681            vec![DType::Primitive(PType::I32, Nullability::Nullable)],
682        );
683        let dtype = DType::Struct(fields, Nullability::Nullable);
684        assert_append_matches_canonical(ConstantArray::new(Scalar::null(dtype), 4))
685    }
686
687    /// A run of identical structs should leave each field constant-encoded rather than materialize
688    /// a value per row per field.
689    #[test]
690    fn test_struct_constant_append_keeps_fields_constant() -> VortexResult<()> {
691        let mut ctx = crate::array_session().create_execution_ctx();
692        let fields = StructFields::new(
693            ["x", "y"].into(),
694            vec![
695                DType::Primitive(PType::I32, Nullability::NonNullable),
696                DType::Utf8(Nullability::NonNullable),
697            ],
698        );
699        let scalar = Scalar::struct_(
700            DType::Struct(fields, Nullability::NonNullable),
701            [
702                Scalar::primitive(42i32, Nullability::NonNullable),
703                Scalar::utf8("hi", Nullability::NonNullable),
704            ],
705        );
706        let array = ConstantArray::new(scalar.clone(), 1_000);
707
708        let mut builder = builder_with_capacity_in(
709            array.dtype(),
710            array.len(),
711            vortex_buffer::BufferAllocatorRef::static_ref(),
712        );
713        array
714            .into_array()
715            .append_to_builder(builder.as_mut(), &mut ctx)?;
716        let result = builder.finish();
717        assert_constant_values(&result, &scalar, 1_000, &mut ctx)?;
718
719        let struct_array = result.as_::<Struct>();
720        for field in 0..2 {
721            assert!(
722                struct_array.unmasked_field(field).is::<Constant>(),
723                "field {field} should have stayed constant-encoded",
724            );
725        }
726        Ok(())
727    }
728
729    #[rstest]
730    #[case::non_uniform(vec![Scalar::from(1i32), Scalar::from(2i32)])]
731    #[case::uniform(vec![Scalar::from(7i32), Scalar::from(7i32)])]
732    fn test_fixed_size_list_constant_append(#[case] elements: Vec<Scalar>) -> VortexResult<()> {
733        let scalar = Scalar::fixed_size_list(
734            Arc::new(DType::Primitive(PType::I32, Nullability::NonNullable)),
735            elements,
736            Nullability::NonNullable,
737        );
738        assert_append_matches_canonical(ConstantArray::new(scalar, 4))
739    }
740
741    #[test]
742    fn test_null_fixed_size_list_constant_append() -> VortexResult<()> {
743        let dtype = DType::FixedSizeList(
744            Arc::new(DType::Primitive(PType::I32, Nullability::NonNullable)),
745            2,
746            Nullability::Nullable,
747        );
748        assert_append_matches_canonical(ConstantArray::new(Scalar::null(dtype), 3))
749    }
750
751    /// A fixed-size list whose elements are all the same scalar tiles a constant array, so the
752    /// tile's chunks stay constant-encoded rather than materializing a value per row.
753    #[test]
754    fn test_uniform_fixed_size_list_constant_append_keeps_elements_constant() -> VortexResult<()> {
755        let mut ctx = crate::array_session().create_execution_ctx();
756        let scalar = Scalar::fixed_size_list(
757            Arc::new(DType::Primitive(PType::I32, Nullability::NonNullable)),
758            vec![Scalar::from(7i32), Scalar::from(7i32)],
759            Nullability::NonNullable,
760        );
761        let array = ConstantArray::new(scalar.clone(), 1_000);
762
763        let mut builder = builder_with_capacity_in(
764            array.dtype(),
765            array.len(),
766            vortex_buffer::BufferAllocatorRef::static_ref(),
767        );
768        array
769            .into_array()
770            .append_to_builder(builder.as_mut(), &mut ctx)?;
771        let result = builder.finish();
772        assert_constant_values(&result, &scalar, 1_000, &mut ctx)?;
773
774        let elements = result.as_::<FixedSizeList>().elements().clone();
775        assert!(
776            elements
777                .as_::<Chunked>()
778                .iter_chunks()
779                .all(|chunk| chunk.is::<Constant>()),
780            "a uniform tile should have stayed constant-encoded",
781        );
782        Ok(())
783    }
784
785    #[test]
786    fn test_extension_constant_append() -> VortexResult<()> {
787        let scalar = Scalar::extension::<Date>(TimeUnit::Days, Scalar::from(Some(42i32)));
788        assert_append_matches_canonical(ConstantArray::new(scalar, 5))
789    }
790
791    /// An extension array is its storage wearing a dtype, so a run of identical values should leave
792    /// the storage constant-encoded.
793    #[test]
794    fn test_extension_constant_append_keeps_storage_constant() -> VortexResult<()> {
795        let mut ctx = crate::array_session().create_execution_ctx();
796        let scalar = Scalar::extension::<Date>(TimeUnit::Days, Scalar::from(Some(42i32)));
797        let array = ConstantArray::new(scalar.clone(), 1_000);
798
799        let mut builder = builder_with_capacity_in(
800            array.dtype(),
801            array.len(),
802            vortex_buffer::BufferAllocatorRef::static_ref(),
803        );
804        array
805            .into_array()
806            .append_to_builder(builder.as_mut(), &mut ctx)?;
807        let result = builder.finish();
808        assert_constant_values(&result, &scalar, 1_000, &mut ctx)?;
809
810        assert!(
811            result.as_::<Extension>().storage_array().is::<Constant>(),
812            "the storage should have stayed constant-encoded",
813        );
814        Ok(())
815    }
816}