Skip to content

Commit 2757294

Browse files
authored
Merge branch 'main' into aggregation-fuzzer-spill-coverage
2 parents 523577b + 2fb472a commit 2757294

8 files changed

Lines changed: 247 additions & 30 deletions

File tree

datafusion/functions-aggregate/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,5 +100,9 @@ harness = false
100100
name = "sliding_max"
101101
harness = false
102102

103+
[[bench]]
104+
name = "variance"
105+
harness = false
106+
103107
[features]
104108
force_hash_collisions = ["datafusion-common/force_hash_collisions"]
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
use std::hint::black_box;
19+
use std::sync::Arc;
20+
21+
use arrow::array::{ArrayRef, Float64Array};
22+
use criterion::{BatchSize, Criterion, criterion_group, criterion_main};
23+
use datafusion_expr::Accumulator;
24+
use datafusion_functions_aggregate::variance::VarianceAccumulator;
25+
use datafusion_functions_aggregate_common::stats::StatsType;
26+
27+
const BATCH_SIZE: usize = 8192;
28+
29+
fn batch_array(null_stride: Option<usize>) -> ArrayRef {
30+
let values = (0..BATCH_SIZE)
31+
.map(|idx| {
32+
if null_stride.is_some_and(|stride| idx % stride == 0) {
33+
None
34+
} else {
35+
Some(idx as f64)
36+
}
37+
})
38+
.collect::<Vec<_>>();
39+
Arc::new(Float64Array::from(values)) as ArrayRef
40+
}
41+
42+
fn update_bench(c: &mut Criterion, name: &str, batch: &ArrayRef) {
43+
c.bench_function(name, |b| {
44+
b.iter(|| {
45+
let mut acc = VarianceAccumulator::try_new(StatsType::Sample).unwrap();
46+
acc.update_batch(std::slice::from_ref(batch)).unwrap();
47+
black_box(acc.evaluate().unwrap())
48+
})
49+
});
50+
}
51+
52+
fn retract_bench(c: &mut Criterion, name: &str, batch: &ArrayRef) {
53+
c.bench_function(name, |b| {
54+
b.iter_batched(
55+
|| {
56+
let mut acc = VarianceAccumulator::try_new(StatsType::Sample).unwrap();
57+
// Accumulate two batches so that retracting one leaves the
58+
// accumulator with rows remaining, as in a sliding window.
59+
acc.update_batch(std::slice::from_ref(batch)).unwrap();
60+
acc.update_batch(std::slice::from_ref(batch)).unwrap();
61+
acc
62+
},
63+
|mut acc| {
64+
acc.retract_batch(std::slice::from_ref(batch)).unwrap();
65+
black_box(acc.evaluate().unwrap())
66+
},
67+
BatchSize::SmallInput,
68+
)
69+
});
70+
}
71+
72+
fn variance_benchmark(c: &mut Criterion) {
73+
let no_nulls = batch_array(None);
74+
let with_nulls = batch_array(Some(10));
75+
76+
update_bench(c, "variance update_batch f64 no_nulls", &no_nulls);
77+
update_bench(c, "variance update_batch f64 with_nulls", &with_nulls);
78+
retract_bench(c, "variance retract_batch f64 no_nulls", &no_nulls);
79+
retract_bench(c, "variance retract_batch f64 with_nulls", &with_nulls);
80+
}
81+
82+
criterion_group!(benches, variance_benchmark);
83+
criterion_main!(benches);

datafusion/functions-aggregate/src/variance.rs

Lines changed: 88 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,23 @@ fn update(count: u64, mean: f64, m2: f64, value: f64) -> (u64, f64, f64) {
326326
(new_count, new_mean, new_m2)
327327
}
328328

329+
/// Inverse of [`update`]: removes a previously accumulated value. Retracting
330+
/// from a state with one or zero values resets the state to empty.
331+
#[inline]
332+
fn retract(count: u64, mean: f64, m2: f64, value: f64) -> (u64, f64, f64) {
333+
if count <= 1 {
334+
return (0, 0.0, 0.0);
335+
}
336+
337+
let new_count = count - 1;
338+
let delta1 = mean - value;
339+
let new_mean = delta1 / new_count as f64 + mean;
340+
let delta2 = new_mean - value;
341+
let new_m2 = m2 - delta1 * delta2;
342+
343+
(new_count, new_mean, new_m2)
344+
}
345+
329346
impl Accumulator for VarianceAccumulator {
330347
fn state(&mut self) -> Result<Vec<ScalarValue>> {
331348
Ok(vec![
@@ -348,22 +365,8 @@ impl Accumulator for VarianceAccumulator {
348365
fn retract_batch(&mut self, values: &[ArrayRef]) -> Result<()> {
349366
let arr = as_float64_array(&values[0])?;
350367
for value in arr.iter().flatten() {
351-
if self.count <= 1 {
352-
self.count = 0;
353-
self.mean = 0.0;
354-
self.m2 = 0.0;
355-
continue;
356-
}
357-
358-
let new_count = self.count - 1;
359-
let delta1 = self.mean - value;
360-
let new_mean = delta1 / new_count as f64 + self.mean;
361-
let delta2 = new_mean - value;
362-
let new_m2 = self.m2 - delta1 * delta2;
363-
364-
self.count -= 1;
365-
self.mean = new_mean;
366-
self.m2 = new_m2;
368+
(self.count, self.mean, self.m2) =
369+
retract(self.count, self.mean, self.m2, value)
367370
}
368371

369372
Ok(())
@@ -691,6 +694,75 @@ mod tests {
691694

692695
use super::*;
693696

697+
#[test]
698+
fn update_batch_ignores_nulls() -> Result<()> {
699+
// An array with nulls must accumulate the same values as a dense
700+
// array of its non-null values.
701+
let dense: ArrayRef = Arc::new(Float64Array::from(vec![1.0, 2.0, 3.0, 4.0]));
702+
let sparse: ArrayRef = Arc::new(Float64Array::from(vec![
703+
Some(1.0),
704+
None,
705+
Some(2.0),
706+
Some(3.0),
707+
None,
708+
Some(4.0),
709+
]));
710+
711+
let mut dense_acc = VarianceAccumulator::try_new(StatsType::Sample)?;
712+
dense_acc.update_batch(std::slice::from_ref(&dense))?;
713+
let mut sparse_acc = VarianceAccumulator::try_new(StatsType::Sample)?;
714+
sparse_acc.update_batch(std::slice::from_ref(&sparse))?;
715+
716+
// Sample variance of {1, 2, 3, 4} is 5/3 (all steps are exact in f64).
717+
assert_eq!(dense_acc.evaluate()?, ScalarValue::Float64(Some(5.0 / 3.0)));
718+
assert_eq!(dense_acc.evaluate()?, sparse_acc.evaluate()?);
719+
Ok(())
720+
}
721+
722+
#[test]
723+
fn retract_batch_ignores_nulls() -> Result<()> {
724+
let values: ArrayRef = Arc::new(Float64Array::from(vec![1.0, 2.0, 3.0, 4.0]));
725+
let dense_retract: ArrayRef = Arc::new(Float64Array::from(vec![1.0, 2.0]));
726+
let sparse_retract: ArrayRef =
727+
Arc::new(Float64Array::from(vec![Some(1.0), None, Some(2.0)]));
728+
729+
let mut dense_acc = VarianceAccumulator::try_new(StatsType::Sample)?;
730+
dense_acc.update_batch(std::slice::from_ref(&values))?;
731+
dense_acc.retract_batch(std::slice::from_ref(&dense_retract))?;
732+
let mut sparse_acc = VarianceAccumulator::try_new(StatsType::Sample)?;
733+
sparse_acc.update_batch(std::slice::from_ref(&values))?;
734+
sparse_acc.retract_batch(std::slice::from_ref(&sparse_retract))?;
735+
736+
// Sample variance of the remaining {3, 4} is 0.5 (all steps are exact
737+
// in f64).
738+
assert_eq!(dense_acc.evaluate()?, ScalarValue::Float64(Some(0.5)));
739+
assert_eq!(dense_acc.evaluate()?, sparse_acc.evaluate()?);
740+
Ok(())
741+
}
742+
743+
#[test]
744+
fn retract_batch_resets_when_underflowing() -> Result<()> {
745+
// Retracting more values than were accumulated resets to the empty
746+
// state, with or without nulls in the retracted batch.
747+
let values: ArrayRef = Arc::new(Float64Array::from(vec![1.0, 2.0]));
748+
let dense_retract: ArrayRef = Arc::new(Float64Array::from(vec![1.0, 2.0, 3.0]));
749+
let sparse_retract: ArrayRef = Arc::new(Float64Array::from(vec![
750+
Some(1.0),
751+
None,
752+
Some(2.0),
753+
Some(3.0),
754+
]));
755+
756+
for retract in [&dense_retract, &sparse_retract] {
757+
let mut acc = VarianceAccumulator::try_new(StatsType::Sample)?;
758+
acc.update_batch(std::slice::from_ref(&values))?;
759+
acc.retract_batch(std::slice::from_ref(retract))?;
760+
assert_eq!(acc.get_count(), 0);
761+
assert_eq!(acc.evaluate()?, ScalarValue::Float64(None));
762+
}
763+
Ok(())
764+
}
765+
694766
#[test]
695767
fn test_groups_accumulator_merge_empty_states() -> Result<()> {
696768
let state_1 = vec![

datafusion/functions/src/datetime/to_date.rs

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ Additional examples can be found [here](https://github.com/apache/datafusion/blo
6161
name = "format_n",
6262
description = r"Optional [Chrono format](https://docs.rs/chrono/latest/chrono/format/strftime/index.html) strings to use to parse the expression. Formats will be tried in the order
6363
they appear with the first successful one being returned. If none of the formats successfully parse the expression
64-
an error will be returned."
64+
an error will be returned. NULL formats are skipped. If every format is NULL the result is NULL."
6565
)
6666
)]
6767
#[derive(Debug, PartialEq, Eq, Hash)]
@@ -519,4 +519,44 @@ mod tests {
519519
panic!("Conversion of {date_str} succeeded, but should have failed. ");
520520
}
521521
}
522+
523+
/// A NULL format must be skipped even when its slot still holds parseable
524+
/// bytes, otherwise it can silently win over a later valid format.
525+
#[test]
526+
fn test_to_date_null_format_slot_retaining_bytes() {
527+
use arrow::buffer::NullBuffer;
528+
529+
// The first format physically holds "%d/%m/%Y", but is marked NULL.
530+
let (offsets, values, _) =
531+
GenericStringArray::<i32>::from(vec!["%d/%m/%Y"]).into_parts();
532+
let formats =
533+
GenericStringArray::new(offsets, values, Some(NullBuffer::new_null(1)));
534+
assert!(formats.is_null(0));
535+
assert_eq!(formats.value(0), "%d/%m/%Y");
536+
537+
// Without the validity check, the first format parses this as 2023-02-01
538+
// and incorrectly wins over the valid second format.
539+
let values = GenericStringArray::<i32>::from(vec!["01/02/2023"]);
540+
let fallback_formats = GenericStringArray::<i32>::from(vec!["%m/%d/%Y"]);
541+
let res = invoke_to_date_with_args(
542+
vec![
543+
ColumnarValue::Array(Arc::new(values)),
544+
ColumnarValue::Array(Arc::new(formats)),
545+
ColumnarValue::Array(Arc::new(fallback_formats)),
546+
],
547+
1,
548+
)
549+
.unwrap();
550+
551+
let ColumnarValue::Array(res) = res else {
552+
panic!("expected an array result");
553+
};
554+
let res = res.as_any().downcast_ref::<Date32Array>().unwrap();
555+
556+
assert!(!res.is_null(0));
557+
assert_eq!(
558+
res.value(0),
559+
Date32Type::parse_formatted("01/02/2023", "%m/%d/%Y").unwrap()
560+
);
561+
}
522562
}

datafusion/functions/src/datetime/to_timestamp.rs

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,8 @@ Additional examples can be found [here](https://github.com/apache/datafusion/blo
8181
description = r#"
8282
Optional [Chrono format](https://docs.rs/chrono/latest/chrono/format/strftime/index.html) strings to use to parse the expression.
8383
Formats will be tried in the order they appear with the first successful one being returned. If none of the formats successfully
84-
parse the expression an error will be returned. Note: parsing of named timezones (e.g. 'America/New_York') using %Z is
84+
parse the expression an error will be returned. NULL formats are skipped. If every format is NULL the result is NULL.
85+
Note: parsing of named timezones (e.g. 'America/New_York') using %Z is
8586
only supported at the end of the string preceded by a space.
8687
"#
8788
)
@@ -131,7 +132,8 @@ Additional examples can be found [here](https://github.com/apache/datafusion/blo
131132
description = r#"
132133
Optional [Chrono format](https://docs.rs/chrono/latest/chrono/format/strftime/index.html) strings to use to parse the expression.
133134
Formats will be tried in the order they appear with the first successful one being returned. If none of the formats successfully
134-
parse the expression an error will be returned. Note: parsing of named timezones (e.g. 'America/New_York') using %Z is
135+
parse the expression an error will be returned. NULL formats are skipped. If every format is NULL the result is NULL.
136+
Note: parsing of named timezones (e.g. 'America/New_York') using %Z is
135137
only supported at the end of the string preceded by a space.
136138
"#
137139
)
@@ -181,7 +183,8 @@ Additional examples can be found [here](https://github.com/apache/datafusion/blo
181183
description = r#"
182184
Optional [Chrono format](https://docs.rs/chrono/latest/chrono/format/strftime/index.html) strings to use to parse the expression.
183185
Formats will be tried in the order they appear with the first successful one being returned. If none of the formats successfully
184-
parse the expression an error will be returned. Note: parsing of named timezones (e.g. 'America/New_York') using %Z is
186+
parse the expression an error will be returned. NULL formats are skipped. If every format is NULL the result is NULL.
187+
Note: parsing of named timezones (e.g. 'America/New_York') using %Z is
185188
only supported at the end of the string preceded by a space.
186189
"#
187190
)
@@ -231,7 +234,8 @@ Additional examples can be found [here](https://github.com/apache/datafusion/blo
231234
description = r#"
232235
Optional [Chrono format](https://docs.rs/chrono/latest/chrono/format/strftime/index.html) strings to use to parse the expression.
233236
Formats will be tried in the order they appear with the first successful one being returned. If none of the formats successfully
234-
parse the expression an error will be returned. Note: parsing of named timezones (e.g. 'America/New_York') using %Z is
237+
parse the expression an error will be returned. NULL formats are skipped. If every format is NULL the result is NULL.
238+
Note: parsing of named timezones (e.g. 'America/New_York') using %Z is
235239
only supported at the end of the string preceded by a space.
236240
"#
237241
)
@@ -280,7 +284,8 @@ Additional examples can be found [here](https://github.com/apache/datafusion/blo
280284
description = r#"
281285
Optional [Chrono format](https://docs.rs/chrono/latest/chrono/format/strftime/index.html) strings to use to parse the expression.
282286
Formats will be tried in the order they appear with the first successful one being returned. If none of the formats successfully
283-
parse the expression an error will be returned. Note: parsing of named timezones (e.g. 'America/New_York') using %Z is
287+
parse the expression an error will be returned. NULL formats are skipped. If every format is NULL the result is NULL.
288+
Note: parsing of named timezones (e.g. 'America/New_York') using %Z is
284289
only supported at the end of the string preceded by a space.
285290
"#
286291
)

datafusion/functions/src/datetime/to_unixtime.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ Integers, unsigned integers, and floats are interpreted as seconds since the uni
5656
),
5757
argument(
5858
name = "format_n",
59-
description = "Optional [Chrono format](https://docs.rs/chrono/latest/chrono/format/strftime/index.html) strings to use to parse the expression. Formats will be tried in the order they appear with the first successful one being returned. If none of the formats successfully parse the expression an error will be returned."
59+
description = "Optional [Chrono format](https://docs.rs/chrono/latest/chrono/format/strftime/index.html) strings to use to parse the expression. Formats will be tried in the order they appear with the first successful one being returned. If none of the formats successfully parse the expression an error will be returned. NULL formats are skipped. If every format is NULL the result is NULL."
6060
)
6161
)]
6262
#[derive(Debug, PartialEq, Eq, Hash)]

datafusion/sqllogictest/test_files/datetime/dates.slt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,14 @@ ORDER BY id
319319
1 2020-09-08
320320
2 NULL
321321

322+
# Skipping NULL formats does not mask a later parse error.
323+
query error DataFusion error: Execution error: Error parsing timestamp from '2020\-09\-08' using format '%q': trailing input
324+
SELECT to_date('2020-09-08', NULL::VARCHAR, '%q')
325+
326+
# Invalid format types are rejected before NULL input propagation.
327+
query error DataFusion error: Execution error: to_date function unsupported data type at index 1: Int64
328+
SELECT to_date(NULL::VARCHAR, 12345)
329+
322330
statement ok
323331
create table ts_utf8_data(ts varchar(100), format varchar(100)) as values
324332
('2020-09-08 12/00/00+00:00', '%Y-%m-%d %H/%M/%S%#z'),

0 commit comments

Comments
 (0)