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::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_in;
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_in(
454            array.dtype(),
455            array.len(),
456            vortex_buffer::BufferAllocatorRef::static_ref(),
457        );
458        array
459            .into_array()
460            .append_to_builder(builder.as_mut(), &mut ctx)?;
461        let result = builder.finish();
462        assert_arrays_eq!(&result, &expected, &mut ctx);
463        Ok(())
464    }
465
466    #[test]
467    fn test_null_constant_append() -> VortexResult<()> {
468        assert_append_matches_canonical(ConstantArray::new(Scalar::null(DType::Null), 5))
469    }
470
471    #[test]
472    fn test_with_buffers_rejects_serialized_scalar_buffer() {
473        let array =
474            ConstantArray::new(Scalar::primitive(42i32, Nullability::NonNullable), 3).into_array();
475        let buffers = array.buffer_handles();
476
477        // SAFETY: the replacement buffers are the array's existing buffers, so the logical values
478        // would be unchanged if the encoding supported buffer replacement.
479        let Err(err) = (unsafe { array.with_buffers(buffers) }) else {
480            panic!("ConstantArray should reject replacing its serialized scalar buffer");
481        };
482        assert!(
483            err.to_string()
484                .contains("does not support in-memory buffer replacement")
485        );
486    }
487
488    #[rstest]
489    #[case::bool_true(true, 5)]
490    #[case::bool_false(false, 3)]
491    fn test_bool_constant_append(#[case] value: bool, #[case] n: usize) -> VortexResult<()> {
492        assert_append_matches_canonical(ConstantArray::new(
493            Scalar::bool(value, Nullability::NonNullable),
494            n,
495        ))
496    }
497
498    #[test]
499    fn test_bool_null_constant_append() -> VortexResult<()> {
500        assert_append_matches_canonical(ConstantArray::new(
501            Scalar::null(DType::Bool(Nullability::Nullable)),
502            4,
503        ))
504    }
505
506    #[rstest]
507    #[case::i32(Scalar::primitive(42i32, Nullability::NonNullable), 5)]
508    #[case::u8(Scalar::primitive(7u8, Nullability::NonNullable), 3)]
509    #[case::f64(Scalar::primitive(1.5f64, Nullability::NonNullable), 4)]
510    #[case::i32_null(Scalar::null(DType::Primitive(PType::I32, Nullability::Nullable)), 3)]
511    fn test_primitive_constant_append(
512        #[case] scalar: Scalar,
513        #[case] n: usize,
514    ) -> VortexResult<()> {
515        assert_append_matches_canonical(ConstantArray::new(scalar, n))
516    }
517
518    #[rstest]
519    #[case::utf8_inline("hi", 5)] // ≤12 bytes: inlined in BinaryView
520    #[case::utf8_noninline("hello world!!", 5)] // >12 bytes: requires buffer block
521    #[case::utf8_empty("", 3)]
522    #[case::utf8_n_zero("hello world!!", 0)] // n=0 with non-inline: must not write orphaned bytes
523    fn test_utf8_constant_append(#[case] value: &str, #[case] n: usize) -> VortexResult<()> {
524        assert_append_matches_canonical(ConstantArray::new(
525            Scalar::utf8(value, Nullability::NonNullable),
526            n,
527        ))
528    }
529
530    #[test]
531    fn test_utf8_null_constant_append() -> VortexResult<()> {
532        assert_append_matches_canonical(ConstantArray::new(
533            Scalar::null(DType::Utf8(Nullability::Nullable)),
534            4,
535        ))
536    }
537
538    #[rstest]
539    #[case::binary_inline(vec![1u8, 2, 3], 5)] // ≤12 bytes: inlined
540    #[case::binary_noninline(vec![0u8; 13], 5)] // >12 bytes: buffer block
541    fn test_binary_constant_append(#[case] value: Vec<u8>, #[case] n: usize) -> VortexResult<()> {
542        assert_append_matches_canonical(ConstantArray::new(
543            Scalar::binary(value, Nullability::NonNullable),
544            n,
545        ))
546    }
547
548    #[test]
549    fn test_binary_null_constant_append() -> VortexResult<()> {
550        assert_append_matches_canonical(ConstantArray::new(
551            Scalar::null(DType::Binary(Nullability::Nullable)),
552            4,
553        ))
554    }
555
556    #[rstest]
557    #[case::non_empty(vec![Scalar::from(1i32), Scalar::from(2i32)], 4)]
558    #[case::empty(vec![], 3)]
559    #[case::n_zero(vec![Scalar::from(1i32)], 0)]
560    fn test_list_constant_append(
561        #[case] elements: Vec<Scalar>,
562        #[case] n: usize,
563    ) -> VortexResult<()> {
564        let scalar = Scalar::list(
565            Arc::new(DType::Primitive(PType::I32, Nullability::NonNullable)),
566            elements,
567            Nullability::NonNullable,
568        );
569        assert_append_matches_canonical(ConstantArray::new(scalar, n))
570    }
571
572    #[test]
573    fn test_null_list_constant_append() -> VortexResult<()> {
574        let dtype = DType::List(
575            Arc::new(DType::Primitive(PType::I32, Nullability::NonNullable)),
576            Nullability::Nullable,
577        );
578        assert_append_matches_canonical(ConstantArray::new(Scalar::null(dtype), 3))
579    }
580
581    /// A run of identical lists appended into a list-view builder shares one copy of its elements
582    /// across the whole run.
583    #[test]
584    fn test_list_constant_append_keeps_one_copy_of_the_elements() -> VortexResult<()> {
585        let mut ctx = crate::array_session().create_execution_ctx();
586        let scalar = Scalar::list(
587            Arc::new(DType::Primitive(PType::I32, Nullability::NonNullable)),
588            vec![Scalar::from(1i32), Scalar::from(2i32), Scalar::from(3i32)],
589            Nullability::NonNullable,
590        );
591        let array = ConstantArray::new(scalar, 1_000);
592
593        let mut builder = builder_with_capacity_in(
594            array.dtype(),
595            array.len(),
596            vortex_buffer::BufferAllocatorRef::static_ref(),
597        );
598        array
599            .into_array()
600            .append_to_builder(builder.as_mut(), &mut ctx)?;
601        let result = builder.finish();
602
603        assert_eq!(
604            result.as_::<ListView>().elements().len(),
605            3,
606            "the run's elements should be stored once, not once per row",
607        );
608        Ok(())
609    }
610
611    /// A `ListBuilder`'s offsets can only describe contiguous lists, so a constant run cannot
612    /// share its elements there and takes the canonical path instead.
613    #[test]
614    fn test_list_constant_append_into_list_builder() -> VortexResult<()> {
615        let mut ctx = crate::array_session().create_execution_ctx();
616        let element_dtype: Arc<DType> =
617            Arc::new(DType::Primitive(PType::I32, Nullability::NonNullable));
618        let scalar = Scalar::list(
619            Arc::clone(&element_dtype),
620            vec![Scalar::from(1i32), Scalar::from(2i32)],
621            Nullability::NonNullable,
622        );
623        let array = ConstantArray::new(scalar, 4).into_array();
624
625        let mut builder = ListBuilder::<u32>::with_capacity_in(
626            element_dtype,
627            Nullability::NonNullable,
628            0,
629            0,
630            vortex_buffer::BufferAllocatorRef::static_ref(),
631        );
632        array.append_to_builder(&mut builder, &mut ctx)?;
633
634        assert_arrays_eq!(&builder.finish(), &array, &mut ctx);
635        Ok(())
636    }
637
638    #[test]
639    fn test_struct_constant_append() -> VortexResult<()> {
640        let fields = StructFields::new(
641            ["x", "y"].into(),
642            vec![
643                DType::Primitive(PType::I32, Nullability::NonNullable),
644                DType::Utf8(Nullability::NonNullable),
645            ],
646        );
647        let scalar = Scalar::struct_(
648            DType::Struct(fields, Nullability::NonNullable),
649            [
650                Scalar::primitive(42i32, Nullability::NonNullable),
651                Scalar::utf8("hi", Nullability::NonNullable),
652            ],
653        );
654        assert_append_matches_canonical(ConstantArray::new(scalar, 3))
655    }
656
657    #[test]
658    fn test_null_struct_constant_append() -> VortexResult<()> {
659        let fields = StructFields::new(
660            ["x"].into(),
661            vec![DType::Primitive(PType::I32, Nullability::Nullable)],
662        );
663        let dtype = DType::Struct(fields, Nullability::Nullable);
664        assert_append_matches_canonical(ConstantArray::new(Scalar::null(dtype), 4))
665    }
666
667    /// A run of identical structs should leave each field constant-encoded rather than materialize
668    /// a value per row per field.
669    #[test]
670    fn test_struct_constant_append_keeps_fields_constant() -> VortexResult<()> {
671        let mut ctx = crate::array_session().create_execution_ctx();
672        let fields = StructFields::new(
673            ["x", "y"].into(),
674            vec![
675                DType::Primitive(PType::I32, Nullability::NonNullable),
676                DType::Utf8(Nullability::NonNullable),
677            ],
678        );
679        let scalar = Scalar::struct_(
680            DType::Struct(fields, Nullability::NonNullable),
681            [
682                Scalar::primitive(42i32, Nullability::NonNullable),
683                Scalar::utf8("hi", Nullability::NonNullable),
684            ],
685        );
686        let array = ConstantArray::new(scalar, 1_000);
687
688        let mut builder = builder_with_capacity_in(
689            array.dtype(),
690            array.len(),
691            vortex_buffer::BufferAllocatorRef::static_ref(),
692        );
693        array
694            .into_array()
695            .append_to_builder(builder.as_mut(), &mut ctx)?;
696        let result = builder.finish();
697
698        let struct_array = result.as_::<Struct>();
699        for field in 0..2 {
700            assert!(
701                struct_array.unmasked_field(field).is::<Constant>(),
702                "field {field} should have stayed constant-encoded",
703            );
704        }
705        Ok(())
706    }
707
708    #[rstest]
709    #[case::non_uniform(vec![Scalar::from(1i32), Scalar::from(2i32)])]
710    #[case::uniform(vec![Scalar::from(7i32), Scalar::from(7i32)])]
711    fn test_fixed_size_list_constant_append(#[case] elements: Vec<Scalar>) -> VortexResult<()> {
712        let scalar = Scalar::fixed_size_list(
713            Arc::new(DType::Primitive(PType::I32, Nullability::NonNullable)),
714            elements,
715            Nullability::NonNullable,
716        );
717        assert_append_matches_canonical(ConstantArray::new(scalar, 4))
718    }
719
720    #[test]
721    fn test_null_fixed_size_list_constant_append() -> VortexResult<()> {
722        let dtype = DType::FixedSizeList(
723            Arc::new(DType::Primitive(PType::I32, Nullability::NonNullable)),
724            2,
725            Nullability::Nullable,
726        );
727        assert_append_matches_canonical(ConstantArray::new(Scalar::null(dtype), 3))
728    }
729
730    /// A fixed-size list whose elements are all the same scalar tiles a constant array, so the
731    /// tile's chunks stay constant-encoded rather than materializing a value per row.
732    #[test]
733    fn test_uniform_fixed_size_list_constant_append_keeps_elements_constant() -> VortexResult<()> {
734        let mut ctx = crate::array_session().create_execution_ctx();
735        let scalar = Scalar::fixed_size_list(
736            Arc::new(DType::Primitive(PType::I32, Nullability::NonNullable)),
737            vec![Scalar::from(7i32), Scalar::from(7i32)],
738            Nullability::NonNullable,
739        );
740        let array = ConstantArray::new(scalar, 1_000);
741
742        let mut builder = builder_with_capacity_in(
743            array.dtype(),
744            array.len(),
745            vortex_buffer::BufferAllocatorRef::static_ref(),
746        );
747        array
748            .into_array()
749            .append_to_builder(builder.as_mut(), &mut ctx)?;
750        let result = builder.finish();
751
752        let elements = result.as_::<FixedSizeList>().elements().clone();
753        assert!(
754            elements
755                .as_::<Chunked>()
756                .iter_chunks()
757                .all(|chunk| chunk.is::<Constant>()),
758            "a uniform tile should have stayed constant-encoded",
759        );
760        Ok(())
761    }
762
763    #[test]
764    fn test_extension_constant_append() -> VortexResult<()> {
765        let scalar = Scalar::extension::<Date>(TimeUnit::Days, Scalar::from(Some(42i32)));
766        assert_append_matches_canonical(ConstantArray::new(scalar, 5))
767    }
768
769    /// An extension array is its storage wearing a dtype, so a run of identical values should leave
770    /// the storage constant-encoded.
771    #[test]
772    fn test_extension_constant_append_keeps_storage_constant() -> VortexResult<()> {
773        let mut ctx = crate::array_session().create_execution_ctx();
774        let scalar = Scalar::extension::<Date>(TimeUnit::Days, Scalar::from(Some(42i32)));
775        let array = ConstantArray::new(scalar, 1_000);
776
777        let mut builder = builder_with_capacity_in(
778            array.dtype(),
779            array.len(),
780            vortex_buffer::BufferAllocatorRef::static_ref(),
781        );
782        array
783            .into_array()
784            .append_to_builder(builder.as_mut(), &mut ctx)?;
785        let result = builder.finish();
786
787        assert!(
788            result.as_::<Extension>().storage_array().is::<Constant>(),
789            "the storage should have stayed constant-encoded",
790        );
791        Ok(())
792    }
793}