Skip to content

Commit 8d15d3a

Browse files
committed
Add list_transform expression
Adds a ListTransform scalar fn implementing DuckDB-style list_transform(l, lambda x: ...) as a Vortex expression. The lambda body is an ordinary Expression stored in the fn's options, evaluated with the list's elements array as its root scope (root() inside the body refers to the element). Execution rewraps the list structure around a deferred apply() of the body to the elements child, so offsets and list validity pass through untouched and no element values are computed eagerly. Includes: - identity (body == root) and fusion (transform-of-transform) rewrites in simplify_untyped, plus list_length(list_transform(l, f)) -> list_length(l) - proto serde of the options-embedded body via the session registry - fallibility delegation to the body, since generic tree walks cannot see options-embedded expressions - design doc under scalar_fn/fns/list_transform/design.md Signed-off-by: Matt Katz <mhkatz97@gmail.com>
1 parent 1743bb0 commit 8d15d3a

6 files changed

Lines changed: 1041 additions & 1 deletion

File tree

vortex-array/src/expr/exprs.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ use crate::scalar_fn::fns::like::Like;
3838
use crate::scalar_fn::fns::like::LikeOptions;
3939
use crate::scalar_fn::fns::list_contains::ListContains;
4040
use crate::scalar_fn::fns::list_length::ListLength;
41+
use crate::scalar_fn::fns::list_transform::ListTransform;
42+
use crate::scalar_fn::fns::list_transform::ListTransformOptions;
4143
use crate::scalar_fn::fns::literal::Literal;
4244
use crate::scalar_fn::fns::mask::Mask;
4345
use crate::scalar_fn::fns::merge::DuplicateHandling;
@@ -765,3 +767,21 @@ pub fn ext_storage(input: Expression) -> Expression {
765767
pub fn list_length(input: Expression) -> Expression {
766768
ListLength.new_expr(EmptyOptions, [input])
767769
}
770+
771+
// ---- ListTransform ----
772+
773+
/// Creates an expression that transforms every element of a list with `body`, preserving the
774+
/// list's structure (offsets and list-level validity). This is akin to DuckDB's
775+
/// `list_transform()` with a lambda.
776+
///
777+
/// `body` is evaluated with the list's elements as its root scope: within `body`, `root()`
778+
/// refers to the element rather than the enclosing row. For example, DuckDB's
779+
/// `list_transform(tags, lambda x: x + 1)` is expressed as:
780+
///
781+
/// ```rust
782+
/// # use vortex_array::expr::{checked_add, get_item, list_transform, lit, root};
783+
/// let expr = list_transform(get_item("tags", root()), checked_add(root(), lit(1)));
784+
/// ```
785+
pub fn list_transform(input: Expression, body: Expression) -> Expression {
786+
ListTransform.new_expr(ListTransformOptions { body }, [input])
787+
}

vortex-array/src/scalar_fn/fns/list_length.rs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ use crate::scalar_fn::EmptyOptions;
3131
use crate::scalar_fn::ExecutionArgs;
3232
use crate::scalar_fn::ScalarFnId;
3333
use crate::scalar_fn::ScalarFnVTable;
34+
use crate::scalar_fn::fns::list_transform::ListTransform;
3435
use crate::scalar_fn::fns::operators::Operator;
3536

3637
/// Number of elements in each list of a `List` or `FixedSizeList` typed array.
@@ -98,6 +99,20 @@ impl ScalarFnVTable for ListLength {
9899
list_length(&input, nullability, ctx)
99100
}
100101

102+
fn simplify_untyped(
103+
&self,
104+
_options: &Self::Options,
105+
expr: &Expression,
106+
) -> VortexResult<Option<Expression>> {
107+
// `list_transform` preserves list lengths, so skip it entirely:
108+
// list_length(list_transform(l, f)) == list_length(l).
109+
let input = expr.child(0);
110+
if input.is::<ListTransform>() {
111+
return Ok(Some(expr.clone().with_children([input.child(0).clone()])?));
112+
}
113+
Ok(None)
114+
}
115+
101116
fn validity(
102117
&self,
103118
_: &Self::Options,
@@ -173,7 +188,7 @@ fn list_length_from_offsets(list: ArrayView<'_, List>) -> VortexResult<ArrayRef>
173188
}
174189

175190
/// Matches an `Array<List>`, `Array<ListView>`, or `Array<FixedSizeList>`
176-
struct AnyList;
191+
pub(crate) struct AnyList;
177192

178193
impl Matcher for AnyList {
179194
type Match<'a> = ();

0 commit comments

Comments
 (0)