Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
089083b
first pass
May 22, 2026
ab6fe03
fix
May 22, 2026
d021314
second claude pass
May 22, 2026
d9f2173
small fixes
May 26, 2026
19499c9
fix
May 26, 2026
f57e4ff
fix
May 26, 2026
f269f1c
clean mod
mhk197 May 27, 2026
6ed6dcc
fix writer
mhk197 May 27, 2026
64aa4b3
fix writer
mhk197 May 27, 2026
d1be472
fix
mhk197 May 27, 2026
51fbc85
improve projection eval
mhk197 May 27, 2026
722f308
projection evaluation
mhk197 May 27, 2026
f5de826
tests
mhk197 May 27, 2026
7b3c477
tests
mhk197 May 27, 2026
d02fae1
fix test
mhk197 May 27, 2026
6f46f86
skip pruning eval
mhk197 May 27, 2026
68795ea
few more tests
mhk197 May 27, 2026
2673177
lint fix
mhk197 May 28, 2026
805b6fc
fix test
mhk197 May 28, 2026
588a020
quick fix
mhk197 May 28, 2026
481a196
add anylist matcher
mhk197 May 28, 2026
1efd3ee
read validity with all-true mask, not caller's mask
mhk197 May 28, 2026
588aae7
narrow elements io for sparse mask instead of defering filtering
mhk197 May 28, 2026
d330644
shortcut on whole-chunk unmasked reads
mhk197 May 28, 2026
96822ab
add required fallback to ListLayoutStrategy for non-list input
mhk197 Jun 17, 2026
4ea77c2
fmt
mhk197 Jun 17, 2026
9082cd9
cleanup
mhk197 Jun 17, 2026
1416df6
fix rebase conflicts
mhk197 Jun 17, 2026
d56ee4e
use ListLayoutStrategy as default leaf under unstable_encodings
mhk197 Jun 17, 2026
43d6986
recurse into nested lists
mhk197 Jun 17, 2026
382803a
fix: update ListLayout::build to use LayoutBuildContext after trait c…
mhk197 Jun 22, 2026
a6a6242
comments
mhk197 Jun 22, 2026
51fc678
fix
mhk197 Jun 22, 2026
1d700d6
clean up writer
mhk197 Jun 23, 2026
7ac9052
Improve list reader
mhk197 Jun 23, 2026
1f83218
Fix list docs
mhk197 Jun 23, 2026
a76cc57
Add list filter evaluation
mhk197 Jun 23, 2026
d471575
Read list lengths from list layout offsets
mhk197 Jun 29, 2026
80a6a8b
Move list expression planning to expr module
mhk197 Jul 1, 2026
04f1cd7
Fix list layout checks after rebase
mhk197 Jul 1, 2026
a5ce3dc
Remove list projection path labels
mhk197 Jul 1, 2026
bf54f42
Refine list expression child classification
mhk197 Jul 1, 2026
30b90c7
remove stale comment
mhk197 Jul 6, 2026
5111e17
simplify projection
mhk197 Jul 6, 2026
165b30f
Gate list layout behind VORTEX_EXPERIMENTAL_LIST_LAYOUT
mhk197 Jul 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions vortex-file/src/strategy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ use vortex_layout::layouts::compressed::CompressingStrategy;
use vortex_layout::layouts::compressed::CompressorPlugin;
use vortex_layout::layouts::dict::writer::DictStrategy;
use vortex_layout::layouts::flat::writer::FlatLayoutStrategy;
use vortex_layout::layouts::list::use_experimental_list_layout;
use vortex_layout::layouts::list::writer::ListLayoutStrategy;
use vortex_layout::layouts::repartition::RepartitionStrategy;
use vortex_layout::layouts::repartition::RepartitionWriterOptions;
use vortex_layout::layouts::table::TableStrategy;
Expand Down Expand Up @@ -242,8 +244,20 @@ impl WriteStrategyBuilder {
Arc::new(FlatLayoutStrategy::default())
};

// 7. for each chunk create a flat layout
let chunked = ChunkedLayoutStrategy::new(Arc::clone(&flat));
// 7. for each chunk create a layout
let leaf: Arc<dyn LayoutStrategy> = if use_experimental_list_layout() {
Arc::new(
ListLayoutStrategy::default()
.with_elements(Arc::clone(&flat))
.with_offsets(Arc::clone(&flat))
.with_validity(Arc::clone(&flat))
.with_fallback(Arc::clone(&flat)),
)
} else {
Arc::clone(&flat)
};

let chunked = ChunkedLayoutStrategy::new(leaf);
// 6. buffer chunks so they end up with closer segment ids physically
let buffered = BufferedStrategy::new(chunked, 2 * ONE_MEG); // 2MB

Expand Down
151 changes: 151 additions & 0 deletions vortex-layout/src/layouts/list/expr.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: Copyright the Vortex contributors

use vortex_array::expr::Expression;
use vortex_array::expr::is_root;
use vortex_array::expr::not;
use vortex_array::expr::root;
use vortex_array::scalar_fn::fns::is_not_null::IsNotNull;
use vortex_array::scalar_fn::fns::is_null::IsNull;
use vortex_array::scalar_fn::fns::list_length::ListLength;
use vortex_error::VortexResult;

/// The deepest list child an expression needs, cheapest first, where I/O cost order is defined as
/// `Validity < OffsetsAndValidity < All`.
///
/// For example:
/// - `is_null(root())` only needs the validity child.
/// - `list_length(root())` only needs the offsets and validity children.
/// - `root()` needs elements, offsets, and validity children.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub(super) enum ListChildrenNeeded {
/// Only the validity child is needed (`is_null` / `is_not_null`).
Validity,
/// The offsets and validity children are needed, but not the element values (`list_length`).
OffsetsAndValidity,
/// All children are needed.
All,
}

/// The minimal list children needed to evaluate `expr`, where `root()` is a field with list dtype.
pub(super) fn get_necessary_list_children(expr: &Expression) -> ListChildrenNeeded {
if is_null_root(expr) {
return ListChildrenNeeded::Validity;
}

if is_list_length_root(expr) {
return ListChildrenNeeded::OffsetsAndValidity;
}

if is_root(expr) {
return ListChildrenNeeded::All;
}

// Otherwise the requirement is the max over the operands. Childless expressions that never
// touch the list, such as literals, fall back to the cheapest usable child.
expr.children()
.iter()
.map(get_necessary_list_children)
.max()
.unwrap_or(ListChildrenNeeded::Validity)
}

fn is_null_root(expr: &Expression) -> bool {
(expr.is::<IsNull>() || expr.is::<IsNotNull>())
&& expr.children().len() == 1
&& is_root(expr.child(0))
}

fn is_list_length_root(expr: &Expression) -> bool {
expr.is::<ListLength>() && expr.children().len() == 1 && is_root(expr.child(0))
}

/// Rewrite a validity-class expression so it can be evaluated against the list's validity bool
/// array (`true` == valid row): `is_not_null(root())` becomes `root()` and `is_null(root())`
/// becomes `not(root())`. All other nodes are rebuilt with rewritten children.
pub(super) fn rewrite_validity_expr(expr: &Expression) -> VortexResult<Expression> {
if expr.is::<IsNotNull>() && expr.children().len() == 1 && is_root(expr.child(0)) {
return Ok(root());
}
if expr.is::<IsNull>() && expr.children().len() == 1 && is_root(expr.child(0)) {
return Ok(not(root()));
}
let children = expr
.children()
.iter()
.map(rewrite_validity_expr)
.collect::<VortexResult<Vec<_>>>()?;
expr.clone().with_children(children)
}

/// Rewrite an offsets-class expression so it can be evaluated against an array of list lengths.
/// `list_length(root())` becomes `root()`. Other references to `root()` are left intact: for
/// offsets-class expressions they can only be validity checks, and the lengths array carries the
/// same validity as the original list.
pub(super) fn rewrite_offsets_expr(expr: &Expression) -> VortexResult<Expression> {
if is_list_length_root(expr) {
return Ok(root());
}

let children = expr
.children()
.iter()
.map(rewrite_offsets_expr)
.collect::<VortexResult<Vec<_>>>()?;
expr.clone().with_children(children)
}

#[cfg(test)]
mod tests {
use rstest::rstest;
use vortex_array::dtype::DType;
use vortex_array::dtype::Nullability;
use vortex_array::dtype::PType;
use vortex_array::expr::cast;
use vortex_array::expr::eq;
use vortex_array::expr::gt;
use vortex_array::expr::is_not_null;
use vortex_array::expr::is_null;
use vortex_array::expr::list_length;
use vortex_array::expr::lit;
use vortex_array::expr::not;
use vortex_array::expr::root;

use super::*;

/// `get_necessary_list_children` keys off the deepest list child an expression touches; `All`
/// is the always-correct default for anything not specifically recognized.
#[rstest]
// `is_null` / `is_not_null` of the list itself need only validity.
#[case::is_null(is_null(root()), ListChildrenNeeded::Validity)]
#[case::is_not_null(is_not_null(root()), ListChildrenNeeded::Validity)]
// Compound over validity-only operands stays validity.
#[case::not_is_null(not(is_null(root())), ListChildrenNeeded::Validity)]
// A list-independent (constant) expression falls to the cheapest usable child.
#[case::constant(lit(5), ListChildrenNeeded::Validity)]
// `list_length(root())` needs offsets and validity, but not elements.
#[case::list_length(list_length(root()), ListChildrenNeeded::OffsetsAndValidity)]
// Compound over offsets-only operands stays offsets.
#[case::list_length_filter(
gt(list_length(root()), lit(1u64)),
ListChildrenNeeded::OffsetsAndValidity
)]
#[case::cast_list_length(
cast(
list_length(root()),
DType::Primitive(PType::I64, Nullability::Nullable),
),
ListChildrenNeeded::OffsetsAndValidity
)]
// A bare list reference needs the elements.
#[case::bare_root(root(), ListChildrenNeeded::All)]
// Any other fn over the list needs the elements.
#[case::not_root(not(root()), ListChildrenNeeded::All)]
// `is_null` only short-circuits to validity when its argument is the list itself.
#[case::is_null_of_derived(is_null(not(root())), ListChildrenNeeded::All)]
// Max over operands: validity + elements => elements.
#[case::validity_and_elements(eq(is_null(root()), root()), ListChildrenNeeded::All)]
fn classify_expr_class(#[case] expr: Expression, #[case] expected: ListChildrenNeeded) {
assert_eq!(get_necessary_list_children(&expr), expected);
}
}
Loading
Loading