Skip to content

Commit 3bf14ba

Browse files
feat(aggregate): register is_sorted and min_max for temporal and UUID extension types (#10616)
- New `AggregateFnSession::register_storage_ordered(ext)` pushes the comparison-based aggregates `is_sorted` and `min_max` down into `ext`'s storage, registering `ExtensionStoragePushdown<IsSorted, E>` and `ExtensionStoragePushdown<MinMax, E>`. - The default aggregate session calls it for `Date`, `Time` and `Timestamp`, whose storage is an integer. - `vortex_uuid::initialize` calls it for `Uuid`. UUIDs are stored as 16 big-endian bytes, so byte order is UUID order. - `is_sorted` and `min_max` now order a `FixedSizeList` of non-nullable `u8` (UUID storage) lexicographically by byte. A shared `fns/fixed_size_bytes.rs` decodes the elements once and slices each value. Other fixed-size lists stay unsupported.
1 parent 7512e99 commit 3bf14ba

10 files changed

Lines changed: 458 additions & 36 deletions

File tree

‎encodings/uuid/src/lib.rs‎

Lines changed: 138 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,9 @@
1313
//! is stored as 16 bytes in big-endian (network) byte order, matching [RFC 4122] and Arrow's
1414
//! [canonical UUID extension].
1515
//!
16-
//! [`initialize`] registers the extension dtype and the Arrow import/export plugins that map it
17-
//! to Arrow's `arrow.uuid` extension type over `FixedSizeBinary[16]` storage.
16+
//! [`initialize`] registers the extension dtype, the Arrow import/export plugins that map it to
17+
//! Arrow's `arrow.uuid` extension type over `FixedSizeBinary[16]` storage, and `is_sorted` and
18+
//! `min_max` over its storage, since big-endian bytes order like the UUIDs they encode.
1819
//!
1920
//! [RFC 4122]: https://www.rfc-editor.org/rfc/rfc4122
2021
//! [canonical UUID extension]: https://arrow.apache.org/docs/format/CanonicalExtensions.html#uuid
@@ -26,6 +27,7 @@ mod vtable;
2627
use std::sync::Arc;
2728

2829
pub use metadata::UuidMetadata;
30+
use vortex_array::aggregate_fn::session::AggregateFnSessionExt;
2931
use vortex_array::dtype::session::DTypeSessionExt;
3032
use vortex_arrow::ArrowSessionExt;
3133
use vortex_session::VortexSession;
@@ -37,9 +39,142 @@ pub struct Uuid;
3739
/// Register UUID extension support with a session.
3840
///
3941
/// This registers the `vortex.uuid` extension dtype along with the Arrow exporter and importer
40-
/// for Arrow's canonical `arrow.uuid` extension type.
42+
/// for Arrow's canonical `arrow.uuid` extension type. UUIDs are stored big-endian, so they compare
43+
/// like their storage bytes, and `is_sorted` and `min_max` are pushed down into the storage.
4144
pub fn initialize(session: &VortexSession) {
4245
session.dtypes().register(Uuid);
46+
session.aggregate_fns().register_storage_ordered(Uuid);
4347
session.arrow().register_exporter(Arc::new(Uuid));
4448
session.arrow().register_importer(Arc::new(Uuid));
4549
}
50+
51+
#[cfg(test)]
52+
mod tests {
53+
use rstest::rstest;
54+
use vortex_array::ArrayRef;
55+
use vortex_array::IntoArray;
56+
use vortex_array::VortexSessionExecute;
57+
use vortex_array::aggregate_fn::Accumulator;
58+
use vortex_array::aggregate_fn::AggregateFnVTable;
59+
use vortex_array::aggregate_fn::DynAccumulator;
60+
use vortex_array::aggregate_fn::ExtensionStoragePushdown;
61+
use vortex_array::aggregate_fn::NumericalAggregateOpts;
62+
use vortex_array::aggregate_fn::fns::is_sorted::IsSorted;
63+
use vortex_array::aggregate_fn::fns::is_sorted::IsSortedOptions;
64+
use vortex_array::aggregate_fn::fns::min_max::MinMax;
65+
use vortex_array::aggregate_fn::fns::min_max::MinMaxResult;
66+
use vortex_array::aggregate_fn::session::AggregateFnSessionExt;
67+
use vortex_array::array_session;
68+
use vortex_array::arrays::ExtensionArray;
69+
use vortex_array::arrays::FixedSizeListArray;
70+
use vortex_array::arrays::PrimitiveArray;
71+
use vortex_array::dtype::DType;
72+
use vortex_array::dtype::Nullability;
73+
use vortex_array::dtype::PType;
74+
use vortex_array::dtype::extension::ExtDType;
75+
use vortex_array::scalar::Scalar;
76+
use vortex_array::validity::Validity;
77+
use vortex_buffer::Buffer;
78+
use vortex_error::VortexResult;
79+
use vortex_session::VortexSession;
80+
81+
use super::*;
82+
83+
fn uuid_session() -> VortexSession {
84+
let session = array_session();
85+
initialize(&session);
86+
session
87+
}
88+
89+
/// A nullable UUID column, with each UUID given by its leading byte.
90+
fn uuid_column(leading_bytes: &[Option<u8>]) -> VortexResult<ArrayRef> {
91+
let bytes = leading_bytes
92+
.iter()
93+
.flat_map(|byte| {
94+
let mut uuid = [0u8; 16];
95+
uuid[0] = byte.unwrap_or_default();
96+
uuid
97+
})
98+
.collect::<Buffer<u8>>();
99+
let storage = FixedSizeListArray::new(
100+
PrimitiveArray::new(bytes, Validity::NonNullable).into_array(),
101+
16,
102+
leading_bytes.iter().map(Option::is_some).collect(),
103+
leading_bytes.len(),
104+
)
105+
.into_array();
106+
let ext_dtype =
107+
ExtDType::try_with_vtable(Uuid, UuidMetadata::default(), storage.dtype().clone())?;
108+
Ok(ExtensionArray::new(ext_dtype.erased(), storage).into_array())
109+
}
110+
111+
#[test]
112+
fn initialize_registers_order_aggregates() {
113+
let session = uuid_session();
114+
for id in [
115+
ExtensionStoragePushdown::new(IsSorted, Uuid).id(),
116+
ExtensionStoragePushdown::new(MinMax, Uuid).id(),
117+
] {
118+
assert!(
119+
session.aggregate_fns().find_plugin(&id).is_some(),
120+
"{id} is not registered"
121+
);
122+
}
123+
}
124+
125+
#[rstest]
126+
// Nulls sort first.
127+
#[case::sorted(&[None, Some(1), Some(2), Some(2)], false, true)]
128+
#[case::unsorted(&[Some(2), Some(1), None, Some(3)], false, false)]
129+
#[case::strict_with_duplicates(&[None, Some(1), Some(2), Some(2)], true, false)]
130+
#[case::strict(&[None, Some(1), Some(2), Some(0xff)], true, true)]
131+
fn is_sorted_orders_by_bytes(
132+
#[case] leading_bytes: &[Option<u8>],
133+
#[case] strict: bool,
134+
#[case] expected: bool,
135+
) -> VortexResult<()> {
136+
let session = uuid_session();
137+
let column = uuid_column(leading_bytes)?;
138+
let mut acc = Accumulator::try_new(
139+
ExtensionStoragePushdown::new(IsSorted, Uuid),
140+
IsSortedOptions { strict },
141+
column.dtype().clone(),
142+
)?;
143+
acc.accumulate(&column, &mut session.create_execution_ctx())?;
144+
assert_eq!(bool::try_from(&acc.finish()?)?, expected);
145+
Ok(())
146+
}
147+
148+
#[test]
149+
fn min_max_orders_by_bytes() -> VortexResult<()> {
150+
let session = uuid_session();
151+
let column = uuid_column(&[Some(7), None, Some(0xff), Some(3)])?;
152+
let mut acc = Accumulator::try_new(
153+
ExtensionStoragePushdown::new(MinMax, Uuid),
154+
NumericalAggregateOpts::default(),
155+
column.dtype().clone(),
156+
)?;
157+
acc.accumulate(&column, &mut session.create_execution_ctx())?;
158+
159+
let uuid = |leading_byte: u8| {
160+
let mut bytes = [0u8; 16];
161+
bytes[0] = leading_byte;
162+
Scalar::fixed_size_list(
163+
DType::Primitive(PType::U8, Nullability::NonNullable),
164+
bytes
165+
.into_iter()
166+
.map(|byte| Scalar::primitive(byte, Nullability::NonNullable))
167+
.collect(),
168+
Nullability::NonNullable,
169+
)
170+
};
171+
assert_eq!(
172+
MinMaxResult::from_scalar(&acc.finish()?)?,
173+
Some(MinMaxResult {
174+
min: uuid(3),
175+
max: uuid(0xff),
176+
})
177+
);
178+
Ok(())
179+
}
180+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// SPDX-FileCopyrightText: Copyright the Vortex contributors
3+
4+
//! Fixed-size lists of non-nullable `u8`, which order lexicographically by byte.
5+
//!
6+
//! This is the storage of byte-string extension types such as UUID, so order-based aggregates can
7+
//! support it without implementing general list comparison.
8+
9+
use vortex_error::VortexResult;
10+
11+
use crate::ExecutionCtx;
12+
use crate::arrays::FixedSizeListArray;
13+
use crate::arrays::PrimitiveArray;
14+
use crate::arrays::fixed_size_list::FixedSizeListArrayExt;
15+
use crate::arrays::fixed_size_list::FixedSizeListArraySlotsExt;
16+
use crate::dtype::DType;
17+
use crate::dtype::Nullability;
18+
use crate::dtype::PType;
19+
20+
/// Whether `dtype` is a fixed-size list of non-nullable `u8`.
21+
pub(crate) fn is_fixed_size_bytes(dtype: &DType) -> bool {
22+
matches!(
23+
dtype,
24+
DType::FixedSizeList(element_dtype, ..)
25+
if matches!(element_dtype.as_ref(), DType::Primitive(PType::U8, Nullability::NonNullable))
26+
)
27+
}
28+
29+
/// The values of a fixed-size list of bytes, each one a slice of the decoded elements.
30+
pub(crate) struct FixedSizeBytes {
31+
elements: PrimitiveArray,
32+
size: usize,
33+
len: usize,
34+
}
35+
36+
impl FixedSizeBytes {
37+
/// Decode the elements of `array`, which must be a fixed-size list of bytes.
38+
pub(crate) fn try_new(
39+
array: &FixedSizeListArray,
40+
ctx: &mut ExecutionCtx,
41+
) -> VortexResult<Self> {
42+
Ok(Self {
43+
elements: array.elements().clone().execute::<PrimitiveArray>(ctx)?,
44+
size: array.list_size() as usize,
45+
len: array.len(),
46+
})
47+
}
48+
49+
/// Every value in order, including the values of null lists.
50+
pub(crate) fn values(&self) -> impl Iterator<Item = &[u8]> {
51+
let bytes = self.elements.as_slice::<u8>();
52+
(0..self.len).map(move |index| &bytes[index * self.size..(index + 1) * self.size])
53+
}
54+
}

‎vortex-array/src/aggregate_fn/fns/is_sorted/extension.rs‎

Lines changed: 0 additions & 22 deletions
This file was deleted.
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// SPDX-FileCopyrightText: Copyright the Vortex contributors
3+
4+
use vortex_error::VortexResult;
5+
6+
use super::IsSortedIteratorExt;
7+
use crate::ExecutionCtx;
8+
use crate::aggregate_fn::fns::fixed_size_bytes::FixedSizeBytes;
9+
use crate::arrays::FixedSizeListArray;
10+
11+
/// Check whether a fixed-size list of bytes is sorted by byte, with nulls first.
12+
pub(super) fn check_fixed_size_bytes_sorted(
13+
array: &FixedSizeListArray,
14+
strict: bool,
15+
ctx: &mut ExecutionCtx,
16+
) -> VortexResult<bool> {
17+
let mask = array
18+
.as_ref()
19+
.validity()?
20+
.execute_mask(array.len(), ctx)?
21+
.to_bit_buffer();
22+
let bytes = FixedSizeBytes::try_new(array, ctx)?;
23+
let iter = bytes
24+
.values()
25+
.zip(mask.iter())
26+
.map(|(value, valid)| valid.then_some(value));
27+
Ok(if strict {
28+
iter.is_strict_sorted()
29+
} else {
30+
iter.is_sorted()
31+
})
32+
}

‎vortex-array/src/aggregate_fn/fns/is_sorted/mod.rs‎

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
mod bool;
55
mod decimal;
6-
mod extension;
6+
mod fixed_size_list;
77
mod primitive;
88
mod varbin;
99

@@ -18,7 +18,7 @@ use vortex_session::registry::CachedId;
1818

1919
use self::bool::check_bool_sorted;
2020
use self::decimal::check_decimal_sorted;
21-
use self::extension::check_extension_sorted;
21+
use self::fixed_size_list::check_fixed_size_bytes_sorted;
2222
use self::primitive::check_primitive_sorted;
2323
use self::varbin::check_varbinview_sorted;
2424
use crate::ArrayRef;
@@ -31,6 +31,7 @@ use crate::aggregate_fn::AggregateArgs;
3131
use crate::aggregate_fn::AggregateFnId;
3232
use crate::aggregate_fn::AggregateFnVTable;
3333
use crate::aggregate_fn::DynAccumulator;
34+
use crate::aggregate_fn::fns::fixed_size_bytes::is_fixed_size_bytes;
3435
use crate::arrays::Constant;
3536
use crate::arrays::Null;
3637
use crate::builtins::ArrayBuiltins;
@@ -59,15 +60,17 @@ impl Display for IsSortedOptions {
5960
/// Compute whether an array is sorted in non-decreasing order.
6061
///
6162
/// Returns `true` for empty arrays and arrays of length 1.
62-
/// Returns `false` for struct, list, and fixed-size list arrays.
63+
/// Returns `false` for struct, list, and fixed-size list arrays, except fixed-size lists of
64+
/// non-nullable `u8`, which are ordered by byte.
6365
pub fn is_sorted(array: &ArrayRef, ctx: &mut ExecutionCtx) -> VortexResult<bool> {
6466
is_sorted_impl(array, false, ctx)
6567
}
6668

6769
/// Compute whether an array is strictly sorted in increasing order (no duplicates).
6870
///
6971
/// Returns `true` for empty arrays and arrays of length 1.
70-
/// Returns `false` for struct, list, and fixed-size list arrays.
72+
/// Returns `false` for struct, list, and fixed-size list arrays, except fixed-size lists of
73+
/// non-nullable `u8`, which are ordered by byte.
7174
pub fn is_strict_sorted(array: &ArrayRef, ctx: &mut ExecutionCtx) -> VortexResult<bool> {
7275
is_sorted_impl(array, true, ctx)
7376
}
@@ -254,6 +257,9 @@ impl AggregateFnVTable for IsSorted {
254257

255258
fn return_dtype(&self, _options: &Self::Options, input_dtype: &DType) -> Option<DType> {
256259
match input_dtype {
260+
DType::FixedSizeList(..) if is_fixed_size_bytes(input_dtype) => {
261+
Some(DType::Bool(Nullability::NonNullable))
262+
}
257263
DType::Null
258264
| DType::List(..)
259265
| DType::FixedSizeList(..)
@@ -272,6 +278,9 @@ impl AggregateFnVTable for IsSorted {
272278

273279
fn partial_dtype(&self, _options: &Self::Options, input_dtype: &DType) -> Option<DType> {
274280
match input_dtype {
281+
DType::FixedSizeList(..) if is_fixed_size_bytes(input_dtype) => {
282+
Some(make_is_sorted_partial_dtype(input_dtype))
283+
}
275284
DType::Null
276285
| DType::List(..)
277286
| DType::FixedSizeList(..)
@@ -514,9 +523,12 @@ impl AggregateFnVTable for IsSorted {
514523
check_varbinview_sorted(v, args.options.strict, ctx)?
515524
}
516525
Canonical::Decimal(d) => check_decimal_sorted(d, args.options.strict, ctx)?,
517-
Canonical::Extension(e) => check_extension_sorted(e, args.options.strict, ctx)?,
526+
Canonical::FixedSizeList(f) => {
527+
check_fixed_size_bytes_sorted(f, args.options.strict, ctx)?
528+
}
518529
Canonical::Null(_) => !args.options.strict,
519-
// Struct, List, FixedSizeList should have been filtered out by return_dtype
530+
// Struct, List, other FixedSizeLists and Extension should have been filtered
531+
// out by return_dtype
520532
_ => unreachable!(),
521533
};
522534

0 commit comments

Comments
 (0)