Skip to main content

promql/
functions.rs

1// Copyright 2023 Greptime Team
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15mod aggr_over_time;
16mod changes;
17mod deriv;
18mod double_exponential_smoothing;
19mod edge_count;
20mod extrapolate_rate;
21mod idelta;
22mod native_histogram;
23mod predict_linear;
24mod quantile;
25mod quantile_aggr;
26mod resets;
27mod round;
28#[cfg(test)]
29mod test_util;
30
31pub use aggr_over_time::{
32    AbsentOverTime, AvgOverTime, CountOverTime, LastOverTime, MaxOverTime, MinOverTime,
33    PresentOverTime, StddevOverTime, StdvarOverTime, SumOverTime,
34};
35pub use changes::Changes;
36use datafusion::arrow::array::{
37    ArrayRef, DictionaryArray, Float64Array, TimestampMillisecondArray,
38};
39use datafusion::error::DataFusionError;
40use datafusion::physical_plan::ColumnarValue;
41use datatypes::arrow::array::Array;
42use datatypes::arrow::datatypes::{DataType, Int64Type};
43pub use deriv::Deriv;
44pub use double_exponential_smoothing::DoubleExponentialSmoothing;
45pub use extrapolate_rate::{Delta, Increase, Rate};
46pub use idelta::IDelta;
47pub use native_histogram::{
48    MixedRange, NativeHistogramAbsentOverTime, NativeHistogramAdd, NativeHistogramAggAvg,
49    NativeHistogramAggSum, NativeHistogramAvg, NativeHistogramAvgOverTime, NativeHistogramChanges,
50    NativeHistogramCount, NativeHistogramCountOverTime, NativeHistogramDelta,
51    NativeHistogramDivScalar, NativeHistogramDrop, NativeHistogramEq, NativeHistogramFraction,
52    NativeHistogramIDelta, NativeHistogramIRate, NativeHistogramIncrease,
53    NativeHistogramLastOverTime, NativeHistogramMulScalar, NativeHistogramNeg,
54    NativeHistogramNotEq, NativeHistogramPresentOverTime, NativeHistogramQuantile,
55    NativeHistogramRate, NativeHistogramResets, NativeHistogramScalarMul, NativeHistogramStddev,
56    NativeHistogramStdvar, NativeHistogramSub, NativeHistogramSum, NativeHistogramSumOverTime,
57    NativeHistogramToString, PromqlFloatToString,
58};
59pub use predict_linear::PredictLinear;
60pub use quantile::QuantileOverTime;
61pub use quantile_aggr::{QUANTILE_NAME, quantile_udaf};
62pub use resets::Resets;
63pub use round::Round;
64
65use crate::range_array::RangeArray;
66
67/// Extracts an array from a `ColumnarValue`.
68///
69/// If the `ColumnarValue` is a scalar, it converts it to an array of size 1.
70pub(crate) fn extract_array(columnar_value: &ColumnarValue) -> Result<ArrayRef, DataFusionError> {
71    match columnar_value {
72        ColumnarValue::Array(array) => Ok(array.clone()),
73        ColumnarValue::Scalar(scalar) => Ok(scalar.to_array_of_size(1)?),
74    }
75}
76
77/// Extracts and validates a range dictionary with the expected value type.
78pub(crate) fn extract_range_dict(
79    columnar_value: &ColumnarValue,
80    func_name: &str,
81    arg_name: &str,
82    expected_value_type: &DataType,
83) -> Result<DictionaryArray<Int64Type>, DataFusionError> {
84    let array = extract_array(columnar_value)?;
85    let dict = array
86        .as_any()
87        .downcast_ref::<DictionaryArray<Int64Type>>()
88        .ok_or_else(|| {
89            DataFusionError::Execution(format!(
90                "{func_name}: expect {arg_name} as DictionaryArray<Int64>, found {}",
91                array.data_type()
92            ))
93        })?
94        .clone();
95
96    if &dict.value_type() != expected_value_type {
97        return Err(DataFusionError::Execution(format!(
98            "{func_name}: expect {arg_name} values of type {expected_value_type}, found {}",
99            dict.value_type()
100        )));
101    }
102
103    RangeArray::try_new(dict.clone()).map_err(DataFusionError::from)?;
104    Ok(dict)
105}
106
107/// Extracts a validated [RangeArray] from a [ColumnarValue].
108pub(crate) fn extract_range_array(
109    columnar_value: &ColumnarValue,
110) -> Result<RangeArray, DataFusionError> {
111    let array = extract_array(columnar_value)?;
112    let dict = array
113        .as_any()
114        .downcast_ref::<DictionaryArray<Int64Type>>()
115        .ok_or_else(|| {
116            DataFusionError::Execution(format!(
117                "expected DictionaryArray<Int64>, found {}",
118                array.data_type()
119            ))
120        })?
121        .clone();
122    RangeArray::try_new(dict).map_err(DataFusionError::from)
123}
124
125/// compensation(Kahan) summation algorithm - a technique for reducing the numerical error
126/// in floating-point arithmetic. The algorithm also includes the modification ("Neumaier improvement")
127/// that reduces the numerical error further in cases
128/// where the numbers being summed have a large difference in magnitude
129/// Prometheus's implementation:
130/// <https://github.com/prometheus/prometheus/blob/f55ab2217984770aa1eecd0f2d5f54580029b1c0/promql/functions.go#L782>
131pub(crate) fn compensated_sum_inc(inc: f64, sum: f64, mut compensation: f64) -> (f64, f64) {
132    let new_sum = sum + inc;
133    if sum.abs() >= inc.abs() {
134        compensation += (sum - new_sum) + inc;
135    } else {
136        compensation += (inc - new_sum) + sum;
137    }
138    (new_sum, compensation)
139}
140
141/// linear_regression performs a least-square linear regression analysis on the
142/// times and values. It return the slope and intercept based on times and values.
143/// Prometheus's implementation: <https://github.com/prometheus/prometheus/blob/90b2f7a540b8a70d8d81372e6692dcbb67ccbaaa/promql/functions.go#L793-L837>
144pub(crate) fn linear_regression(
145    times: &TimestampMillisecondArray,
146    values: &Float64Array,
147    intercept_time: i64,
148) -> (Option<f64>, Option<f64>) {
149    linear_regression_slice(times.values(), values, 0, values.len(), intercept_time)
150}
151
152pub(crate) fn linear_regression_slice(
153    times: &[i64],
154    values: &Float64Array,
155    offset: usize,
156    len: usize,
157    intercept_time: i64,
158) -> (Option<f64>, Option<f64>) {
159    linear_regression_slices(times, offset, values, offset, len, intercept_time)
160}
161
162pub(crate) fn linear_regression_slices(
163    times: &[i64],
164    time_offset: usize,
165    values: &Float64Array,
166    value_offset: usize,
167    len: usize,
168    intercept_time: i64,
169) -> (Option<f64>, Option<f64>) {
170    let raw_values = values.values();
171    let has_nulls = values.null_count() > 0;
172    let mut count: f64 = 0.0;
173    let mut sum_x: f64 = 0.0;
174    let mut sum_y: f64 = 0.0;
175    let mut sum_xy: f64 = 0.0;
176    let mut sum_x2: f64 = 0.0;
177    let mut comp_x: f64 = 0.0;
178    let mut comp_y: f64 = 0.0;
179    let mut comp_xy: f64 = 0.0;
180    let mut comp_x2: f64 = 0.0;
181
182    let mut const_y = true;
183    let mut init_y = None;
184
185    for i in 0..len {
186        let time_idx = time_offset + i;
187        let value_idx = value_offset + i;
188        if has_nulls && values.is_null(value_idx) {
189            continue;
190        }
191        let value = raw_values[value_idx];
192        let time = times[time_idx] as f64;
193        let initial = init_y.get_or_insert(value);
194        if const_y && count > 0.0 && value != *initial {
195            const_y = false;
196        }
197        count += 1.0;
198        let x = (time - intercept_time as f64) / 1e3f64;
199        (sum_x, comp_x) = compensated_sum_inc(x, sum_x, comp_x);
200        (sum_y, comp_y) = compensated_sum_inc(value, sum_y, comp_y);
201        (sum_xy, comp_xy) = compensated_sum_inc(x * value, sum_xy, comp_xy);
202        (sum_x2, comp_x2) = compensated_sum_inc(x * x, sum_x2, comp_x2);
203    }
204
205    if count < 2.0 {
206        return (None, None);
207    }
208
209    if const_y {
210        let init_y = init_y.unwrap();
211        if !init_y.is_finite() {
212            return (None, None);
213        }
214        return (Some(0.0), Some(init_y));
215    }
216
217    sum_x += comp_x;
218    sum_y += comp_y;
219    sum_xy += comp_xy;
220    sum_x2 += comp_x2;
221
222    let cov_xy = sum_xy - sum_x * sum_y / count;
223    let var_x = sum_x2 - sum_x * sum_x / count;
224
225    let slope = cov_xy / var_x;
226    let intercept = sum_y / count - slope * sum_x / count;
227
228    (Some(slope), Some(intercept))
229}
230
231#[cfg(test)]
232mod test {
233    use std::sync::Arc;
234
235    use datafusion::physical_plan::ColumnarValue;
236    use datatypes::arrow::array::Int64Array;
237    use datatypes::arrow::datatypes::Int64Type;
238
239    use super::*;
240    use crate::range_array::RangeArray;
241
242    #[test]
243    fn calculate_linear_regression_none() {
244        let ts_array = TimestampMillisecondArray::from_iter(
245            [
246                0i64, 300, 600, 900, 1200, 1500, 1800, 2100, 2400, 2700, 3000,
247            ]
248            .into_iter()
249            .map(Some),
250        );
251        let values_array = Float64Array::from_iter([
252            1.0 / 0.0,
253            1.0 / 0.0,
254            1.0 / 0.0,
255            1.0 / 0.0,
256            1.0 / 0.0,
257            1.0 / 0.0,
258            1.0 / 0.0,
259            1.0 / 0.0,
260            1.0 / 0.0,
261            1.0 / 0.0,
262        ]);
263        let (slope, intercept) = linear_regression(&ts_array, &values_array, ts_array.value(0));
264        assert_eq!(slope, None);
265        assert_eq!(intercept, None);
266    }
267
268    #[test]
269    fn calculate_linear_regression_value_is_const() {
270        let ts_array = TimestampMillisecondArray::from_iter(
271            [
272                0i64, 300, 600, 900, 1200, 1500, 1800, 2100, 2400, 2700, 3000,
273            ]
274            .into_iter()
275            .map(Some),
276        );
277        let values_array =
278            Float64Array::from_iter([10.0, 10.0, 10.0, 10.0, 10.0, 10.0, 10.0, 10.0, 10.0, 10.0]);
279        let (slope, intercept) = linear_regression(&ts_array, &values_array, ts_array.value(0));
280        assert_eq!(slope, Some(0.0));
281        assert_eq!(intercept, Some(10.0));
282    }
283
284    #[test]
285    fn calculate_linear_regression() {
286        let ts_array = TimestampMillisecondArray::from_iter(
287            [
288                0i64, 300, 600, 900, 1200, 1500, 1800, 2100, 2400, 2700, 3000,
289            ]
290            .into_iter()
291            .map(Some),
292        );
293        let values_array = Float64Array::from_iter([
294            0.0, 10.0, 20.0, 30.0, 40.0, 0.0, 10.0, 20.0, 30.0, 40.0, 50.0,
295        ]);
296        let (slope, intercept) = linear_regression(&ts_array, &values_array, ts_array.value(0));
297        assert_eq!(slope, Some(10.606060606060607));
298        assert_eq!(intercept, Some(6.818181818181815));
299
300        let (slope, intercept) = linear_regression(&ts_array, &values_array, 3000);
301        assert_eq!(slope, Some(10.606060606060607));
302        assert_eq!(intercept, Some(38.63636363636364));
303    }
304
305    #[test]
306    fn calculate_linear_regression_value_have_none() {
307        let ts_array = TimestampMillisecondArray::from_iter(
308            [
309                0i64, 300, 600, 900, 1200, 1350, 1500, 1800, 2100, 2400, 2550, 2700, 3000,
310            ]
311            .into_iter()
312            .map(Some),
313        );
314        let values_array: Float64Array = [
315            Some(0.0),
316            Some(10.0),
317            Some(20.0),
318            Some(30.0),
319            Some(40.0),
320            None,
321            Some(0.0),
322            Some(10.0),
323            Some(20.0),
324            Some(30.0),
325            None,
326            Some(40.0),
327            Some(50.0),
328        ]
329        .into_iter()
330        .collect();
331        let (slope, intercept) = linear_regression(&ts_array, &values_array, ts_array.value(0));
332        assert_eq!(slope, Some(10.606060606060607));
333        assert_eq!(intercept, Some(6.818181818181815));
334    }
335
336    #[test]
337    fn calculate_linear_regression_value_all_none() {
338        let ts_array = TimestampMillisecondArray::from_iter([0i64, 300, 600].into_iter().map(Some));
339        let values_array: Float64Array = [None, None, None].into_iter().collect();
340        let (slope, intercept) = linear_regression(&ts_array, &values_array, ts_array.value(0));
341        assert_eq!(slope, None);
342        assert_eq!(intercept, None);
343    }
344
345    // From prometheus `promql/functions_test.go` case `TestKahanSum`
346    #[test]
347    fn test_kahan_sum() {
348        let inputs = vec![1.0, 10.0f64.powf(100.0), 1.0, -10.0f64.powf(100.0)];
349
350        let mut sum = 0.0;
351        let mut c = 0f64;
352
353        for v in inputs {
354            (sum, c) = compensated_sum_inc(v, sum, c);
355        }
356        assert_eq!(sum + c, 2.0)
357    }
358
359    #[test]
360    fn extract_range_array_rejects_external_dictionary_with_null_keys() {
361        let keys = Int64Array::from_iter([Some(0), None]);
362        let values = Arc::new(Float64Array::from_iter([1.0, 2.0]));
363        let dict = DictionaryArray::<Int64Type>::try_new(keys, values).unwrap();
364
365        let err = extract_range_array(&ColumnarValue::Array(Arc::new(dict))).unwrap_err();
366        assert!(err.to_string().contains("Empty range is not expected"));
367    }
368
369    #[test]
370    fn extract_range_array_accepts_internal_packed_ranges() {
371        let values = Arc::new(Float64Array::from_iter([1.0, 2.0, 3.0]));
372        let range_array = RangeArray::from_ranges(values, [(0, 2), (1, 2)]).unwrap();
373
374        let extracted =
375            extract_range_array(&ColumnarValue::Array(Arc::new(range_array.into_dict()))).unwrap();
376
377        assert_eq!(extracted.get_offset_length(0), Some((0, 2)));
378        assert_eq!(extracted.get_offset_length(1), Some((1, 2)));
379    }
380}