Skip to content

Commit 64c3464

Browse files
committed
fixes
Signed-off-by: Robert Kruszewski <github@robertk.io>
1 parent a74f80b commit 64c3464

7 files changed

Lines changed: 208 additions & 239 deletions

File tree

‎vortex-array/src/arrays/constant/vtable/mod.rs‎

Lines changed: 180 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use std::fmt::Debug;
55
use std::hash::Hash;
66
use std::hash::Hasher;
77

8+
use itertools::Itertools;
89
use vortex_buffer::ByteBufferMut;
910
use vortex_error::VortexExpect;
1011
use vortex_error::VortexResult;
@@ -26,26 +27,29 @@ use crate::array::ArrayId;
2627
use crate::array::ArrayView;
2728
use crate::array::VTable;
2829
use crate::array::unsupported_buffer_replacement;
30+
use crate::arrays::ExtensionArray;
2931
use crate::arrays::constant::ConstantData;
3032
use crate::arrays::constant::compute::rules::PARENT_RULES;
3133
use crate::arrays::constant::vtable::canonical::constant_canonicalize;
3234
use crate::buffer::BufferHandle;
3335
use crate::builders::ArrayBuilder;
3436
use crate::builders::BoolBuilder;
3537
use crate::builders::DecimalBuilder;
36-
use crate::builders::ExtensionBuilder;
3738
use crate::builders::FixedSizeListBuilder;
39+
use crate::builders::ListViewBuilder;
3840
use crate::builders::NullBuilder;
3941
use crate::builders::PrimitiveBuilder;
40-
use crate::builders::StructBuilder;
4142
use crate::builders::VarBinViewBuilder;
43+
use crate::builders::builder_with_capacity;
4244
use crate::canonical::Canonical;
4345
use crate::dtype::DType;
46+
use crate::dtype::OffsetBuilderPType;
4447
use crate::match_each_decimal_value;
45-
use crate::match_each_list_builder;
48+
use crate::match_each_listview_builder;
4649
use crate::match_each_native_ptype;
4750
use crate::match_each_varbin_builder;
4851
use crate::scalar::DecimalValue;
52+
use crate::scalar::ListScalar;
4953
use crate::scalar::Scalar;
5054
use crate::scalar::ScalarValue;
5155
use crate::serde::ArrayChildren;
@@ -257,20 +261,22 @@ impl VTable for Constant {
257261
}
258262
}
259263
DType::List(..) => append_constant_list_run(array, n, builder, ctx)?,
260-
DType::Struct(..) => match builder.as_any_mut().downcast_mut::<StructBuilder>() {
261-
Some(b) => b.append_constant(scalar.as_struct(), n, ctx)?,
262-
None => append_via_canonical(array, builder, ctx)?,
263-
},
264-
DType::Extension(..) => match builder.as_any_mut().downcast_mut::<ExtensionBuilder>() {
265-
Some(b) => b.append_constant(scalar.as_extension(), n, ctx)?,
266-
None => append_via_canonical(array, builder, ctx)?,
267-
},
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+
}
268274
DType::FixedSizeList(..) => {
269-
match builder.as_any_mut().downcast_mut::<FixedSizeListBuilder>() {
270-
Some(b) => b.append_constant(scalar.as_list(), n, ctx)?,
271-
None => append_via_canonical(array, builder, ctx)?,
272-
}
275+
append_constant_fixed_size_list_run(array, n, builder, ctx)?
273276
}
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.
274280
// TODO: add a fast path for DType::Union once it has a builder.
275281
_ => append_via_canonical(array, builder, ctx)?,
276282
}
@@ -279,20 +285,22 @@ impl VTable for Constant {
279285
}
280286
}
281287

282-
/// Appends the constant list `array` as one repeated run per list builder.
288+
/// Appends the constant list `array` as one run sharing a single copy of its elements.
283289
///
284-
/// Only the concrete list builders can record a run from the scalar; any other builder for a list
285-
/// dtype has to go the long way around.
286-
// The complexity comes from the expansion of `match_each_list_builder!`.
287-
#[expect(clippy::cognitive_complexity)]
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.
288295
fn append_constant_list_run(
289296
array: ArrayView<'_, Constant>,
290297
n: usize,
291298
builder: &mut dyn ArrayBuilder,
292299
ctx: &mut ExecutionCtx,
293300
) -> VortexResult<()> {
294301
let scalar = array.scalar();
295-
match match_each_list_builder!(builder, |b| b.append_constant_list(
302+
match match_each_listview_builder!(builder, |b| append_repeated_list_run(
303+
b,
296304
scalar.as_list(),
297305
n,
298306
ctx
@@ -302,6 +310,71 @@ fn append_constant_list_run(
302310
}
303311
}
304312

313+
/// Appends the list `scalar` to a [`ListViewBuilder`] `n` times, storing its elements once.
314+
fn 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.
343+
fn 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+
305378
/// Appends `array` by canonicalizing it first, for the dtypes with no fast path of their own.
306379
fn append_via_canonical(
307380
array: ArrayView<'_, Constant>,
@@ -347,16 +420,22 @@ mod tests {
347420

348421
use crate::IntoArray;
349422
use crate::VortexSessionExecute;
423+
use crate::arrays::Chunked;
350424
use crate::arrays::Constant;
351425
use crate::arrays::ConstantArray;
352426
use crate::arrays::Extension;
353427
use crate::arrays::FixedSizeList;
428+
use crate::arrays::ListView;
354429
use crate::arrays::Struct;
430+
use crate::arrays::chunked::ChunkedArrayExt;
355431
use crate::arrays::constant::vtable::canonical::constant_canonicalize;
356432
use crate::arrays::extension::ExtensionArrayExt;
357433
use crate::arrays::fixed_size_list::FixedSizeListArraySlotsExt;
434+
use crate::arrays::listview::ListViewArraySlotsExt;
358435
use crate::arrays::struct_::StructArrayExt;
359436
use crate::assert_arrays_eq;
437+
use crate::builders::ArrayBuilder;
438+
use crate::builders::ListBuilder;
360439
use crate::builders::builder_with_capacity;
361440
use crate::dtype::DType;
362441
use crate::dtype::Nullability;
@@ -470,6 +549,79 @@ mod tests {
470549
))
471550
}
472551

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+
473625
#[test]
474626
fn test_struct_constant_append() -> VortexResult<()> {
475627
let fields = StructFields::new(
@@ -558,8 +710,8 @@ mod tests {
558710
assert_append_matches_canonical(ConstantArray::new(Scalar::null(dtype), 3))
559711
}
560712

561-
/// A fixed-size list whose elements are all the same scalar tiles to a constant array, so the
562-
/// elements child should not be materialized at all.
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.
563715
#[test]
564716
fn test_uniform_fixed_size_list_constant_append_keeps_elements_constant() -> VortexResult<()> {
565717
let mut ctx = crate::array_session().create_execution_ctx();
@@ -576,8 +728,12 @@ mod tests {
576728
.append_to_builder(builder.as_mut(), &mut ctx)?;
577729
let result = builder.finish();
578730

731+
let elements = result.as_::<FixedSizeList>().elements().clone();
579732
assert!(
580-
result.as_::<FixedSizeList>().elements().is::<Constant>(),
733+
elements
734+
.as_::<Chunked>()
735+
.iter_chunks()
736+
.all(|chunk| chunk.is::<Constant>()),
581737
"a uniform tile should have stayed constant-encoded",
582738
);
583739
Ok(())

‎vortex-array/src/builders/extension.rs‎

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ use vortex_error::vortex_ensure;
99
use crate::ArrayRef;
1010
use crate::ExecutionCtx;
1111
use crate::IntoArray;
12-
use crate::arrays::ConstantArray;
1312
use crate::arrays::ExtensionArray;
1413
use crate::arrays::extension::ExtensionArrayExt;
1514
use crate::builders::ArrayBuilder;
@@ -56,24 +55,6 @@ impl ExtensionBuilder {
5655
self.storage.append_array(array.storage_array(), ctx)
5756
}
5857

59-
/// Appends the same extension `value` `n` times.
60-
///
61-
/// An extension array is its storage array wearing a dtype, so a run of identical values is a
62-
/// constant storage array, which goes in as a single chunk and stays constant-encoded.
63-
pub(crate) fn append_constant(
64-
&mut self,
65-
value: ExtScalar,
66-
n: usize,
67-
ctx: &mut ExecutionCtx,
68-
) -> VortexResult<()> {
69-
if n == 0 {
70-
return Ok(());
71-
}
72-
73-
let storage = ConstantArray::new(value.to_storage_scalar(), n).into_array();
74-
self.storage.append_array(&storage, ctx)
75-
}
76-
7758
/// Finishes the builder directly into a [`ExtensionArray`].
7859
pub fn finish_into_extension(&mut self) -> ExtensionArray {
7960
let storage = self.storage.finish();

0 commit comments

Comments
 (0)