Skip to content

Commit 541c583

Browse files
test: trace reductions and executions of scan ops over complex and compressed arrays (#8828)
## Summary Stacked on #7814 (the `trace_op` execution/optimization tracing harness). Builds out trace-test coverage for how take/filter/compare/like reduce and execute over complex arrays, using the harness from the base PR: **vortex-array** (promotes `test_harness/trace.rs` to a directory module `trace/mod.rs` + `trace/tests.rs`; harness source unchanged): - take (`DictArray`) over a chunked array — no reduce rule fires; execution canonicalizes the chunked values chunk-by-chunk before the dict take kernel runs. - filter over a struct with complex children (dict-of-strings + chunked fields) — `FilterStructRule` pushes the filter into each field, `FilterReduceAdaptor(Dict)` rewrites the dict field at optimize time. - compare (`Binary` Eq vs constant) over a dict — `DictionaryScalarFnValuesPushDownRule` reduces the compare into the dictionary values. - like over a dict-of-strings — `LikeReduceAdaptor(Dict)` reduces into the dictionary values. **vortex-runend** (new `trace_tests.rs`, `insta` added as dev-dependency): - compare vs constant reduces via `RunEndScalarFnRule` at optimize time; filter and take execute via the `FilterExecuteAdaptor`/`TakeExecuteAdaptor` parent kernels. **vortex-btrblocks** (new `trace_tests.rs`; `insta`, `tpchgen`, `tpchgen-arrow`, `arrow-array` added as dev-dependencies): generates TPC-H lineitem (SF 0.001, 4096 rows, deterministic — tpchgen and the compressor's sampling seed are both fixed), compresses it with `BtrBlocksCompressor`, and traces TPC-H-style scan predicates over the resulting encodings: - `l_shipdate >= const` over `ext(date) → for → bitpacked` — extension compare kernel at execution time, with `CastReduceAdaptor(FoR)/(BitPacked)` reductions inside the arrow fallback. - `l_quantity < const` over `decimal_byte_parts → dict → bitpacked` — the decimal_byte_parts compare kernel pushes into the byte-parts dictionary, where `DictionaryScalarFnValuesPushDownRule` reduces the compare to the 50 dictionary values before decode. - `l_shipmode = const` over dict-of-FSST — dict pushdown at optimize time, FSST compare kernel + dict decode at execution time. - `l_comment LIKE` over FSST — the FSST like kernel matches in compressed space. - filter over a struct of compressed columns — `FilterStructRule` plus per-encoding pushdown via `DecimalBytePartsFilterPushDownRule`, `ExtensionFilterPushDownRule`, `FoRFilterPushDownRule`, and `FilterReduceAdaptor(Dict)`, leaving execution a no-op. - take over a struct of compressed columns — `TakeReduceAdaptor(Struct)` absorbs the take entirely at optimize time. Each btrblocks test also asserts the canonical result matches running the same operation over the uncompressed column. A 1MiB TPC-H text pool replaces the spec-default 300MiB one, cutting per-test setup from ~9s to ~0.5s without changing the traces. ## Testing This PR is tests-only; no production code changes. - `cargo nextest run -p vortex-array` — 2961 passed (includes the 8 trace tests, run in parallel to confirm the thread-local recorder is deterministic). - `cargo nextest run -p vortex-runend` — 61 passed. - `cargo nextest run -p vortex-btrblocks` — 43 passed. - `cargo clippy --all-targets` on the three touched crates — clean. - `cargo +nightly fmt --all` and `git diff --check` — clean. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01RMBcCiwpC3dbGLXRbK7UuR --- _Generated by [Claude Code](https://claude.ai/code/session_01RMBcCiwpC3dbGLXRbK7UuR)_ --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 7d3ca20 commit 541c583

9 files changed

Lines changed: 1391 additions & 510 deletions

File tree

Cargo.lock

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

encodings/runend/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ workspace = true
3232
arrow-array = { workspace = true }
3333
arrow-schema = { workspace = true }
3434
divan = { workspace = true }
35+
insta = { workspace = true }
3536
itertools = { workspace = true }
3637
rand = { workspace = true }
3738
rstest = { workspace = true }

encodings/runend/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ mod iter;
1818
mod kernel;
1919
mod ops;
2020
mod rules;
21+
#[cfg(test)]
22+
#[cfg(not(codspeed))]
23+
mod trace_tests;
2124

2225
#[doc(hidden)]
2326
pub mod _benchmarking {
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// SPDX-FileCopyrightText: Copyright the Vortex contributors
3+
4+
//! Snapshot tests tracing how reductions and executions flow through run-end arrays.
5+
6+
use vortex_array::ArrayRef;
7+
use vortex_array::Canonical;
8+
use vortex_array::ExecutionCtx;
9+
use vortex_array::IntoArray;
10+
use vortex_array::arrays::BoolArray;
11+
use vortex_array::arrays::ConstantArray;
12+
use vortex_array::arrays::DictArray;
13+
use vortex_array::arrays::FilterArray;
14+
use vortex_array::arrays::PrimitiveArray;
15+
use vortex_array::arrays::scalar_fn::ScalarFnFactoryExt;
16+
use vortex_array::assert_arrays_eq;
17+
use vortex_array::optimizer::ArrayOptimizer;
18+
use vortex_array::scalar::Scalar;
19+
use vortex_array::scalar_fn::fns::binary::Binary;
20+
use vortex_array::scalar_fn::fns::operators::Operator;
21+
use vortex_array::session::ArraySession;
22+
use vortex_array::test_harness::trace::trace_op;
23+
use vortex_error::VortexResult;
24+
use vortex_mask::Mask;
25+
use vortex_session::VortexSession;
26+
27+
use crate::RunEnd;
28+
29+
fn execution_ctx() -> ExecutionCtx {
30+
ExecutionCtx::new(VortexSession::empty().with::<ArraySession>())
31+
}
32+
33+
/// Run-end encoding of `[1, 1, 1, 2, 2, 3, 3, 3, 3]`: ends `[3, 5, 9]`, values `[1, 2, 3]`.
34+
fn runend_array() -> VortexResult<ArrayRef> {
35+
Ok(RunEnd::encode(
36+
PrimitiveArray::from_iter([1i32, 1, 1, 2, 2, 3, 3, 3, 3]).into_array(),
37+
&mut execution_ctx(),
38+
)?
39+
.into_array())
40+
}
41+
42+
#[test]
43+
fn trace_compare_on_runend() -> VortexResult<()> {
44+
let runend = runend_array()?;
45+
let rhs = ConstantArray::new(Scalar::from(2i32), runend.len()).into_array();
46+
let compared = Binary.try_new_array(runend.len(), Operator::Eq, [runend, rhs])?;
47+
48+
let traced = trace_op(|| compared.optimize())?;
49+
insta::assert_snapshot!(traced.trace.to_string(), @"
50+
optimize root=vortex.binary(bool, len=9) session=false
51+
reduce_parent static:RunEndScalarFnRule slot=0 parent=vortex.binary(bool, len=9) child=vortex.runend(i32, len=9) -> vortex.runend(bool, len=9)
52+
done output=vortex.runend(bool, len=9)
53+
");
54+
55+
let optimized = traced.output;
56+
let traced = trace_op(|| {
57+
optimized
58+
.execute::<Canonical>(&mut execution_ctx())
59+
.map(IntoArray::into_array)
60+
})?;
61+
62+
assert_arrays_eq!(
63+
traced.output,
64+
BoolArray::from_iter([false, false, false, true, true, false, false, false, false])
65+
);
66+
insta::assert_snapshot!(traced.trace.to_string(), @"
67+
execute_until target=AnyCanonical root=vortex.runend(bool, len=9)
68+
iter 0 current=vortex.runend(bool, len=9) builder_active=false
69+
execute_until target=AnyCanonical root=vortex.binary(bool, len=3)
70+
iter 0 current=vortex.binary(bool, len=3) builder_active=false
71+
optimize root=vortex.cast(i32?, len=3) session=false
72+
reduce_parent static:CastReduceAdaptor(Primitive) slot=0 parent=vortex.cast(i32?, len=3) child=vortex.primitive(i32, len=3) -> vortex.primitive(i32?, len=3)
73+
done output=vortex.primitive(i32?, len=3)
74+
optimize root=vortex.slice(i32, len=1) session=false
75+
reduce_parent static:SliceReduceAdaptor(Constant) slot=0 parent=vortex.slice(i32, len=1) child=vortex.constant(i32, len=3) -> vortex.constant(i32, len=1)
76+
done output=vortex.constant(i32, len=1)
77+
optimize root=vortex.cast(i32?, len=1) session=false
78+
reduce_parent static:CastReduceAdaptor(Constant) slot=0 parent=vortex.cast(i32?, len=1) child=vortex.constant(i32, len=1) -> vortex.constant(i32?, len=1)
79+
done output=vortex.constant(i32?, len=1)
80+
execute_until target=AnyCanonical root=vortex.constant(i32?, len=1)
81+
iter 0 current=vortex.constant(i32?, len=1) builder_active=false
82+
Done array=vortex.primitive(i32?, len=1)
83+
iter 1 current=vortex.primitive(i32?, len=1) builder_active=false
84+
return output=vortex.primitive(i32?, len=1)
85+
Done array=vortex.bool(bool, len=3)
86+
iter 1 current=vortex.bool(bool, len=3) builder_active=false
87+
return output=vortex.bool(bool, len=3)
88+
Done array=vortex.bool(bool, len=9)
89+
iter 1 current=vortex.bool(bool, len=9) builder_active=false
90+
return output=vortex.bool(bool, len=9)
91+
");
92+
93+
Ok(())
94+
}
95+
96+
#[test]
97+
fn trace_filter_on_runend() -> VortexResult<()> {
98+
let runend = runend_array()?;
99+
let filtered = FilterArray::try_new(
100+
runend,
101+
Mask::from_iter([true, false, false, true, true, false, false, true, false]),
102+
)?
103+
.into_array();
104+
105+
let traced = trace_op(|| {
106+
filtered
107+
.execute::<Canonical>(&mut execution_ctx())
108+
.map(IntoArray::into_array)
109+
})?;
110+
111+
assert_arrays_eq!(traced.output, PrimitiveArray::from_iter([1i32, 2, 2, 3]));
112+
insta::assert_snapshot!(traced.trace.to_string(), @"
113+
execute_until target=AnyCanonical root=vortex.filter(i32, len=4)
114+
iter 0 current=vortex.filter(i32, len=4) builder_active=false
115+
child_execute_parent static:kernel[2] slot=0 parent=vortex.filter(i32, len=4) child=vortex.runend(i32, len=9) -> vortex.dict(i32, len=4)
116+
iter 1 current=vortex.dict(i32, len=4) builder_active=false
117+
child_execute_parent static:kernel[3] slot=1 parent=vortex.dict(i32, len=4) child=vortex.primitive(i32, len=3) -> vortex.primitive(i32, len=4)
118+
iter 2 current=vortex.primitive(i32, len=4) builder_active=false
119+
return output=vortex.primitive(i32, len=4)
120+
");
121+
122+
Ok(())
123+
}
124+
125+
#[test]
126+
fn trace_take_on_runend() -> VortexResult<()> {
127+
let runend = runend_array()?;
128+
// A take is expressed as a `DictArray` whose codes are the take indices.
129+
let indices = PrimitiveArray::from_iter([8u64, 0, 4, 4]).into_array();
130+
let take = DictArray::try_new(indices, runend)?.into_array();
131+
132+
let traced = trace_op(|| {
133+
take.execute::<Canonical>(&mut execution_ctx())
134+
.map(IntoArray::into_array)
135+
})?;
136+
137+
assert_arrays_eq!(traced.output, PrimitiveArray::from_iter([3i32, 1, 2, 2]));
138+
insta::assert_snapshot!(traced.trace.to_string(), @"
139+
execute_until target=AnyCanonical root=vortex.dict(i32, len=4)
140+
iter 0 current=vortex.dict(i32, len=4) builder_active=false
141+
child_execute_parent static:kernel[3] slot=1 parent=vortex.dict(i32, len=4) child=vortex.runend(i32, len=9) -> vortex.dict(i32, len=4)
142+
iter 1 current=vortex.dict(i32, len=4) builder_active=false
143+
child_execute_parent static:kernel[3] slot=1 parent=vortex.dict(i32, len=4) child=vortex.primitive(i32, len=3) -> vortex.primitive(i32, len=4)
144+
iter 2 current=vortex.primitive(i32, len=4) builder_active=false
145+
return output=vortex.primitive(i32, len=4)
146+
");
147+
148+
Ok(())
149+
}

0 commit comments

Comments
 (0)