Skip to content

Commit e47595d

Browse files
committed
Pushdown some expressions to Dict layout reader
Signed-off-by: Mikhail Kot <mikhail@spiraldb.com>
1 parent 2528dca commit e47595d

4 files changed

Lines changed: 170 additions & 9 deletions

File tree

vortex-array/src/expr/analysis/annotation.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,23 @@ pub fn descendent_annotations<A: AnnotationFn>(
4747
visitor.annotations
4848
}
4949

50+
pub fn annotations<A: AnnotationFn>(
51+
expr: &Expression,
52+
annotate: A,
53+
) -> Annotations<'_, A::Annotation> {
54+
let mut visitor = DirectAnnotationVisitor {
55+
annotations: Default::default(),
56+
annotate,
57+
};
58+
expr.accept(&mut visitor).vortex_expect("Infallible");
59+
visitor.annotations
60+
}
61+
62+
struct DirectAnnotationVisitor<'a, A: AnnotationFn> {
63+
annotations: Annotations<'a, A::Annotation>,
64+
annotate: A,
65+
}
66+
5067
struct AnnotationVisitor<'a, A: AnnotationFn> {
5168
annotations: Annotations<'a, A::Annotation>,
5269
annotate: A,
@@ -84,3 +101,19 @@ impl<'a, A: AnnotationFn> NodeVisitor<'a> for AnnotationVisitor<'a, A> {
84101
Ok(TraversalOrder::Continue)
85102
}
86103
}
104+
105+
impl<'a, A: AnnotationFn> NodeVisitor<'a> for DirectAnnotationVisitor<'a, A> {
106+
type NodeTy = Expression;
107+
fn visit_down(&mut self, node: &'a Self::NodeTy) -> VortexResult<TraversalOrder> {
108+
let annotations = (self.annotate)(node);
109+
if annotations.is_empty() {
110+
Ok(TraversalOrder::Continue)
111+
} else {
112+
self.annotations
113+
.entry(node)
114+
.or_default()
115+
.extend(annotations);
116+
Ok(TraversalOrder::Skip)
117+
}
118+
}
119+
}

vortex-array/src/expr/transform/partition.rs

Lines changed: 110 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
use std::fmt::Display;
55
use std::fmt::Formatter;
6+
use std::hash::Hash;
67

78
use itertools::Itertools;
89
use vortex_error::VortexExpect;
@@ -19,13 +20,50 @@ use crate::expr::analysis::Annotation;
1920
use crate::expr::analysis::AnnotationFn;
2021
use crate::expr::analysis::Annotations;
2122
use crate::expr::analysis::descendent_annotations;
23+
use crate::expr::annotations;
2224
use crate::expr::get_item;
25+
use crate::expr::is_root;
26+
use crate::expr::label_tree;
2327
use crate::expr::pack;
2428
use crate::expr::root;
2529
use crate::expr::traversal::NodeExt;
2630
use crate::expr::traversal::NodeRewriter;
2731
use crate::expr::traversal::Transformed;
2832
use crate::expr::traversal::TraversalOrder;
33+
use crate::scalar_fn::is_negative_cost;
34+
35+
/// Split expression into two parts:
36+
///
37+
/// left is the outer part that we want to apply to array after canonicalizing.
38+
/// right is the optional inner part that we want to apply to array before
39+
/// canonicalizing.
40+
///
41+
/// We want to push to array only if expression has a negative cost, is
42+
/// infallible and null-insensitive.
43+
pub fn split_expression_for_pushdown(
44+
expr: Expression,
45+
dtype: &DType,
46+
) -> VortexResult<(Expression, Option<Expression>)> {
47+
let references_root = label_tree(&expr, is_root, |acc, &child| acc | child);
48+
let annotations = annotations(&expr, |expr| {
49+
let signature = expr.signature();
50+
if !signature.is_fallible()
51+
&& !signature.is_null_sensitive()
52+
&& is_negative_cost(expr.id())
53+
&& references_root.get(&expr).copied().unwrap_or(true)
54+
{
55+
vec![""] // we want a single annotation so an empty literal is fine
56+
} else {
57+
vec![]
58+
}
59+
});
60+
let partition = partition_annotations(expr.clone(), dtype, annotations)?;
61+
if partition.partitions.is_empty() {
62+
Ok((partition.root, None))
63+
} else {
64+
Ok((partition.root, Some(partition.partitions[0].clone())))
65+
}
66+
}
2967

3068
/// Partition an expression into sub-expressions that are uniquely associated with an annotation.
3169
/// A root expression is also returned that can be used to recombine the results of the partitions
@@ -49,11 +87,22 @@ where
4987
{
5088
// Annotate each expression with the annotations that any of its descendent expressions have.
5189
let annotations = descendent_annotations(&expr, annotate_fn);
90+
partition_annotations(expr.clone(), scope, annotations)
91+
}
5292

93+
pub fn partition_annotations<A>(
94+
expr: Expression,
95+
scope: &DType,
96+
annotations: Annotations<A>,
97+
) -> VortexResult<PartitionedExpr<A>>
98+
where
99+
A: Display + Clone + Eq + Hash,
100+
FieldName: From<A>,
101+
{
53102
// Now we split the original expression into sub-expressions based on the annotations, and
54103
// generate a root expression to re-assemble the results.
55-
let mut splitter = StructFieldExpressionSplitter::<A::Annotation>::new(&annotations);
56-
let root = expr.clone().rewrite(&mut splitter)?.value;
104+
let mut splitter = StructFieldExpressionSplitter::<A>::new(&annotations);
105+
let root = expr.rewrite(&mut splitter)?.value;
57106

58107
let mut partitions = Vec::with_capacity(splitter.sub_expressions.len());
59108
let mut partition_annotations = Vec::with_capacity(splitter.sub_expressions.len());
@@ -205,10 +254,16 @@ where
205254
mod tests {
206255
use rstest::fixture;
207256
use rstest::rstest;
257+
use vortex_array::expr::Expression;
258+
use vortex_array::expr::byte_length;
259+
use vortex_array::expr::cast;
260+
use vortex_array::expr::like;
208261

262+
use super::split_expression_for_pushdown;
209263
use super::*;
210264
use crate::dtype::DType;
211265
use crate::dtype::Nullability::NonNullable;
266+
use crate::dtype::PType;
212267
use crate::dtype::PType::I32;
213268
use crate::dtype::StructFields;
214269
use crate::expr::analysis::make_free_field_annotator;
@@ -348,4 +403,57 @@ mod tests {
348403
let expected_b = pack([("b_0", pack([("b", col("b"))], NonNullable))], NonNullable);
349404
assert_eq!(part_b, &expected_b, "{part_b} {expected_b}");
350405
}
406+
407+
fn pushed_inner(exprs: impl IntoIterator<Item = Expression>) -> Expression {
408+
pack(
409+
exprs
410+
.into_iter()
411+
.enumerate()
412+
.map(|(idx, e)| (format!("_{idx}"), e)),
413+
NonNullable,
414+
)
415+
}
416+
417+
fn pushed_ref(idx: usize) -> Expression {
418+
get_item(format!("_{idx}"), get_item("", root()))
419+
}
420+
421+
#[test]
422+
fn split_expr_root() {
423+
let (outer, inner) = split_expression_for_pushdown(root(), &DType::Null).unwrap();
424+
assert_eq!(outer, root());
425+
assert_eq!(inner, None);
426+
}
427+
428+
#[test]
429+
fn split_expr_partial_pushdown() {
430+
// cast is fallible, thus not pushed
431+
let target = DType::Primitive(PType::I64, NonNullable);
432+
let expr = cast(byte_length(root()), target.clone());
433+
let (outer, inner) =
434+
split_expression_for_pushdown(expr, &DType::Utf8(false.into())).unwrap();
435+
// [0] = cast([1], dtype)
436+
// [1] = byte_length(root)
437+
assert_eq!(outer, cast(pushed_ref(0), target));
438+
assert_eq!(inner, Some(pushed_inner([byte_length(root())])));
439+
}
440+
441+
#[test]
442+
fn split_expr_full_pushdown() {
443+
let expr = byte_length(root());
444+
let (outer, inner) =
445+
split_expression_for_pushdown(expr, &DType::Utf8(false.into())).unwrap();
446+
assert_eq!(outer, pushed_ref(0));
447+
assert_eq!(inner, Some(pushed_inner([byte_length(root())])));
448+
}
449+
450+
#[test]
451+
fn split_expr_no_pushdown() {
452+
// like is fallible, thus not pushed. lit() does not reference root()
453+
let expr = like(root(), lit(1u64));
454+
let (outer, inner) =
455+
split_expression_for_pushdown(expr.clone(), &DType::Utf8(true.into())).unwrap();
456+
assert_eq!(outer, expr);
457+
assert_eq!(inner, None);
458+
}
351459
}

vortex-array/src/scalar_fn/mod.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@
99
1010
use vortex_session::registry::Id;
1111

12+
use crate::scalar_fn::fns::byte_length::ByteLength;
13+
use crate::scalar_fn::fns::get_item::GetItem;
14+
use crate::scalar_fn::fns::literal::Literal;
15+
1216
mod vtable;
1317
pub use vtable::*;
1418

@@ -48,3 +52,16 @@ mod sealed {
4852
/// This can be the **only** implementor for [`super::typed::DynScalarFn`].
4953
impl<V: ScalarFnVTable> Sealed for TypedScalarFnInstance<V> {}
5054
}
55+
56+
/// A scalar function has a negative cost if applying it to an array and
57+
/// canonicalizing is cheaper than canonicalizing an array and applying it.
58+
///
59+
/// Example of negative cost expressions are byte_length() and get_item() since
60+
/// they don't depend on input size.
61+
///
62+
/// Example of non-negative cost expression is like()
63+
pub fn is_negative_cost(id: ScalarFnId) -> bool {
64+
id == ScalarFnVTable::id(&ByteLength)
65+
|| id == ScalarFnVTable::id(&GetItem)
66+
|| id == ScalarFnVTable::id(&Literal)
67+
}

vortex-layout/src/layouts/dict/reader.rs

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ use vortex_array::dtype::DType;
2020
use vortex_array::dtype::FieldMask;
2121
use vortex_array::expr::Expression;
2222
use vortex_array::expr::root;
23+
use vortex_array::expr::transform::split_expression_for_pushdown;
2324
use vortex_array::optimizer::ArrayOptimizer;
2425
use vortex_error::VortexError;
2526
use vortex_error::VortexExpect;
@@ -100,10 +101,7 @@ impl DictReader {
100101
)
101102
.vortex_expect("must construct dict values array evaluation")
102103
.map_err(Arc::new)
103-
.map(move |array| {
104-
let array = array?;
105-
Ok(SharedArray::new(array).into_array())
106-
})
104+
.map(move |array| Ok(SharedArray::new(array?).into_array()))
107105
.boxed()
108106
.shared()
109107
})
@@ -229,13 +227,18 @@ impl LayoutReader for DictReader {
229227
mask: MaskFuture,
230228
) -> VortexResult<BoxFuture<'static, VortexResult<ArrayRef>>> {
231229
// TODO: fix up expr partitioning with fallible & null sensitive annotations
232-
let values_eval = self.values_array();
233230
let codes_eval = self
234231
.codes
235232
.projection_evaluation(row_range, &root(), mask)
236233
.map_err(|err| err.with_context("While evaluating projection on codes"))?;
237-
let expr = expr.clone();
238234

235+
let (expr_outer, expr_inner) = split_expression_for_pushdown(expr.clone(), self.dtype())?;
236+
237+
let values_eval = if let Some(inner) = expr_inner {
238+
self.values_eval(inner)
239+
} else {
240+
self.values_array()
241+
};
239242
let all_values_referenced = self.layout.has_all_values_referenced();
240243
Ok(async move {
241244
let (values, codes) = try_join!(values_eval.map_err(VortexError::from), codes_eval)?;
@@ -252,7 +255,7 @@ impl LayoutReader for DictReader {
252255
.into_array()
253256
.optimize()?;
254257

255-
array.apply(&expr)
258+
array.apply(&expr_outer)
256259
}
257260
.boxed())
258261
}

0 commit comments

Comments
 (0)