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 vortex_buffer::ByteBufferMut;
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::constant::ConstantData;
30use crate::arrays::constant::compute::rules::PARENT_RULES;
31use crate::arrays::constant::vtable::canonical::constant_canonicalize;
32use crate::buffer::BufferHandle;
33use crate::builders::ArrayBuilder;
34use crate::builders::BoolBuilder;
35use crate::builders::DecimalBuilder;
36use crate::builders::NullBuilder;
37use crate::builders::PrimitiveBuilder;
38use crate::builders::VarBinViewBuilder;
39use crate::canonical::Canonical;
40use crate::dtype::DType;
41use crate::match_each_decimal_value;
42use crate::match_each_native_ptype;
43use crate::match_each_varbin_builder;
44use crate::scalar::DecimalValue;
45use crate::scalar::Scalar;
46use crate::scalar::ScalarValue;
47use crate::serde::ArrayChildren;
48pub(crate) mod canonical;
49mod operations;
50mod validity;
51
52/// A [`Constant`]-encoded Vortex array.
53pub type ConstantArray = Array<Constant>;
54
55#[derive(Clone, Debug)]
56pub struct Constant;
57
58impl ArrayHash for ConstantData {
59    fn array_hash<H: Hasher>(&self, state: &mut H, _accuracy: EqMode) {
60        self.scalar.hash(state);
61    }
62}
63
64impl ArrayEq for ConstantData {
65    fn array_eq(&self, other: &Self, _accuracy: EqMode) -> bool {
66        self.scalar == other.scalar
67    }
68}
69
70impl VTable for Constant {
71    type TypedArrayData = ConstantData;
72
73    type OperationsVTable = Self;
74    type ValidityVTable = Self;
75
76    fn id(&self) -> ArrayId {
77        static ID: CachedId = CachedId::new("vortex.constant");
78        *ID
79    }
80
81    fn validate(
82        &self,
83        data: &ConstantData,
84        dtype: &DType,
85        _len: usize,
86        _slots: &[Option<ArrayRef>],
87    ) -> VortexResult<()> {
88        vortex_ensure!(
89            data.scalar.dtype() == dtype,
90            "ConstantArray scalar dtype does not match outer dtype"
91        );
92        Ok(())
93    }
94
95    fn nbuffers(_array: ArrayView<'_, Self>) -> usize {
96        1
97    }
98
99    fn buffer(array: ArrayView<'_, Self>, idx: usize) -> BufferHandle {
100        match idx {
101            0 => BufferHandle::new_host(
102                ScalarValue::to_proto_bytes::<ByteBufferMut>(array.scalar.value()).freeze(),
103            ),
104            _ => vortex_panic!("ConstantArray buffer index {idx} out of bounds"),
105        }
106    }
107
108    fn buffer_name(_array: ArrayView<'_, Self>, idx: usize) -> Option<String> {
109        match idx {
110            0 => Some("scalar".to_string()),
111            _ => None,
112        }
113    }
114
115    fn with_buffers(
116        &self,
117        array: ArrayView<'_, Self>,
118        buffers: &[BufferHandle],
119    ) -> VortexResult<ArrayParts<Self>> {
120        unsupported_buffer_replacement(array, buffers)
121    }
122
123    fn slot_name(_array: ArrayView<'_, Self>, idx: usize) -> String {
124        vortex_panic!("ConstantArray slot_name index {idx} out of bounds")
125    }
126
127    fn serialize(
128        _array: ArrayView<'_, Self>,
129        _session: &VortexSession,
130    ) -> VortexResult<Option<Vec<u8>>> {
131        // HACK: Because the scalar is stored in the buffers, we do not need to serialize the
132        // metadata at all.
133        Ok(Some(vec![]))
134    }
135
136    fn deserialize(
137        &self,
138        dtype: &DType,
139        len: usize,
140        _metadata: &[u8],
141
142        buffers: &[BufferHandle],
143        _children: &dyn ArrayChildren,
144        session: &VortexSession,
145    ) -> VortexResult<ArrayParts<Self>> {
146        vortex_ensure!(
147            buffers.len() == 1,
148            "Expected 1 buffer, got {}",
149            buffers.len()
150        );
151
152        let buffer = buffers[0].clone().try_to_host_sync()?;
153        let bytes: &[u8] = buffer.as_ref();
154
155        let scalar_value = ScalarValue::from_proto_bytes(bytes, dtype, session)?;
156        let scalar = Scalar::try_new(dtype.clone(), scalar_value)?;
157
158        Ok(ArrayParts::new(
159            self.clone(),
160            dtype.clone(),
161            len,
162            ConstantData::new(scalar),
163        ))
164    }
165
166    fn reduce_parent(
167        array: ArrayView<'_, Self>,
168        parent: &ArrayRef,
169        child_idx: usize,
170    ) -> VortexResult<Option<ArrayRef>> {
171        PARENT_RULES.evaluate(array, parent, child_idx)
172    }
173
174    fn execute(array: Array<Self>, ctx: &mut ExecutionCtx) -> VortexResult<ExecutionResult> {
175        Ok(ExecutionResult::done(constant_canonicalize(
176            array.as_view(),
177            ctx,
178        )?))
179    }
180
181    fn append_to_builder(
182        array: ArrayView<'_, Self>,
183        builder: &mut dyn ArrayBuilder,
184        ctx: &mut ExecutionCtx,
185    ) -> VortexResult<()> {
186        let n = array.len();
187        let scalar = array.scalar();
188
189        match array.dtype() {
190            DType::Null => append_value_or_nulls::<NullBuilder>(builder, true, n, |_| {}),
191            DType::Bool(_) => {
192                append_value_or_nulls::<BoolBuilder>(builder, scalar.is_null(), n, |b| {
193                    b.append_values(
194                        scalar
195                            .as_bool()
196                            .value()
197                            .vortex_expect("non-null bool scalar must have a value"),
198                        n,
199                    );
200                })
201            }
202            DType::Primitive(ptype, _) => {
203                match_each_native_ptype!(ptype, |P| {
204                    append_value_or_nulls::<PrimitiveBuilder<P>>(
205                        builder,
206                        scalar.is_null(),
207                        n,
208                        |b| {
209                            let value = P::try_from(scalar)
210                                .vortex_expect("Couldn't unwrap constant scalar to primitive");
211                            b.append_n_values(value, n);
212                        },
213                    );
214                });
215            }
216            DType::Decimal(..) => {
217                append_value_or_nulls::<DecimalBuilder>(builder, scalar.is_null(), n, |b| {
218                    let value = scalar
219                        .as_decimal()
220                        .decimal_value()
221                        .vortex_expect("non-null decimal scalar must have a value");
222                    match_each_decimal_value!(value, |v| { b.append_n_values(v, n) });
223                });
224            }
225            DType::Utf8(_) => {
226                if let Some(result) = match_each_varbin_builder!(builder, |builder| {
227                    builder.append_scalar_repeated(scalar, n)
228                }) {
229                    result?;
230                } else {
231                    append_value_or_nulls::<VarBinViewBuilder>(builder, scalar.is_null(), n, |b| {
232                        let value = scalar
233                            .as_utf8()
234                            .value()
235                            .vortex_expect("non-null utf8 scalar must have a value");
236                        b.append_n_values(value.as_bytes(), n);
237                    });
238                }
239            }
240            DType::Binary(_) => {
241                if let Some(result) = match_each_varbin_builder!(builder, |builder| {
242                    builder.append_scalar_repeated(scalar, n)
243                }) {
244                    result?;
245                } else {
246                    append_value_or_nulls::<VarBinViewBuilder>(builder, scalar.is_null(), n, |b| {
247                        let value = scalar
248                            .as_binary()
249                            .value()
250                            .vortex_expect("non-null binary scalar must have a value");
251                        b.append_n_values(value, n);
252                    });
253                }
254            }
255            // TODO: add fast paths for DType::Struct, DType::List, DType::FixedSizeList, DType::Extension.
256            _ => {
257                let canonical = array
258                    .array()
259                    .clone()
260                    .execute::<Canonical>(ctx)?
261                    .into_array();
262                canonical.append_to_builder(builder, ctx)?;
263            }
264        }
265
266        Ok(())
267    }
268}
269
270/// Downcasts `builder` to `B`, then either appends `n` nulls or calls `fill` with the typed
271/// builder depending on `is_null`.
272///
273/// `is_null` must only be `true` when the builder is nullable.
274fn append_value_or_nulls<B: ArrayBuilder + 'static>(
275    builder: &mut dyn ArrayBuilder,
276    is_null: bool,
277    n: usize,
278    fill: impl FnOnce(&mut B),
279) {
280    let b = builder
281        .as_any_mut()
282        .downcast_mut::<B>()
283        .vortex_expect("builder dtype must match array dtype");
284    if is_null {
285        // SAFETY: is_null=true only when the scalar (and thus the builder) is nullable.
286        unsafe { b.append_nulls_unchecked(n) };
287    } else {
288        fill(b);
289    }
290}
291
292#[cfg(test)]
293mod tests {
294    use rstest::rstest;
295    use vortex_error::VortexResult;
296
297    use crate::IntoArray;
298    use crate::VortexSessionExecute;
299    use crate::arrays::ConstantArray;
300    use crate::arrays::constant::vtable::canonical::constant_canonicalize;
301    use crate::assert_arrays_eq;
302    use crate::builders::builder_with_capacity;
303    use crate::dtype::DType;
304    use crate::dtype::Nullability;
305    use crate::dtype::PType;
306    use crate::dtype::StructFields;
307    use crate::scalar::Scalar;
308
309    /// Appends `array` into a fresh builder and asserts the result matches `constant_canonicalize`.
310    fn assert_append_matches_canonical(array: ConstantArray) -> VortexResult<()> {
311        let mut ctx = crate::array_session().create_execution_ctx();
312
313        let expected = constant_canonicalize(array.as_view(), &mut ctx)?.into_array();
314        let mut builder = builder_with_capacity(array.dtype(), array.len());
315        array
316            .into_array()
317            .append_to_builder(builder.as_mut(), &mut ctx)?;
318        let result = builder.finish();
319        assert_arrays_eq!(&result, &expected, &mut ctx);
320        Ok(())
321    }
322
323    #[test]
324    fn test_null_constant_append() -> VortexResult<()> {
325        assert_append_matches_canonical(ConstantArray::new(Scalar::null(DType::Null), 5))
326    }
327
328    #[test]
329    fn test_with_buffers_rejects_serialized_scalar_buffer() {
330        let array =
331            ConstantArray::new(Scalar::primitive(42i32, Nullability::NonNullable), 3).into_array();
332        let buffers = array.buffer_handles();
333
334        // SAFETY: the replacement buffers are the array's existing buffers, so the logical values
335        // would be unchanged if the encoding supported buffer replacement.
336        let Err(err) = (unsafe { array.with_buffers(buffers) }) else {
337            panic!("ConstantArray should reject replacing its serialized scalar buffer");
338        };
339        assert!(
340            err.to_string()
341                .contains("does not support in-memory buffer replacement")
342        );
343    }
344
345    #[rstest]
346    #[case::bool_true(true, 5)]
347    #[case::bool_false(false, 3)]
348    fn test_bool_constant_append(#[case] value: bool, #[case] n: usize) -> VortexResult<()> {
349        assert_append_matches_canonical(ConstantArray::new(
350            Scalar::bool(value, Nullability::NonNullable),
351            n,
352        ))
353    }
354
355    #[test]
356    fn test_bool_null_constant_append() -> VortexResult<()> {
357        assert_append_matches_canonical(ConstantArray::new(
358            Scalar::null(DType::Bool(Nullability::Nullable)),
359            4,
360        ))
361    }
362
363    #[rstest]
364    #[case::i32(Scalar::primitive(42i32, Nullability::NonNullable), 5)]
365    #[case::u8(Scalar::primitive(7u8, Nullability::NonNullable), 3)]
366    #[case::f64(Scalar::primitive(1.5f64, Nullability::NonNullable), 4)]
367    #[case::i32_null(Scalar::null(DType::Primitive(PType::I32, Nullability::Nullable)), 3)]
368    fn test_primitive_constant_append(
369        #[case] scalar: Scalar,
370        #[case] n: usize,
371    ) -> VortexResult<()> {
372        assert_append_matches_canonical(ConstantArray::new(scalar, n))
373    }
374
375    #[rstest]
376    #[case::utf8_inline("hi", 5)] // ≤12 bytes: inlined in BinaryView
377    #[case::utf8_noninline("hello world!!", 5)] // >12 bytes: requires buffer block
378    #[case::utf8_empty("", 3)]
379    #[case::utf8_n_zero("hello world!!", 0)] // n=0 with non-inline: must not write orphaned bytes
380    fn test_utf8_constant_append(#[case] value: &str, #[case] n: usize) -> VortexResult<()> {
381        assert_append_matches_canonical(ConstantArray::new(
382            Scalar::utf8(value, Nullability::NonNullable),
383            n,
384        ))
385    }
386
387    #[test]
388    fn test_utf8_null_constant_append() -> VortexResult<()> {
389        assert_append_matches_canonical(ConstantArray::new(
390            Scalar::null(DType::Utf8(Nullability::Nullable)),
391            4,
392        ))
393    }
394
395    #[rstest]
396    #[case::binary_inline(vec![1u8, 2, 3], 5)] // ≤12 bytes: inlined
397    #[case::binary_noninline(vec![0u8; 13], 5)] // >12 bytes: buffer block
398    fn test_binary_constant_append(#[case] value: Vec<u8>, #[case] n: usize) -> VortexResult<()> {
399        assert_append_matches_canonical(ConstantArray::new(
400            Scalar::binary(value, Nullability::NonNullable),
401            n,
402        ))
403    }
404
405    #[test]
406    fn test_binary_null_constant_append() -> VortexResult<()> {
407        assert_append_matches_canonical(ConstantArray::new(
408            Scalar::null(DType::Binary(Nullability::Nullable)),
409            4,
410        ))
411    }
412
413    #[test]
414    fn test_struct_constant_append() -> VortexResult<()> {
415        let fields = StructFields::new(
416            ["x", "y"].into(),
417            vec![
418                DType::Primitive(PType::I32, Nullability::NonNullable),
419                DType::Utf8(Nullability::NonNullable),
420            ],
421        );
422        let scalar = Scalar::struct_(
423            DType::Struct(fields, Nullability::NonNullable),
424            [
425                Scalar::primitive(42i32, Nullability::NonNullable),
426                Scalar::utf8("hi", Nullability::NonNullable),
427            ],
428        );
429        assert_append_matches_canonical(ConstantArray::new(scalar, 3))
430    }
431
432    #[test]
433    fn test_null_struct_constant_append() -> VortexResult<()> {
434        let fields = StructFields::new(
435            ["x"].into(),
436            vec![DType::Primitive(PType::I32, Nullability::Nullable)],
437        );
438        let dtype = DType::Struct(fields, Nullability::Nullable);
439        assert_append_matches_canonical(ConstantArray::new(Scalar::null(dtype), 4))
440    }
441}