From 620594b82b272f74bc75b5c313a590c6cc8bfe31 Mon Sep 17 00:00:00 2001 From: Rinat Shigapov Date: Wed, 4 Feb 2026 17:06:42 +0800 Subject: [PATCH] 4832: Implement IsSortedKernel and MinMaxKernel for List, ListView, and FixedSizeList --- vortex-array/Cargo.toml | 4 + vortex-array/benches/list_compute.rs | 115 ++++++++++++ .../fixed_size_list/compute/is_sorted.rs | 49 ++++- .../arrays/fixed_size_list/compute/min_max.rs | 36 +++- .../src/arrays/list/compute/is_sorted.rs | 171 +++++++++++++++++- .../src/arrays/list/compute/min_max.rs | 104 ++++++++++- .../src/arrays/listview/compute/is_sorted.rs | 49 ++++- .../src/arrays/listview/compute/min_max.rs | 36 +++- 8 files changed, 536 insertions(+), 28 deletions(-) create mode 100644 vortex-array/benches/list_compute.rs diff --git a/vortex-array/Cargo.toml b/vortex-array/Cargo.toml index 9ba20be3db3..d52631d65e7 100644 --- a/vortex-array/Cargo.toml +++ b/vortex-array/Cargo.toml @@ -156,6 +156,10 @@ harness = false name = "varbinview_zip" harness = false +[[bench]] +name = "list_compute" +harness = false + [[bench]] name = "take_primitive" harness = false diff --git a/vortex-array/benches/list_compute.rs b/vortex-array/benches/list_compute.rs new file mode 100644 index 00000000000..96ebee26545 --- /dev/null +++ b/vortex-array/benches/list_compute.rs @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright the Vortex contributors + +#![allow(clippy::unwrap_used)] + +use std::sync::Arc; + +use divan::Bencher; +use vortex_array::ArrayRef; +use vortex_array::builders::{ArrayBuilder, ListBuilder}; +use vortex_array::compute::is_sorted; +use vortex_array::compute::min_max; +use vortex_dtype::{DType, Nullability, PType}; + +fn main() { + divan::main(); +} + +const ARRAY_SIZE: usize = 1_000; +const LIST_SIZE: usize = 10; + +fn create_sorted_list_array() -> ArrayRef { + let element_dtype = Arc::new(DType::Primitive(PType::I32, Nullability::NonNullable)); + let nullability = Nullability::NonNullable; + let mut builder = ListBuilder::::new(element_dtype.clone(), nullability); + + for i in 0..ARRAY_SIZE { + let list_elements: Vec = (0..LIST_SIZE).map(|j| (i * LIST_SIZE + j) as i32).collect(); + let list_scalar = list_elements + .into_iter() + .map(|x| vortex_scalar::Scalar::primitive(x, Nullability::NonNullable)) + .collect::>(); + let list = vortex_scalar::Scalar::list( + element_dtype.clone(), + list_scalar, + Nullability::NonNullable, + ); + builder.append_value(list.as_list()).unwrap(); + } + + builder.finish() +} + +fn create_almost_sorted_list_array() -> ArrayRef { + // Create an array where the last two elements are swapped + // For simplicity, we'll create a new array with the swap + let element_dtype = Arc::new(DType::Primitive(PType::I32, Nullability::NonNullable)); + let nullability = Nullability::NonNullable; + let mut builder = ListBuilder::::new(element_dtype.clone(), nullability); + + for i in 0..ARRAY_SIZE { + let list_elements: Vec = if i == ARRAY_SIZE - 2 { + // Second to last: use last elements + ((ARRAY_SIZE - 1) * LIST_SIZE..ARRAY_SIZE * LIST_SIZE) + .map(|x| x as i32) + .collect() + } else if i == ARRAY_SIZE - 1 { + // Last: use second to last elements + ((ARRAY_SIZE - 2) * LIST_SIZE..(ARRAY_SIZE - 1) * LIST_SIZE) + .map(|x| x as i32) + .collect() + } else { + (i * LIST_SIZE..(i + 1) * LIST_SIZE).map(|x| x as i32).collect() + }; + + let list_scalar = list_elements + .into_iter() + .map(|x| vortex_scalar::Scalar::primitive(x, Nullability::NonNullable)) + .collect::>(); + let list = vortex_scalar::Scalar::list( + element_dtype.clone(), + list_scalar, + Nullability::NonNullable, + ); + builder.append_value(list.as_list()).unwrap(); + } + + builder.finish() +} + +#[divan::bench] +fn is_sorted_list_sorted(bencher: Bencher) { + let arr = create_sorted_list_array(); + + bencher + .with_inputs(|| &arr) + .bench_refs(|arr| is_sorted(*arr).unwrap()); +} + +#[divan::bench] +fn is_sorted_list_almost_sorted(bencher: Bencher) { + let arr = create_almost_sorted_list_array(); + + bencher + .with_inputs(|| &arr) + .bench_refs(|arr| is_sorted(*arr).unwrap()); +} + +#[divan::bench] +fn min_max_list_sorted(bencher: Bencher) { + let arr = create_sorted_list_array(); + + bencher + .with_inputs(|| &arr) + .bench_refs(|arr| min_max(*arr).unwrap()); +} + +#[divan::bench] +fn min_max_list_almost_sorted(bencher: Bencher) { + let arr = create_almost_sorted_list_array(); + + bencher + .with_inputs(|| &arr) + .bench_refs(|arr| min_max(*arr).unwrap()); +} \ No newline at end of file diff --git a/vortex-array/src/arrays/fixed_size_list/compute/is_sorted.rs b/vortex-array/src/arrays/fixed_size_list/compute/is_sorted.rs index b1a719a1ce1..d66fe9eba7f 100644 --- a/vortex-array/src/arrays/fixed_size_list/compute/is_sorted.rs +++ b/vortex-array/src/arrays/fixed_size_list/compute/is_sorted.rs @@ -1,7 +1,10 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: Copyright the Vortex contributors +use std::cmp::Ordering; + use vortex_error::VortexResult; +use vortex_scalar::ListScalar; use crate::arrays::FixedSizeListArray; use crate::arrays::FixedSizeListVTable; @@ -9,16 +12,48 @@ use crate::compute::IsSortedKernel; use crate::compute::IsSortedKernelAdapter; use crate::register_kernel; -/// IsSorted implementation for [`FixedSizeListArray`]. +/// Implementation of IsSortedKernel for FixedSizeListArray. +/// +/// This implementation uses lexicographic comparison of list elements. +/// Since all lists have the same fixed size, comparison is straightforward element-wise. +/// Null lists are considered the smallest values. +/// Non-comparable lists (which shouldn't occur for lists with the same element type) +/// are treated as making the array not sorted. impl IsSortedKernel for FixedSizeListVTable { - fn is_sorted(&self, _array: &FixedSizeListArray) -> VortexResult> { - // This would require comparing lists lexicographically. - Ok(None) + fn is_sorted(&self, array: &FixedSizeListArray) -> VortexResult> { + if array.len() <= 1 { + return Ok(Some(true)); + } + for i in 0..array.len() - 1 { + let scalar_a = array.scalar_at(i)?; + let scalar_b = array.scalar_at(i + 1)?; + let a = ListScalar::try_from(&scalar_a)?; + let b = ListScalar::try_from(&scalar_b)?; + // For is_sorted, we allow Less and Equal, but not Greater or incomparable (None) + match a.partial_cmp(&b) { + Some(Ordering::Greater) | None => return Ok(Some(false)), + _ => {} + } + } + Ok(Some(true)) } - fn is_strict_sorted(&self, _array: &FixedSizeListArray) -> VortexResult> { - // This would require comparing lists lexicographically without duplicates. - Ok(None) + fn is_strict_sorted(&self, array: &FixedSizeListArray) -> VortexResult> { + if array.len() <= 1 { + return Ok(Some(true)); + } + for i in 0..array.len() - 1 { + let scalar_a = array.scalar_at(i)?; + let scalar_b = array.scalar_at(i + 1)?; + let a = ListScalar::try_from(&scalar_a)?; + let b = ListScalar::try_from(&scalar_b)?; + // For is_strict_sorted, we only allow Less, not Equal, Greater, or incomparable (None) + match a.partial_cmp(&b) { + Some(Ordering::Greater | Ordering::Equal) | None => return Ok(Some(false)), + _ => {} + } + } + Ok(Some(true)) } } diff --git a/vortex-array/src/arrays/fixed_size_list/compute/min_max.rs b/vortex-array/src/arrays/fixed_size_list/compute/min_max.rs index 4e3d3d1f25e..9ae2a4d0bce 100644 --- a/vortex-array/src/arrays/fixed_size_list/compute/min_max.rs +++ b/vortex-array/src/arrays/fixed_size_list/compute/min_max.rs @@ -2,6 +2,8 @@ // SPDX-FileCopyrightText: Copyright the Vortex contributors use vortex_error::VortexResult; +use vortex_scalar::ListScalar; +use vortex_scalar::Scalar; use crate::arrays::FixedSizeListArray; use crate::arrays::FixedSizeListVTable; @@ -12,9 +14,37 @@ use crate::register_kernel; /// MinMax implementation for [`FixedSizeListArray`]. impl MinMaxKernel for FixedSizeListVTable { - fn min_max(&self, _array: &FixedSizeListArray) -> VortexResult> { - // This would require finding the lexicographically minimum and maximum lists. - Ok(None) + fn min_max(&self, array: &FixedSizeListArray) -> VortexResult> { + let mut min: Option = None; + let mut max: Option = None; + for i in 0..array.len() { + let scalar = array.scalar_at(i)?; + if scalar.is_null() { + continue; + } + let list_scalar = ListScalar::try_from(&scalar)?; + if let Some(current_min) = &min { + let current_min_list = ListScalar::try_from(current_min)?; + if list_scalar < current_min_list { + min = Some(scalar.clone()); + } + } else { + min = Some(scalar.clone()); + } + if let Some(current_max) = &max { + let current_max_list = ListScalar::try_from(current_max)?; + if list_scalar > current_max_list { + max = Some(scalar.clone()); + } + } else { + max = Some(scalar.clone()); + } + } + match (min, max) { + (Some(min), Some(max)) => Ok(Some(MinMaxResult { min, max })), + (None, None) => Ok(None), + _ => unreachable!("min and max should be set together or both remain None"), + } } } diff --git a/vortex-array/src/arrays/list/compute/is_sorted.rs b/vortex-array/src/arrays/list/compute/is_sorted.rs index 6761f3ddf11..148c2f64a70 100644 --- a/vortex-array/src/arrays/list/compute/is_sorted.rs +++ b/vortex-array/src/arrays/list/compute/is_sorted.rs @@ -1,7 +1,10 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: Copyright the Vortex contributors +use std::cmp::Ordering; + use vortex_error::VortexResult; +use vortex_scalar::ListScalar; use crate::arrays::ListArray; use crate::arrays::ListVTable; @@ -9,16 +12,172 @@ use crate::compute::IsSortedKernel; use crate::compute::IsSortedKernelAdapter; use crate::register_kernel; +/// Implementation of IsSortedKernel for ListArray. +/// +/// This implementation uses lexicographic comparison of list elements. +/// Lists are compared element-wise; if one list is a prefix of another, +/// the shorter list is considered less than the longer one. +/// Null lists are considered the smallest values. +/// Non-comparable lists (which shouldn't occur for lists with the same element type) +/// are treated as making the array not sorted. impl IsSortedKernel for ListVTable { - fn is_sorted(&self, _array: &ListArray) -> VortexResult> { - // This would require comparing lists lexicographically. - Ok(None) + fn is_sorted(&self, array: &ListArray) -> VortexResult> { + if array.len() <= 1 { + return Ok(Some(true)); + } + for i in 0..array.len() - 1 { + let scalar_a = array.scalar_at(i)?; + let scalar_b = array.scalar_at(i + 1)?; + let a = ListScalar::try_from(&scalar_a)?; + let b = ListScalar::try_from(&scalar_b)?; + // For is_sorted, we allow Less and Equal, but not Greater or incomparable (None) + match a.partial_cmp(&b) { + Some(Ordering::Greater) | None => return Ok(Some(false)), + _ => {} + } + } + Ok(Some(true)) } - fn is_strict_sorted(&self, _array: &ListArray) -> VortexResult> { - // This would require comparing lists lexicographically without duplicates. - Ok(None) + fn is_strict_sorted(&self, array: &ListArray) -> VortexResult> { + if array.len() <= 1 { + return Ok(Some(true)); + } + for i in 0..array.len() - 1 { + let scalar_a = array.scalar_at(i)?; + let scalar_b = array.scalar_at(i + 1)?; + let a = ListScalar::try_from(&scalar_a)?; + let b = ListScalar::try_from(&scalar_b)?; + // For is_strict_sorted, we only allow Less, not Equal, Greater, or incomparable (None) + match a.partial_cmp(&b) { + Some(Ordering::Greater | Ordering::Equal) | None => return Ok(Some(false)), + _ => {} + } + } + Ok(Some(true)) } } register_kernel!(IsSortedKernelAdapter(ListVTable).lift()); + +#[cfg(test)] +mod tests { + use std::sync::Arc; + + use vortex_dtype::{DType, Nullability, PType}; + use vortex_scalar::Scalar; + + use crate::ArrayRef; + use crate::builders::{ArrayBuilder as _, ListBuilder}; + use crate::compute::is_sorted; + use crate::compute::is_strict_sorted; + + fn create_list_array(values: Vec>>, validity: Vec) -> ArrayRef { + let element_dtype = Arc::new(DType::Primitive(PType::I32, Nullability::NonNullable)); + let nullability = Nullability::Nullable; + let mut builder = ListBuilder::::new(element_dtype, nullability); + for i in 0..validity.len() { + if validity[i] { + let value = values[i].as_ref().unwrap(); + let list_scalar = value.into_iter().map(|x| Scalar::primitive(*x, Nullability::NonNullable)).collect::>(); + let list = Scalar::list(Arc::new(DType::Primitive(PType::I32, Nullability::NonNullable)), list_scalar, Nullability::NonNullable); + builder.append_value(list.as_list()).unwrap(); + } else { + builder.append_null(); + } + } + builder.finish() + } + + #[test] + fn test_list_is_sorted_i32_sorted() { + // Sorted: [1,2], [2,3], [3,4] + let list = create_list_array(vec![Some(vec![1,2]), Some(vec![2,3]), Some(vec![3,4])], vec![true, true, true]); + assert_eq!(is_sorted(list.as_ref()).unwrap(), Some(true)); + } + + #[test] + fn test_list_is_sorted_i32_unsorted() { + // Unsorted: [1,2], [3,4], [2,3] + let list = create_list_array(vec![Some(vec![1,2]), Some(vec![3,4]), Some(vec![2,3])], vec![true, true, true]); + assert_eq!(is_sorted(list.as_ref()).unwrap(), Some(false)); + } + + #[test] + fn test_list_is_sorted_i32_duplicates() { + // With duplicates: [1,2], [1,2], [3,4] + let list = create_list_array(vec![Some(vec![1,2]), Some(vec![1,2]), Some(vec![3,4])], vec![true, true, true]); + assert_eq!(is_sorted(list.as_ref()).unwrap(), Some(true)); + } + + #[test] + fn test_list_is_strict_sorted_i32_strict() { + // Strict sorted: [1,2], [3,4], [5,6] + let list = create_list_array(vec![Some(vec![1,2]), Some(vec![3,4]), Some(vec![5,6])], vec![true, true, true]); + assert_eq!(is_strict_sorted(list.as_ref()).unwrap(), Some(true)); + } + + #[test] + fn test_list_is_strict_sorted_i32_not_strict() { + // Not strict: [1,2], [1,2], [3,4] + let list = create_list_array(vec![Some(vec![1,2]), Some(vec![1,2]), Some(vec![3,4])], vec![true, true, true]); + assert_eq!(is_strict_sorted(list.as_ref()).unwrap(), Some(false)); + } + + #[test] + fn test_list_is_sorted_with_null_beginning() { + // Null at beginning: null, [1,2], [2,3] + let list = create_list_array(vec![None, Some(vec![1,2]), Some(vec![2,3])], vec![false, true, true]); + assert_eq!(is_sorted(list.as_ref()).unwrap(), Some(true)); + } + + #[test] + fn test_list_is_sorted_with_null_middle() { + // Null in middle: [1,2], null, [3,4] + let list = create_list_array(vec![Some(vec![1,2]), None, Some(vec![3,4])], vec![true, false, true]); + assert_eq!(is_sorted(list.as_ref()).unwrap(), Some(false)); + } + + #[test] + fn test_list_is_sorted_with_null_end() { + // Null at end: [1,2], [2,3], null + let list = create_list_array(vec![Some(vec![1,2]), Some(vec![2,3]), None], vec![true, true, false]); + assert_eq!(is_sorted(list.as_ref()).unwrap(), Some(false)); + } + + #[test] + fn test_list_is_sorted_empty() { + // Empty list + let list = create_list_array(vec![], vec![]); + assert_eq!(is_sorted(list.as_ref()).unwrap(), Some(true)); + } + + #[test] + fn test_list_is_sorted_single() { + // Single element + let list = create_list_array(vec![Some(vec![1,2])], vec![true]); + assert_eq!(is_sorted(list.as_ref()).unwrap(), Some(true)); + } + + #[test] + fn test_list_is_sorted_different_lengths() { + // [1] < [1,2], so sorted + let list = create_list_array(vec![Some(vec![1]), Some(vec![1,2])], vec![true, true]); + assert_eq!(is_sorted(list.as_ref()).unwrap(), Some(true)); + + // [1,2] > [1], so not sorted + let list = create_list_array(vec![Some(vec![1,2]), Some(vec![1])], vec![true, true]); + assert_eq!(is_sorted(list.as_ref()).unwrap(), Some(false)); + } + + #[test] + fn test_list_is_strict_sorted_different_lengths() { + // [1] < [1,2], so strict sorted + let list = create_list_array(vec![Some(vec![1]), Some(vec![1,2])], vec![true, true]); + assert_eq!(is_strict_sorted(list.as_ref()).unwrap(), Some(true)); + + // [1,2] > [1], so not strict sorted + let list = create_list_array(vec![Some(vec![1,2]), Some(vec![1])], vec![true, true]); + assert_eq!(is_strict_sorted(list.as_ref()).unwrap(), Some(false)); + } +} diff --git a/vortex-array/src/arrays/list/compute/min_max.rs b/vortex-array/src/arrays/list/compute/min_max.rs index 8faea22eb5c..9968788a752 100644 --- a/vortex-array/src/arrays/list/compute/min_max.rs +++ b/vortex-array/src/arrays/list/compute/min_max.rs @@ -2,6 +2,8 @@ // SPDX-FileCopyrightText: Copyright the Vortex contributors use vortex_error::VortexResult; +use vortex_scalar::ListScalar; +use vortex_scalar::Scalar; use crate::arrays::ListArray; use crate::arrays::ListVTable; @@ -11,10 +13,106 @@ use crate::compute::MinMaxResult; use crate::register_kernel; impl MinMaxKernel for ListVTable { - fn min_max(&self, _array: &ListArray) -> VortexResult> { - // This would require finding the lexicographically minimum and maximum lists. - Ok(None) + fn min_max(&self, array: &ListArray) -> VortexResult> { + let mut min: Option = None; + let mut max: Option = None; + for i in 0..array.len() { + let scalar = array.scalar_at(i)?; + if scalar.is_null() { + continue; + } + let list_scalar = ListScalar::try_from(&scalar)?; + if let Some(current_min) = &min { + let current_min_list = ListScalar::try_from(current_min)?; + if list_scalar < current_min_list { + min = Some(scalar.cast(&array.dtype().as_nonnullable())?); + } + } else { + min = Some(scalar.cast(&array.dtype().as_nonnullable())?); + } + if let Some(current_max) = &max { + let current_max_list = ListScalar::try_from(current_max)?; + if list_scalar > current_max_list { + max = Some(scalar.cast(&array.dtype().as_nonnullable())?); + } + } else { + max = Some(scalar.cast(&array.dtype().as_nonnullable())?); + } + } + match (min, max) { + (Some(min), Some(max)) => Ok(Some(MinMaxResult { min, max })), + (None, None) => Ok(None), + _ => unreachable!("min and max should be set together or both remain None"), + } } } register_kernel!(MinMaxKernelAdapter(ListVTable).lift()); + +#[cfg(test)] +mod tests { + use std::sync::Arc; + + use vortex_dtype::{DType, Nullability, PType}; + use vortex_scalar::Scalar; + + use crate::ArrayRef; + use crate::builders::{ArrayBuilder as _, ListBuilder}; + use crate::compute::min_max; + + fn create_list_array(values: Vec>>, validity: Vec) -> ArrayRef { + let element_dtype = Arc::new(DType::Primitive(PType::I32, Nullability::NonNullable)); + let nullability = Nullability::Nullable; + let mut builder = ListBuilder::::new(element_dtype, nullability); + for i in 0..validity.len() { + if validity[i] { + let value = values[i].as_ref().unwrap(); + let list_scalar = value.into_iter().map(|x| Scalar::primitive(*x, Nullability::NonNullable)).collect::>(); + let list = Scalar::list(Arc::new(DType::Primitive(PType::I32, Nullability::NonNullable)), list_scalar, Nullability::NonNullable); + builder.append_value(list.as_list()).unwrap(); + } else { + builder.append_null(); + } + } + builder.finish() + } + + #[test] + fn test_list_min_max_i32() { + // Lists: [1,2], [2,3], [3,4] + let list = create_list_array(vec![Some(vec![1,2]), Some(vec![2,3]), Some(vec![3,4])], vec![true, true, true]); + let result = min_max(list.as_ref()).unwrap().unwrap(); + assert_eq!(result.min, vec![1i32, 2].into()); + assert_eq!(result.max, vec![3i32, 4].into()); + } + + #[test] + fn test_list_min_max_with_nulls() { + // Lists: null, [1,2], [2,3] + let list = create_list_array(vec![None, Some(vec![1,2]), Some(vec![2,3])], vec![false, true, true]); + let result = min_max(list.as_ref()).unwrap().unwrap(); + assert_eq!(result.min, vec![1i32, 2].into()); + assert_eq!(result.max, vec![2i32, 3].into()); + } + + #[test] + fn test_list_min_max_all_nulls() { + // All nulls + let element_dtype = Arc::new(DType::Primitive(PType::I32, Nullability::NonNullable)); + let nullability = Nullability::Nullable; + let mut builder = ListBuilder::::new(element_dtype, nullability); + builder.append_null(); + let list = builder.finish(); + assert!(min_max(list.as_ref()).unwrap().is_none()); + } + + #[test] + fn test_list_min_max_empty() { + // Empty array + let element_dtype = Arc::new(DType::Primitive(PType::I32, Nullability::NonNullable)); + let nullability = Nullability::Nullable; + let mut builder = ListBuilder::::new(element_dtype, nullability); + let list = builder.finish(); + assert!(min_max(list.as_ref()).unwrap().is_none()); + } +} diff --git a/vortex-array/src/arrays/listview/compute/is_sorted.rs b/vortex-array/src/arrays/listview/compute/is_sorted.rs index fcc97736bb5..1bc382b448d 100644 --- a/vortex-array/src/arrays/listview/compute/is_sorted.rs +++ b/vortex-array/src/arrays/listview/compute/is_sorted.rs @@ -1,7 +1,10 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: Copyright the Vortex contributors +use std::cmp::Ordering; + use vortex_error::VortexResult; +use vortex_scalar::ListScalar; use crate::arrays::ListViewArray; use crate::arrays::ListViewVTable; @@ -9,15 +12,49 @@ use crate::compute::IsSortedKernel; use crate::compute::IsSortedKernelAdapter; use crate::register_kernel; +/// Implementation of IsSortedKernel for ListViewArray. +/// +/// This implementation uses lexicographic comparison of list elements. +/// Lists are compared element-wise; if one list is a prefix of another, +/// the shorter list is considered less than the longer one. +/// Null lists are considered the smallest values. +/// Non-comparable lists (which shouldn't occur for lists with the same element type) +/// are treated as making the array not sorted. impl IsSortedKernel for ListViewVTable { - fn is_sorted(&self, _array: &ListViewArray) -> VortexResult> { - // This would require comparing lists lexicographically. - Ok(None) + fn is_sorted(&self, array: &ListViewArray) -> VortexResult> { + if array.len() <= 1 { + return Ok(Some(true)); + } + for i in 0..array.len() - 1 { + let scalar_a = array.scalar_at(i)?; + let scalar_b = array.scalar_at(i + 1)?; + let a = ListScalar::try_from(&scalar_a)?; + let b = ListScalar::try_from(&scalar_b)?; + // For is_sorted, we allow Less and Equal, but not Greater or incomparable (None) + match a.partial_cmp(&b) { + Some(Ordering::Greater) | None => return Ok(Some(false)), + _ => {} + } + } + Ok(Some(true)) } - fn is_strict_sorted(&self, _array: &ListViewArray) -> VortexResult> { - // This would require comparing lists lexicographically without duplicates. - Ok(None) + fn is_strict_sorted(&self, array: &ListViewArray) -> VortexResult> { + if array.len() <= 1 { + return Ok(Some(true)); + } + for i in 0..array.len() - 1 { + let scalar_a = array.scalar_at(i)?; + let scalar_b = array.scalar_at(i + 1)?; + let a = ListScalar::try_from(&scalar_a)?; + let b = ListScalar::try_from(&scalar_b)?; + // For is_strict_sorted, we only allow Less, not Equal, Greater, or incomparable (None) + match a.partial_cmp(&b) { + Some(Ordering::Greater | Ordering::Equal) | None => return Ok(Some(false)), + _ => {} + } + } + Ok(Some(true)) } } diff --git a/vortex-array/src/arrays/listview/compute/min_max.rs b/vortex-array/src/arrays/listview/compute/min_max.rs index 6596cc7a596..e8a3ff86b34 100644 --- a/vortex-array/src/arrays/listview/compute/min_max.rs +++ b/vortex-array/src/arrays/listview/compute/min_max.rs @@ -2,6 +2,8 @@ // SPDX-FileCopyrightText: Copyright the Vortex contributors use vortex_error::VortexResult; +use vortex_scalar::ListScalar; +use vortex_scalar::Scalar; use crate::arrays::ListViewArray; use crate::arrays::ListViewVTable; @@ -11,9 +13,37 @@ use crate::compute::MinMaxResult; use crate::register_kernel; impl MinMaxKernel for ListViewVTable { - fn min_max(&self, _array: &ListViewArray) -> VortexResult> { - // This would require finding the lexicographically minimum and maximum lists. - Ok(None) + fn min_max(&self, array: &ListViewArray) -> VortexResult> { + let mut min: Option = None; + let mut max: Option = None; + for i in 0..array.len() { + let scalar = array.scalar_at(i)?; + if scalar.is_null() { + continue; + } + let list_scalar = ListScalar::try_from(&scalar)?; + if let Some(current_min) = &min { + let current_min_list = ListScalar::try_from(current_min)?; + if list_scalar < current_min_list { + min = Some(scalar.clone()); + } + } else { + min = Some(scalar.clone()); + } + if let Some(current_max) = &max { + let current_max_list = ListScalar::try_from(current_max)?; + if list_scalar > current_max_list { + max = Some(scalar.clone()); + } + } else { + max = Some(scalar.clone()); + } + } + match (min, max) { + (Some(min), Some(max)) => Ok(Some(MinMaxResult { min, max })), + (None, None) => Ok(None), + _ => unreachable!("min and max should be set together or both remain None"), + } } }