Skip to content

Commit 0d24650

Browse files
committed
Define experimental RowFn contracts
Signed-off-by: Connor Tsui <connor.tsui20@gmail.com>
1 parent 204d1d4 commit 0d24650

25 files changed

Lines changed: 2107 additions & 1 deletion

File tree

‎vortex-array/Cargo.toml‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,8 @@ cudarc = ["dep:cudarc"]
7979
table-display = ["dep:tabled"]
8080
_test-harness = ["dep:goldenfile", "dep:rstest", "dep:rstest_reuse"]
8181
serde = ["dep:serde", "vortex-buffer/serde", "vortex-mask/serde"]
82+
# Exposes experimental row-function APIs without compatibility guarantees.
83+
unstable_row_fns = []
8284

8385
[dev-dependencies]
8486
divan = { workspace = true }
@@ -90,7 +92,11 @@ rstest = { workspace = true }
9092
serde_json = { workspace = true }
9193
serde_test = { workspace = true }
9294
test-with = { workspace = true }
93-
vortex-array = { path = ".", features = ["_test-harness", "table-display"] }
95+
vortex-array = { path = ".", features = [
96+
"_test-harness",
97+
"table-display",
98+
"unstable_row_fns",
99+
] }
94100

95101
[[bench]]
96102
name = "aggregate_max"

‎vortex-array/src/scalar_fn/mod.rs‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@
66
//! This module contains the [`ScalarFnVTable`] trait and all built-in scalar function
77
//! implementations. Expressions ([`crate::expr::Expression`]) reference scalar functions
88
//! at each node.
9+
//!
10+
//! Use `unstable::row::RowFn` for strict functions whose natural kernel computes one row at a
11+
//! time. It derives decoding, constant handling, null propagation, output construction, and
12+
//! validity. This experimental API requires the `unstable_row_fns` feature and has no compatibility
13+
//! guarantees. Implement [`ScalarFnVTable`] directly when the natural kernel is columnar, aliases
14+
//! an input, or may produce null from otherwise valid inputs.
915
1016
use vortex_session::registry::Id;
1117

@@ -35,6 +41,12 @@ pub use options::*;
3541
mod signature;
3642
pub use signature::*;
3743

44+
#[cfg(feature = "unstable_row_fns")]
45+
pub mod unstable;
46+
#[cfg(not(feature = "unstable_row_fns"))]
47+
#[allow(dead_code, unused_imports)]
48+
pub(crate) mod unstable;
49+
3850
pub mod fns;
3951
pub mod internal;
4052
pub mod session;
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// SPDX-FileCopyrightText: Copyright the Vortex contributors
3+
4+
//! Experimental scalar-function APIs without compatibility guarantees.
5+
//!
6+
//! These APIs can change or disappear without a deprecation period. External users must enable
7+
//! the corresponding `unstable_*` Cargo feature before importing them.
8+
9+
pub mod row;
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// SPDX-FileCopyrightText: Copyright the Vortex contributors
3+
4+
//! Outcomes returned by encoding-aware and row-oriented kernels.
5+
6+
mod outcome;
7+
pub use outcome::RowExecution;
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// SPDX-FileCopyrightText: Copyright the Vortex contributors
3+
4+
use vortex_error::VortexError;
5+
use vortex_error::VortexResult;
6+
7+
use crate::ArrayRef;
8+
9+
/// The outcome of a row loop before batch execution decides whether an error is observable.
10+
///
11+
/// Together with the surrounding [`VortexResult`], this represents three outcomes:
12+
///
13+
/// - `Err(error)` is a non-retryable execution or immediate row error.
14+
/// - [`Output`](Self::Output) is a successful row loop.
15+
/// - [`DeferredError`](Self::DeferredError) is failure evidence from a completed row loop.
16+
///
17+
/// A dense loop may evaluate values behind nulls. Its deferred error is therefore not necessarily
18+
/// observable: batch execution can retry over only valid rows, suppressing an error that came from
19+
/// a null row while preserving one from a valid row. A plain `VortexResult<ArrayRef>` would lose
20+
/// the distinction between that retryable error and an error for which retrying cannot help.
21+
///
22+
/// Once execution is known to contain only valid rows, converting this outcome into a
23+
/// `VortexResult<ArrayRef>` turns [`DeferredError`](Self::DeferredError) into an ordinary error.
24+
pub enum RowExecution {
25+
/// The successfully built, full-length output column.
26+
Output(ArrayRef),
27+
28+
/// An error constructed from failure evidence reduced across a completed row loop.
29+
DeferredError(VortexError),
30+
}
31+
32+
impl From<RowExecution> for VortexResult<ArrayRef> {
33+
fn from(execution: RowExecution) -> Self {
34+
match execution {
35+
RowExecution::Output(output) => Ok(output),
36+
RowExecution::DeferredError(error) => Err(error),
37+
}
38+
}
39+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// SPDX-FileCopyrightText: Copyright the Vortex contributors
3+
4+
//! Scalar functions computed one row at a time.
5+
//!
6+
//! This module is experimental and has no compatibility guarantees. External users must enable
7+
//! the `unstable_row_fns` Cargo feature before importing it.
8+
//!
9+
//! Start with [`RowFn`] to define an operation and [`RowVisitor`] to select one of its output
10+
//! capabilities. [`InputElement`] and [`ElementTuple`] describe how columns become row values.
11+
//! [`OutputElement`] covers independent owned values, while [`OutputSink`] supports outputs that
12+
//! need row handles or shared batch state. [`SinkResult`] describes how a sink-writing closure
13+
//! reports errors.
14+
//!
15+
//! The internal executor owns decoding, batch constants, null propagation, allocation, and
16+
//! validity. A visitor's prepare closure may derive shared state from constant operands once per
17+
//! batch.
18+
19+
mod execute;
20+
pub use execute::RowExecution;
21+
22+
mod row_fn;
23+
pub use row_fn::RowFn;
24+
25+
mod types;
26+
pub use types::ElementTuple;
27+
pub use types::IndexedElementTuple;
28+
pub use types::InitializedElement;
29+
pub use types::InputElement;
30+
pub use types::OutputElement;
31+
pub use types::OutputSink;
32+
pub use types::SinkResult;
33+
pub use types::UninitElementSink;
34+
35+
mod visitor;
36+
pub use visitor::RowVisitor;
37+
38+
mod vtable;
39+
pub use vtable::execute_rows;
40+
pub use vtable::row_fn_return_dtype;
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// SPDX-FileCopyrightText: Copyright the Vortex contributors
3+
4+
//! Scalar functions computed one row at a time.
5+
6+
use std::fmt::Debug;
7+
use std::fmt::Display;
8+
use std::hash::Hash;
9+
10+
use vortex_error::VortexResult;
11+
use vortex_error::vortex_bail;
12+
use vortex_session::VortexSession;
13+
14+
use super::visitor::RowVisitor;
15+
use crate::ArrayRef;
16+
use crate::ExecutionCtx;
17+
use crate::dtype::DType;
18+
use crate::scalar_fn::ScalarFnId;
19+
use crate::scalar_fn::unstable::row::RowExecution;
20+
21+
/// A scalar function computed one row at a time.
22+
///
23+
/// Declare the argument names and use [`dispatch`](Self::dispatch) to choose concrete element and
24+
/// sink types for each accepted dtype combination.
25+
///
26+
/// Every `RowFn` receives the standard [`ScalarFnVTable`] implementation. A function that needs
27+
/// custom scalar-function hooks instead implements `ScalarFnVTable` on its public type and
28+
/// delegates row execution to a private `RowFn` kernel with [`row_fn_return_dtype`] and
29+
/// [`execute_rows`]. Implement only `ScalarFnVTable` when the natural kernel is columnar.
30+
///
31+
/// [`ScalarFnVTable`]: crate::scalar_fn::ScalarFnVTable
32+
/// [`execute_rows`]: crate::scalar_fn::unstable::row::execute_rows
33+
/// [`row_fn_return_dtype`]: crate::scalar_fn::unstable::row::row_fn_return_dtype
34+
pub trait RowFn: 'static + Sized + Clone + Send + Sync {
35+
/// Options for this function, or [`EmptyOptions`](crate::scalar_fn::EmptyOptions) for none.
36+
type Options: 'static + Send + Sync + Clone + Debug + Display + PartialEq + Eq + Hash;
37+
38+
/// The arguments in display order. Its length is the function's exact arity.
39+
const ARG_NAMES: &'static [&'static str];
40+
41+
/// Whether any legal dispatch or encoding-aware execution can raise a semantic error.
42+
///
43+
/// The framework checks this at compile time for every fallible dispatched element or result.
44+
/// A conservative `true` is allowed when only some dtype choices are fallible.
45+
///
46+
/// Set this to `true` when [`reduce_encoded`](Self::reduce_encoded) can return a semantic
47+
/// error or [`RowExecution::DeferredError`]. Compile-time checks cover dispatched element and
48+
/// result types, but cannot inspect that hook.
49+
/// [`OutputSink`](crate::scalar_fn::unstable::row::OutputSink) lifecycle methods only report
50+
/// incidental execution failures. Semantic sink errors must come from the row callback.
51+
///
52+
/// Semantic errors are defined by
53+
/// [`ScalarFnVTable::is_fallible`](crate::scalar_fn::ScalarFnVTable::is_fallible).
54+
const FALLIBLE: bool = false;
55+
56+
/// Returns the ID of the scalar function.
57+
fn id(&self) -> ScalarFnId;
58+
59+
/// Serialize this function's options, or return `None` when the function is not serializable.
60+
fn serialize(&self, options: &Self::Options) -> VortexResult<Option<Vec<u8>>> {
61+
_ = options;
62+
Ok(None)
63+
}
64+
65+
/// Restore options written by [`serialize`](Self::serialize).
66+
fn deserialize(
67+
&self,
68+
_metadata: &[u8],
69+
_session: &VortexSession,
70+
) -> VortexResult<Self::Options> {
71+
vortex_bail!("Expression {} is not deserializable", self.id())
72+
}
73+
74+
/// Choose element types for these input dtypes and visit the framework with them.
75+
///
76+
/// Plan time and run time both call this method, so the choice **must** be a pure function of
77+
/// `options` and `args`. The framework rejects a change to the derived nullable execution
78+
/// policy before row execution. It cannot compare the remaining types, preparation values, or
79+
/// closure behavior, so those must also remain stable. Cross-argument dtype validation belongs
80+
/// here.
81+
fn dispatch<V: RowVisitor<Self::Options>>(
82+
&self,
83+
options: &Self::Options,
84+
args: &[DType],
85+
visitor: V,
86+
) -> VortexResult<V::VisitResult>;
87+
88+
/// Try an encoding-aware implementation before decoding the inputs into row elements.
89+
///
90+
/// `None` continues to the dispatched row loop. [`Output`](RowExecution::Output) skips that
91+
/// loop and may remain encoded or lazy. [`DeferredError`](RowExecution::DeferredError) reruns
92+
/// only valid rows when null payloads may have caused the failure. For non-nullary functions,
93+
/// batch execution calls this hook at most once with the original, unfiltered arrays; slices
94+
/// and compacted retries do not reach it.
95+
///
96+
/// Like a dense row closure, this hook must be total over every stored payload, including
97+
/// payloads behind null rows. An `Err` is immediately user-visible and is never suppressed or
98+
/// retried through the row layer.
99+
///
100+
/// # Requirements
101+
///
102+
/// - `output.len()` **must** equal `args[0].len()`.
103+
/// - The output dtype **must** match the planned dtype when ignoring nullability.
104+
/// - The output **must not** introduce a null where every input is valid.
105+
///
106+
/// The framework skips this hook for nullary functions.
107+
fn reduce_encoded(
108+
&self,
109+
options: &Self::Options,
110+
args: &[ArrayRef],
111+
ctx: &mut ExecutionCtx,
112+
) -> VortexResult<Option<RowExecution>> {
113+
_ = (options, args, ctx);
114+
Ok(None)
115+
}
116+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// SPDX-FileCopyrightText: Copyright the Vortex contributors
3+
4+
use vortex_buffer::BitBuffer;
5+
use vortex_error::VortexResult;
6+
use vortex_error::vortex_ensure;
7+
8+
use crate::ArrayRef;
9+
use crate::ExecutionCtx;
10+
use crate::IntoArray;
11+
use crate::arrays::BoolArray;
12+
use crate::dtype::DType;
13+
use crate::dtype::Nullability;
14+
use crate::scalar_fn::unstable::row::InputElement;
15+
use crate::scalar_fn::unstable::row::OutputElement;
16+
use crate::validity::Validity;
17+
18+
// SAFETY: the per-row view is a bit buffer, and its reported length is the buffer length.
19+
unsafe impl InputElement for bool {
20+
type Column = BitBuffer;
21+
type View<'a> = &'a BitBuffer;
22+
type Elem<'a> = bool;
23+
24+
// Every bit of the buffer is readable, valid or not.
25+
const DENSE_SAFE: bool = true;
26+
const DECODE_FALLIBLE: bool = false;
27+
28+
fn validate(dtype: &DType) -> VortexResult<()> {
29+
vortex_ensure!(
30+
matches!(dtype, DType::Bool(_)),
31+
"expected a Bool column, got {dtype}",
32+
);
33+
Ok(())
34+
}
35+
36+
fn decode(array: ArrayRef, ctx: &mut ExecutionCtx) -> VortexResult<Self::Column> {
37+
Ok(array.execute::<BoolArray>(ctx)?.into_bit_buffer())
38+
}
39+
40+
fn get(column: &Self::Column, index: usize) -> bool {
41+
column.value(index)
42+
}
43+
44+
fn view(column: &Self::Column) -> Self::View<'_> {
45+
column
46+
}
47+
48+
fn view_len(view: &Self::View<'_>) -> usize {
49+
view.len()
50+
}
51+
52+
fn get_from_view<'a>(view: &Self::View<'a>, index: usize) -> bool
53+
where
54+
Self: 'a,
55+
{
56+
view.value(index)
57+
}
58+
59+
unsafe fn get_from_view_unchecked<'a>(view: &Self::View<'a>, index: usize) -> bool
60+
where
61+
Self: 'a,
62+
{
63+
// SAFETY: forwarded from this method's contract.
64+
unsafe { view.value_unchecked(index) }
65+
}
66+
}
67+
68+
impl OutputElement for bool {
69+
fn element_dtype() -> DType {
70+
DType::Bool(Nullability::NonNullable)
71+
}
72+
73+
fn build(values: Vec<Self>) -> ArrayRef {
74+
// `From<Vec<bool>>` uses the bulk bit-packing path.
75+
BoolArray::new(BitBuffer::from(values), Validity::NonNullable).into_array()
76+
}
77+
}

0 commit comments

Comments
 (0)