Skip to content

Commit da08e03

Browse files
committed
Make null-tolerant decoding opt in
Signed-off-by: Connor Tsui <connor.tsui20@gmail.com>
1 parent 9b01776 commit da08e03

5 files changed

Lines changed: 144 additions & 3 deletions

File tree

‎vortex-array/src/scalar_fn/unstable/row/types/element/bool.rs‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ unsafe impl InputElement for bool {
3737
Ok(array.execute::<BoolArray>(ctx)?.into_bit_buffer())
3838
}
3939

40+
fn can_decode_null_tolerant(_array: &ArrayRef) -> VortexResult<bool> {
41+
Ok(true)
42+
}
43+
4044
fn get(column: &Self::Column, index: usize) -> bool {
4145
column.value(index)
4246
}

‎vortex-array/src/scalar_fn/unstable/row/types/element/input.rs‎

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,17 @@ pub unsafe trait InputElement: 'static {
5757
/// invocation-invariant work into this method.
5858
fn decode(array: ArrayRef, ctx: &mut ExecutionCtx) -> VortexResult<Self::Column>;
5959

60-
/// Decode `array` _without_ assuming every row is valid, or `Ok(None)` when this element
61-
/// cannot for this particular array.
60+
/// Whether [`decode_null_tolerant`](Self::decode_null_tolerant) can decode this array.
61+
///
62+
/// The conservative default declines. An implementation whose ordinary decode is safe and
63+
/// infallible over null payloads can return `true`. Other implementations can inspect `array`
64+
/// and opt in only for supported representations.
65+
fn can_decode_null_tolerant(_array: &ArrayRef) -> VortexResult<bool> {
66+
Ok(false)
67+
}
68+
69+
/// Decode `array` _without_ assuming every row is valid, or return `Ok(None)` when this element
70+
/// cannot decode this particular array.
6271
///
6372
/// Override this for a non-dense-safe representation that can still place safe placeholders in
6473
/// null slots. The skip-invalid executor never reads those slots.
@@ -69,7 +78,7 @@ pub unsafe trait InputElement: 'static {
6978
array: ArrayRef,
7079
ctx: &mut ExecutionCtx,
7180
) -> VortexResult<Option<Self::Column>> {
72-
if Self::DENSE_SAFE {
81+
if Self::can_decode_null_tolerant(&array)? {
7382
Self::decode(array, ctx).map(Some)
7483
} else {
7584
Ok(None)

‎vortex-array/src/scalar_fn/unstable/row/types/element/primitive.rs‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ unsafe impl<T: NativePType> InputElement for T {
4444
Ok(array.execute::<PrimitiveArray>(ctx)?.into_buffer::<T>())
4545
}
4646

47+
fn can_decode_null_tolerant(_array: &ArrayRef) -> VortexResult<bool> {
48+
Ok(true)
49+
}
50+
4751
fn get(column: &Self::Column, index: usize) -> T {
4852
column[index]
4953
}

‎vortex-array/src/scalar_fn/unstable/row/types/element/tuple/element_tuple.rs‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,16 @@ impl<T: InputElement> ArgColumn<T> {
6363
.map(Self))
6464
}
6565

66+
fn can_decode_null_tolerant(array: &ArrayRef) -> VortexResult<bool> {
67+
// Batch execution short-circuits null constants before selecting this path, so a
68+
// non-empty constant can always use the ordinary decode.
69+
if batch_constant(array).is_some() && !array.is_empty() {
70+
return Ok(true);
71+
}
72+
73+
T::can_decode_null_tolerant(array)
74+
}
75+
6676
fn get(&self, index: usize) -> T::Elem<'_> {
6777
match &self.0 {
6878
ArgColumnKind::PerRow(column) => T::get(column, index),
@@ -153,6 +163,12 @@ pub trait ElementTuple: 'static + private::Sealed {
153163
/// A dense deferred-error retry starts another invocation over filtered valid rows.
154164
fn decode(args: &dyn ExecutionArgs, ctx: &mut ExecutionCtx) -> VortexResult<Self::Columns>;
155165

166+
/// Whether every input can be decoded without assuming that all rows are valid.
167+
///
168+
/// The tuple checks this before decoding any column, so a decline does not discard work from
169+
/// earlier arguments.
170+
fn can_decode_null_tolerant(args: &dyn ExecutionArgs) -> VortexResult<bool>;
171+
156172
/// Decode every input column once while tolerating null rows.
157173
///
158174
/// Return `Ok(None)` when an argument has no null-tolerant representation. The skip-invalid
@@ -225,6 +241,10 @@ impl ElementTuple for () {
225241
Ok(())
226242
}
227243

244+
fn can_decode_null_tolerant(_args: &dyn ExecutionArgs) -> VortexResult<bool> {
245+
Ok(true)
246+
}
247+
228248
fn decode_null_tolerant(
229249
_args: &dyn ExecutionArgs,
230250
_ctx: &mut ExecutionCtx,
@@ -291,10 +311,21 @@ macro_rules! element_tuple {
291311
Ok(($(ArgColumn::<$t>::decode(args.get($idx)?, ctx)?,)+))
292312
}
293313

314+
fn can_decode_null_tolerant(args: &dyn ExecutionArgs) -> VortexResult<bool> {
315+
Ok($({
316+
let array = args.get($idx)?;
317+
ArgColumn::<$t>::can_decode_null_tolerant(&array)?
318+
} &&)+ true)
319+
}
320+
294321
fn decode_null_tolerant(
295322
args: &dyn ExecutionArgs,
296323
ctx: &mut ExecutionCtx,
297324
) -> VortexResult<Option<Self::Columns>> {
325+
if !Self::can_decode_null_tolerant(args)? {
326+
return Ok(None);
327+
}
328+
298329
Ok(Some((
299330
$(match ArgColumn::<$t>::decode_null_tolerant(args.get($idx)?, ctx)? {
300331
Some(column) => column,

‎vortex-array/src/scalar_fn/unstable/row/types/element/tuple/tests.rs‎

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,114 @@
11
// SPDX-License-Identifier: Apache-2.0
22
// SPDX-FileCopyrightText: Copyright the Vortex contributors
33

4+
use std::sync::atomic::AtomicUsize;
5+
use std::sync::atomic::Ordering;
6+
7+
use vortex_buffer::Buffer;
48
use vortex_error::VortexResult;
59
use vortex_error::vortex_bail;
610
use vortex_mask::Mask;
711

12+
use super::ElementTuple;
813
use super::element_tuple::batch_constant;
14+
use crate::ArrayRef;
15+
use crate::ExecutionCtx;
916
use crate::IntoArray;
17+
use crate::VortexSessionExecute;
18+
use crate::array_session;
1019
use crate::arrays::Constant;
1120
use crate::arrays::ConstantArray;
1221
use crate::arrays::ExtensionArray;
1322
use crate::arrays::MaskedArray;
23+
use crate::arrays::PrimitiveArray;
24+
use crate::dtype::DType;
1425
use crate::dtype::Nullability;
1526
use crate::extension::datetime::TimeUnit;
1627
use crate::extension::datetime::Timestamp;
28+
use crate::scalar_fn::VecExecutionArgs;
29+
use crate::scalar_fn::unstable::row::InputElement;
1730
use crate::validity::Validity;
1831

32+
static DECODE_CALLS: AtomicUsize = AtomicUsize::new(0);
33+
34+
macro_rules! i64_test_element {
35+
($element:ident, $decode_fallible:literal $(, $can_decode:item)?) => {
36+
struct $element;
37+
38+
// SAFETY: the view and unchecked access delegate to the `i64` implementation.
39+
unsafe impl InputElement for $element {
40+
type Column = Buffer<i64>;
41+
type View<'a> = &'a [i64];
42+
type Elem<'a> = i64;
43+
44+
const DENSE_SAFE: bool = true;
45+
const DECODE_FALLIBLE: bool = $decode_fallible;
46+
47+
fn validate(dtype: &DType) -> VortexResult<()> {
48+
<i64 as InputElement>::validate(dtype)
49+
}
50+
51+
fn decode(array: ArrayRef, ctx: &mut ExecutionCtx) -> VortexResult<Self::Column> {
52+
DECODE_CALLS.fetch_add(1, Ordering::Relaxed);
53+
<i64 as InputElement>::decode(array, ctx)
54+
}
55+
56+
$($can_decode)?
57+
58+
fn get(column: &Self::Column, index: usize) -> i64 {
59+
<i64 as InputElement>::get(column, index)
60+
}
61+
62+
fn view(column: &Self::Column) -> Self::View<'_> {
63+
<i64 as InputElement>::view(column)
64+
}
65+
66+
fn view_len(view: &Self::View<'_>) -> usize {
67+
<i64 as InputElement>::view_len(view)
68+
}
69+
70+
fn get_from_view<'a>(view: &Self::View<'a>, index: usize) -> i64
71+
where
72+
Self: 'a,
73+
{
74+
<i64 as InputElement>::get_from_view(view, index)
75+
}
76+
77+
unsafe fn get_from_view_unchecked<'a>(view: &Self::View<'a>, index: usize) -> i64
78+
where
79+
Self: 'a,
80+
{
81+
// SAFETY: forwarded from this method's contract.
82+
unsafe { <i64 as InputElement>::get_from_view_unchecked(view, index) }
83+
}
84+
}
85+
};
86+
}
87+
88+
i64_test_element!(
89+
DecodeProbe,
90+
false,
91+
fn can_decode_null_tolerant(_array: &ArrayRef) -> VortexResult<bool> {
92+
Ok(true)
93+
}
94+
);
95+
i64_test_element!(DenseFallible, true);
96+
97+
#[test]
98+
fn test_null_tolerant_decline_precedes_decoding() -> VortexResult<()> {
99+
DECODE_CALLS.store(0, Ordering::Relaxed);
100+
let first = PrimitiveArray::from_iter([1_i64, 2]).into_array();
101+
let second = PrimitiveArray::from_iter([3_i64, 4]).into_array();
102+
let args = VecExecutionArgs::new(vec![first, second], 2);
103+
let mut ctx = array_session().create_execution_ctx();
104+
105+
let columns = <(DecodeProbe, DenseFallible)>::decode_null_tolerant(&args, &mut ctx)?;
106+
107+
assert!(columns.is_none());
108+
assert_eq!(DECODE_CALLS.load(Ordering::Relaxed), 0);
109+
Ok(())
110+
}
111+
19112
#[test]
20113
fn test_batch_constant_unwraps_filtered_masked_constant() -> VortexResult<()> {
21114
let child = ConstantArray::new(7_i64, 3).into_array();

0 commit comments

Comments
 (0)