Skip to content

Commit 5549426

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

22 files changed

Lines changed: 1572 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: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
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+
//! [`unstable::row`] defines experimental contracts for strict functions whose natural kernel
11+
//! computes one row at a time. This API requires the `unstable_row_fns` feature and has no
12+
//! compatibility guarantees.
913
1014
use vortex_session::registry::Id;
1115

@@ -35,6 +39,12 @@ pub use options::*;
3539
mod signature;
3640
pub use signature::*;
3741

42+
#[cfg(feature = "unstable_row_fns")]
43+
pub mod unstable;
44+
#[cfg(not(feature = "unstable_row_fns"))]
45+
#[allow(dead_code, unused_imports)]
46+
pub(crate) mod unstable;
47+
3848
pub mod fns;
3949
pub mod internal;
4050
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: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
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. [`RowExecution`] represents the outcome of an encoding-aware or row-oriented
14+
//! kernel before an execution backend resolves deferred failures.
15+
16+
mod execute;
17+
pub use execute::RowExecution;
18+
19+
mod row_fn;
20+
pub use row_fn::RowFn;
21+
22+
mod types;
23+
pub use types::ElementTuple;
24+
pub use types::IndexedElementTuple;
25+
pub use types::InitializedElement;
26+
pub use types::InputElement;
27+
pub use types::OutputElement;
28+
pub use types::OutputSink;
29+
pub use types::SinkResult;
30+
pub use types::UninitElementSink;
31+
32+
mod visitor;
33+
pub use visitor::RowVisitor;
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
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+
/// An execution backend can implement the sealed [`RowVisitor`] contract to plan and execute the
27+
/// concrete signature selected by [`dispatch`](Self::dispatch).
28+
pub trait RowFn: 'static + Sized + Clone + Send + Sync {
29+
/// Options for this function, or [`EmptyOptions`](crate::scalar_fn::EmptyOptions) for none.
30+
type Options: 'static + Send + Sync + Clone + Debug + Display + PartialEq + Eq + Hash;
31+
32+
/// The arguments in display order. Its length is the function's exact arity.
33+
const ARG_NAMES: &'static [&'static str];
34+
35+
/// Whether any legal dispatch or encoding-aware execution can raise a semantic error.
36+
///
37+
/// The framework checks this at compile time for every fallible dispatched element or result.
38+
/// A conservative `true` is allowed when only some dtype choices are fallible.
39+
///
40+
/// Set this to `true` when [`reduce_encoded`](Self::reduce_encoded) can return a semantic
41+
/// error or [`RowExecution::DeferredError`]. Compile-time checks cover dispatched element and
42+
/// result types, but cannot inspect that hook.
43+
///
44+
/// Semantic errors are defined by
45+
/// [`ScalarFnVTable::is_fallible`](crate::scalar_fn::ScalarFnVTable::is_fallible).
46+
const FALLIBLE: bool = false;
47+
48+
/// Returns the ID of the scalar function.
49+
fn id(&self) -> ScalarFnId;
50+
51+
/// Serialize this function's options, or return `None` when the function is not serializable.
52+
fn serialize(&self, options: &Self::Options) -> VortexResult<Option<Vec<u8>>> {
53+
_ = options;
54+
Ok(None)
55+
}
56+
57+
/// Restore options written by [`serialize`](Self::serialize).
58+
fn deserialize(
59+
&self,
60+
_metadata: &[u8],
61+
_session: &VortexSession,
62+
) -> VortexResult<Self::Options> {
63+
vortex_bail!("Expression {} is not deserializable", self.id())
64+
}
65+
66+
/// Choose element types for these input dtypes and visit the framework with them.
67+
///
68+
/// Plan time and run time both call this method, so the choice **must** be a pure function of
69+
/// `options` and `args`. Cross-argument dtype validation belongs here.
70+
fn dispatch<V: RowVisitor<Self::Options>>(
71+
&self,
72+
options: &Self::Options,
73+
args: &[DType],
74+
visitor: V,
75+
) -> VortexResult<V::VisitResult>;
76+
77+
/// Try an encoding-aware implementation before decoding the inputs into row elements.
78+
///
79+
/// `None` continues to the dispatched row loop. [`Output`](RowExecution::Output) skips that
80+
/// loop and may remain encoded or lazy. [`DeferredError`](RowExecution::DeferredError) reruns
81+
/// only valid rows when null payloads may have caused the failure. For non-nullary functions,
82+
/// batch execution calls this hook at most once with the original, unfiltered arrays; slices
83+
/// and compacted retries do not reach it.
84+
///
85+
/// Like a dense row closure, this hook must be total over every stored payload, including
86+
/// payloads behind null rows. An `Err` is immediately user-visible and is never suppressed or
87+
/// retried through the row layer.
88+
///
89+
/// # Requirements
90+
///
91+
/// - `output.len()` **must** equal `args[0].len()`.
92+
/// - The output dtype **must** match the planned dtype when ignoring nullability.
93+
/// - The output **must not** introduce a null where every input is valid.
94+
///
95+
/// The framework skips this hook for nullary functions.
96+
fn reduce_encoded(
97+
&self,
98+
options: &Self::Options,
99+
args: &[ArrayRef],
100+
ctx: &mut ExecutionCtx,
101+
) -> VortexResult<Option<RowExecution>> {
102+
_ = (options, args, ctx);
103+
Ok(None)
104+
}
105+
}
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)