Skip to content

Commit 088118b

Browse files
authored
Derive live statistics proofs for dynamic expressions (#10869)
Extract generic dynamic predicate falsification from #10665. Keep proofs live across predicate generations and conservatively retain rows when a proof or required statistic is unavailable. Signed-off-by: Nicholas Gates <nick@nickgates.com>
1 parent bdae6a7 commit 088118b

3 files changed

Lines changed: 348 additions & 0 deletions

File tree

‎vortex-expr/src/stats/rewrite.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ use vortex_utils::iter::ReduceBalancedIterExt;
2020
use crate::stats::session::StatsSessionExt;
2121

2222
mod builtins;
23+
mod dynamic_expression;
2324
#[cfg(test)]
2425
mod fn_tests;
2526
mod like_variant;

‎vortex-expr/src/stats/rewrite/builtins.rs‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ use vortex_array::scalar_fn::fns::between::Between;
3636
use vortex_array::scalar_fn::fns::binary::Binary;
3737
use vortex_array::scalar_fn::fns::cast::Cast;
3838
use vortex_array::scalar_fn::fns::dynamic::DynamicComparison;
39+
use vortex_array::scalar_fn::fns::dynamic_expression::DynamicExpression;
3940
use vortex_array::scalar_fn::fns::is_nan::IsNan;
4041
use vortex_array::scalar_fn::fns::is_not_null::IsNotNull;
4142
use vortex_array::scalar_fn::fns::is_null::IsNull;
@@ -51,6 +52,8 @@ use vortex_session::VortexSession;
5152

5253
use crate::stats::bound::stat;
5354
use crate::stats::rewrite::StatsRewriteRule;
55+
use crate::stats::rewrite::dynamic_expression::StatsProof;
56+
use crate::stats::rewrite::dynamic_expression::dynamic_stats_proof;
5457
use crate::stats::rewrite::falsify;
5558
use crate::stats::rewrite::like_variant::LikeVariant;
5659
use crate::stats::session::StatsSession;
@@ -75,6 +78,7 @@ pub(crate) fn register_builtins(session: &StatsSession) {
7578
session.register_rewrite(ListContainsAllNonNanStatsRewrite);
7679
session.register_rewrite(DynamicComparisonNanCountStatsRewrite);
7780
session.register_rewrite(DynamicComparisonAllNonNanStatsRewrite);
81+
session.register_rewrite(DynamicExpressionStatsRewrite);
7882
}
7983

8084
fn row_count() -> BoundExpression {
@@ -1348,3 +1352,28 @@ mod tests {
13481352
Ok(())
13491353
}
13501354
}
1355+
1356+
#[derive(Debug)]
1357+
struct DynamicExpressionStatsRewrite;
1358+
1359+
impl StatsRewriteRule for DynamicExpressionStatsRewrite {
1360+
fn scalar_fn_id(&self) -> ScalarFnId {
1361+
DynamicExpression.id()
1362+
}
1363+
1364+
fn falsify(
1365+
&self,
1366+
expr: &BoundExpression,
1367+
session: &VortexSession,
1368+
) -> VortexResult<Option<BoundExpression>> {
1369+
dynamic_stats_proof(expr, session, StatsProof::Falsify)
1370+
}
1371+
1372+
fn satisfy(
1373+
&self,
1374+
expr: &BoundExpression,
1375+
session: &VortexSession,
1376+
) -> VortexResult<Option<BoundExpression>> {
1377+
dynamic_stats_proof(expr, session, StatsProof::Satisfy)
1378+
}
1379+
}
Lines changed: 318 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,318 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// SPDX-FileCopyrightText: Copyright the Vortex contributors
3+
4+
//! Stats proofs of dynamic expressions.
5+
6+
use std::fmt::Display;
7+
use std::fmt::Formatter;
8+
use std::sync::Arc;
9+
10+
use vortex_array::aggregate_fn::AggregateFnRef;
11+
use vortex_array::aggregate_fn::AggregateFnVTableExt;
12+
use vortex_array::aggregate_fn::EmptyOptions as AggregateEmptyOptions;
13+
use vortex_array::aggregate_fn::fns::all_nan::AllNan;
14+
use vortex_array::aggregate_fn::fns::all_non_nan::AllNonNan;
15+
use vortex_array::aggregate_fn::fns::all_non_null::AllNonNull;
16+
use vortex_array::aggregate_fn::fns::all_null::AllNull;
17+
use vortex_array::dtype::DType;
18+
use vortex_array::dtype::FieldNames;
19+
use vortex_array::dtype::Nullability;
20+
use vortex_array::dtype::StructFields;
21+
use vortex_array::expr::BoundExpression;
22+
use vortex_array::expr::bound::get_item;
23+
use vortex_array::expr::bound::lit;
24+
use vortex_array::expr::stats::Stat;
25+
use vortex_array::scalar_fn::EmptyOptions;
26+
use vortex_array::scalar_fn::ScalarFnVTableExt;
27+
use vortex_array::scalar_fn::fns::dynamic_expression::DynamicExpression;
28+
use vortex_array::scalar_fn::fns::dynamic_expression::DynamicExpressionOptions;
29+
use vortex_array::scalar_fn::fns::dynamic_expression::DynamicExpressionSource;
30+
use vortex_array::scalar_fn::fns::dynamic_expression::input_field_name;
31+
use vortex_array::scalar_fn::fns::get_item::GetItem;
32+
use vortex_array::scalar_fn::fns::stat::StatFn;
33+
use vortex_array::scalar_fn::internal::row_count::RowCount;
34+
use vortex_error::VortexResult;
35+
use vortex_session::VortexSession;
36+
37+
use crate::stats::bound::stat;
38+
use crate::stats::rewrite::BoundExpressionPruneExt;
39+
40+
/// Which stats proof a [`StatsProofSource`] builds.
41+
#[derive(Clone, Copy, Debug)]
42+
pub(crate) enum StatsProof {
43+
Falsify,
44+
Satisfy,
45+
}
46+
47+
/// Builds a dynamic stats proof for a dynamic expression.
48+
///
49+
/// Zone pruning builds and lowers the proof of a predicate once, then evaluates it again when a
50+
/// dynamic expression changes. Thus the proof is itself a dynamic expression. Its children are
51+
/// the statistics of the inputs that a proof can use, and the row count, so lowering binds them
52+
/// to the stats of each zone. Each generation proves the current predicate again.
53+
pub(crate) fn dynamic_stats_proof(
54+
expr: &BoundExpression,
55+
session: &VortexSession,
56+
proof: StatsProof,
57+
) -> VortexResult<Option<BoundExpression>> {
58+
let options = expr.as_::<DynamicExpression>();
59+
let input_dtypes = expr
60+
.children()
61+
.iter()
62+
.map(|child| child.dtype().clone())
63+
.collect::<Vec<_>>();
64+
65+
let mut stats = Vec::new();
66+
let mut children = Vec::new();
67+
for (input_idx, input) in expr.children().iter().enumerate() {
68+
for aggregate_fn in proof_aggregates() {
69+
if aggregate_fn.return_dtype(input.dtype()).is_some() {
70+
children.push(stat(input.clone(), aggregate_fn.clone()));
71+
stats.push((input_idx, aggregate_fn));
72+
}
73+
}
74+
}
75+
children.push(RowCount.try_new_bound_expr(EmptyOptions, [])?);
76+
77+
let source = StatsProofSource {
78+
predicate: options.clone(),
79+
input_dtypes,
80+
stats,
81+
proof,
82+
session: session.clone(),
83+
};
84+
Ok(Some(DynamicExpression.try_new_bound_expr(
85+
DynamicExpressionOptions::new(Arc::new(source)),
86+
children,
87+
)?))
88+
}
89+
90+
/// The statistics a proof of a dynamic expression can read.
91+
fn proof_aggregates() -> Vec<AggregateFnRef> {
92+
[Stat::Min, Stat::Max, Stat::NullCount, Stat::NaNCount]
93+
.into_iter()
94+
.filter_map(|stat| stat.aggregate_fn())
95+
.chain([
96+
AllNull.bind(AggregateEmptyOptions),
97+
AllNonNull.bind(AggregateEmptyOptions),
98+
AllNan.bind(AggregateEmptyOptions),
99+
AllNonNan.bind(AggregateEmptyOptions),
100+
])
101+
.collect()
102+
}
103+
104+
/// The source of a dynamic stats proof, see [`dynamic_stats_proof`].
105+
#[derive(Debug)]
106+
struct StatsProofSource {
107+
predicate: DynamicExpressionOptions,
108+
input_dtypes: Vec<DType>,
109+
/// The input and aggregate of each statistic child, in child order. The row count follows.
110+
stats: Vec<(usize, AggregateFnRef)>,
111+
proof: StatsProof,
112+
session: VortexSession,
113+
}
114+
115+
impl StatsProofSource {
116+
/// Replace the statistics and row count in `proof` by fields of `scope`. Returns `None` when
117+
/// the proof reads a statistic that is not a child.
118+
fn bind_children(
119+
&self,
120+
proof: &BoundExpression,
121+
scope: &DType,
122+
) -> VortexResult<Option<BoundExpression>> {
123+
if let Some(options) = proof.as_opt::<StatFn>() {
124+
let child = proof.child(0);
125+
let Some(field) = child.as_opt::<GetItem>() else {
126+
return Ok(None);
127+
};
128+
if !child.child(0).is_root() {
129+
return Ok(None);
130+
}
131+
let Some(stat_idx) = self.stats.iter().position(|(input_idx, aggregate_fn)| {
132+
input_field_name(*input_idx) == *field && aggregate_fn == options.aggregate_fn()
133+
}) else {
134+
return Ok(None);
135+
};
136+
return Ok(Some(scope_field(stat_idx, scope)?));
137+
}
138+
139+
if proof.is::<RowCount>() {
140+
return Ok(Some(scope_field(self.stats.len(), scope)?));
141+
}
142+
143+
if proof.is_root() {
144+
return Ok(None);
145+
}
146+
147+
let mut children = Vec::with_capacity(proof.children().len());
148+
for child in proof.children() {
149+
let Some(child) = self.bind_children(child, scope)? else {
150+
return Ok(None);
151+
};
152+
children.push(child);
153+
}
154+
Ok(Some(proof.clone().with_children(children)?))
155+
}
156+
}
157+
158+
fn scope_field(idx: usize, scope: &DType) -> VortexResult<BoundExpression> {
159+
Ok(get_item(
160+
input_field_name(idx),
161+
BoundExpression::new_root(scope.clone()),
162+
))
163+
}
164+
165+
impl Display for StatsProofSource {
166+
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
167+
let proof = match self.proof {
168+
StatsProof::Falsify => "falsify",
169+
StatsProof::Satisfy => "satisfy",
170+
};
171+
write!(f, "{proof}({})", self.predicate)
172+
}
173+
}
174+
175+
impl DynamicExpressionSource for StatsProofSource {
176+
fn generation(&self) -> u64 {
177+
self.predicate.source().generation()
178+
}
179+
180+
fn current(&self, scope: &DType) -> VortexResult<BoundExpression> {
181+
let names = (0..self.input_dtypes.len())
182+
.map(input_field_name)
183+
.collect::<FieldNames>();
184+
let input_scope = DType::Struct(
185+
StructFields::new(names, self.input_dtypes.clone()),
186+
Nullability::NonNullable,
187+
);
188+
let predicate = self.predicate.current(&input_scope)?.optimize_recursive()?;
189+
let proof = match self.proof {
190+
StatsProof::Falsify => predicate.falsify(&self.session)?,
191+
StatsProof::Satisfy => predicate.satisfy(&self.session)?,
192+
};
193+
194+
// A proof that is `false` proves nothing.
195+
Ok(match proof {
196+
Some(proof) => self
197+
.bind_children(&proof, scope)?
198+
.unwrap_or_else(|| lit(false)),
199+
None => lit(false),
200+
})
201+
}
202+
}
203+
204+
#[cfg(test)]
205+
mod tests {
206+
use std::sync::atomic::AtomicU64;
207+
use std::sync::atomic::Ordering;
208+
209+
use vortex_array::IntoArray;
210+
use vortex_array::VortexSessionExecute;
211+
use vortex_array::arrays::BoolArray;
212+
use vortex_array::arrays::ConstantArray;
213+
use vortex_array::arrays::StructArray;
214+
use vortex_array::assert_arrays_eq;
215+
use vortex_array::dtype::PType;
216+
use vortex_array::expr::col;
217+
use vortex_array::expr::dynamic_expression;
218+
use vortex_array::expr::get_item as unbound_get_item;
219+
use vortex_array::expr::lit as unbound_lit;
220+
use vortex_array::expr::lt;
221+
use vortex_array::expr::pack;
222+
use vortex_array::expr::root;
223+
use vortex_array::scalar::Scalar;
224+
use vortex_array::validity::Validity;
225+
use vortex_buffer::buffer;
226+
227+
use super::*;
228+
use crate::stats::StatsSession;
229+
230+
#[derive(Debug, Default)]
231+
struct Threshold(AtomicU64);
232+
233+
impl Display for Threshold {
234+
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
235+
write!(f, "threshold")
236+
}
237+
}
238+
239+
impl DynamicExpressionSource for Threshold {
240+
fn generation(&self) -> u64 {
241+
self.0.load(Ordering::SeqCst)
242+
}
243+
244+
fn current(&self, scope: &DType) -> VortexResult<BoundExpression> {
245+
if self.generation().is_multiple_of(3) {
246+
unbound_lit(true).bind(scope)
247+
} else {
248+
lt(
249+
unbound_get_item(
250+
"value",
251+
pack(
252+
[("value", unbound_get_item(input_field_name(0), root()))],
253+
Nullability::NonNullable,
254+
),
255+
),
256+
unbound_lit(if self.generation() == 2 { 8u64 } else { 5u64 }),
257+
)
258+
.bind(scope)
259+
}
260+
}
261+
}
262+
263+
#[test]
264+
fn existing_proofs_follow_new_generations() -> VortexResult<()> {
265+
let session = VortexSession::empty().with::<StatsSession>();
266+
let threshold = Arc::new(Threshold::default());
267+
let dtype = DType::struct_(
268+
[("a", DType::Primitive(PType::U64, Nullability::NonNullable))],
269+
Nullability::NonNullable,
270+
);
271+
let predicate = dynamic_expression(
272+
Arc::clone(&threshold) as Arc<dyn DynamicExpressionSource>,
273+
[col("a")],
274+
)
275+
.bind(&dtype)?;
276+
let proofs = [
277+
predicate.falsify(&session)?.unwrap(),
278+
predicate.satisfy(&session)?.unwrap(),
279+
];
280+
let columns = proofs[0]
281+
.children()
282+
.iter()
283+
.map(|child| {
284+
if let Some(options) = child.as_opt::<StatFn>() {
285+
match Stat::from_aggregate_fn(options.aggregate_fn()) {
286+
Some(Stat::Min) => return buffer![7u64].into_array(),
287+
Some(Stat::Max) => return buffer![9u64].into_array(),
288+
Some(Stat::NullCount) => return buffer![0u64].into_array(),
289+
_ => {}
290+
}
291+
}
292+
ConstantArray::new(Scalar::null(child.dtype().as_nullable()), 1).into_array()
293+
})
294+
.collect::<Vec<_>>();
295+
let names = (0..columns.len())
296+
.map(input_field_name)
297+
.collect::<FieldNames>();
298+
let stats = StructArray::try_new(names, columns, 1, Validity::NonNullable)?.into_array();
299+
let mut ctx = session.create_execution_ctx();
300+
for (generation, expected) in [
301+
(0, [false, true]),
302+
(1, [true, false]),
303+
(2, [false, false]),
304+
(3, [false, true]),
305+
] {
306+
threshold.0.store(generation, Ordering::SeqCst);
307+
for (proof, expected) in proofs.iter().zip(expected) {
308+
let proof = proof.as_::<DynamicExpression>().current(stats.dtype())?;
309+
assert_arrays_eq!(
310+
stats.clone().apply_bound(&proof)?,
311+
BoolArray::from_iter([expected]),
312+
&mut ctx
313+
);
314+
}
315+
}
316+
Ok(())
317+
}
318+
}

0 commit comments

Comments
 (0)