Skip to content

Commit c661189

Browse files
committed
Refine list expression child classification
Signed-off-by: Matt Katz <mhkatz97@gmail.com>
1 parent 55b7cca commit c661189

2 files changed

Lines changed: 61 additions & 63 deletions

File tree

vortex-layout/src/layouts/list/expr.rs

Lines changed: 47 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -10,56 +10,50 @@ use vortex_array::scalar_fn::fns::is_null::IsNull;
1010
use vortex_array::scalar_fn::fns::list_length::ListLength;
1111
use vortex_error::VortexResult;
1212

13-
/// The deepest list child an expression needs, cheapest first.
13+
/// The deepest list child an expression needs, cheapest first, where I/O cost order is defined as
14+
/// `Validity < OffsetsAndValidity < All`.
1415
///
15-
/// Drives "fetch as little as possible": a projection/filter that only inspects the list's
16-
/// null-ness needs the validity child; `list_length(root())` needs offsets plus validity; and
17-
/// everything else needs the element values. The ordering `Validity < Offsets < Elements` lets us
18-
/// take the max over the operands of a compound expression.
16+
/// For example:
17+
/// - `is_null(root())` only needs the validity child.
18+
/// - `list_length(root())` only needs the offsets and validity children.
19+
/// - `root()` needs elements, offsets, and validity children.
1920
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
20-
pub(super) enum ExprClass {
21-
/// Only the list's validity is needed (`is_null` / `is_not_null` of the list itself).
21+
pub(super) enum ListChildrenNeeded {
22+
/// Only the validity child is needed (`is_null` / `is_not_null`).
2223
Validity,
23-
/// The list offsets are needed, but not the element values (`list_length` of the list itself).
24-
Offsets,
25-
/// The element values are needed (everything else).
26-
Elements,
24+
/// The offsets and validity children are needed, but not the element values (`list_length`).
25+
OffsetsAndValidity,
26+
/// All children are needed.
27+
All,
2728
}
2829

29-
/// Classify `expr` by the deepest list child it touches, where `root()` is the list.
30-
///
31-
/// The exact shapes `is_null(root())` / `is_not_null(root())` need only validity, and
32-
/// `list_length(root())` needs offsets plus validity. Every other access to the list, including a
33-
/// bare `root()`, falls through to [`ExprClass::Elements`], which is always correct.
34-
pub(super) fn classify(expr: &Expression) -> ExprClass {
35-
// `is_null(root())` / `is_not_null(root())` need only the list's own validity. Note this is
36-
// the list's null-ness, not the validity of some derived value, so the child must be `root()`.
37-
if (expr.is::<IsNull>() || expr.is::<IsNotNull>())
38-
&& expr.children().len() == 1
39-
&& is_root(expr.child(0))
40-
{
41-
return ExprClass::Validity;
30+
/// The minimal list children needed to evaluate `expr`, where `root()` is a field with list dtype.
31+
pub(super) fn get_necessary_list_children(expr: &Expression) -> ListChildrenNeeded {
32+
if is_null_root(expr) {
33+
return ListChildrenNeeded::Validity;
4234
}
4335

44-
// `list_length(root())` only needs adjacent offset deltas. List validity is still needed
45-
// because `list_length(NULL)` is NULL.
4636
if is_list_length_root(expr) {
47-
return ExprClass::Offsets;
37+
return ListChildrenNeeded::OffsetsAndValidity;
4838
}
4939

50-
// A bare reference to the list needs its elements.
5140
if is_root(expr) {
52-
return ExprClass::Elements;
41+
return ListChildrenNeeded::All;
5342
}
5443

55-
// Otherwise the requirement is the max over the operands. Operands that never touch the list
56-
// (e.g. literals) contribute nothing, so an expression that never references `root()` is
57-
// treated as the cheapest class.
44+
// Otherwise the requirement is the max over the operands. Childless expressions that never
45+
// touch the list, such as literals, fall back to the cheapest usable child.
5846
expr.children()
5947
.iter()
60-
.map(classify)
48+
.map(get_necessary_list_children)
6149
.max()
62-
.unwrap_or(ExprClass::Validity)
50+
.unwrap_or(ListChildrenNeeded::Validity)
51+
}
52+
53+
fn is_null_root(expr: &Expression) -> bool {
54+
(expr.is::<IsNull>() || expr.is::<IsNotNull>())
55+
&& expr.children().len() == 1
56+
&& is_root(expr.child(0))
6357
}
6458

6559
fn is_list_length_root(expr: &Expression) -> bool {
@@ -119,36 +113,39 @@ mod tests {
119113

120114
use super::*;
121115

122-
/// `classify` keys off the deepest list child an expression touches; `Elements` is the
123-
/// always-correct default for anything not specifically recognized.
116+
/// `get_necessary_list_children` keys off the deepest list child an expression touches; `All`
117+
/// is the always-correct default for anything not specifically recognized.
124118
#[rstest]
125119
// `is_null` / `is_not_null` of the list itself need only validity.
126-
#[case::is_null(is_null(root()), ExprClass::Validity)]
127-
#[case::is_not_null(is_not_null(root()), ExprClass::Validity)]
120+
#[case::is_null(is_null(root()), ListChildrenNeeded::Validity)]
121+
#[case::is_not_null(is_not_null(root()), ListChildrenNeeded::Validity)]
128122
// Compound over validity-only operands stays validity.
129-
#[case::not_is_null(not(is_null(root())), ExprClass::Validity)]
130-
// A list-independent (constant) expression falls to the cheapest class.
131-
#[case::constant(lit(5), ExprClass::Validity)]
123+
#[case::not_is_null(not(is_null(root())), ListChildrenNeeded::Validity)]
124+
// A list-independent (constant) expression falls to the cheapest usable child.
125+
#[case::constant(lit(5), ListChildrenNeeded::Validity)]
132126
// `list_length(root())` needs offsets and validity, but not elements.
133-
#[case::list_length(list_length(root()), ExprClass::Offsets)]
127+
#[case::list_length(list_length(root()), ListChildrenNeeded::OffsetsAndValidity)]
134128
// Compound over offsets-only operands stays offsets.
135-
#[case::list_length_filter(gt(list_length(root()), lit(1u64)), ExprClass::Offsets)]
129+
#[case::list_length_filter(
130+
gt(list_length(root()), lit(1u64)),
131+
ListChildrenNeeded::OffsetsAndValidity
132+
)]
136133
#[case::cast_list_length(
137134
cast(
138135
list_length(root()),
139136
DType::Primitive(PType::I64, Nullability::Nullable),
140137
),
141-
ExprClass::Offsets
138+
ListChildrenNeeded::OffsetsAndValidity
142139
)]
143140
// A bare list reference needs the elements.
144-
#[case::bare_root(root(), ExprClass::Elements)]
141+
#[case::bare_root(root(), ListChildrenNeeded::All)]
145142
// Any other fn over the list needs the elements.
146-
#[case::not_root(not(root()), ExprClass::Elements)]
143+
#[case::not_root(not(root()), ListChildrenNeeded::All)]
147144
// `is_null` only short-circuits to validity when its argument is the list itself.
148-
#[case::is_null_of_derived(is_null(not(root())), ExprClass::Elements)]
145+
#[case::is_null_of_derived(is_null(not(root())), ListChildrenNeeded::All)]
149146
// Max over operands: validity + elements => elements.
150-
#[case::validity_and_elements(eq(is_null(root()), root()), ExprClass::Elements)]
151-
fn classify_expr_class(#[case] expr: Expression, #[case] expected: ExprClass) {
152-
assert_eq!(classify(&expr), expected);
147+
#[case::validity_and_elements(eq(is_null(root()), root()), ListChildrenNeeded::All)]
148+
fn classify_expr_class(#[case] expr: Expression, #[case] expected: ListChildrenNeeded) {
149+
assert_eq!(get_necessary_list_children(&expr), expected);
153150
}
154151
}

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

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,8 @@ use crate::LayoutReaderRef;
4040
use crate::RowSplits;
4141
use crate::SplitRange;
4242
use crate::layouts::list::ListLayout;
43-
use crate::layouts::list::expr::ExprClass;
44-
use crate::layouts::list::expr::classify;
43+
use crate::layouts::list::expr::ListChildrenNeeded;
44+
use crate::layouts::list::expr::get_necessary_list_children;
4545
use crate::layouts::list::expr::rewrite_offsets_expr;
4646
use crate::layouts::list::expr::rewrite_validity_expr;
4747
use crate::segments::SegmentSource;
@@ -105,9 +105,9 @@ impl ListReader {
105105
})
106106
}
107107

108-
/// Projection for [`ExprClass::Validity`] expressions (`is_null` / `is_not_null` of the list):
109-
/// reads only the validity childsynthesizing all-valid for a non-nullable list — and never
110-
/// touches the offsets or elements.
108+
/// Projection for [`ListChildrenNeeded::Validity`] expressions (`is_null` / `is_not_null` of
109+
/// the list): reads only the validity child, synthesizing all-valid for a non-nullable list,
110+
/// and never touches the offsets or elements.
111111
fn project_validity(
112112
&self,
113113
row_range: &Range<u64>,
@@ -143,8 +143,8 @@ impl ListReader {
143143
.boxed())
144144
}
145145

146-
/// Projection for [`ExprClass::Elements`] expressions (everything else): materializes the list
147-
/// (offsets + elements + validity) and applies the expression.
146+
/// Projection for [`ListChildrenNeeded::All`] expressions: materializes the list (offsets +
147+
/// elements + validity) and applies the expression.
148148
fn project_elements(
149149
&self,
150150
row_range: &Range<u64>,
@@ -177,8 +177,9 @@ impl ListReader {
177177
.boxed())
178178
}
179179

180-
/// Projection for [`ExprClass::Offsets`] expressions (`list_length(root())` and expressions
181-
/// composed from it): reads offsets and list validity, but never touches element values.
180+
/// Projection for [`ListChildrenNeeded::OffsetsAndValidity`] expressions (`list_length(root())`
181+
/// and expressions composed from it): reads offsets and list validity, but never touches
182+
/// element values.
182183
fn project_offsets(
183184
&self,
184185
row_range: &Range<u64>,
@@ -485,10 +486,10 @@ impl LayoutReader for ListReader {
485486
mask: MaskFuture,
486487
) -> VortexResult<ArrayFuture> {
487488
// Read as little as possible based on which list children the expression needs.
488-
match classify(expr) {
489-
ExprClass::Validity => self.project_validity(row_range, expr, mask),
490-
ExprClass::Offsets => self.project_offsets(row_range, expr, mask),
491-
ExprClass::Elements => self.project_elements(row_range, expr, mask),
489+
match get_necessary_list_children(expr) {
490+
ListChildrenNeeded::Validity => self.project_validity(row_range, expr, mask),
491+
ListChildrenNeeded::OffsetsAndValidity => self.project_offsets(row_range, expr, mask),
492+
ListChildrenNeeded::All => self.project_elements(row_range, expr, mask),
492493
}
493494
}
494495
}

0 commit comments

Comments
 (0)