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_buffer::ByteBufferMut;
10use vortex_error::VortexExpect;
11use vortex_error::VortexResult;
12use vortex_error::vortex_ensure;
13use vortex_error::vortex_panic;
14use vortex_session::VortexSession;
15use vortex_session::registry::CachedId;
16
17use crate::ArrayEq;
18use crate::ArrayHash;
19use crate::ArrayParts;
20use crate::ArrayRef;
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;
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!(
97            data.scalar.dtype() == dtype,
98            "ConstantArray scalar dtype does not match outer dtype"
99        );
100        Ok(())
101    }
102
103    fn nbuffers(_array: ArrayView<'_, Self>) -> usize {
104        1
105    }
106
107    fn buffer(array: ArrayView<'_, Self>, idx: usize) -> BufferHandle {
108        match idx {
109            0 => BufferHandle::new_host(
110                ScalarValue::to_proto_bytes::<ByteBufferMut>(array.scalar.value()).freeze(),
111            ),
112            _ => vortex_panic!("ConstantArray buffer index {idx} out of bounds"),
113        }
114    }
115
116    fn buffer_name(_array: ArrayView<'_, Self>, idx: usize) -> Option<String> {
117        match idx {
118            0 => Some("scalar".to_string()),
119            _ => None,
120        }
121    }
122
123    fn with_buffers(
124        &self,
125        array: ArrayView<'_, Self>,
126        buffers: &[BufferHandle],
127    ) -> VortexResult<ArrayParts<Self>> {
128        unsupported_buffer_replacement(array, buffers)
129    }
130
131    fn slot_name(_array: ArrayView<'_, Self>, idx: usize) -> String {
132        vortex_panic!("ConstantArray slot_name index {idx} out of bounds")
133    }
134
135    fn serialize(
136        _array: ArrayView<'_, Self>,
137        _session: &VortexSession,
138    ) -> VortexResult<Option<Vec<u8>>> {
139        // HACK: Because the scalar is stored in the buffers, we do not need to serialize the
140        // metadata at all.
141        Ok(Some(vec![]))
142    }
143
144    fn deserialize(
145        &self,
146        dtype: &DType,
147        len: usize,
148        _metadata: &[u8],
149
150        buffers: &[BufferHandle],
151        _children: &dyn ArrayChildren,
152        session: &VortexSession,
153    ) -> VortexResult<ArrayParts<Self>> {
154        vortex_ensure!(
155            buffers.len() == 1,
156            "Expected 1 buffer, got {}",
157            buffers.len()
158        );
159
160        let buffer = buffers[0].clone().try_to_host_sync()?;
161        let bytes: &[u8] = buffer.as_ref();
162
163        let scalar_value = ScalarValue::from_proto_bytes(bytes, dtype, session)?;
164        let scalar = Scalar::try_new(dtype.clone(), scalar_value)?;
165
166        Ok(ArrayParts::new(
167            self.clone(),
168            dtype.clone(),
169            len,
170            ConstantData::new(scalar),
171        ))
172    }
173
174    fn reduce_parent(
175        array: ArrayView<'_, Self>,
176        parent: &ArrayRef,
177        child_idx: usize,
178    ) -> VortexResult<Option<ArrayRef>> {
179        PARENT_RULES.evaluate(array, parent, child_idx)
180    }
181
182    fn execute(array: Array<Self>, ctx: &mut ExecutionCtx) -> VortexResult<ExecutionResult> {
183        Ok(ExecutionResult::done(constant_canonicalize(
184            array.as_view(),
185            ctx,
186        )?))
187    }
188
189    fn append_to_builder(
190        array: ArrayView<'_, Self>,
191        builder: &mut dyn ArrayBuilder,
192        ctx: &mut ExecutionCtx,
193    ) -> VortexResult<()> {
194        let n = array.len();
195        let scalar = array.scalar();
196
197        match array.dtype() {
198            DType::Null => append_value_or_nulls::<NullBuilder>(builder, true, n, |_| {}),
199            DType::Bool(_) => {
200                append_value_or_nulls::<BoolBuilder>(builder, scalar.is_null(), n, |b| {
201                    b.append_values(
202                        scalar
203                            .as_bool()
204                            .value()
205                            .vortex_expect("non-null bool scalar must have a value"),
206                        n,
207                    );
208                })
209            }
210            DType::Primitive(ptype, _) => {
211                match_each_native_ptype!(ptype, |P| {
212                    append_value_or_nulls::<PrimitiveBuilder<P>>(
213                        builder,
214                        scalar.is_null(),
215                        n,
216                        |b| {
217                            let value = P::try_from(scalar)
218                                .vortex_expect("Couldn't unwrap constant scalar to primitive");
219                            b.append_n_values(value, n);
220                        },
221                    );
222                });
223            }
224            DType::Decimal(..) => {
225                append_value_or_nulls::<DecimalBuilder>(builder, scalar.is_null(), n, |b| {
226                    let value = scalar
227                        .as_decimal()
228                        .decimal_value()
229                        .vortex_expect("non-null decimal scalar must have a value");
230                    match_each_decimal_value!(value, |v| { b.append_n_values(v, n) });
231                });
232            }
233            DType::Utf8(_) => {
234                if let Some(result) = match_each_varbin_builder!(builder, |builder| {
235                    builder.append_scalar_repeated(scalar, n)
236                }) {
237                    result?;
238                } else {
239                    append_value_or_nulls::<VarBinViewBuilder>(builder, scalar.is_null(), n, |b| {
240                        let value = scalar
241                            .as_utf8()
242                            .value()
243                            .vortex_expect("non-null utf8 scalar must have a value");
244                        b.append_n_values(value.as_bytes(), n);
245                    });
246                }
247            }
248            DType::Binary(_) => {
249                if let Some(result) = match_each_varbin_builder!(builder, |builder| {
250                    builder.append_scalar_repeated(scalar, n)
251                }) {
252                    result?;
253                } else {
254                    append_value_or_nulls::<VarBinViewBuilder>(builder, scalar.is_null(), n, |b| {
255                        let value = scalar
256                            .as_binary()
257                            .value()
258                            .vortex_expect("non-null binary scalar must have a value");
259                        b.append_n_values(value, n);
260                    });
261                }
262            }
263            DType::List(..) => append_constant_list_run(array, n, builder, ctx)?,
264            DType::Extension(ext_dtype) => {
265                // An extension array is its storage wearing a dtype, so a run of identical values
266                // is a constant storage array, which stays constant-encoded in the builder.
267                // Canonicalizing instead would materialize the storage: see the note in
268                // `constant_canonicalize` about `ExtensionConstantRule`.
269                let storage = ConstantArray::new(scalar.as_extension().to_storage_scalar(), n);
270                ExtensionArray::new(ext_dtype.clone(), storage.into_array())
271                    .into_array()
272                    .append_to_builder(builder, ctx)?
273            }
274            DType::FixedSizeList(..) => {
275                append_constant_fixed_size_list_run(array, n, builder, ctx)?
276            }
277            // The remaining dtypes canonicalize cheaply: a constant struct canonicalizes to
278            // constant fields, and a constant map to views sharing one copy of the entries, so
279            // appending the canonical array preserves the run's economy.
280            // TODO: add a fast path for DType::Union once it has a builder.
281            _ => append_via_canonical(array, builder, ctx)?,
282        }
283
284        Ok(())
285    }
286}
287
288/// Appends the constant list `array` as one run sharing a single copy of its elements.
289///
290/// The list's elements materialize once, and
291/// [`ListViewBuilder::append_array_as_repeated_list`] points the run's `n` views at that one
292/// copy. Only a list-view builder has a layout that can share elements; any other builder for a
293/// list dtype - a [`ListBuilder`](crate::builders::ListBuilder), whose offsets can only describe
294/// contiguous lists - appends the canonical run instead.
295fn append_constant_list_run(
296    array: ArrayView<'_, Constant>,
297    n: usize,
298    builder: &mut dyn ArrayBuilder,
299    ctx: &mut ExecutionCtx,
300) -> VortexResult<()> {
301    let scalar = array.scalar();
302    match match_each_listview_builder!(builder, |b| append_repeated_list_run(
303        b,
304        scalar.as_list(),
305        n,
306        ctx
307    )) {
308        Some(result) => result,
309        None => append_via_canonical(array, builder, ctx),
310    }
311}
312
313/// Appends the list `scalar` to a [`ListViewBuilder`] `n` times, storing its elements once.
314fn append_repeated_list_run<O: OffsetBuilderPType, S: OffsetBuilderPType>(
315    builder: &mut ListViewBuilder<O, S>,
316    scalar: ListScalar,
317    n: usize,
318    ctx: &mut ExecutionCtx,
319) -> VortexResult<()> {
320    if n == 0 {
321        return Ok(());
322    }
323
324    let Some(elements) = scalar.elements() else {
325        // A null run stores no elements at all.
326        builder.append_nulls(n);
327        return Ok(());
328    };
329
330    let mut elements_builder = builder_with_capacity(scalar.element_dtype(), elements.len());
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 = builder_with_capacity(builder.element_dtype(), elements.len());
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::IntoArray;
422    use crate::VortexSessionExecute;
423    use crate::arrays::Chunked;
424    use crate::arrays::Constant;
425    use crate::arrays::ConstantArray;
426    use crate::arrays::Extension;
427    use crate::arrays::FixedSizeList;
428    use crate::arrays::ListView;
429    use crate::arrays::Struct;
430    use crate::arrays::chunked::ChunkedArrayExt;
431    use crate::arrays::constant::vtable::canonical::constant_canonicalize;
432    use crate::arrays::extension::ExtensionArrayExt;
433    use crate::arrays::fixed_size_list::FixedSizeListArraySlotsExt;
434    use crate::arrays::listview::ListViewArraySlotsExt;
435    use crate::arrays::struct_::StructArrayExt;
436    use crate::assert_arrays_eq;
437    use crate::builders::ArrayBuilder;
438    use crate::builders::ListBuilder;
439    use crate::builders::builder_with_capacity;
440    use crate::dtype::DType;
441    use crate::dtype::Nullability;
442    use crate::dtype::PType;
443    use crate::dtype::StructFields;
444    use crate::extension::datetime::Date;
445    use crate::extension::datetime::TimeUnit;
446    use crate::scalar::Scalar;
447
448    /// Appends `array` into a fresh builder and asserts the result matches `constant_canonicalize`.
449    fn assert_append_matches_canonical(array: ConstantArray) -> VortexResult<()> {
450        let mut ctx = crate::array_session().create_execution_ctx();
451
452        let expected = constant_canonicalize(array.as_view(), &mut ctx)?.into_array();
453        let mut builder = builder_with_capacity(array.dtype(), array.len());
454        array
455            .into_array()
456            .append_to_builder(builder.as_mut(), &mut ctx)?;
457        let result = builder.finish();
458        assert_arrays_eq!(&result, &expected, &mut ctx);
459        Ok(())
460    }
461
462    #[test]
463    fn test_null_constant_append() -> VortexResult<()> {
464        assert_append_matches_canonical(ConstantArray::new(Scalar::null(DType::Null), 5))
465    }
466
467    #[test]
468    fn test_with_buffers_rejects_serialized_scalar_buffer() {
469        let array =
470            ConstantArray::new(Scalar::primitive(42i32, Nullability::NonNullable), 3).into_array();
471        let buffers = array.buffer_handles();
472
473        // SAFETY: the replacement buffers are the array's existing buffers, so the logical values
474        // would be unchanged if the encoding supported buffer replacement.
475        let Err(err) = (unsafe { array.with_buffers(buffers) }) else {
476            panic!("ConstantArray should reject replacing its serialized scalar buffer");
477        };
478        assert!(
479            err.to_string()
480                .contains("does not support in-memory buffer replacement")
481        );
482    }
483
484    #[rstest]
485    #[case::bool_true(true, 5)]
486    #[case::bool_false(false, 3)]
487    fn test_bool_constant_append(#[case] value: bool, #[case] n: usize) -> VortexResult<()> {
488        assert_append_matches_canonical(ConstantArray::new(
489            Scalar::bool(value, Nullability::NonNullable),
490            n,
491        ))
492    }
493
494    #[test]
495    fn test_bool_null_constant_append() -> VortexResult<()> {
496        assert_append_matches_canonical(ConstantArray::new(
497            Scalar::null(DType::Bool(Nullability::Nullable)),
498            4,
499        ))
500    }
501
502    #[rstest]
503    #[case::i32(Scalar::primitive(42i32, Nullability::NonNullable), 5)]
504    #[case::u8(Scalar::primitive(7u8, Nullability::NonNullable), 3)]
505    #[case::f64(Scalar::primitive(1.5f64, Nullability::NonNullable), 4)]
506    #[case::i32_null(Scalar::null(DType::Primitive(PType::I32, Nullability::Nullable)), 3)]
507    fn test_primitive_constant_append(
508        #[case] scalar: Scalar,
509        #[case] n: usize,
510    ) -> VortexResult<()> {
511        assert_append_matches_canonical(ConstantArray::new(scalar, n))
512    }
513
514    #[rstest]
515    #[case::utf8_inline("hi", 5)] // ≤12 bytes: inlined in BinaryView
516    #[case::utf8_noninline("hello world!!", 5)] // >12 bytes: requires buffer block
517    #[case::utf8_empty("", 3)]
518    #[case::utf8_n_zero("hello world!!", 0)] // n=0 with non-inline: must not write orphaned bytes
519    fn test_utf8_constant_append(#[case] value: &str, #[case] n: usize) -> VortexResult<()> {
520        assert_append_matches_canonical(ConstantArray::new(
521            Scalar::utf8(value, Nullability::NonNullable),
522            n,
523        ))
524    }
525
526    #[test]
527    fn test_utf8_null_constant_append() -> VortexResult<()> {
528        assert_append_matches_canonical(ConstantArray::new(
529            Scalar::null(DType::Utf8(Nullability::Nullable)),
530            4,
531        ))
532    }
533
534    #[rstest]
535    #[case::binary_inline(vec![1u8, 2, 3], 5)] // ≤12 bytes: inlined
536    #[case::binary_noninline(vec![0u8; 13], 5)] // >12 bytes: buffer block
537    fn test_binary_constant_append(#[case] value: Vec<u8>, #[case] n: usize) -> VortexResult<()> {
538        assert_append_matches_canonical(ConstantArray::new(
539            Scalar::binary(value, Nullability::NonNullable),
540            n,
541        ))
542    }
543
544    #[test]
545    fn test_binary_null_constant_append() -> VortexResult<()> {
546        assert_append_matches_canonical(ConstantArray::new(
547            Scalar::null(DType::Binary(Nullability::Nullable)),
548            4,
549        ))
550    }
551
552    #[rstest]
553    #[case::non_empty(vec![Scalar::from(1i32), Scalar::from(2i32)], 4)]
554    #[case::empty(vec![], 3)]
555    #[case::n_zero(vec![Scalar::from(1i32)], 0)]
556    fn test_list_constant_append(
557        #[case] elements: Vec<Scalar>,
558        #[case] n: usize,
559    ) -> VortexResult<()> {
560        let scalar = Scalar::list(
561            Arc::new(DType::Primitive(PType::I32, Nullability::NonNullable)),
562            elements,
563            Nullability::NonNullable,
564        );
565        assert_append_matches_canonical(ConstantArray::new(scalar, n))
566    }
567
568    #[test]
569    fn test_null_list_constant_append() -> VortexResult<()> {
570        let dtype = DType::List(
571            Arc::new(DType::Primitive(PType::I32, Nullability::NonNullable)),
572            Nullability::Nullable,
573        );
574        assert_append_matches_canonical(ConstantArray::new(Scalar::null(dtype), 3))
575    }
576
577    /// A run of identical lists appended into a list-view builder shares one copy of its elements
578    /// across the whole run.
579    #[test]
580    fn test_list_constant_append_keeps_one_copy_of_the_elements() -> VortexResult<()> {
581        let mut ctx = crate::array_session().create_execution_ctx();
582        let scalar = Scalar::list(
583            Arc::new(DType::Primitive(PType::I32, Nullability::NonNullable)),
584            vec![Scalar::from(1i32), Scalar::from(2i32), Scalar::from(3i32)],
585            Nullability::NonNullable,
586        );
587        let array = ConstantArray::new(scalar, 1_000);
588
589        let mut builder = builder_with_capacity(array.dtype(), array.len());
590        array
591            .into_array()
592            .append_to_builder(builder.as_mut(), &mut ctx)?;
593        let result = builder.finish();
594
595        assert_eq!(
596            result.as_::<ListView>().elements().len(),
597            3,
598            "the run's elements should be stored once, not once per row",
599        );
600        Ok(())
601    }
602
603    /// A `ListBuilder`'s offsets can only describe contiguous lists, so a constant run cannot
604    /// share its elements there and takes the canonical path instead.
605    #[test]
606    fn test_list_constant_append_into_list_builder() -> VortexResult<()> {
607        let mut ctx = crate::array_session().create_execution_ctx();
608        let element_dtype: Arc<DType> =
609            Arc::new(DType::Primitive(PType::I32, Nullability::NonNullable));
610        let scalar = Scalar::list(
611            Arc::clone(&element_dtype),
612            vec![Scalar::from(1i32), Scalar::from(2i32)],
613            Nullability::NonNullable,
614        );
615        let array = ConstantArray::new(scalar, 4).into_array();
616
617        let mut builder =
618            ListBuilder::<u32>::with_capacity(element_dtype, Nullability::NonNullable, 0, 0);
619        array.append_to_builder(&mut builder, &mut ctx)?;
620
621        assert_arrays_eq!(&builder.finish(), &array, &mut ctx);
622        Ok(())
623    }
624
625    #[test]
626    fn test_struct_constant_append() -> VortexResult<()> {
627        let fields = StructFields::new(
628            ["x", "y"].into(),
629            vec![
630                DType::Primitive(PType::I32, Nullability::NonNullable),
631                DType::Utf8(Nullability::NonNullable),
632            ],
633        );
634        let scalar = Scalar::struct_(
635            DType::Struct(fields, Nullability::NonNullable),
636            [
637                Scalar::primitive(42i32, Nullability::NonNullable),
638                Scalar::utf8("hi", Nullability::NonNullable),
639            ],
640        );
641        assert_append_matches_canonical(ConstantArray::new(scalar, 3))
642    }
643
644    #[test]
645    fn test_null_struct_constant_append() -> VortexResult<()> {
646        let fields = StructFields::new(
647            ["x"].into(),
648            vec![DType::Primitive(PType::I32, Nullability::Nullable)],
649        );
650        let dtype = DType::Struct(fields, Nullability::Nullable);
651        assert_append_matches_canonical(ConstantArray::new(Scalar::null(dtype), 4))
652    }
653
654    /// A run of identical structs should leave each field constant-encoded rather than materialize
655    /// a value per row per field.
656    #[test]
657    fn test_struct_constant_append_keeps_fields_constant() -> VortexResult<()> {
658        let mut ctx = crate::array_session().create_execution_ctx();
659        let fields = StructFields::new(
660            ["x", "y"].into(),
661            vec![
662                DType::Primitive(PType::I32, Nullability::NonNullable),
663                DType::Utf8(Nullability::NonNullable),
664            ],
665        );
666        let scalar = Scalar::struct_(
667            DType::Struct(fields, Nullability::NonNullable),
668            [
669                Scalar::primitive(42i32, Nullability::NonNullable),
670                Scalar::utf8("hi", Nullability::NonNullable),
671            ],
672        );
673        let array = ConstantArray::new(scalar, 1_000);
674
675        let mut builder = builder_with_capacity(array.dtype(), array.len());
676        array
677            .into_array()
678            .append_to_builder(builder.as_mut(), &mut ctx)?;
679        let result = builder.finish();
680
681        let struct_array = result.as_::<Struct>();
682        for field in 0..2 {
683            assert!(
684                struct_array.unmasked_field(field).is::<Constant>(),
685                "field {field} should have stayed constant-encoded",
686            );
687        }
688        Ok(())
689    }
690
691    #[rstest]
692    #[case::non_uniform(vec![Scalar::from(1i32), Scalar::from(2i32)])]
693    #[case::uniform(vec![Scalar::from(7i32), Scalar::from(7i32)])]
694    fn test_fixed_size_list_constant_append(#[case] elements: Vec<Scalar>) -> VortexResult<()> {
695        let scalar = Scalar::fixed_size_list(
696            Arc::new(DType::Primitive(PType::I32, Nullability::NonNullable)),
697            elements,
698            Nullability::NonNullable,
699        );
700        assert_append_matches_canonical(ConstantArray::new(scalar, 4))
701    }
702
703    #[test]
704    fn test_null_fixed_size_list_constant_append() -> VortexResult<()> {
705        let dtype = DType::FixedSizeList(
706            Arc::new(DType::Primitive(PType::I32, Nullability::NonNullable)),
707            2,
708            Nullability::Nullable,
709        );
710        assert_append_matches_canonical(ConstantArray::new(Scalar::null(dtype), 3))
711    }
712
713    /// A fixed-size list whose elements are all the same scalar tiles a constant array, so the
714    /// tile's chunks stay constant-encoded rather than materializing a value per row.
715    #[test]
716    fn test_uniform_fixed_size_list_constant_append_keeps_elements_constant() -> VortexResult<()> {
717        let mut ctx = crate::array_session().create_execution_ctx();
718        let scalar = Scalar::fixed_size_list(
719            Arc::new(DType::Primitive(PType::I32, Nullability::NonNullable)),
720            vec![Scalar::from(7i32), Scalar::from(7i32)],
721            Nullability::NonNullable,
722        );
723        let array = ConstantArray::new(scalar, 1_000);
724
725        let mut builder = builder_with_capacity(array.dtype(), array.len());
726        array
727            .into_array()
728            .append_to_builder(builder.as_mut(), &mut ctx)?;
729        let result = builder.finish();
730
731        let elements = result.as_::<FixedSizeList>().elements().clone();
732        assert!(
733            elements
734                .as_::<Chunked>()
735                .iter_chunks()
736                .all(|chunk| chunk.is::<Constant>()),
737            "a uniform tile should have stayed constant-encoded",
738        );
739        Ok(())
740    }
741
742    #[test]
743    fn test_extension_constant_append() -> VortexResult<()> {
744        let scalar = Scalar::extension::<Date>(TimeUnit::Days, Scalar::from(Some(42i32)));
745        assert_append_matches_canonical(ConstantArray::new(scalar, 5))
746    }
747
748    /// An extension array is its storage wearing a dtype, so a run of identical values should leave
749    /// the storage constant-encoded.
750    #[test]
751    fn test_extension_constant_append_keeps_storage_constant() -> VortexResult<()> {
752        let mut ctx = crate::array_session().create_execution_ctx();
753        let scalar = Scalar::extension::<Date>(TimeUnit::Days, Scalar::from(Some(42i32)));
754        let array = ConstantArray::new(scalar, 1_000);
755
756        let mut builder = builder_with_capacity(array.dtype(), array.len());
757        array
758            .into_array()
759            .append_to_builder(builder.as_mut(), &mut ctx)?;
760        let result = builder.finish();
761
762        assert!(
763            result.as_::<Extension>().storage_array().is::<Constant>(),
764            "the storage should have stayed constant-encoded",
765        );
766        Ok(())
767    }
768}