Skip to content

Commit a745604

Browse files
authored
Decode RowFn constants directly (#9680)
## Summary - Tracking Issue: #9128 - Depends on #9626 Adds a constant-specific representation to `InputElement`, so batch constants no longer become one-row decoded columns. ## Changes `decode_constant` retains the logical batch length while primitive and Boolean inputs extract one native value. Complex inputs can keep ordinary column decoding behind the same hook. ## API Changes Adds `InputElement::Constant`, `decode_constant`, and `get_constant`. Signed-off-by: Connor Tsui <connor.tsui20@gmail.com>
1 parent 3a1838c commit a745604

8 files changed

Lines changed: 167 additions & 36 deletions

File tree

vortex-array/src/scalar_fn/unstable/row/batch/tests.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ struct DenseRetryI64;
9898
// SAFETY: the view is a slice, and its reported length is the buffer length.
9999
unsafe impl InputElement for ValidOnlyI64 {
100100
type Column = Buffer<i64>;
101+
type Constant = i64;
101102
type View<'a> = &'a [i64];
102103
type Elem<'a> = i64;
103104

@@ -112,6 +113,10 @@ unsafe impl InputElement for ValidOnlyI64 {
112113
<i64 as InputElement>::decode(array, ctx)
113114
}
114115

116+
fn decode_constant(array: ArrayRef, ctx: &mut ExecutionCtx) -> VortexResult<Self::Constant> {
117+
<i64 as InputElement>::decode_constant(array, ctx)
118+
}
119+
115120
fn can_decode_null_tolerant(_array: &ArrayRef) -> VortexResult<bool> {
116121
Ok(true)
117122
}
@@ -120,6 +125,10 @@ unsafe impl InputElement for ValidOnlyI64 {
120125
column[index]
121126
}
122127

128+
fn get_constant(constant: &Self::Constant) -> Self::Elem<'_> {
129+
*constant
130+
}
131+
123132
fn view(column: &Self::Column) -> Self::View<'_> {
124133
column.as_slice()
125134
}
@@ -132,6 +141,7 @@ unsafe impl InputElement for ValidOnlyI64 {
132141
// SAFETY: the view is a slice, and its reported length is the buffer length.
133142
unsafe impl InputElement for FilterOnlyI64 {
134143
type Column = Buffer<i64>;
144+
type Constant = i64;
135145
type View<'a> = &'a [i64];
136146
type Elem<'a> = i64;
137147

@@ -152,10 +162,21 @@ unsafe impl InputElement for FilterOnlyI64 {
152162
Ok(values)
153163
}
154164

165+
fn decode_constant(array: ArrayRef, ctx: &mut ExecutionCtx) -> VortexResult<Self::Constant> {
166+
let value = <i64 as InputElement>::decode_constant(array, ctx)?;
167+
vortex_ensure!(value != i64::MIN, "test input contains an invalid payload",);
168+
169+
Ok(value)
170+
}
171+
155172
fn get(column: &Self::Column, index: usize) -> Self::Elem<'_> {
156173
column[index]
157174
}
158175

176+
fn get_constant(constant: &Self::Constant) -> Self::Elem<'_> {
177+
*constant
178+
}
179+
159180
fn view(column: &Self::Column) -> Self::View<'_> {
160181
column.as_slice()
161182
}
@@ -168,6 +189,7 @@ unsafe impl InputElement for FilterOnlyI64 {
168189
// SAFETY: the view is a slice, and its reported length is the buffer length.
169190
unsafe impl InputElement for DenseRetryI64 {
170191
type Column = Buffer<i64>;
192+
type Constant = i64;
171193
type View<'a> = &'a [i64];
172194
type Elem<'a> = i64;
173195

@@ -182,10 +204,18 @@ unsafe impl InputElement for DenseRetryI64 {
182204
<i64 as InputElement>::decode(array, ctx)
183205
}
184206

207+
fn decode_constant(array: ArrayRef, ctx: &mut ExecutionCtx) -> VortexResult<Self::Constant> {
208+
<i64 as InputElement>::decode_constant(array, ctx)
209+
}
210+
185211
fn get(column: &Self::Column, index: usize) -> Self::Elem<'_> {
186212
column[index]
187213
}
188214

215+
fn get_constant(constant: &Self::Constant) -> Self::Elem<'_> {
216+
*constant
217+
}
218+
189219
fn view(column: &Self::Column) -> Self::View<'_> {
190220
column.as_slice()
191221
}

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,25 @@
44
use vortex_buffer::BitBuffer;
55
use vortex_compute::lane_kernels::IndexedSource;
66
use vortex_error::VortexResult;
7+
use vortex_error::vortex_bail;
78
use vortex_error::vortex_ensure;
89

910
use crate::ArrayRef;
1011
use crate::ExecutionCtx;
1112
use crate::IntoArray;
1213
use crate::arrays::BoolArray;
14+
use crate::arrays::Constant;
1315
use crate::dtype::DType;
1416
use crate::dtype::Nullability;
17+
use crate::scalar::ScalarValue;
1518
use crate::scalar_fn::unstable::row::InputElement;
1619
use crate::scalar_fn::unstable::row::OutputElement;
1720
use crate::validity::Validity;
1821

1922
// SAFETY: the view is a bit buffer, and its reported length is the buffer length.
2023
unsafe impl InputElement for bool {
2124
type Column = BitBuffer;
25+
type Constant = bool;
2226
type View<'a> = &'a BitBuffer;
2327
type Elem<'a> = bool;
2428

@@ -38,6 +42,21 @@ unsafe impl InputElement for bool {
3842
Ok(array.execute::<BoolArray>(ctx)?.into_bit_buffer())
3943
}
4044

45+
fn decode_constant(array: ArrayRef, _ctx: &mut ExecutionCtx) -> VortexResult<Self::Constant> {
46+
let Some(constant) = array.as_opt::<Constant>() else {
47+
vortex_bail!(
48+
"a Boolean batch constant must use the Constant encoding, got {}",
49+
array.encoding_id()
50+
);
51+
};
52+
let scalar = constant.scalar();
53+
let Some(ScalarValue::Bool(value)) = scalar.value() else {
54+
vortex_bail!("a Boolean batch constant must contain a non-null value, got {scalar}");
55+
};
56+
57+
Ok(*value)
58+
}
59+
4160
fn can_decode_null_tolerant(_array: &ArrayRef) -> VortexResult<bool> {
4261
Ok(true)
4362
}
@@ -46,6 +65,10 @@ unsafe impl InputElement for bool {
4665
column.value(index)
4766
}
4867

68+
fn get_constant(constant: &Self::Constant) -> bool {
69+
*constant
70+
}
71+
4972
fn view(column: &Self::Column) -> Self::View<'_> {
5073
column
5174
}

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

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33

44
//! Typed decoding and row access for one input column.
55
//!
6-
//! [`InputElement`] separates invocation-wide decoding from the checked and unchecked access paths
7-
//! used by row kernels.
6+
//! [`InputElement`] separates invocation-wide column and batch-constant decoding from the checked
7+
//! and unchecked access paths used by row kernels.
88
99
use vortex_error::VortexResult;
1010

@@ -27,6 +27,11 @@ pub unsafe trait InputElement: 'static {
2727
/// The decoded column representation supporting `O(1)` row access.
2828
type Column;
2929

30+
/// The decoded representation of one non-null batch-constant input.
31+
///
32+
/// This representation stores one logical value regardless of the input array's batch length.
33+
type Constant;
34+
3035
/// The row-loop view of a decoded column.
3136
///
3237
/// This can borrow a cheaper representation than [`Column`](Self::Column). Primitive elements,
@@ -62,6 +67,12 @@ pub unsafe trait InputElement: 'static {
6267
/// and other invocation-invariant work into this method.
6368
fn decode(array: ArrayRef, ctx: &mut ExecutionCtx) -> VortexResult<Self::Column>;
6469

70+
/// Decode one non-null batch-constant input.
71+
///
72+
/// `array` retains its logical batch length. Implementations can extract one value directly
73+
/// without slicing or materializing a one-row column.
74+
fn decode_constant(array: ArrayRef, ctx: &mut ExecutionCtx) -> VortexResult<Self::Constant>;
75+
6576
/// Whether [`decode_null_tolerant`](Self::decode_null_tolerant) can decode this array.
6677
///
6778
/// The conservative default declines. An implementation whose ordinary decode is safe and
@@ -90,6 +101,9 @@ pub unsafe trait InputElement: 'static {
90101
/// Read one row without repeating batch-constant work from [`decode`](Self::decode).
91102
fn get(column: &Self::Column, index: usize) -> Self::Elem<'_>;
92103

104+
/// Read the element stored in a decoded batch constant.
105+
fn get_constant(constant: &Self::Constant) -> Self::Elem<'_>;
106+
93107
/// Borrow the representation used inside the row loop.
94108
///
95109
/// Executors call this before the hot loop. For every index below the returned view's length,

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

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,20 @@ use vortex_error::vortex_ensure_eq;
99
use crate::ArrayRef;
1010
use crate::ExecutionCtx;
1111
use crate::IntoArray;
12+
use crate::arrays::Constant;
1213
use crate::arrays::PrimitiveArray;
1314
use crate::dtype::DType;
1415
use crate::dtype::NativePType;
1516
use crate::dtype::Nullability;
17+
use crate::scalar::ScalarValue;
1618
use crate::scalar_fn::unstable::row::InputElement;
1719
use crate::scalar_fn::unstable::row::OutputElement;
1820
use crate::validity::Validity;
1921

2022
// SAFETY: the view is a native slice, and its reported length is the slice length.
2123
unsafe impl<T: NativePType> InputElement for T {
2224
type Column = Buffer<T>;
25+
type Constant = T;
2326
type View<'a> = &'a [T];
2427
type Elem<'a> = T;
2528

@@ -44,6 +47,24 @@ unsafe impl<T: NativePType> InputElement for T {
4447
Ok(array.execute::<PrimitiveArray>(ctx)?.into_buffer::<T>())
4548
}
4649

50+
fn decode_constant(array: ArrayRef, _ctx: &mut ExecutionCtx) -> VortexResult<Self::Constant> {
51+
let Some(constant) = array.as_opt::<Constant>() else {
52+
vortex_bail!(
53+
"a primitive batch constant must use the Constant encoding, got {}",
54+
array.encoding_id()
55+
);
56+
};
57+
let scalar = constant.scalar();
58+
let Some(ScalarValue::Primitive(value)) = scalar.value() else {
59+
vortex_bail!(
60+
"a primitive batch constant must contain a non-null {} value, got {scalar}",
61+
T::PTYPE
62+
);
63+
};
64+
65+
value.cast::<T>()
66+
}
67+
4768
fn can_decode_null_tolerant(_array: &ArrayRef) -> VortexResult<bool> {
4869
Ok(true)
4970
}
@@ -52,6 +73,10 @@ unsafe impl<T: NativePType> InputElement for T {
5273
column[index]
5374
}
5475

76+
fn get_constant(constant: &Self::Constant) -> T {
77+
*constant
78+
}
79+
5580
fn view(column: &Self::Column) -> Self::View<'_> {
5681
column.as_slice()
5782
}

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

Lines changed: 18 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ use crate::scalar_fn::ExecutionArgs;
2121
use crate::scalar_fn::unstable::row::InputElement;
2222
use crate::scalar_fn::unstable::row::ViewLen;
2323

24-
/// One decoded input, collapsed to a single row when it is constant for the batch.
24+
/// One decoded input, with batch constants represented by a single value.
2525
pub struct ArgColumn<T: InputElement>(
2626
/// The decoded argument, classified by how the row loop addresses it.
2727
pub(super) ArgColumnKind<T>,
@@ -31,40 +31,34 @@ pub(super) enum ArgColumnKind<T: InputElement> {
3131
/// A decoded column covering the full batch; executors validate its length before traversal.
3232
Column(T::Column),
3333

34-
/// Exactly one decoded row, established by [`ArgColumn::try_from_const`].
35-
Const(T::Column),
34+
/// One decoded batch-constant value.
35+
Const(T::Constant),
3636
}
3737

3838
impl<T: InputElement> ArgColumn<T> {
39-
fn try_from_const(column: T::Column) -> VortexResult<Self> {
40-
let decoded_len = T::view(&column).len();
41-
vortex_ensure_eq!(
42-
decoded_len,
43-
1,
44-
"a decoded batch-constant input must contain exactly 1 row, got {decoded_len}",
45-
);
46-
47-
Ok(Self(ArgColumnKind::Const(column)))
48-
}
49-
5039
fn decode(array: ArrayRef, ctx: &mut ExecutionCtx) -> VortexResult<Self> {
51-
// An empty input has no row 0 to slice, and its row loop runs zero times either way.
40+
// An empty input has no value to decode, and its row loop runs zero times either way.
5241
if let Some(const_array) = batch_const(&array)
5342
&& !array.is_empty()
5443
{
55-
return Self::try_from_const(T::decode(const_array.slice(0..1)?, ctx)?);
44+
return Ok(Self(ArgColumnKind::Const(T::decode_constant(
45+
const_array,
46+
ctx,
47+
)?)));
5648
}
5749

5850
Ok(Self(ArgColumnKind::Column(T::decode(array, ctx)?)))
5951
}
6052

6153
fn decode_null_tolerant(array: ArrayRef, ctx: &mut ExecutionCtx) -> VortexResult<Option<Self>> {
62-
// Batch execution short-circuits null constants before selecting a strategy, so a
63-
// constant reaching this path is non-null and can use the ordinary decode.
54+
// Batch execution short-circuits null constants before selecting a strategy.
6455
if let Some(const_array) = batch_const(&array)
6556
&& !array.is_empty()
6657
{
67-
return Self::try_from_const(T::decode(const_array.slice(0..1)?, ctx)?).map(Some);
58+
return T::decode_constant(const_array, ctx)
59+
.map(ArgColumnKind::Const)
60+
.map(Self)
61+
.map(Some);
6862
}
6963

7064
Ok(T::decode_null_tolerant(array, ctx)?
@@ -85,7 +79,7 @@ impl<T: InputElement> ArgColumn<T> {
8579
fn get(&self, index: usize) -> T::Elem<'_> {
8680
match &self.0 {
8781
ArgColumnKind::Column(column) => T::get(column, index),
88-
ArgColumnKind::Const(column) => T::get(column, 0),
82+
ArgColumnKind::Const(constant) => T::get_constant(constant),
8983
}
9084
}
9185

@@ -97,7 +91,7 @@ impl<T: InputElement> ArgColumn<T> {
9791
}
9892

9993
fn addresses_rows(&self, row_count: usize) -> bool {
100-
// A constant is validated when constructed and is always read at index zero.
94+
// A constant represents one value for every logical row.
10195
match &self.0 {
10296
ArgColumnKind::Column(column) => T::view(column).len() == row_count,
10397
ArgColumnKind::Const(_) => true,
@@ -107,7 +101,7 @@ impl<T: InputElement> ArgColumn<T> {
107101
fn const_value(&self) -> Option<T::Elem<'_>> {
108102
match &self.0 {
109103
ArgColumnKind::Column(_) => None,
110-
ArgColumnKind::Const(column) => Some(T::get(column, 0)),
104+
ArgColumnKind::Const(constant) => Some(T::get_constant(constant)),
111105
}
112106
}
113107
}
@@ -215,8 +209,8 @@ pub trait ElementTuple: 'static + private::Sealed {
215209
/// Whether every argument decoded at full batch length contains exactly `row_count` rows.
216210
///
217211
/// `Columns` cannot implement [`ViewLen`] because a batch-constant `ArgColumn` stores one
218-
/// decoded row while logically addressing the full batch. The batch-constant constructor
219-
/// validates that one-row representation, so this method checks only non-constant columns.
212+
/// decoded value while logically addressing the full batch. This method therefore checks only
213+
/// non-constant columns.
220214
fn decoded_lens_match(columns: &Self::Columns, row_count: usize) -> bool;
221215

222216
/// Read one row from borrowed views.

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

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,9 @@ pub(in crate::scalar_fn::unstable::row) fn decoded_source<'a, Args: IndexedEleme
5252
enum ArgColumnSource<'a, T: InputElement> {
5353
Rows(T::View<'a>),
5454

55-
/// A validated one-row view that logically addresses `row_count` rows.
55+
/// One decoded value that logically addresses `row_count` rows.
5656
Constant {
57-
view: T::View<'a>,
57+
constant: &'a T::Constant,
5858
row_count: usize,
5959
},
6060
}
@@ -66,10 +66,10 @@ impl<'a, T: InputElement> ArgColumnSource<'a, T> {
6666
let view = T::view(column);
6767
(view.len() == row_count).then_some(Self::Rows(view))
6868
}
69-
ArgColumnKind::Const(column) => {
70-
let view = T::view(column);
71-
(view.len() == 1).then_some(Self::Constant { view, row_count })
72-
}
69+
ArgColumnKind::Const(constant) => Some(Self::Constant {
70+
constant,
71+
row_count,
72+
}),
7373
}
7474
}
7575
}
@@ -91,10 +91,7 @@ impl<'a, T: InputElement> IndexedSource for ArgColumnSource<'a, T> {
9191
// caller guarantees that `index` is below the source length.
9292
unsafe { T::get_from_view_unchecked(view, index) }
9393
}
94-
Self::Constant { view, .. } => {
95-
// SAFETY: `try_new` checked that this exact retained view contains row zero.
96-
unsafe { T::get_from_view_unchecked(view, 0) }
97-
}
94+
Self::Constant { constant, .. } => T::get_constant(constant),
9895
}
9996
}
10097
}

0 commit comments

Comments
 (0)