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