From 50002c6830db32ea2b25728aaa468259d14f03d1 Mon Sep 17 00:00:00 2001 From: "Lei, HUANG" <6406592+v0y4g3r@users.noreply.github.com> Date: Sun, 18 Oct 2026 23:32:17 -0700 Subject: [PATCH] feat(promql): add evaluated series math kernels (#9672) * feat(promql): add evaluated series math kernels Signed-off-by: Lei, HUANG * fix(promql): drop metric name in evaluated series math like Prometheus Prometheus' simpleFloatFunc drops __name__ together with __type__ and __unit__. Check abs, ceil, floor, sqrt and round against fixtures generated by the Prometheus v3.15.0 engine. Signed-off-by: Lei, HUANG * fix(promql): drop empty labels with the metric name in series math Prometheus has no empty-valued labels, so a kept empty value would hide a same-labelset collision from the caller. Round shares its calculation with the UDF, so check it only against the Prometheus fixtures. Signed-off-by: Lei, HUANG * chore: exclude promql golden fixtures from typos Signed-off-by: Lei, HUANG * docs(promql): point series round at Prometheus funcRound Signed-off-by: Lei, HUANG * chore: allow the Unicode category Nd in typos Signed-off-by: Lei, HUANG * perf(promql): drop reserved and empty labels in one rebuild drop_metric_name removed empty-valued labels one at a time, and each MetricName::remove copies the whole label array, so the cost grew with the square of the number of empty labels. Filter the labels once and rebuild the name with MetricName::try_new. Signed-off-by: Lei, HUANG --------- Signed-off-by: Lei, HUANG --- src/promql/src/functions/round.rs | 26 +- src/promql/src/lib.rs | 1 + src/promql/src/map.rs | 129 ++++++ src/promql/src/map/prometheus_cases.txt | 398 +++++++++++++++++++ src/promql/src/map/tests.rs | 504 ++++++++++++++++++++++++ src/promql/src/map/tests/prometheus.rs | 234 +++++++++++ 6 files changed, 1280 insertions(+), 12 deletions(-) create mode 100644 src/promql/src/map.rs create mode 100644 src/promql/src/map/prometheus_cases.txt create mode 100644 src/promql/src/map/tests.rs create mode 100644 src/promql/src/map/tests/prometheus.rs diff --git a/src/promql/src/functions/round.rs b/src/promql/src/functions/round.rs index c5ba4c218c0..5db461332cf 100644 --- a/src/promql/src/functions/round.rs +++ b/src/promql/src/functions/round.rs @@ -66,7 +66,7 @@ impl Round { let null_array = Float64Array::new_null(value_array.len()); return Ok(ColumnarValue::Array(Arc::new(null_array))); }; - let op = |a: f64| round_to_nearest(a, nearest); + let op = |a: f64| Self::round_to_nearest(a, nearest); let result: PrimitiveArray = value_array.as_primitive::().unary(op); Ok(ColumnarValue::Array(Arc::new(result) as _)) @@ -83,23 +83,25 @@ impl Round { )), )?; - let result: PrimitiveArray = - datatypes::arrow::compute::binary(value_array, nearest_array, round_to_nearest) - .map_err(|err: ArrowError| { - DataFusionError::ArrowError(Box::new(err), None) - })?; + let result: PrimitiveArray = datatypes::arrow::compute::binary( + value_array, + nearest_array, + Self::round_to_nearest, + ) + .map_err(|err: ArrowError| DataFusionError::ArrowError(Box::new(err), None))?; Ok(ColumnarValue::Array(Arc::new(result) as _)) } } } -} -/// Rounds like Prometheus: ties round up (towards +Inf), and multiplying by the inverse -/// of `nearest` follows its float behavior, including NaN for `nearest == 0`. -fn round_to_nearest(value: f64, nearest: f64) -> f64 { - let inverse = 1.0 / nearest; - (value * inverse + 0.5).floor() / inverse + /// Follows Prometheus `funcRound`: multiplying by the inverse of `nearest` and dividing by it + /// again (not `/ nearest` then `* nearest`) can move a `floor` result by a whole step. + /// `nearest == 0` gives NaN; with a positive `nearest`, ties round towards +Inf. + pub(crate) fn round_to_nearest(value: f64, nearest: f64) -> f64 { + let inverse = 1.0 / nearest; + (value * inverse + 0.5).floor() / inverse + } } #[cfg(test)] diff --git a/src/promql/src/lib.rs b/src/promql/src/lib.rs index f4a4fca99c4..737d90a4581 100644 --- a/src/promql/src/lib.rs +++ b/src/promql/src/lib.rs @@ -19,6 +19,7 @@ mod go_sort; pub mod grid_series; pub mod histogram_eval; pub mod instant_eval; +pub mod map; pub mod map_label; pub mod map_time; mod metrics; diff --git a/src/promql/src/map.rs b/src/promql/src/map.rs new file mode 100644 index 00000000000..5e22906c954 --- /dev/null +++ b/src/promql/src/map.rs @@ -0,0 +1,129 @@ +// Copyright 2023 Greptime Team +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Pointwise kernels for evaluated series, independent of planning and execution nodes. +//! +//! Expression column naming and omission of missing points belong to the planner/output adapter; +//! these kernels retain the shared field schema and represent missing grid positions by validity. + +#[cfg(test)] +mod tests; + +use datafusion::error::{DataFusionError, Result}; +use datatypes::arrow::array::Float64Array; +use datatypes::series::MetricName; +use datatypes::series::samples::{DenseSamplesRef, Samples, Series, SeriesFields, SeriesSchema}; +use datatypes::series::time::TimeAxis; + +use crate::functions::Round; + +/// Float math supported by the evaluated-series kernel. +#[derive(Clone, Copy, Debug)] +pub enum MathOp { + /// Absolute value. + Abs, + /// Rounds towards positive infinity. + Ceil, + /// Rounds towards negative infinity. + Floor, + /// IEEE square root, returning NaN for negative input. + Sqrt, + /// Rounds to a constant multiple; use `1.0` for PromQL's default argument. + Round { nearest: f64 }, +} + +/// Applies float math to every field of one evaluated series and drops the metric name, like +/// Prometheus' `simpleFloatFunc`. +/// +/// The explicit schema and actual grid range are preserved. Fields are processed in their own +/// chunk boundaries, without expanding to the full grid or densifying chunked samples. Missing +/// samples stay missing; ordinary NaNs remain values. An empty or all-missing input is valid. +/// +/// Eval must resolve raw staleness before calling this kernel. Evaluated samples use validity, +/// not NaN payloads, for missingness: arithmetic can produce the reserved stale bit pattern too. +/// Raw time axes and histogram/mixed segments are rejected. Unsupported query types must be +/// admitted to the ordinary path before execution; this function neither falls back nor returns +/// partial results. +/// +/// Distinct inputs can share a label set once the name is dropped. Merging them, or failing +/// when both have a value at one step, is left to the caller (Prometheus +/// `mergeSeriesWithSameLabelset`). +pub fn map_math(schema: &SeriesSchema, series: &Series, op: MathOp) -> Result { + if !matches!(series.time, TimeAxis::Grid(_)) { + return Err(DataFusionError::Execution( + "Map math requires an evaluated grid time axis".into(), + )); + } + let fields = series + .fields + .iter() + .map(|samples| map_samples(samples, op)) + .collect::>>()?; + let fields = SeriesFields::try_from_vec(fields).map_err(series_error)?; + let metric = drop_metric_name(&series.metric)?; + Series::try_new(schema, metric, series.time.clone(), fields).map_err(series_error) +} + +/// Prometheus `DropReserved(schema.IsMetadataLabel)` drops the type and unit labels with the +/// name. Empty values are dropped too: Prometheus has no empty labels, and keeping them would +/// hide a same-labelset collision from the caller. +pub(crate) fn drop_metric_name(metric: &MetricName) -> Result { + // One rebuild: removing labels one by one copies the label array each time. + let tags = metric + .tags() + .iter() + .filter(|label| { + !label.value.is_empty() && !matches!(label.name.as_ref(), "__type__" | "__unit__") + }) + .cloned() + .collect(); + MetricName::try_new(None, tags).map_err(series_error) +} + +impl MathOp { + fn apply(self, floats: &Float64Array) -> Float64Array { + match self { + Self::Abs => floats.unary(f64::abs), + Self::Ceil => floats.unary(f64::ceil), + Self::Floor => floats.unary(f64::floor), + Self::Sqrt => floats.unary(f64::sqrt), + Self::Round { nearest } => { + floats.unary(|sample| Round::round_to_nearest(sample, nearest)) + } + } + } +} + +fn map_samples(samples: &Samples, op: MathOp) -> Result { + let chunks = samples + .segments() + .iter() + .map(|segment| map_float_segment(segment, op)) + .collect::>>()?; + Samples::try_from_chunks(chunks).map_err(series_error) +} + +fn map_float_segment(segment: &Samples, op: MathOp) -> Result { + let Some(DenseSamplesRef::Float(floats)) = segment.dense() else { + return Err(DataFusionError::NotImplemented( + "Map math supports only float samples; histogram/mixed samples are not supported" + .into(), + )); + }; + Ok(Samples::from_float(op.apply(floats))) +} + +fn series_error(error: datatypes::error::Error) -> DataFusionError { + DataFusionError::External(Box::new(error)) +} diff --git a/src/promql/src/map/prometheus_cases.txt b/src/promql/src/map/prometheus_cases.txt new file mode 100644 index 00000000000..76c71343913 --- /dev/null +++ b/src/promql/src/map/prometheus_cases.txt @@ -0,0 +1,398 @@ +# Generated by promql-compat cmd/narthex-golden-map built for amd64 (Prometheus v3.15.0 +# engine, go1.26.5, seed 1): 48 math. A case lists its operation, the PromQL +# expression, the grid, the inputs read back from the engine and the result series. +# Points are `grid index:hex value bits`. +case math abs +expr abs(a) +grid 219680840871 1 5 +series __name__=a,job=j1,zone=z1 ; 0:c0210e147ae147ae 1:c083700000000000 2:c0e0acc000000000 3:800000000000001d 4:c07a4a0000000000 +result job=j1,zone=z1 ; 0:40210e147ae147ae 1:4083700000000000 2:40e0acc000000000 3:000000000000001d 4:407a4a0000000000 +end +case math abs +expr abs(a) +grid -1083181880376 60000 9 +series __name__=a,host=h1,job=j1 ; 1:c020d00000000000 2:8000000000000001 3:7c55a9b1e1c0627c 4:403af33333333334 5:7ff0000000000000 6:fdcc30f03c00b384 7:3fe677ced916872b 8:fa539f7693acc750 +result host=h1,job=j1 ; 1:4020d00000000000 2:0000000000000001 3:7c55a9b1e1c0627c 4:403af33333333334 5:7ff0000000000000 6:7dcc30f03c00b384 7:3fe677ced916872b 8:7a539f7693acc750 +end +case math abs +expr abs(a) +grid 2320841668853 1 3 +series __name__=a,host=h1,zone=z2 ; 1:c014d9999999999a 2:4008000000000000 +series __name__=a,host=h2 ; 0:4013d1eb851eb852 2:4014000000000000 +series __name__=a,host=h2,job=j2 ; 0:c04f700000000000 1:c01ce8f5c28f5c29 2:fff0000000000000 +result host=h1,zone=z2 ; 1:4014d9999999999a 2:4008000000000000 +result host=h2 ; 0:4013d1eb851eb852 2:4014000000000000 +result host=h2,job=j2 ; 0:404f700000000000 1:401ce8f5c28f5c29 2:7ff0000000000000 +end +case math abs +expr abs(a) +grid -2210646068282 60000 3 +series __name__=a,host=h1,zone=z2 ; 0:408e680000000000 1:c118a49800000000 2:4000000000000000 +result host=h1,zone=z2 ; 0:408e680000000000 1:4118a49800000000 2:4000000000000000 +end +case math abs +expr abs(a) +grid 1248740558131 1 6 +series __name__=a,host=h2,job=j1,zone=z1 ; 0:3fe0000000000000 2:40e1a6c000000000 3:40f1a21000000000 4:4110b55800000000 5:40e6391000000000 +result host=h2,job=j1,zone=z1 ; 0:3fe0000000000000 2:40e1a6c000000000 3:40f1a21000000000 4:4110b55800000000 5:40e6391000000000 +end +case math abs +expr abs(a) +grid 1493340200482 15000 5 +series __name__=a,host=h2,job=j2,zone=z1 ; 0:c081cf0000000000 1:408acb0000000000 2:c00f7ae147ae147b 3:000000165354657a 4:c0824e0000000000 +series __name__=a,job=j2,zone=z2 ; 0:40c005e000000000 1:c022b00000000000 2:bfe1be76c8b43958 3:4058073333333334 +series __name__=a,zone=z2 ; 0:4015e7ae147ae148 1:bfe0000000000000 2:40f579a800000000 3:c08eca0000000000 4:8000000003b6a28c +result host=h2,job=j2,zone=z1 ; 0:4081cf0000000000 1:408acb0000000000 2:400f7ae147ae147b 3:000000165354657a 4:40824e0000000000 +result job=j2,zone=z2 ; 0:40c005e000000000 1:4022b00000000000 2:3fe1be76c8b43958 3:4058073333333334 +result zone=z2 ; 0:4015e7ae147ae148 1:3fe0000000000000 2:40f579a800000000 3:408eca0000000000 4:0000000003b6a28c +end +case math ceil +expr ceil(a) +grid 2030383452465 15000 7 +series __name__=a,host=h1,job=j2 ; 0:401b2147ae147ae1 1:ffe1ccf385ebc8a0 2:4024000000000000 3:c0f7589000000000 4:c01c000000000000 5:0000000000000000 6:bfe3b74bc6a7ef9e +series __name__=a,job=j2,zone=z2 ; 0:c0a0068000000000 1:40c2412000000000 2:c11fd08800000000 3:40f14f4000000000 4:c0f648b000000000 6:ffe1ccf385ebc8a0 +result host=h1,job=j2 ; 0:401c000000000000 1:ffe1ccf385ebc8a0 2:4024000000000000 3:c0f7589000000000 4:c01c000000000000 5:0000000000000000 6:8000000000000000 +result job=j2,zone=z2 ; 0:c0a0060000000000 1:40c2418000000000 2:c11fd08800000000 3:40f14f4000000000 4:c0f648b000000000 6:ffe1ccf385ebc8a0 +end +case math ceil +expr ceil(a) +grid -2295928214828 1 7 +series __name__=a,host=h1 ; 0:7fefffffffffffff 1:80001a78509b389c 3:7acb7cd9a5ea2f80 6:800000000000001c +series __name__=a,host=h1,job=j1 ; 2:c0d6922000000000 4:000000000000000f 5:bff0000000000000 +series __name__=a,host=h1,job=j2,zone=z1 ; 2:4000000000000000 3:40f19bd000000000 4:c0568ccccccccccd 5:c0bc9d0000000000 +result host=h1 ; 0:7fefffffffffffff 1:8000000000000000 3:7acb7cd9a5ea2f80 6:8000000000000000 +result host=h1,job=j1 ; 2:c0d6920000000000 4:3ff0000000000000 5:bff0000000000000 +result host=h1,job=j2,zone=z1 ; 2:4000000000000000 3:40f19bd000000000 4:c056800000000000 5:c0bc9d0000000000 +end +case math ceil +expr ceil(a) +grid 288693432093 1 10 +series __name__=a,host=h2 ; 0:3fea666666666667 1:40858f0000000000 2:fd20e89049e21476 3:f9f391b611275f20 4:c0116147ae147ae1 5:7ff8000000000001 6:4020000000000000 9:3ff0000000000000 +series __name__=a,host=h2,job=j1 ; 1:40cd90c000000000 2:0000000000000001 3:bfb2e147ae147ae1 4:3fe41fbe76c8b439 5:7e2ee5ddbbc90e98 6:8002b0ee2e12172a 7:401f428f5c28f5c3 8:c01c000000000000 9:0000000000000003 +result host=h2 ; 0:3ff0000000000000 1:4085900000000000 2:fd20e89049e21476 3:f9f391b611275f20 4:c010000000000000 5:7ff8000000000001 6:4020000000000000 9:3ff0000000000000 +result host=h2,job=j1 ; 1:40cd910000000000 2:3ff0000000000000 3:8000000000000000 4:3ff0000000000000 5:7e2ee5ddbbc90e98 6:8000000000000000 7:4020000000000000 8:c01c000000000000 9:3ff0000000000000 +end +case math ceil +expr ceil(a) +grid 2052594281964 1000 4 +series __name__=a,host=h1,job=j2,zone=z1 ; 0:c0e73f9000000000 2:c048100000000000 3:40be190000000000 +series __name__=a,host=h1,zone=z2 ; 1:c123459800000000 3:c0282ccccccccccd +series __name__=a,job=j2,zone=z1 ; 0:bfd7333333333333 1:f896e9a156d2b750 3:fcefe8af01db2170 +result host=h1,job=j2,zone=z1 ; 0:c0e73f8000000000 2:c048000000000000 3:40be190000000000 +result host=h1,zone=z2 ; 1:c123459800000000 3:c028000000000000 +result job=j2,zone=z1 ; 0:8000000000000000 1:f896e9a156d2b750 3:fcefe8af01db2170 +end +case math ceil +expr ceil(a) +grid 305490743228 60000 9 +series __name__=a,host=h1,job=j2,zone=z2 ; 0:4011c00000000000 1:4008000000000000 2:c0fdb19000000000 3:c0c13c8000000000 4:4122a28200000000 6:c10dada800000000 7:4000000000000000 8:4120d49800000000 +series __name__=a,host=h2,job=j1,zone=z2 ; 0:401c27ae147ae148 1:fa6899d641714788 2:ffefffffffffffff 3:c05f400000000000 4:bfedbc6a7ef9db23 5:0000000000000000 6:3fdae147ae147ae1 8:802c7cc387a67ec0 +series __name__=a,host=h2,zone=z1 ; 2:ffe1ccf385ebc8a0 3:0000000000000001 6:7c0a489f44473ee4 7:782a67fa6c1344b4 +result host=h1,job=j2,zone=z2 ; 0:4014000000000000 1:4008000000000000 2:c0fdb19000000000 3:c0c13c8000000000 4:4122a28200000000 6:c10dada800000000 7:4000000000000000 8:4120d49800000000 +result host=h2,job=j1,zone=z2 ; 0:4020000000000000 1:fa6899d641714788 2:ffefffffffffffff 3:c05f400000000000 4:8000000000000000 5:0000000000000000 6:3ff0000000000000 8:8000000000000000 +result host=h2,zone=z1 ; 2:ffe1ccf385ebc8a0 3:3ff0000000000000 6:7c0a489f44473ee4 7:782a67fa6c1344b4 +end +case math ceil +expr ceil(a) +grid 768183472652 15000 4 +series __name__=a,host=h2,job=j1,zone=z2 ; 0:ffefffffffffffff 1:40ebee0000000000 3:4022200000000000 +result host=h2,job=j1,zone=z2 ; 0:ffefffffffffffff 1:40ebee0000000000 3:4024000000000000 +end +case math floor +expr floor(a) +grid -3544116501466 60000 8 +series __name__=a,host=h2,job=j1,zone=z1 ; 0:802dc0445669d422 1:0000000000000002 2:c0e8e0c000000000 3:c01c000000000000 +series __name__=a,host=h2,job=j1,zone=z2 ; 1:78d6bfa04792fb40 2:c050780000000000 3:0000000059b707f6 4:c0b7818000000000 5:4127eef400000000 6:c0f1733000000000 7:c10dd8a000000000 +series __name__=a,host=h2,zone=z1 ; 1:7b162d38e8c8ffd4 3:c01c000000000000 4:7a9956047b631df8 7:c0af8d8000000000 +result host=h2,job=j1,zone=z1 ; 0:bff0000000000000 1:0000000000000000 2:c0e8e0c000000000 3:c01c000000000000 +result host=h2,job=j1,zone=z2 ; 1:78d6bfa04792fb40 2:c050800000000000 3:0000000000000000 4:c0b7820000000000 5:4127eef400000000 6:c0f1733000000000 7:c10dd8a000000000 +result host=h2,zone=z1 ; 1:7b162d38e8c8ffd4 3:c01c000000000000 4:7a9956047b631df8 7:c0af8e0000000000 +end +case math floor +expr floor(a) +grid 3110963353051 15000 4 +series __name__=a,host=h1,job=j1,zone=z1 ; 0:bfd6f7ced916872b 1:3fd49ba5e353f7cf 2:7cf8e2149a0c0ea0 3:c079f00000000000 +result host=h1,job=j1,zone=z1 ; 0:bff0000000000000 1:0000000000000000 2:7cf8e2149a0c0ea0 3:c079f00000000000 +end +case math floor +expr floor(a) +grid -2988969278171 1 3 +series __name__=a,job=j1,zone=z2 ; 0:4341c37937e08000 1:c127a6b000000000 2:8000000000000000 +result job=j1,zone=z2 ; 0:4341c37937e08000 1:c127a6b000000000 2:8000000000000000 +end +case math floor +expr floor(a) +grid -2632120620786 15000 8 +series __name__=a,zone=z1 ; 0:7ff8000000000001 1:4121335200000000 2:f872592494d05e60 4:c086330000000000 6:7fe1ccf385ebc8a0 7:bff0000000000000 +result zone=z1 ; 0:7ff8000000000001 1:4121335200000000 2:f872592494d05e60 4:c086380000000000 6:7fe1ccf385ebc8a0 7:bff0000000000000 +end +case math floor +expr floor(a) +grid -725694921407 15000 9 +series __name__=a,host=h1,job=j1 ; 0:800000006e8b16cd 1:fa2ebb4882039496 2:40f7b26800000000 3:40b9a00000000000 4:3fe0000000000000 5:40b9ce4000000000 7:bfc370a3d70a3d71 +result host=h1,job=j1 ; 0:bff0000000000000 1:fa2ebb4882039496 2:40f7b26000000000 3:40b9a00000000000 4:0000000000000000 5:40b9ce0000000000 7:bff0000000000000 +end +case math floor +expr floor(a) +grid -951820580376 1000 9 +series __name__=a ; 0:bfec872b020c49bb 1:c0c0d88000000000 2:3ff0000000000000 3:000fffffffffffff 4:c04771999999999a 7:c0a3178000000000 8:406b140000000000 +series __name__=a,host=h1,job=j2,zone=z2 ; 0:8000000000000001 1:401c000000000000 2:fea65e065a8a2a94 5:c08f110000000000 6:3ff0000000000000 7:3fd6353f7ced9169 +result - ; 0:bff0000000000000 1:c0c0d88000000000 2:3ff0000000000000 3:0000000000000000 4:c047800000000000 7:c0a3180000000000 8:406b000000000000 +result host=h1,job=j2,zone=z2 ; 0:bff0000000000000 1:401c000000000000 2:fea65e065a8a2a94 5:c08f180000000000 6:3ff0000000000000 7:0000000000000000 +end +case math sqrt +expr sqrt(a) +grid 360879133452 1 4 +series __name__=a,host=h2,job=j1 ; 1:4020000000000000 2:7ff0000000000000 +series __name__=a,zone=z2 ; 1:4008000000000000 3:000fffffffffffff +result host=h2,job=j1 ; 1:4006a09e667f3bcd 2:7ff0000000000000 +result zone=z2 ; 1:3ffbb67ae8584caa 3:1fffffffffffffff +end +case math sqrt +expr sqrt(a) +grid 3408288750287 60000 7 +series __name__=a,host=h1,job=j2,zone=z2 ; 1:8000000000016c4a 4:c0b304c000000000 5:bfe122d0e5604189 6:c010000000000000 +series __name__=a,zone=z2 ; 0:c018000000000000 1:c0801c0000000000 3:7fefffffffffffff 4:40f319d800000000 +result host=h1,job=j2,zone=z2 ; 1:fff8000000000000 4:fff8000000000000 5:fff8000000000000 6:fff8000000000000 +result zone=z2 ; 0:fff8000000000000 1:fff8000000000000 3:5fefffffffffffff 4:40717b5acfe20ea7 +end +case math sqrt +expr sqrt(a) +grid 1581145206376 15000 6 +series __name__=a,host=h1,job=j2,zone=z2 ; 0:4111d85c00000000 1:4008000000000000 2:80000000005f05dc 4:0000000000000002 5:40a99b0000000000 +series __name__=a,zone=z1 ; 0:4341c37937e08000 1:3fe347ae147ae148 2:c021db851eb851ec 4:80000000000257c7 5:bfc849ba5e353f7d +series __name__=a,zone=z2 ; 1:404e666666666667 2:c0d14c2000000000 3:3ff0000000000000 4:402193d70a3d70a4 5:bfd56e978d4fdf3c +result host=h1,job=j2,zone=z2 ; 0:4080e5bca789e64e 1:3ffbb67ae8584caa 2:fff8000000000000 4:1e66a09e667f3bcd 5:404c9fee1d0b2e27 +result zone=z1 ; 0:4197d78400000000 1:3fe8d6b3b35c8629 2:fff8000000000000 4:fff8000000000000 5:fff8000000000000 +result zone=z2 ; 1:401f3092ece5bc36 2:fff8000000000000 3:3ff0000000000000 4:4007b77715376505 5:fff8000000000000 +end +case math sqrt +expr sqrt(a) +grid -1962160767282 60000 5 +series __name__=a,host=h1,job=j2,zone=z1 ; 0:bfe0000000000000 1:ffefffffffffffff 3:40c368c000000000 4:c01e8ccccccccccd +series __name__=a,host=h2,job=j2,zone=z2 ; 0:c00ca147ae147ae1 1:bff0000000000000 3:c1214fa400000000 4:c023770a3d70a3d7 +result host=h1,job=j2,zone=z1 ; 0:fff8000000000000 1:fff8000000000000 3:4058ebf7f9932c00 4:fff8000000000000 +result host=h2,job=j2,zone=z2 ; 0:fff8000000000000 1:fff8000000000000 3:fff8000000000000 4:fff8000000000000 +end +case math sqrt +expr sqrt(a) +grid 1348888560782 60000 5 +series __name__=a,host=h1 ; 0:c0f78ce800000000 1:7dd845edd8013610 2:4008000000000000 3:40bffe0000000000 4:40b38bc000000000 +series __name__=a,host=h2,job=j2,zone=z2 ; 0:8000000000000015 2:4008000000000000 3:c0bb990000000000 4:800000000000304d +series __name__=a,job=j2,zone=z1 ; 0:f8d7b7fd453652b6 1:fc0c5487d15de900 2:412195f400000000 3:3fe0000000000000 4:c010000000000000 +result host=h1 ; 0:fff8000000000000 1:5ee3b505cc2390b1 2:3ffbb67ae8584caa 3:40569fe95eb7dd64 4:4051af2e1d829cb0 +result host=h2,job=j2,zone=z2 ; 0:fff8000000000000 2:3ffbb67ae8584caa 3:fff8000000000000 4:fff8000000000000 +result job=j2,zone=z1 ; 0:fff8000000000000 1:fff8000000000000 2:4087b8e3fd11a620 3:3fe6a09e667f3bcd 4:fff8000000000000 +end +case math sqrt +expr sqrt(a) +grid 471619062567 1 4 +series __name__=a,host=h1,job=j2,zone=z2 ; 0:fb5cce50d24c3e94 1:412a265600000000 2:40520a6666666667 3:402e6ccccccccccd +series __name__=a,zone=z1 ; 0:4008000000000000 1:0000000000000000 3:fa74d01a2a81ef9c +result host=h1,job=j2,zone=z2 ; 0:fff8000000000000 1:408ced67475c5d9d 2:4020fd5d290afe95 3:400f33db3bef5d6f +result zone=z1 ; 0:3ffbb67ae8584caa 1:0000000000000000 3:fff8000000000000 +end +case math round 3ff0000000000000 +expr round(a) +grid -3022986188931 1000 4 +series __name__=a,host=h1,job=j1,zone=z1 ; 1:c10d0d8000000000 +series __name__=a,host=h2,job=j2,zone=z2 ; 0:c020000000000000 1:c0b23e0000000000 3:c0847c0000000000 +series __name__=a,job=j1 ; 0:c12c176800000000 1:7cc952a9e608b874 3:7df02c2e3b40b0c0 +result host=h1,job=j1,zone=z1 ; 1:c10d0d8000000000 +result host=h2,job=j2,zone=z2 ; 0:c020000000000000 1:c0b23e0000000000 3:c084780000000000 +result job=j1 ; 0:c12c176800000000 1:7cc952a9e608b874 3:7df02c2e3b40b0c0 +end +case math round 3ff0000000000000 +expr round(a) +grid 2091185698579 15000 4 +series __name__=a,host=h1,job=j1,zone=z2 ; 0:c014dc28f5c28f5c 2:0000000004bc4ca6 3:c022666666666667 +series __name__=a,job=j1,zone=z1 ; 0:8000001d70ebc600 1:c11391c400000000 2:405291999999999a +result host=h1,job=j1,zone=z2 ; 0:c014000000000000 2:0000000000000000 3:c022000000000000 +result job=j1,zone=z1 ; 0:0000000000000000 1:c11391c400000000 2:4052800000000000 +end +case math round 3ff0000000000000 +expr round(a) +grid 1147230677471 15000 5 +series __name__=a,host=h1,job=j1 ; 0:bfe25e353f7ced92 1:80000007caacec44 4:bfe0000000000000 +series __name__=a,host=h1,zone=z1 ; 0:4000000000000000 1:4024000000000000 2:c024000000000000 3:fc09bd4e2758d562 4:00000000000fe1be +series __name__=a,host=h2,zone=z2 ; 0:3fec99999999999a 1:c01f000000000000 3:41154d2000000000 4:40863c0000000000 +result host=h1,job=j1 ; 0:bff0000000000000 1:0000000000000000 4:0000000000000000 +result host=h1,zone=z1 ; 0:4000000000000000 1:4024000000000000 2:c024000000000000 3:fc09bd4e2758d562 4:0000000000000000 +result host=h2,zone=z2 ; 0:3ff0000000000000 1:c020000000000000 3:41154d2000000000 4:4086400000000000 +end +case math round 3ff0000000000000 +expr round(a) +grid -2682458523828 1 4 +series __name__=a,host=h1,job=j1 ; 0:bfe0f0a3d70a3d71 1:c056926666666667 2:c008c7ae147ae148 3:bfd1fdf3b645a1cb +series __name__=a,host=h2,job=j1,zone=z1 ; 0:4010000000000000 2:4341c37937e08000 3:4008000000000000 +series __name__=a,host=h2,job=j2,zone=z1 ; 1:40068ccccccccccd 2:3ff0000000000000 3:40af9a0000000000 +result host=h1,job=j1 ; 0:bff0000000000000 1:c056800000000000 2:c008000000000000 3:0000000000000000 +result host=h2,job=j1,zone=z1 ; 0:4010000000000000 2:4341c37937e08000 3:4008000000000000 +result host=h2,job=j2,zone=z1 ; 1:4008000000000000 2:3ff0000000000000 3:40af9a0000000000 +end +case math round 3ff0000000000000 +expr round(a, 1) +grid -2013284349647 15000 5 +series __name__=a,host=h1,zone=z2 ; 0:800000000000ab55 3:800000118741c447 4:c0e0cd9000000000 +series __name__=a,zone=z1 ; 0:40f22d2000000000 1:c0555ccccccccccd 3:c122287000000000 4:c072480000000000 +result host=h1,zone=z2 ; 0:0000000000000000 3:0000000000000000 4:c0e0cd8000000000 +result zone=z1 ; 0:40f22d2000000000 1:c055400000000000 3:c122287000000000 4:c072400000000000 +end +case math round 3ff0000000000000 +expr round(a, 1) +grid -1934277777347 60000 9 +series __name__=a,job=j2,zone=z1 ; 0:79498be2a0084080 1:40ea932000000000 3:4115aae000000000 5:ffe1ccf385ebc8a0 6:40b77dc000000000 7:40fae2d000000000 8:0000000000000000 +result job=j2,zone=z1 ; 0:79498be2a0084080 1:40ea932000000000 3:4115aae000000000 5:ffe1ccf385ebc8a0 6:40b77e0000000000 7:40fae2d000000000 8:0000000000000000 +end +case math round 3fe0000000000000 +expr round(a, 0.5) +grid -1733790336954 15000 7 +series __name__=a,host=h1,zone=z2 ; 1:4008000000000000 2:c0b8948000000000 3:4123524a00000000 4:40152f5c28f5c28f 6:fea794b1b4e30050 +result host=h1,zone=z2 ; 1:4008000000000000 2:c0b8948000000000 3:4123524a00000000 4:4016000000000000 6:fea794b1b4e30050 +end +case math round 3fe0000000000000 +expr round(a, 0.5) +grid 1861395254097 1 10 +series __name__=a,host=h1,job=j1,zone=z2 ; 1:c341c37937e08000 2:411b8ac800000000 4:8000007c3d9a1780 5:80000001c0d0aa17 7:0000000000066342 8:4014000000000000 9:405249999999999a +series __name__=a,host=h2,job=j2,zone=z1 ; 2:c010000000000000 5:40ddd26000000000 6:405cf00000000000 7:40b246c000000000 8:78f187976c2eea08 9:c0f4677000000000 +result host=h1,job=j1,zone=z2 ; 1:c341c37937e08000 2:411b8ac800000000 4:0000000000000000 5:0000000000000000 7:0000000000000000 8:4014000000000000 9:4052400000000000 +result host=h2,job=j2,zone=z1 ; 2:c010000000000000 5:40ddd26000000000 6:405d000000000000 7:40b2470000000000 8:78f187976c2eea08 9:c0f4677000000000 +end +case math round 3fb999999999999a +expr round(a, 0.1) +grid 892799548686 15000 4 +series __name__=a,host=h2,job=j1 ; 0:3fd77ced916872b0 1:c0f571d800000000 2:000fffffffffffff +series __name__=a,host=h2,zone=z2 ; 1:7d2967c5469db91e 3:7fe1ccf385ebc8a0 +series __name__=a,job=j1,zone=z2 ; 0:7fe1ccf385ebc8a0 1:3ff0000000000000 2:40f0e69000000000 3:c089a40000000000 +result host=h2,job=j1 ; 0:3fd999999999999a 1:c0f571d800000000 2:0000000000000000 +result host=h2,zone=z2 ; 1:7d2967c5469db91e 3:7ff0000000000000 +result job=j1,zone=z2 ; 0:7ff0000000000000 1:3ff0000000000000 2:40f0e69000000000 3:c089a40000000000 +end +case math round 3fb999999999999a +expr round(a, 0.1) +grid 1935929279437 1 6 +series __name__=a,host=h2,job=j1 ; 0:4121076000000000 1:40eff54000000000 2:4081b30000000000 4:40e0059000000000 +series __name__=a,host=h2,zone=z2 ; 0:4008000000000000 2:c08e3d0000000000 3:80000000000761f1 4:3fe1428f5c28f5c3 +series __name__=a,job=j2,zone=z2 ; 1:4086120000000000 3:4120f0ea00000000 5:8000000000000000 +result host=h2,job=j1 ; 0:4121076000000000 1:40eff54000000000 2:4081b33333333333 4:40e0059000000000 +result host=h2,zone=z2 ; 0:4008000000000000 2:c08e3ccccccccccd 3:0000000000000000 4:3fe0000000000000 +result job=j2,zone=z2 ; 1:4086126666666666 3:4120f0ea00000000 5:0000000000000000 +end +case math round 4008000000000000 +expr round(a, 3) +grid -1472300150492 1 5 +series __name__=a,host=h2,job=j2,zone=z2 ; 1:bfe0000000000000 2:8000000000000074 3:40c1d8c000000000 4:4341c37937e08000 +result host=h2,job=j2,zone=z2 ; 1:0000000000000000 2:0000000000000000 3:40c1d90000000000 4:4341c37937e08000 +end +case math round 4008000000000000 +expr round(a, 3) +grid -553570169856 60000 3 +series __name__=a,host=h1,zone=z2 ; 1:f90da661f15293b8 2:ffe1ccf385ebc8a0 +series __name__=a,zone=z1 ; 0:40236ccccccccccd 1:4095590000000000 2:4008828f5c28f5c3 +result host=h1,zone=z2 ; 1:f90da661f15293b7 2:ffe1ccf385ebc8a0 +result zone=z1 ; 0:4022000000000000 1:4095540000000000 2:4008000000000000 +end +case math round c000000000000000 +expr round(a, (-2)) +grid 793533970766 60000 4 +series __name__=a,host=h1,job=j2,zone=z1 ; 1:c0763c0000000000 2:40e8399000000000 3:4125f51800000000 +series __name__=a,host=h1,job=j2,zone=z2 ; 1:3ff0000000000000 2:bfebd604189374bd 3:bfee570a3d70a3d7 +series __name__=a,job=j2,zone=z1 ; 0:4004fd70a3d70a3e 1:4000000000000000 2:c341c37937e08000 +result host=h1,job=j2,zone=z1 ; 1:c076400000000000 2:40e8398000000000 3:4125f51800000000 +result host=h1,job=j2,zone=z2 ; 1:8000000000000000 2:8000000000000000 3:8000000000000000 +result job=j2,zone=z1 ; 0:4000000000000000 1:4000000000000000 2:c341c37937e08000 +end +case math round c000000000000000 +expr round(a, (-2)) +grid 1566718578387 15000 5 +series __name__=a,host=h2,zone=z2 ; 0:3fe6851eb851eb85 1:c0f3948000000000 2:4056eccccccccccd 3:4020000000000000 4:c089650000000000 +series __name__=a,job=j2,zone=z2 ; 0:c0f0c5c000000000 1:ffefffffffffffff 2:c018e66666666667 3:c0ef4fa000000000 4:3fe0000000000000 +series __name__=a,zone=z2 ; 0:c07f820000000000 2:8000000000000089 3:3fe91fbe76c8b439 4:3fe0000000000000 +result host=h2,zone=z2 ; 0:8000000000000000 1:c0f3948000000000 2:4057000000000000 3:4020000000000000 4:c089600000000000 +result job=j2,zone=z2 ; 0:c0f0c5c000000000 1:ffefffffffffffff 2:c018000000000000 3:c0ef4fc000000000 4:8000000000000000 +result zone=z2 ; 0:c07f800000000000 2:8000000000000000 3:8000000000000000 4:8000000000000000 +end +case math round 0000000000000000 +expr round(a, 0) +grid -1026082157982 60000 7 +series __name__=a,host=h1,zone=z2 ; 0:7ff0000000000000 1:c341c37937e08000 2:c018000000000000 3:7ff8000000000001 4:c024000000000000 5:4056533333333334 6:7ff8000000000001 +series __name__=a,job=j2,zone=z2 ; 0:40abb48000000000 4:3ff0000000000000 5:4052580000000000 6:4082180000000000 +series __name__=a,zone=z1 ; 0:8000000000000001 2:c024000000000000 4:c0f2694800000000 5:3fe0000000000000 6:bfe0000000000000 +result host=h1,zone=z2 ; 0:fff8000000000000 1:fff8000000000000 2:fff8000000000000 3:7ff8000000000001 4:fff8000000000000 5:fff8000000000000 6:7ff8000000000001 +result job=j2,zone=z2 ; 0:fff8000000000000 4:fff8000000000000 5:fff8000000000000 6:fff8000000000000 +result zone=z1 ; 0:fff8000000000000 2:fff8000000000000 4:fff8000000000000 5:fff8000000000000 6:fff8000000000000 +end +case math round 0000000000000000 +expr round(a, 0) +grid -2669275454038 60000 5 +series __name__=a,host=h1,job=j2 ; 0:407aae0000000000 1:4022e1eb851eb852 2:ffe1ccf385ebc8a0 3:7fe1ccf385ebc8a0 4:c0be4c4000000000 +series __name__=a,host=h2,job=j2,zone=z2 ; 0:c0f03ab000000000 1:c00a51eb851eb852 2:bfe8353f7ced9169 3:c101ce9800000000 4:4023e28f5c28f5c3 +result host=h1,job=j2 ; 0:fff8000000000000 1:fff8000000000000 2:fff8000000000000 3:fff8000000000000 4:fff8000000000000 +result host=h2,job=j2,zone=z2 ; 0:fff8000000000000 1:fff8000000000000 2:fff8000000000000 3:fff8000000000000 4:fff8000000000000 +end +case math round 7ff8000000000001 +expr round(a, NaN) +grid -119798354174 1000 5 +series __name__=a,job=j2 ; 0:40aff18000000000 1:c020000000000000 2:40f39ac000000000 4:4341c37937e08000 +result job=j2 ; 0:7ff8000000000001 1:7ff8000000000001 2:7ff8000000000001 4:7ff8000000000001 +end +case math round 7ff8000000000001 +expr round(a, NaN) +grid -2544211762022 60000 5 +series __name__=a,host=h1,job=j2 ; 1:80000000000000d7 2:bfe2a1cac083126f 3:c039b33333333334 +series __name__=a,host=h2,zone=z1 ; 0:4008000000000000 2:ffe1ccf385ebc8a0 3:4064b80000000000 +series __name__=a,host=h2,zone=z2 ; 0:c02189999999999a 2:bfd34189374bc6a8 3:fc2814243a77b35e +result host=h1,job=j2 ; 1:7ff8000000000001 2:7ff8000000000001 3:7ff8000000000001 +result host=h2,zone=z1 ; 0:7ff8000000000001 2:7ff8000000000001 3:7ff8000000000001 +result host=h2,zone=z2 ; 0:7ff8000000000001 2:7ff8000000000001 3:7ff8000000000001 +end +case math round 7ff0000000000000 +expr round(a, Inf) +grid 877674442098 1 10 +series __name__=a ; 2:c0b4438000000000 3:c117450800000000 5:bfc4f5c28f5c28f6 8:401671eb851eb852 +series __name__=a,host=h1,job=j1,zone=z1 ; 1:0000000000000000 3:7fefffffffffffff 5:bfed916872b020c5 6:4000000000000000 8:c12ab1fc00000000 9:f8483959c1011e02 +result - ; 2:fff8000000000000 3:fff8000000000000 5:fff8000000000000 8:fff8000000000000 +result host=h1,job=j1,zone=z1 ; 1:fff8000000000000 3:fff8000000000000 5:fff8000000000000 6:fff8000000000000 8:fff8000000000000 9:fff8000000000000 +end +case math round 7ff0000000000000 +expr round(a, Inf) +grid 2715442887313 15000 8 +series __name__=a,host=h1 ; 0:c341c37937e08000 1:8000000352c83617 2:c01bc7ae147ae148 3:4083920000000000 4:401e6147ae147ae1 5:401c000000000000 6:7ff0000000000000 7:c0d01d0000000000 +series __name__=a,host=h2,job=j1,zone=z2 ; 0:f85d39a07811acb8 1:4039a66666666667 2:8029d1b066bfc15c 3:405264cccccccccd 4:41154b2c00000000 5:c0f4363800000000 6:7fefffffffffffff +series __name__=a,job=j1,zone=z2 ; 0:0000000000000001 1:410a13c800000000 4:fb2862529867c73a 7:c020000000000000 +result host=h1 ; 0:fff8000000000000 1:fff8000000000000 2:fff8000000000000 3:fff8000000000000 4:fff8000000000000 5:fff8000000000000 6:fff8000000000000 7:fff8000000000000 +result host=h2,job=j1,zone=z2 ; 0:fff8000000000000 1:fff8000000000000 2:fff8000000000000 3:fff8000000000000 4:fff8000000000000 5:fff8000000000000 6:fff8000000000000 +result job=j1,zone=z2 ; 0:fff8000000000000 1:fff8000000000000 4:fff8000000000000 7:fff8000000000000 +end +case math round 01a56e1fc2f8f359 +expr round(a, 1e-300) +grid -3966454058935 15000 4 +series __name__=a,host=h1,zone=z2 ; 1:7fe1ccf385ebc8a0 2:c0c34b6000000000 3:c10a1f8000000000 +series __name__=a,host=h2 ; 1:c0408ccccccccccd 2:4014000000000000 3:4020000000000000 +result host=h1,zone=z2 ; 1:7ff0000000000000 2:c0c34b6000000000 3:c10a1f8000000000 +result host=h2 ; 1:c0408ccccccccccd 2:4014000000000000 3:4020000000000000 +end +case math round 01a56e1fc2f8f359 +expr round(a, 1e-300) +grid -716571138391 15000 7 +series __name__=a,job=j2 ; 0:40534d999999999a 1:7fe1ccf385ebc8a0 2:c0774c0000000000 3:c008000000000000 4:3fe8ced916872b02 5:c020b70a3d70a3d7 6:7fe1ccf385ebc8a0 +result job=j2 ; 0:40534d999999999a 1:7ff0000000000000 2:c0774c0000000000 3:c008000000000000 4:3fe8ced916872b03 5:c020b70a3d70a3d7 6:7ff0000000000000 +end +case math round 3fd0000000000000 +expr round(a, 0.25) +grid 571617009559 1 10 +series __name__=a,host=h1,job=j1,zone=z1 ; 0:40e753e000000000 1:ffefffffffffffff 2:408a8a0000000000 4:8000000000ca7e01 5:7e4c0821a7721ad8 6:4022000000000000 7:c0f255c000000000 8:c080840000000000 9:c041cccccccccccd +series __name__=a,job=j2,zone=z1 ; 0:bfee4fdf3b645a1d 2:00190396fed4d716 5:3ff4147ae147ae15 6:c0d10a8000000000 8:0000000000f835bd 9:c0737e0000000000 +series __name__=a,zone=z1 ; 0:4008000000000000 1:bfe16a7ef9db22d1 2:7c981b9fc7dc7ca0 3:00000000000024d9 4:0000000000000003 5:0000000000000008 6:bff0000000000000 7:4088e30000000000 8:40c1aa8000000000 9:4052aa6666666667 +result host=h1,job=j1,zone=z1 ; 0:40e753e000000000 1:fff0000000000000 2:408a8a0000000000 4:0000000000000000 5:7e4c0821a7721ad8 6:4022000000000000 7:c0f255c000000000 8:c080840000000000 9:c041c00000000000 +result job=j2,zone=z1 ; 0:bff0000000000000 2:0000000000000000 5:3ff4000000000000 6:c0d10a8000000000 8:0000000000000000 9:c0737c0000000000 +result zone=z1 ; 0:4008000000000000 1:bfe0000000000000 2:7c981b9fc7dc7ca0 3:0000000000000000 4:0000000000000000 5:0000000000000000 6:bff0000000000000 7:4088e40000000000 8:40c1aa8000000000 9:4052b00000000000 +end +case math round 3fd0000000000000 +expr round(a, 0.25) +grid 24248430186 1 5 +series __name__=a,host=h1,zone=z1 ; 0:c010000000000000 1:4082150000000000 2:7b27f6e2e1474872 3:c11bb1d800000000 4:800000068c10face +series __name__=a,job=j1,zone=z2 ; 0:40adec0000000000 1:7e12ad13ecfb769e 3:40163851eb851eb8 4:f9d77d15cf193c48 +result host=h1,zone=z1 ; 0:c010000000000000 1:4082160000000000 2:7b27f6e2e1474872 3:c11bb1d800000000 4:0000000000000000 +result job=j1,zone=z2 ; 0:40adec0000000000 1:7e12ad13ecfb769e 3:4016000000000000 4:f9d77d15cf193c48 +end diff --git a/src/promql/src/map/tests.rs b/src/promql/src/map/tests.rs new file mode 100644 index 00000000000..a7746a0a283 --- /dev/null +++ b/src/promql/src/map/tests.rs @@ -0,0 +1,504 @@ +// Copyright 2023 Greptime Team +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::sync::Arc; + +use common_query::prometheus::PROMETHEUS_STALE_NAN_BITS; +use datafusion::functions::math; +use datafusion_common::config::ConfigOptions; +use datafusion_expr::{ColumnarValue, ScalarFunctionArgs}; +use datatypes::arrow::array::{AsArray, UInt32Array}; +use datatypes::arrow::buffer::NullBuffer; +use datatypes::arrow::datatypes::{DataType, Field, Float64Type}; +use datatypes::series::histogram::{ + BucketSideView, CounterResetHint, HistogramBatch, HistogramBatchBuilder, HistogramView, +}; +use datatypes::series::samples::{FieldSpec, HistogramSamples, SampleRef}; +use datatypes::series::time::{EvalGrid, GridRange, GridSpec}; +use datatypes::series::{Label, MetricName}; + +use super::*; +use crate::functions::IeeeSqrt; + +mod prometheus; + +fn schema(names: &[&str]) -> SeriesSchema { + SeriesSchema { + fields: names + .iter() + .map(|name| FieldSpec { + name: (*name).into(), + }) + .collect(), + eval_grid: Some(Arc::new( + EvalGrid::try_new(GridSpec::try_new(1000, 500, 64).unwrap()).unwrap(), + )), + } +} + +fn series(schema: &SeriesSchema, start: usize, fields: Vec) -> Series { + let time = TimeAxis::Grid( + GridRange::try_new(schema.eval_grid.clone().unwrap(), start, fields[0].len()).unwrap(), + ); + let metric = MetricName::try_new( + Some("requests".into()), + vec![ + Label { + name: "host".into(), + value: "a".into(), + }, + Label { + name: "empty".into(), + value: "".into(), + }, + ], + ) + .unwrap(); + Series::try_new( + schema, + metric, + time, + SeriesFields::try_from_vec(fields).unwrap(), + ) + .unwrap() +} + +fn floats(samples: &[Option]) -> Samples { + Samples::from_float(Float64Array::from(samples.to_vec())) +} + +fn assert_floats(actual: &Samples, expected: &[Option]) { + assert_eq!(actual.len(), expected.len()); + for (index, (actual, expected)) in actual.iter().zip(expected).enumerate() { + match (actual, expected) { + (SampleRef::Missing, None) => {} + (SampleRef::Float(actual), Some(expected)) if expected.is_nan() => { + assert!(actual.is_nan(), "sample {index}: {actual} is not NaN"); + } + (SampleRef::Float(actual), Some(expected)) => { + assert_eq!(actual.to_bits(), expected.to_bits(), "sample {index}"); + } + _ => panic!("unexpected sample kind at {index}"), + } + } +} + +#[test] +fn math_preserves_missing_and_uses_ieee_float_semantics() { + // Boundaries: negative values, signed zero, infinities, ordinary NaN and Missing. + let schema = schema(&["value"]); + let input = series( + &schema, + 3, + vec![floats(&[ + Some(-2.5), + Some(-0.5), + Some(-0.0), + Some(0.0), + Some(4.0), + Some(f64::INFINITY), + Some(f64::NEG_INFINITY), + Some(f64::NAN), + None, + ])], + ); + let cases = [ + ( + MathOp::Abs, + [ + 2.5, + 0.5, + 0.0, + 0.0, + 4.0, + f64::INFINITY, + f64::INFINITY, + f64::NAN, + ], + ), + ( + MathOp::Ceil, + [ + -2.0, + -0.0, + -0.0, + 0.0, + 4.0, + f64::INFINITY, + f64::NEG_INFINITY, + f64::NAN, + ], + ), + ( + MathOp::Floor, + [ + -3.0, + -1.0, + -0.0, + 0.0, + 4.0, + f64::INFINITY, + f64::NEG_INFINITY, + f64::NAN, + ], + ), + ( + MathOp::Sqrt, + [ + f64::NAN, + f64::NAN, + -0.0, + 0.0, + 2.0, + f64::INFINITY, + f64::NAN, + f64::NAN, + ], + ), + ( + MathOp::Round { nearest: 1.0 }, + [ + -2.0, + 0.0, + 0.0, + 0.0, + 4.0, + f64::INFINITY, + f64::NEG_INFINITY, + f64::NAN, + ], + ), + ]; + for (op, expected) in cases { + let output = map_math(&schema, &input, op).unwrap(); + let expected = expected + .into_iter() + .map(Some) + .chain([None]) + .collect::>(); + assert_floats(output.fields.get(0).unwrap(), &expected); + } +} + +#[test] +fn short_multifield_slices_keep_absolute_grid_and_labels() { + let schema = schema(&["left", "right"]); + let first = floats(&[Some(99.0), Some(-1.2), None, Some(-3.2), Some(99.0)]); + let second = floats(&[Some(99.0), Some(4.8), Some(-0.0), None, Some(99.0)]); + let fields = vec![ + Samples::try_from_chunks(vec![first.slice(1, 2).unwrap(), first.slice(3, 1).unwrap()]) + .unwrap(), + Samples::try_from_chunks(vec![ + second.slice(1, 1).unwrap(), + second.slice(2, 2).unwrap(), + ]) + .unwrap(), + ]; + let input = series(&schema, 7, fields); + let output = map_math(&schema, &input, MathOp::Floor).unwrap(); + let TimeAxis::Grid(range) = &output.time else { + panic!("lost grid axis") + }; + assert_eq!((range.start(), range.len()), (7, 3)); + assert!(Arc::ptr_eq( + range.grid(), + schema.eval_grid.as_ref().unwrap() + )); + assert_eq!(output.time.timestamp(0), Some(4500)); + assert_eq!(output.time.timestamp(2), Some(5500)); + // Prometheus drops the name; main keeps it because its HTTP adapter follows Call arguments. + assert_eq!(output.metric.name(), None); + assert_eq!(output.metric.tags(), &input.metric.tags()[1..]); + assert_eq!(input.metric.name(), Some("requests")); + assert_floats( + input.fields.get(0).unwrap(), + &[Some(-1.2), None, Some(-3.2)], + ); + for (field, lengths) in output.fields.iter().zip([[2, 1], [1, 2]]) { + assert_eq!( + field + .segments() + .iter() + .map(Samples::len) + .collect::>(), + lengths + ); + } + drop(input); + drop(schema); + drop(first); + drop(second); + assert_floats( + output.fields.get(0).unwrap(), + &[Some(-2.0), None, Some(-4.0)], + ); + assert_floats( + output.fields.get(1).unwrap(), + &[Some(4.0), Some(-0.0), None], + ); +} + +#[test] +fn empty_and_all_missing_ranges_are_not_expanded() { + let schema = schema(&["value"]); + // Empty at grid end, one missing point, and a longer all-missing range. + for (start, len) in [(64, 0), (9, 1), (2, 4)] { + let input = series(&schema, start, vec![floats(&vec![None; len])]); + for op in [ + MathOp::Abs, + MathOp::Ceil, + MathOp::Floor, + MathOp::Sqrt, + MathOp::Round { nearest: 0.0 }, + ] { + let output = map_math(&schema, &input, op).unwrap(); + let TimeAxis::Grid(range) = &output.time else { + panic!("lost grid axis") + }; + assert_eq!((range.start(), range.len()), (start, len)); + assert_floats(output.fields.get(0).unwrap(), &vec![None; len]); + } + } +} + +#[test] +fn evaluated_missingness_depends_on_validity_not_nan_payloads() { + let schema = schema(&["value"]); + let stale = f64::from_bits(PROMETHEUS_STALE_NAN_BITS); + let ordinary_nan = f64::from_bits(0x7ff8_0000_0000_0042); + let input = series( + &schema, + 1, + vec![Samples::from_float(Float64Array::new( + vec![stale, ordinary_nan, -1.0].into(), + Some(NullBuffer::from(vec![false, true, true])), + ))], + ); + let output = map_math(&schema, &input, MathOp::Abs).unwrap(); + assert_floats( + output.fields.get(0).unwrap(), + &[None, Some(ordinary_nan), Some(1.0)], + ); + let SampleRef::Float(actual_nan) = output.fields.get(0).unwrap().sample(1) else { + panic!("lost NaN") + }; + assert_eq!(actual_nan.to_bits(), ordinary_nan.to_bits()); + + // Removing the sign of this ordinary NaN produces the reserved raw stale bits. A later Map + // must not reinterpret that computed value as a missing sample or reject the evaluated input. + let negative_nan = f64::from_bits(PROMETHEUS_STALE_NAN_BITS | (1_u64 << 63)); + let input = series(&schema, 1, vec![floats(&[Some(negative_nan)])]); + let absolute = map_math(&schema, &input, MathOp::Abs).unwrap(); + let SampleRef::Float(computed_nan) = absolute.fields.get(0).unwrap().sample(0) else { + panic!("lost computed NaN") + }; + assert_eq!(computed_nan.to_bits(), PROMETHEUS_STALE_NAN_BITS); + let rounded = map_math(&schema, &absolute, MathOp::Ceil).unwrap(); + assert_floats(rounded.fields.get(0).unwrap(), &[Some(f64::NAN)]); +} + +fn histogram_batch() -> Arc { + let empty_side = BucketSideView { + span_offsets: &[], + span_lengths: &[], + buckets: &[], + }; + let mut builder = HistogramBatchBuilder::new(); + builder + .push(HistogramView { + schema: 1, + zero_threshold: 0.01, + sum: 0.0, + count: 1.0, + zero_count: 1.0, + reset_hint: CounterResetHint::Unknown, + start_timestamp: None, + custom_values: &[], + positive: empty_side, + negative: empty_side, + }) + .unwrap(); + builder.finish().unwrap() +} + +#[test] +fn histogram_and_mixed_fields_are_rejected_without_partial_success() { + let batch = histogram_batch(); + let histogram = Samples::try_from_histogram(UInt32Array::from(vec![0]), batch.clone()).unwrap(); + let mixed = Samples::try_from_mixed( + Float64Array::from(vec![Some(1.0), None]), + HistogramSamples::try_new(UInt32Array::from(vec![None, Some(0)]), batch.clone()).unwrap(), + ) + .unwrap(); + let null_histogram = Samples::try_from_histogram(UInt32Array::from(vec![None]), batch).unwrap(); + let chunks = Samples::try_from_chunks(vec![floats(&[Some(-1.0)]), histogram.clone()]).unwrap(); + let schema = schema(&["float", "unsupported"]); + for unsupported in [histogram, mixed, null_histogram, chunks] { + let prefix = floats(&vec![Some(-2.0); unsupported.len()]); + let input = series(&schema, 2, vec![prefix, unsupported]); + assert!(matches!( + map_math(&schema, &input, MathOp::Abs), + Err(DataFusionError::NotImplemented(_)) + )); + assert_floats( + input.fields.get(0).unwrap(), + &vec![Some(-2.0); input.time.len()], + ); + } +} + +#[test] +fn raw_time_axes_are_not_treated_as_evaluated_grids() { + let mut schema = schema(&["value"]); + let input = series(&schema, 2, vec![floats(&[Some(-1.0), Some(2.0)])]); + schema.eval_grid = None; + for time in [ + TimeAxis::try_new(vec![2000, 2500].into()).unwrap(), + TimeAxis::try_from_chunks(vec![vec![2000].into(), vec![2500].into()]).unwrap(), + ] { + let raw = + Series::try_new(&schema, input.metric.clone(), time, input.fields.clone()).unwrap(); + assert!( + matches!(map_math(&schema, &raw, MathOp::Abs), Err(DataFusionError::Execution(reason)) if reason.contains("grid")) + ); + } +} + +#[test] +fn mismatched_schema_and_mutated_field_lengths_return_typed_errors() { + let schema = schema(&["value"]); + let mut input = series(&schema, 2, vec![floats(&[Some(-1.0), Some(2.0)])]); + let mut other_grid = schema.clone(); + other_grid.eval_grid = Some(Arc::new( + EvalGrid::try_new(GridSpec::try_new(0, 500, 64).unwrap()).unwrap(), + )); + assert!( + matches!(map_math(&other_grid, &input, MathOp::Abs), Err(DataFusionError::External(error)) if error.is::()) + ); + input.fields = SeriesFields::one(floats(&[Some(-1.0)])); + assert!( + matches!(map_math(&schema, &input, MathOp::Abs), Err(DataFusionError::External(error)) if error.is::()) + ); +} + +fn ordinary_math(op: MathOp, input: Float64Array) -> Float64Array { + // Calibration against main's UDFs, whose values agree with Prometheus for these functions; + // the Prometheus engine's own results are checked in `prometheus`. + let number_rows = input.len(); + let args = vec![ColumnarValue::Array(Arc::new(input))]; + let udf = match op { + MathOp::Abs => math::abs(), + MathOp::Ceil => math::ceil(), + MathOp::Floor => math::floor(), + MathOp::Sqrt => Arc::new(IeeeSqrt::scalar_udf()), + MathOp::Round { .. } => unreachable!("round is checked against Prometheus fixtures"), + }; + let arg_fields = args + .iter() + .map(|arg| Arc::new(Field::new("value", arg.data_type(), true))) + .collect(); + let output = udf + .invoke_with_args(ScalarFunctionArgs { + args, + arg_fields, + number_rows, + return_field: Arc::new(Field::new("output", DataType::Float64, true)), + config_options: Arc::new(ConfigOptions::default()), + }) + .unwrap(); + let ColumnarValue::Array(output) = output else { + panic!("expected array") + }; + output.as_primitive::().clone() +} + +fn float_boundary_samples() -> Float64Array { + let mut samples = vec![ + Some(-2.5), + Some((-0.5_f64).next_down()), + Some(-0.5), + Some((-0.5_f64).next_up()), + Some(0.5_f64.next_down()), + Some(0.5), + Some(0.5_f64.next_up()), + Some(2.5), + Some(123.456), + ]; + // Both signs, every exponent, and mantissa boundaries cover zeros, subnormals, normal + // magnitudes, infinities, and signaling/quiet NaNs without a random seed or new dependency. + for exponent in 0..=0x7ff_u64 { + for mantissa in [0, 1, 1 << 51, (1 << 52) - 1] { + for sign in [0, 1_u64 << 63] { + samples.push(Some(f64::from_bits(sign | (exponent << 52) | mantissa))); + } + } + samples.push(None); + } + Float64Array::from(samples) +} + +#[test] +fn math_matches_main_udfs_across_float_boundaries() { + let input = float_boundary_samples(); + let mut schema = schema(&["dense", "chunked"]); + let grid_len = u64::try_from(input.len() + 6).unwrap(); + schema.eval_grid = Some(Arc::new( + EvalGrid::try_new(GridSpec::try_new(1000, 500, grid_len).unwrap()).unwrap(), + )); + let chunks = (0..input.len()) + .step_by(61) + .map(|offset| Samples::from_float(input.slice(offset, 61.min(input.len() - offset)))) + .collect(); + let series = series( + &schema, + 5, + vec![ + Samples::from_float(input.clone()), + Samples::try_from_chunks(chunks).unwrap(), + ], + ); + // Round shares its calculation with the UDF; the Prometheus fixtures check it instead. + let cases = [MathOp::Abs, MathOp::Ceil, MathOp::Floor, MathOp::Sqrt]; + for op in cases { + let output = map_math(&schema, &series, op).unwrap(); + let expected = ordinary_math(op, input.clone()).iter().collect::>(); + for field in output.fields.iter() { + assert_floats(field, &expected); + } + assert_eq!(output.metric.name(), None); + assert_eq!(output.metric.tags(), &series.metric.tags()[1..]); + let TimeAxis::Grid(range) = output.time else { + panic!("lost grid axis") + }; + assert_eq!((range.start(), range.len()), (5, input.len())); + } +} + +#[test] +fn math_drops_reserved_metadata_and_empty_labels_with_the_name() { + let schema = schema(&["value"]); + let mut input = series(&schema, 0, vec![floats(&[Some(-1.0)])]); + for label in ["__type__", "__unit__", "__other__"] { + input.metric.set(label, "x".into()).unwrap(); + } + let output = map_math(&schema, &input, MathOp::Abs).unwrap(); + let names = output + .metric + .tags() + .iter() + .map(|label| label.name.as_ref()) + .collect::>(); + assert_eq!(output.metric.name(), None); + assert_eq!(names, ["__other__", "host"]); +} diff --git a/src/promql/src/map/tests/prometheus.rs b/src/promql/src/map/tests/prometheus.rs new file mode 100644 index 00000000000..05aada80070 --- /dev/null +++ b/src/promql/src/map/tests/prometheus.rs @@ -0,0 +1,234 @@ +// Copyright 2023 Greptime Team +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Golden cases evaluated by the Prometheus engine; see the header of `prometheus_cases.txt`. + +use std::sync::Arc; + +use datatypes::arrow::array::Float64Array; +use datatypes::series::samples::{ + FieldSpec, SampleRef, Samples, Series, SeriesFields, SeriesSchema, +}; +use datatypes::series::time::{EvalGrid, GridRange, GridSpec, TimeAxis}; +use datatypes::series::{Label, MetricName}; + +use crate::map::{MathOp, map_math}; + +const CASES: &str = include_str!("../prometheus_cases.txt"); + +type Points = Vec<(usize, f64)>; + +pub(super) struct Case { + pub(super) header: Vec, + pub(super) expr: String, + pub(super) schema: SeriesSchema, + pub(super) series: Vec, + pub(super) scalar: Option, + pub(super) result: Vec<(MetricName, Points)>, +} + +pub(super) fn cases() -> Vec { + let mut cases = Vec::new(); + let mut lines = CASES.lines().filter(|line| !line.starts_with('#')); + while let Some(line) = lines.next() { + let header = line.strip_prefix("case ").expect("case line"); + let expr = lines + .next() + .unwrap() + .strip_prefix("expr ") + .unwrap() + .to_string(); + let grid: Vec = lines.next().unwrap()["grid ".len()..] + .split(' ') + .map(|v| v.parse().unwrap()) + .collect(); + let schema = SeriesSchema { + fields: vec![FieldSpec { + name: "value".into(), + }] + .into(), + eval_grid: Some(Arc::new( + EvalGrid::try_new(GridSpec::try_new(grid[0], grid[1], grid[2] as u64).unwrap()) + .unwrap(), + )), + }; + let mut case = Case { + header: header.split(' ').map(String::from).collect(), + expr, + series: Vec::new(), + scalar: None, + result: Vec::new(), + schema, + }; + for line in lines.by_ref() { + if line == "end" { + break; + } + let (kind, rest) = line.split_once(' ').unwrap(); + let (labels, points) = rest.split_once(" ;").unwrap(); + let (metric, points) = (parse_metric(labels), parse_points(points)); + match kind { + "series" => { + if let Some(series) = grid_series(&case.schema, metric, &points) { + case.series.push(series); + } + } + "scalar" => case.scalar = grid_series(&case.schema, metric, &points), + "result" => case.result.push((metric, points)), + _ => panic!("unknown line {line}"), + } + } + cases.push(case); + } + cases +} + +fn parse_metric(labels: &str) -> MetricName { + let mut name = None; + let mut tags = Vec::new(); + for label in labels + .split(',') + .filter(|label| *label != "-" && !label.is_empty()) + { + let (key, value) = label.split_once('=').unwrap(); + if key == "__name__" { + name = Some(Arc::from(value)); + } else { + tags.push(Label { + name: key.into(), + value: value.into(), + }); + } + } + MetricName::try_new(name, tags).unwrap() +} + +fn parse_points(points: &str) -> Points { + points + .split_whitespace() + .map(|point| { + let (index, bits) = point.split_once(':').unwrap(); + ( + index.parse().unwrap(), + f64::from_bits(u64::from_str_radix(bits, 16).unwrap()), + ) + }) + .collect() +} + +pub(super) fn hex(bits: &str) -> f64 { + f64::from_bits(u64::from_str_radix(bits, 16).unwrap()) +} + +/// Builds the trimmed series Eval produces: from the first to the last point, nulls between. +fn grid_series(schema: &SeriesSchema, metric: MetricName, points: &Points) -> Option { + let (&(start, _), &(last, _)) = points.first().zip(points.last())?; + let mut values = vec![None; last - start + 1]; + for &(index, value) in points { + values[index - start] = Some(value); + } + let range = GridRange::try_new(schema.eval_grid.clone().unwrap(), start, values.len()).unwrap(); + Some( + Series::try_new( + schema, + metric, + TimeAxis::Grid(range), + SeriesFields::one(Samples::from_float(Float64Array::from(values))), + ) + .unwrap(), + ) +} + +/// Compares labels, the trimmed absolute range and value bits; any NaN matches any NaN. +pub(super) fn assert_result(case: &Case, mut actual: Vec) { + actual.sort_by_key(|series| label_key(&series.metric)); + let mut expected = case.result.iter().collect::>(); + expected.sort_by_key(|(metric, _)| label_key(metric)); + let context = || format!("{} ({})", case.expr, case.header.join(" ")); + assert_eq!(actual.len(), expected.len(), "{}", context()); + for (actual, (metric, points)) in actual.iter().zip(expected) { + assert_eq!(&actual.metric, metric, "{}", context()); + let TimeAxis::Grid(range) = &actual.time else { + panic!("{}: lost grid axis", context()); + }; + let mut present = Vec::new(); + for field in actual.fields.iter() { + for (offset, sample) in field.iter().enumerate() { + match sample { + SampleRef::Float(value) => present.push((range.start() + offset, value)), + SampleRef::Missing => {} + _ => panic!("{}: non-float output", context()), + } + } + } + assert_eq!( + (range.start(), range.start() + range.len()), + (points[0].0, points.last().unwrap().0 + 1), + "{}: untrimmed range", + context() + ); + assert_eq!(present.len(), points.len(), "{}", context()); + for (&(index, actual), &(expected_index, expected)) in present.iter().zip(points) { + assert_eq!(index, expected_index, "{}", context()); + assert!( + actual.to_bits() == expected.to_bits() || (actual.is_nan() && expected.is_nan()), + "{} at {index}: {actual:e} ({:016x}) != {expected:e} ({:016x})", + context(), + actual.to_bits(), + expected.to_bits(), + ); + } + } +} + +fn label_key(metric: &MetricName) -> Vec<(String, String)> { + let mut key = metric + .tags() + .iter() + .map(|label| (label.name.to_string(), label.value.to_string())) + .collect::>(); + if let Some(name) = metric.name() { + key.insert(0, ("__name__".into(), name.into())); + } + key +} + +fn math_op(header: &[String]) -> Option { + Some(match header[1].as_str() { + "abs" => MathOp::Abs, + "ceil" => MathOp::Ceil, + "floor" => MathOp::Floor, + "sqrt" => MathOp::Sqrt, + "round" => MathOp::Round { + nearest: hex(&header[2]), + }, + _ => return None, + }) +} + +#[test] +fn math_matches_prometheus() { + let mut matched = 0; + for case in cases().iter().filter(|case| case.header[0] == "math") { + let op = math_op(&case.header).unwrap_or_else(|| panic!("unmapped {}", case.expr)); + let output = case + .series + .iter() + .map(|series| map_math(&case.schema, series, op).unwrap()) + .collect(); + assert_result(case, output); + matched += 1; + } + assert!(matched >= 48, "only {matched} math cases"); +}