Skip to content

Commit d308d66

Browse files
committed
fix: compare for struct with constant items
subtle bug introduced in #5412, where the nested elements are constant, would change the len of `lhs` and cause the assertion to fail. Fix is to assert the length earlier. Signed-off-by: Andrew Duffy <[email protected]>
1 parent 243deeb commit d308d66

File tree

3 files changed

+82
-6
lines changed

3 files changed

+82
-6
lines changed

vortex-array/src/compute/compare.rs

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,9 @@ fn arrow_compare(
294294
right: &dyn Array,
295295
operator: Operator,
296296
) -> VortexResult<ArrayRef> {
297+
assert_eq!(left.len(), right.len());
298+
let len = left.len();
299+
297300
let nullable = left.dtype().is_nullable() || right.dtype().is_nullable();
298301

299302
let array = if left.dtype().is_nested() || right.dtype().is_nested() {
@@ -312,8 +315,6 @@ fn arrow_compare(
312315
);
313316

314317
let cmp = make_comparator(lhs, rhs, SortOptions::default())?;
315-
assert_eq!(lhs.len(), rhs.len());
316-
let len = lhs.len();
317318
let values = (0..len)
318319
.map(|i| {
319320
let cmp = cmp(i, i);
@@ -365,6 +366,7 @@ pub fn scalar_cmp(lhs: &Scalar, rhs: &Scalar, operator: Operator) -> Scalar {
365366
#[cfg(test)]
366367
mod tests {
367368
use rstest::rstest;
369+
use vortex_dtype::{FieldName, FieldNames};
368370

369371
use super::*;
370372
use crate::ToCanonical;
@@ -572,4 +574,30 @@ mod tests {
572574
assert!(!bool_result.bit_buffer().value(1)); // {false, 2} > {false, 2} = false
573575
assert!(bool_result.bit_buffer().value(2)); // {true, 3} > {false, 4} = true (bool field takes precedence)
574576
}
577+
578+
#[test]
579+
fn test_empty_struct_compare() {
580+
let empty1 = StructArray::try_new(
581+
FieldNames::from(Vec::<FieldName>::new()),
582+
Vec::new(),
583+
5,
584+
Validity::NonNullable,
585+
)
586+
.unwrap();
587+
588+
let empty2 = StructArray::try_new(
589+
FieldNames::from(Vec::<FieldName>::new()),
590+
Vec::new(),
591+
5,
592+
Validity::NonNullable,
593+
)
594+
.unwrap();
595+
596+
let result = compare(empty1.as_ref(), empty2.as_ref(), Operator::Eq).unwrap();
597+
let result = result.to_bool();
598+
599+
for idx in 0..5 {
600+
assert!(result.bit_buffer().value(idx));
601+
}
602+
}
575603
}

vortex-layout/src/layouts/struct_/reader.rs

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,7 @@ impl LayoutReader for StructReader {
261261
}
262262
}
263263

264+
#[allow(clippy::unwrap_in_result)]
264265
fn projection_evaluation(
265266
&self,
266267
row_range: &Range<u64>,
@@ -331,7 +332,7 @@ mod tests {
331332
use itertools::Itertools;
332333
use rstest::{fixture, rstest};
333334
use vortex_array::arrays::{BoolArray, StructArray};
334-
use vortex_array::expr::{col, eq, get_item, gt, lit, or, pack, root, select};
335+
use vortex_array::expr::{Expression, col, eq, get_item, gt, lit, or, pack, root, select};
335336
use vortex_array::validity::Validity;
336337
use vortex_array::{Array, ArrayContext, IntoArray, MaskFuture, ToCanonical};
337338
use vortex_buffer::buffer;
@@ -346,6 +347,36 @@ mod tests {
346347
use crate::sequence::{SequenceId, SequentialArrayStreamExt};
347348
use crate::{LayoutRef, LayoutStrategy};
348349

350+
#[fixture]
351+
fn empty_struct() -> (Arc<dyn SegmentSource>, LayoutRef) {
352+
let ctx = ArrayContext::empty();
353+
let segments = Arc::new(TestSegments::default());
354+
let (ptr, eof) = SequenceId::root().split();
355+
let strategy =
356+
StructStrategy::new(FlatLayoutStrategy::default(), FlatLayoutStrategy::default());
357+
let layout = block_on(|handle| {
358+
strategy.write_stream(
359+
ctx,
360+
segments.clone(),
361+
StructArray::try_new(
362+
Vec::<FieldName>::new().into(),
363+
vec![],
364+
5,
365+
Validity::NonNullable,
366+
)
367+
.unwrap()
368+
.into_array()
369+
.to_array_stream()
370+
.sequenced(ptr),
371+
eof,
372+
handle,
373+
)
374+
})
375+
.unwrap();
376+
377+
(segments, layout)
378+
}
379+
349380
#[fixture]
350381
/// Create a chunked layout with three chunks of primitive arrays.
351382
fn struct_layout() -> (Arc<dyn SegmentSource>, LayoutRef) {
@@ -633,4 +664,21 @@ mod tests {
633664
Scalar::primitive(6, Nullability::Nullable)
634665
);
635666
}
667+
668+
#[rstest]
669+
fn test_empty_struct(
670+
#[from(empty_struct)] (segments, layout): (Arc<dyn SegmentSource>, LayoutRef),
671+
) {
672+
let reader = layout.new_reader("".into(), segments).unwrap();
673+
let expr = pack(Vec::<(String, Expression)>::new(), Nullability::Nullable);
674+
675+
let project = reader
676+
.projection_evaluation(&(0..5), &expr, MaskFuture::new_true(5))
677+
.unwrap();
678+
679+
let result = block_on(move |_| project).unwrap();
680+
assert!(result.dtype().is_struct());
681+
682+
assert_eq!(result.len(), 5);
683+
}
636684
}

vortex-layout/src/layouts/struct_/writer.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,11 @@ impl LayoutStrategy for StructStrategy {
6464
vortex_bail!("StructLayout must have unique field names");
6565
}
6666

67+
let is_nullable = dtype.is_nullable();
68+
6769
// Optimization: when there are no fields, don't spawn any work and just write a trivial
6870
// StructLayout.
69-
if struct_dtype.nfields() == 0 {
71+
if struct_dtype.nfields() == 0 && !is_nullable {
7072
let row_count = stream
7173
.try_fold(
7274
0u64,
@@ -77,8 +79,6 @@ impl LayoutStrategy for StructStrategy {
7779
}
7880

7981
// stream<struct_chunk> -> stream<vec<column_chunk>>
80-
let is_nullable = dtype.is_nullable();
81-
8282
let columns_vec_stream = stream.map(move |chunk| {
8383
let (sequence_id, chunk) = chunk?;
8484
let mut sequence_pointer = sequence_id.descend();

0 commit comments

Comments
 (0)