diff --git a/AGENTS.md b/AGENTS.md index 20b2689a401..4ff990a5542 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -62,6 +62,7 @@ a module map, read/write paths, change-coupling points, and gotchas: - [`src/servers/AGENTS.md`](src/servers/AGENTS.md) - [`src/operator/AGENTS.md`](src/operator/AGENTS.md) - [`src/mito2/AGENTS.md`](src/mito2/AGENTS.md) +- [`src/promql/AGENTS.md`](src/promql/AGENTS.md) - [`src/metric-engine/AGENTS.md`](src/metric-engine/AGENTS.md) - [`src/flow/AGENTS.md`](src/flow/AGENTS.md) - [`src/frontend/AGENTS.md`](src/frontend/AGENTS.md) diff --git a/Cargo.lock b/Cargo.lock index ea2a530d6b5..a42a20af081 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11377,6 +11377,7 @@ dependencies = [ "snafu 0.8.6", "store-api", "tokio", + "twox-hash", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 702498ee5e5..c870b88c0b6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -278,6 +278,7 @@ tracing = "0.1" tracing-appender = "0.2" tracing-opentelemetry = "0.33.0" tracing-subscriber = { version = "0.3", features = ["env-filter", "json", "fmt"] } +twox-hash = { version = "2.1", default-features = false, features = ["xxhash64"] } typetag = "0.2" uddsketch-rs = "0.1.0" uuid = { version = "1.17", features = ["serde", "v4", "v7", "fast-rng"] } diff --git a/src/common/meta/src/node_manager.rs b/src/common/meta/src/node_manager.rs index 512952c9c99..d18e2cc8471 100644 --- a/src/common/meta/src/node_manager.rs +++ b/src/common/meta/src/node_manager.rs @@ -18,21 +18,10 @@ use api::region::RegionResponse; use api::v1::flow::{DirtyWindowRequests, FlowRequest, FlowResponse}; use api::v1::region::{InsertRequests, RegionRequest}; pub use common_base::AffectedRows; -use common_error::ext::{BoxedError, PlainError}; -use common_error::status_code::StatusCode; -use common_query::error::Error as CommonQueryError; -use common_query::request::{ - PreparedNamedRawSeries, QueryRequest, RAW_SERIES_REQUEST_EXTENSION_KEY, RawSeriesRequest, -}; -use common_query::series_frame::{raw_series_frame_ordering, unpack_raw_series}; +use common_query::request::QueryRequest; use common_recordbatch::SendableRecordBatchStream; -use common_recordbatch::adapter::DfRecordBatchStreamAdapter; -use datatypes::series::SeriesOrdering; -use datatypes::series::samples::SeriesSchema; -use snafu::{ResultExt, location}; -use store_api::region_engine::{RawLabelSource, SendableRawSeriesStream}; -use crate::error::{ExternalSnafu, Result}; +use crate::error::Result; use crate::peer::Peer; /// The trait for handling requests to datanode. @@ -43,111 +32,6 @@ pub trait Datanode: Send + Sync { /// Handles query requests async fn handle_query(&self, request: QueryRequest) -> Result; - - /// Prepares a bound raw-series query without executing it. - /// - /// Implementations that do not support preflight return `Ok(None)` without execution or a - /// validation guarantee; failures other than unsupported preparation are returned as errors. - async fn prepare_raw_series_query( - &self, - _request: QueryRequest, - _binding: RawSeriesRequest, - ) -> Result> { - Ok(None) - } - - /// Non-consumingly checks whether a named raw-series preflight is supported. - /// - /// Returns the approved binding and ordering on support, or `None` when the request is - /// explicitly unsupported; failures that are not unsupported preparation are returned as - /// errors. This point-in-time check does not reserve a plan, and execution revalidates - /// support. - async fn prepare_named_raw_series_query( - &self, - _request: QueryRequest, - ) -> Result> { - Ok(None) - } - - /// Handles a bound raw-series query and decodes its series frames. - /// - /// The binding is an expectation supplied by the already-authorized internal planner; it is - /// not evidence of provenance or an independent authorization grant. - async fn handle_raw_series_query( - &self, - request: QueryRequest, - binding: RawSeriesRequest, - ) -> Result { - let (request, expected_schema, label_source) = bind_raw_series_query(request, &binding)?; - let stream = self.handle_query(request).await?; - let ordering = raw_series_frame_ordering(stream.schema().arrow_schema().as_ref()); - let stream = Box::pin(DfRecordBatchStreamAdapter::new(stream)); - ordering - .and_then(|ordering| unpack_raw_series(stream, expected_schema, label_source, ordering)) - .map_err(|error| { - BoxedError::new(CommonQueryError::GeneralDataFusion { - error, - location: location!(), - }) - }) - .context(ExternalSnafu) - } -} - -/// Binds a raw-series request to the query context without executing the query. -pub fn bind_raw_series_query( - mut request: QueryRequest, - binding: &RawSeriesRequest, -) -> Result<(QueryRequest, Arc, RawLabelSource)> { - if request.region_id != binding.physical_metadata.region_id { - return Err(BoxedError::new(PlainError::new( - "raw-series request region does not match binding".to_string(), - StatusCode::InvalidArguments, - ))) - .context(ExternalSnafu); - } - let (expected_schema, label_source) = binding - .bind(Arc::new(binding.physical_metadata.clone())) - .map_err(|error| { - BoxedError::new(CommonQueryError::GeneralDataFusion { - error, - location: location!(), - }) - }) - .context(ExternalSnafu)?; - if request - .header - .as_ref() - .and_then(|header| header.query_context.as_ref()) - .is_some_and(|context| { - context - .extensions - .contains_key(RAW_SERIES_REQUEST_EXTENSION_KEY) - }) - { - return Err(BoxedError::new(PlainError::new( - "raw-series request marker already exists".to_string(), - StatusCode::InvalidArguments, - ))) - .context(ExternalSnafu); - } - let extension = binding - .to_extension_value() - .map_err(|error| { - BoxedError::new(CommonQueryError::GeneralDataFusion { - error, - location: location!(), - }) - }) - .context(ExternalSnafu)?; - request - .header - .get_or_insert_default() - .query_context - .get_or_insert_default() - .extensions - .insert(RAW_SERIES_REQUEST_EXTENSION_KEY.to_string(), extension); - Ok((request, expected_schema, label_source)) } impl dyn Datanode + '_ { diff --git a/src/common/query/src/request.rs b/src/common/query/src/request.rs index 829fa40b22d..c61ff41da7e 100644 --- a/src/common/query/src/request.rs +++ b/src/common/query/src/request.rs @@ -14,13 +14,8 @@ mod base64_serde; mod initial_remote_dyn_filter_reg; -mod named_raw_series; mod raw_series_request; -pub use self::named_raw_series::{ - PREPARE_RAW_SERIES_ACTION, PreparedNamedRawSeries, decode_prepare_raw_series_response, - encode_prepare_raw_series_response, -}; pub use self::raw_series_request::{ RAW_SERIES_REQUEST_EXTENSION_KEY, RawSeriesRequest, RawSeriesTag, take_raw_series_request, }; diff --git a/src/common/query/src/request/named_raw_series.rs b/src/common/query/src/request/named_raw_series.rs deleted file mode 100644 index 1a6cf8f8521..00000000000 --- a/src/common/query/src/request/named_raw_series.rs +++ /dev/null @@ -1,387 +0,0 @@ -// 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. - -//! Named raw-series preflight contract. -//! -//! The contract is the JSON body of the [`PREPARE_RAW_SERIES_ACTION`] action. -//! It carries the already-bound raw-series request and the ordering the -//! approving node can guarantee for it, or an explicit unsupported marker. - -use datafusion_common::{DataFusionError, Result as DataFusionResult}; -use datatypes::series::SeriesOrdering; -use serde::{Deserialize, Deserializer, Serialize}; - -use crate::request::RawSeriesRequest; - -/// Action name of the named raw-series preflight contract. -pub const PREPARE_RAW_SERIES_ACTION: &str = "greptime.prepare_raw_series.v1"; - -/// A named raw-series request approved for preflight. -#[derive(Clone, Debug)] -pub struct PreparedNamedRawSeries { - /// The bound raw-series request approved by the preparing node. - pub binding: RawSeriesRequest, - /// The ordering guaranteed by the preparing node for the binding. - pub ordering: SeriesOrdering, -} - -/// Encodes a preflight response body for [`PREPARE_RAW_SERIES_ACTION`]. -/// -/// `Some` approves the request after revalidating its binding through the -/// existing [`RawSeriesRequest`] validation, while `None` encodes the frozen -/// unsupported shape. Not every [`SeriesOrdering`] can be reported: interleaved -/// orderings are rejected. -pub fn encode_prepare_raw_series_response( - supported: Option<&PreparedNamedRawSeries>, -) -> DataFusionResult> { - let supported = match supported { - Some(prepared) => { - prepared.binding.to_extension_value()?; - Some(Supported { - binding: prepared.binding.clone(), - ordering: WireOrdering::from_series_ordering(prepared.ordering)?, - }) - } - None => None, - }; - let response = PrepareRawSeriesResponse { - version: PREPARE_RAW_SERIES_VERSION, - supported, - }; - serde_json::to_vec(&response).map_err(|_| invalid_response_error()) -} - -/// Decodes a preflight response body for [`PREPARE_RAW_SERIES_ACTION`]. -/// -/// Returns `Some` for an approving body and `None` for the frozen unsupported -/// shape. The body is decoded directly as a typed JSON envelope, so duplicate -/// keys, unknown fields, unknown versions, and malformed bindings are rejected. -pub fn decode_prepare_raw_series_response( - body: &[u8], -) -> DataFusionResult> { - let response: PrepareRawSeriesResponse = - serde_json::from_slice(body).map_err(|_| invalid_response_error())?; - if response.version != PREPARE_RAW_SERIES_VERSION { - return Err(unsupported_version_error()); - } - let Some(supported) = response.supported else { - return Ok(None); - }; - supported.binding.to_extension_value()?; - Ok(Some(PreparedNamedRawSeries { - binding: supported.binding, - ordering: supported.ordering.into_series_ordering(), - })) -} - -/// Wire-format version of the named raw-series preflight contract. -const PREPARE_RAW_SERIES_VERSION: u32 = 1; - -/// Private JSON envelope of [`PREPARE_RAW_SERIES_ACTION`]. -/// -/// `supported` is required but nullable: a missing field is rejected instead of -/// defaulting, while an explicit `null` is the unsupported encoding. -#[derive(Debug, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -struct PrepareRawSeriesResponse { - version: u32, - #[serde(deserialize_with = "deserialize_required_supported")] - supported: Option, -} - -/// Private shape of an approving response. -#[derive(Debug, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -struct Supported { - binding: RawSeriesRequest, - ordering: WireOrdering, -} - -/// Private mirror of the orderings this contract can report. -#[derive(Clone, Copy, Debug, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -enum WireOrdering { - Contiguous, - ReadKeyThenTime, -} - -impl WireOrdering { - /// Rejects orderings the contract cannot report. - fn from_series_ordering(ordering: SeriesOrdering) -> DataFusionResult { - match ordering { - SeriesOrdering::Contiguous => Ok(Self::Contiguous), - SeriesOrdering::KeyThenTime => Ok(Self::ReadKeyThenTime), - SeriesOrdering::Interleaved => Err(unsupported_ordering_error()), - } - } - - fn into_series_ordering(self) -> SeriesOrdering { - match self { - Self::Contiguous => SeriesOrdering::Contiguous, - Self::ReadKeyThenTime => SeriesOrdering::KeyThenTime, - } - } -} - -/// Requires the `supported` field while still accepting an explicit `null`. -fn deserialize_required_supported<'de, D>(deserializer: D) -> Result, D::Error> -where - D: Deserializer<'de>, -{ - Option::::deserialize(deserializer) -} - -fn invalid_response_error() -> DataFusionError { - DataFusionError::Plan("Invalid prepare raw-series response".to_string()) -} - -fn unsupported_version_error() -> DataFusionError { - DataFusionError::Plan("Unsupported prepare raw-series response version".to_string()) -} - -fn unsupported_ordering_error() -> DataFusionError { - DataFusionError::Plan("Unsupported prepare raw-series response ordering".to_string()) -} - -#[cfg(test)] -mod tests { - use std::sync::Arc; - - use store_api::storage::RegionId; - - use super::*; - - const SUPPORTED_FIXTURE: &str = include_str!( - "../../../../../tests/compatibility/wire/named_raw_series_prepare/v1_supported.json" - ); - const UNSUPPORTED_FIXTURE: &str = include_str!( - "../../../../../tests/compatibility/wire/named_raw_series_prepare/v1_unsupported.json" - ); - const BINDING_FIXTURE: &str = - include_str!("../../../../../tests/compatibility/wire/raw_series_request/v1_request.json"); - - fn fixture_binding() -> RawSeriesRequest { - RawSeriesRequest::from_extension_value(BINDING_FIXTURE).unwrap() - } - - fn fixture_body() -> String { - format!( - r#"{{"version":1,"supported":{{"binding":{},"ordering":"contiguous"}}}}"#, - BINDING_FIXTURE.trim() - ) - } - - #[test] - fn frozen_fixtures_decode_supported_and_unsupported() { - let prepared = decode_prepare_raw_series_response(SUPPORTED_FIXTURE.as_bytes()) - .unwrap() - .expect("supported fixture must decode as approved"); - assert_eq!(prepared.ordering, SeriesOrdering::Contiguous); - assert_eq!( - prepared.binding.physical_metadata.region_id, - RegionId::new(1, 1) - ); - assert_eq!( - prepared.binding.to_extension_value().unwrap(), - fixture_binding().to_extension_value().unwrap() - ); - - let (schema, _) = prepared - .binding - .bind(Arc::new(prepared.binding.physical_metadata.clone())) - .unwrap(); - assert_eq!( - schema - .fields - .iter() - .map(|field| field.name.as_ref()) - .collect::>(), - ["aux", "value"] - ); - - assert!( - decode_prepare_raw_series_response(UNSUPPORTED_FIXTURE.as_bytes()) - .unwrap() - .is_none() - ); - } - - #[test] - fn prepare_response_round_trips_both_orderings() { - for (ordering, wire_name) in [ - (SeriesOrdering::Contiguous, "contiguous"), - (SeriesOrdering::KeyThenTime, "read_key_then_time"), - ] { - let prepared = PreparedNamedRawSeries { - binding: fixture_binding(), - ordering, - }; - let encoded = encode_prepare_raw_series_response(Some(&prepared)).unwrap(); - let json: serde_json::Value = serde_json::from_slice(&encoded).unwrap(); - assert_eq!(json["version"].as_u64(), Some(1)); - assert_eq!(json["supported"]["ordering"].as_str(), Some(wire_name)); - assert!(json["supported"]["binding"].is_object()); - - let decoded = decode_prepare_raw_series_response(&encoded) - .unwrap() - .expect("approved response must decode as supported"); - assert_eq!(decoded.ordering, ordering); - assert_eq!( - decoded.binding.to_extension_value().unwrap(), - prepared.binding.to_extension_value().unwrap() - ); - } - } - - #[test] - fn unsupported_prepare_response_has_frozen_shape() { - let encoded = encode_prepare_raw_series_response(None).unwrap(); - assert_eq!( - std::str::from_utf8(&encoded).unwrap(), - r#"{"version":1,"supported":null}"# - ); - assert!( - decode_prepare_raw_series_response(&encoded) - .unwrap() - .is_none() - ); - } - - #[test] - fn prepare_response_encoding_rejects_interleaved_and_malformed_binding() { - let interleaved = PreparedNamedRawSeries { - binding: fixture_binding(), - ordering: SeriesOrdering::Interleaved, - }; - assert!(encode_prepare_raw_series_response(Some(&interleaved)).is_err()); - - let mut malformed = fixture_binding(); - malformed.field_ids.push(3); - let malformed = PreparedNamedRawSeries { - binding: malformed, - ordering: SeriesOrdering::Contiguous, - }; - assert!(encode_prepare_raw_series_response(Some(&malformed)).is_err()); - } - - #[test] - fn prepare_response_rejects_malformed_bodies() { - let body = fixture_body(); - let cases = [ - ("empty body", String::new()), - ("not json", "{".to_string()), - ("top-level array", "[]".to_string()), - ("missing version", r#"{"supported":null}"#.to_string()), - ( - "zero version", - r#"{"version":0,"supported":null}"#.to_string(), - ), - ( - "unknown version", - r#"{"version":2,"supported":null}"#.to_string(), - ), - ( - "unknown envelope field", - r#"{"version":1,"supported":null,"extra":true}"#.to_string(), - ), - ("missing supported", r#"{"version":1}"#.to_string()), - ( - "duplicate version", - r#"{"version":1,"version":1,"supported":null}"#.to_string(), - ), - ( - "duplicate supported", - r#"{"version":1,"supported":null,"supported":null}"#.to_string(), - ), - ( - "supported missing binding", - r#"{"version":1,"supported":{"ordering":"contiguous"}}"#.to_string(), - ), - ( - "supported missing ordering", - body.replace(r#","ordering":"contiguous""#, ""), - ), - ( - "supported unknown field", - body.replace( - r#""ordering":"contiguous""#, - r#""ordering":"contiguous","extra":true"#, - ), - ), - ( - "interleaved ordering", - body.replace(r#""ordering":"contiguous""#, r#""ordering":"interleaved""#), - ), - ( - "unknown ordering", - body.replace(r#""ordering":"contiguous""#, r#""ordering":"future""#), - ), - ( - "non-string ordering", - body.replace(r#""ordering":"contiguous""#, r#""ordering":1"#), - ), - ( - "unknown nested binding field", - body.replace( - r#""metric_name":"cpu""#, - r#""metric_name":"cpu","extra":true"#, - ), - ), - ( - "duplicate nested binding field", - body.replace( - r#""metric_name":"cpu""#, - r#""metric_name":"cpu","metric_name":"cpu""#, - ), - ), - ( - "unknown nested tag field", - body.replace( - r#"{"column_id":1,"name":"host"}"#, - r#"{"column_id":1,"name":"host","extra":true}"#, - ), - ), - ( - "duplicate nested tag field", - body.replace( - r#"{"column_id":1,"name":"host"}"#, - r#"{"column_id":1,"name":"host","name":"host"}"#, - ), - ), - ( - "malformed binding version", - body.replace( - r#"{"version":1,"physical_metadata""#, - r#"{"version":2,"physical_metadata""#, - ), - ), - ( - "duplicate binding field id", - body.replace(r#""field_ids":[3,2]"#, r#""field_ids":[3,3]"#), - ), - ( - "non-field binding id", - body.replace(r#""field_ids":[3,2]"#, r#""field_ids":[1]"#), - ), - ]; - - for (name, candidate) in cases { - assert!( - decode_prepare_raw_series_response(candidate.as_bytes()).is_err(), - "must reject {name}" - ); - } - assert!(decode_prepare_raw_series_response(body.as_bytes()).is_ok()); - } -} diff --git a/src/mito2/AGENTS.md b/src/mito2/AGENTS.md index f70aa622a27..95b9eddae07 100644 --- a/src/mito2/AGENTS.md +++ b/src/mito2/AGENTS.md @@ -26,7 +26,7 @@ snapshot isolation). It implements the `RegionEngine` trait from `store-api`. | `compaction` | `src/mito2/src/compaction/` | Compaction scheduler (`scheduler.rs` + `scheduler/`), TWCS picker, strict-window manual picker, compactor, memory control | | `access_layer` | `src/mito2/src/access_layer.rs` | SST read/write over the object store | | `sst` | `src/mito2/src/sst/` | Parquet format, file metadata, index layout | -| `series_index` | `src/mito2/src/series_index/` | Series index writer/searcher, bucket planning, catalogs, worker-owned reconciliation (`maintenance.rs`), shared approximate usage (`task.rs`), catalogs, and index lifecycle | +| `series_index` | `src/mito2/src/series_index/` | Format interfaces (`format.rs`), index writer/searcher, per-SST samples artifacts (`artifact.rs`), bucket planning, catalogs, reconciliation (`maintenance.rs`), and deferred purge | | `read` | `src/mito2/src/read/` | `ScanRegion`, merge, dedup, projection, streaming | | `manifest` | `src/mito2/src/manifest/` | `RegionManifestManager`, manifest actions/edits | | `cache` | `src/mito2/src/cache.rs` | Write/file/page caches | @@ -66,6 +66,7 @@ filtered `RecordBatch` stream. and defer new builds when the shared estimate is full. Cleanup still prunes obsolete range entries and expires series indexes to reclaim published usage. Concurrent builds may overshoot; closed-region files are not counted. Series handles and the SST purger own file deletion. +- **Series format PoC**: samples are rebuildable local SST sidecars. Optional writer interfaces (`format.rs`) integrate with reconciliation, catalog publication, and deferred purge. Defaults retain the original Parquet writer and disable samples. Prepared sources, read APIs, and concrete new formats are deferred; ordinary scans only use Parquet index artifacts. - **Series-index coverage** (`series_index/catalog.rs`): `SeriesIndexEntry` stores compaction-window width and SST summaries keyed by aligned start in both catalogs and Parquet footers. - **Request types** (`request.rs`): usually tied to proto definitions consumed by `datanode`. diff --git a/src/mito2/src/engine/scan_test.rs b/src/mito2/src/engine/scan_test.rs index a893a234adc..68469e85324 100644 --- a/src/mito2/src/engine/scan_test.rs +++ b/src/mito2/src/engine/scan_test.rs @@ -1549,6 +1549,7 @@ async fn check_two_phase_series_scan( let store = region.series_index_store.clone().unwrap(); let sequence = region.flushed_sequence(); let entry = SeriesIndexEntry { + artifact: None, file_size: 0, index_uuid: FileId::random(), bucket_start: Timestamp::new_millisecond(0), @@ -4907,7 +4908,7 @@ async fn test_mito_handle_query_raw_series_candidate_projection_and_mvcc() { .scan_series_partition(&QueryScanContext::default(), &metrics, partition, &source) .unwrap() .expect("supported millisecond PerSeries scan"); - assert_eq!(SeriesOrdering::Contiguous, stream.ordering()); + assert_eq!(SeriesOrdering::KeyThenTime, stream.ordering()); assert!(stream.schema().eval_grid.is_none()); assert_eq!( vec!["field_1", "field_0"], @@ -4925,7 +4926,7 @@ async fn test_mito_handle_query_raw_series_candidate_projection_and_mvcc() { !closed_keys.contains(chunk.primary_key.as_slice()), "no chunk may follow a key's is_last chunk" ); - let times = chunk.time.as_buffer().as_ref(); + let times = chunk.time.as_buffer(); assert_eq!(2, chunk.fields.len()); assert_eq!(times.len(), chunk.fields.get(0).unwrap().len()); assert_eq!(times.len(), chunk.fields.get(1).unwrap().len()); @@ -4994,7 +4995,7 @@ async fn test_mito_handle_query_raw_series_candidate_projection_and_mvcc() { } // Compare keyed visible tuples through a separately opened legacy scanner. // A partition cannot be consumed through both paths. - let mut legacy_request = request; + let mut legacy_request = request.clone(); legacy_request.projection = Some(vec![0, 3, 2, 1, 4]); legacy_request.preserve_pk_dictionary_encoding = false; let legacy = engine @@ -5090,6 +5091,130 @@ async fn test_mito_handle_query_raw_series_candidate_projection_and_mvcc() { .collect::>(); raw_visible.sort_by(|a, b| a.0.cmp(&b.0).then(a.1.cmp(&b.1))); assert_eq!(raw_visible, visible); + + // Exercise the production table physical consumer over a fresh real Mito scanner. + use common_query::series_frame::{raw_series_frame_ordering, unpack_raw_series}; + use datafusion::execution::TaskContext; + use datafusion::physical_plan::ExecutionPlan; + use table::table::scan::RegionScanExec; + let native_scanner = engine + .handle_query(region_id, request.clone()) + .await + .unwrap(); + let native_metadata = native_scanner.metadata(); + let native_tags = native_metadata + .column_metadatas + .iter() + .filter(|c| c.semantic_type == SemanticType::Tag) + .map(|c| BoundTag { + column_id: c.column_id, + name: Arc::from(c.column_schema.name.as_str()), + }) + .collect::>(); + let native_source = RawLabelSource::PrimaryKey { + metric_name: Arc::from("fixture_metric"), + physical_metadata: native_metadata, + logical_tags: native_tags.into(), + }; + let native_plan = Arc::new(RegionScanExec::new(native_scanner, request.clone(), None).unwrap()); + let native_plan = native_plan + .try_to_series_scan(native_source.clone()) + .unwrap() + .expect("Mito raw-series capability"); + let native_schema = native_plan.series_schema().clone(); + let native_frames = native_plan + .execute(0, Arc::new(TaskContext::default())) + .unwrap(); + // Unpack under the frame's own declaration. A legacy producer declares `KeyThenTime`, and + // only that declaration makes the decoder check that keys increase. + let frame_ordering = raw_series_frame_ordering(native_frames.schema().as_ref()).unwrap(); + assert_eq!( + SeriesOrdering::KeyThenTime, + frame_ordering, + "the legacy producer must declare key-then-time frames" + ); + let unpacked = + unpack_raw_series(native_frames, native_schema, native_source, frame_ordering).unwrap(); + let native_schema = unpacked.schema().clone(); + let mut native = unpacked.try_collect::>().await.unwrap(); + assert_eq!( + vec!["field_1", "field_0"], + native_schema + .fields + .iter() + .map(|field| field.name.as_ref()) + .collect::>() + ); + let mut native_visible = native + .drain(..) + .flat_map(|chunk| { + let key = chunk.primary_key.to_vec(); + // The frame binding is the raw primary key, so decode it back to the fixture tag to + // keep the raw rows comparable with the legacy path. + let tag = if key == expected_keys[0] { + "A" + } else if key == expected_keys[1] { + "B" + } else { + panic!("unexpected raw primary key {key:?}") + }; + let times = chunk.time.as_buffer(); + let is_last = chunk.is_last; + let mut rows = Vec::with_capacity(times.len()); + for (i, time) in times.iter().enumerate() { + let SampleRef::Float(f1) = chunk.fields.get(0).unwrap().sample(i) else { + panic!("expected f1 float") + }; + let f0 = match chunk.fields.get(1).unwrap().sample(i) { + SampleRef::Float(value) => Some(value), + SampleRef::Missing => None, + _ => panic!("expected f0 float or null"), + }; + rows.push(( + key.clone(), + tag.to_string(), + *time, + f1, + f0, + is_last && i + 1 == times.len(), + )); + } + rows + }) + .collect::>(); + let mut expected_native = raw + .iter() + .map(|(key, time, f1, f0, _)| (key.to_vec(), *time, *f1, *f0)) + .collect::>(); + type NativeRow = (Vec, String, i64, f64, Option, bool); + let sort_native = |rows: &mut Vec| { + rows.sort_by(|a, b| a.0.cmp(&b.0).then(a.2.cmp(&b.2))); + }; + sort_native(&mut native_visible); + expected_native.sort_by(|a, b| a.0.cmp(&b.0).then(a.1.cmp(&b.1))); + assert_eq!( + expected_native, + native_visible + .iter() + .map(|(key, _, time, f1, f0, _)| (key.clone(), *time, *f1, *f0)) + .collect::>() + ); + for key in &expected_keys { + assert_eq!( + 1, + native_visible + .iter() + .filter(|(row_key, _, _, _, _, is_last)| row_key == key && *is_last) + .count() + ); + } + let mut decoded_visible = native_visible + .iter() + .map(|(_, tag, time, f1, f0, _)| (tag.clone(), *time, *f1, *f0)) + .collect::>(); + decoded_visible.sort_by(|a, b| a.0.cmp(&b.0).then(a.1.cmp(&b.1))); + assert_eq!(visible, decoded_visible); + // Apply the typed predicate and final projection through the ordinary legacy filter path. assert_eq!(3, visible.len()); assert_eq!( @@ -5102,6 +5227,1024 @@ async fn test_mito_handle_query_raw_series_candidate_projection_and_mvcc() { ); } +/// A `TwoPhase` scan declares [`SeriesOrdering::Contiguous`] on the scanner, on every partition +/// stream, and therefore in every frame it packs; the frame adapter validates that declaration. +/// +/// The ordered merge that rejects a non-`KeyThenTime` input is covered by +/// `promql::source::merge::tests::rejects_unsupported_ordering_and_empty_input_without_polling`. +#[tokio::test] +async fn test_two_phase_native_raw_stream_declares_contiguous_frames() { + use common_query::series_frame::{ + pack_raw_series, raw_series_frame_ordering, unpack_raw_series, + }; + use datatypes::series::SeriesOrdering; + use datatypes::series::samples::SeriesSchema; + use store_api::region_engine::{BoundTag, QueryScanContext, RawLabelSource}; + use store_api::region_request::RegionPutRequest; + + let mut env = TestEnv::with_prefix("test_two_phase_native_raw_contiguous_frames").await; + let engine = env + .create_engine(MitoConfig { + experimental_series_scan_v2: true, + ..Default::default() + }) + .await; + let region_id = RegionId::new(1, 1); + let metadata = Arc::new(sst_region_metadata_with_encoding( + PrimaryKeyEncoding::Sparse, + )); + let mut create = CreateRequestBuilder::new().build(); + create.column_metadatas = metadata.column_metadatas.clone(); + create.primary_key = metadata.primary_key.clone(); + create + .options + .insert(PRIMARY_KEY_ENCODING.to_string(), "sparse".to_string()); + create + .options + .insert("memtable.type".to_string(), "bulk".to_string()); + create + .options + .insert("sst_format".to_string(), "flat".to_string()); + let full_schema = test_util::rows_schema(&create); + let mut key_schema = full_schema[0].clone(); + key_schema.column_name = PRIMARY_KEY_COLUMN_NAME.to_string(); + key_schema.datatype = ColumnDataType::Binary.into(); + key_schema.semantic_type = SemanticType::Tag.into(); + let row_schema = vec![key_schema, full_schema[5].clone(), full_schema[4].clone()]; + engine + .handle_request(region_id, RegionRequest::Create(create)) + .await + .unwrap(); + let sparse_rows = Rows { + schema: row_schema, + rows: vec![ + row(vec![ + ValueData::BinaryValue(new_sparse_primary_key(&["a", "x"], &metadata, 10, 0)), + ValueData::TimestampMillisecondValue(10), + ValueData::U64Value(1), + ]), + row(vec![ + ValueData::BinaryValue(new_sparse_primary_key(&["b", "y"], &metadata, 20, 0)), + ValueData::TimestampMillisecondValue(20), + ValueData::U64Value(2), + ]), + ], + }; + engine + .handle_request( + region_id, + RegionRequest::Put(RegionPutRequest { + rows: sparse_rows, + hint: Some(WriteHint { + primary_key_encoding: api::v1::PrimaryKeyEncoding::Sparse.into(), + }), + skip_wal: false, + partition_expr_version: None, + }), + ) + .await + .unwrap(); + test_util::flush_region(&engine, region_id, None).await; + let request = ScanRequest { + distribution: Some(TimeSeriesDistribution::PerSeries), + preserve_pk_dictionary_encoding: true, + ..Default::default() + }; + let selected = engine.scanner(region_id, request.clone()).await.unwrap(); + let Scanner::Series(series) = selected else { + panic!("sparse PerSeries request must use SeriesScan"); + }; + assert_eq!("two_phase", series.mode()); + let mut scanner = engine.handle_query(region_id, request).await.unwrap(); + assert_eq!(SeriesOrdering::Contiguous, scanner.raw_series_ordering()); + let metadata = scanner.metadata(); + let source = RawLabelSource::PrimaryKey { + metric_name: Arc::from("fixture_metric"), + physical_metadata: metadata.clone(), + logical_tags: metadata + .column_metadatas + .iter() + .filter(|column| { + column.semantic_type == SemanticType::Tag + && !store_api::storage::consts::is_internal_column(&column.column_schema.name) + && !store_api::metric_engine_consts::is_metric_engine_internal_column( + &column.column_schema.name, + ) + }) + .map(|column| BoundTag { + column_id: column.column_id, + name: Arc::from(column.column_schema.name.as_str()), + }) + .collect::>() + .into(), + }; + let ranges = scanner.properties().partitions.clone(); + scanner + .prepare(PrepareRequest { + ranges: Some(ranges.clone()), + target_partitions: Some(ranges.len()), + ..Default::default() + }) + .unwrap(); + let metrics = ExecutionPlanMetricsSet::default(); + let mut schema: Option> = None; + let mut chunks = Vec::new(); + for partition in 0..ranges.len() { + let stream = scanner + .scan_series_partition(&QueryScanContext::default(), &metrics, partition, &source) + .unwrap() + .unwrap(); + // The storage-side contract: every partition stream reports the scanner's declaration. + assert_eq!(scanner.raw_series_ordering(), stream.ordering()); + assert_eq!(SeriesOrdering::Contiguous, stream.ordering()); + schema.get_or_insert_with(|| Arc::clone(stream.schema())); + chunks.extend(stream.try_collect::>().await.unwrap()); + } + assert_eq!( + 2, + chunks.iter().map(|chunk| chunk.time.len()).sum::() + ); + + // The frames the adapter receives carry that declaration, and the adapter validates it + // through `pack`/`unpack_raw_series` without ever seeing a sorted key guarantee. + let schema = schema.expect("the scan must emit at least one partition stream"); + let packed = pack_raw_series(Box::pin(ChainedRawSeriesStream::new( + Arc::clone(&schema), + source.clone(), + SeriesOrdering::Contiguous, + chunks, + ))) + .unwrap(); + let ordering = raw_series_frame_ordering(packed.schema().as_ref()).unwrap(); + assert_eq!(SeriesOrdering::Contiguous, ordering); + let validated = unpack_raw_series(packed, schema, source, ordering) + .unwrap() + .try_collect::>() + .await + .unwrap(); + assert_eq!( + 2, + validated + .iter() + .map(|chunk| chunk.time.len()) + .sum::() + ); +} + +#[tokio::test] +async fn test_two_phase_raw_partition_out_of_range_keeps_scanner_usable() { + use datatypes::series::samples::SampleRef; + use store_api::region_engine::QueryScanContext; + + let mut env = TestEnv::with_prefix("test_two_phase_raw_partition_out_of_range").await; + let engine = env + .create_engine(MitoConfig { + experimental_series_scan_v2: true, + ..Default::default() + }) + .await; + let region_id = RegionId::new(1, 1); + let (metadata, row_schema) = create_two_phase_sparse_region(&engine, region_id).await; + let expected = [ + (new_sparse_primary_key(&["a", "x"], &metadata, 10, 0), 10, 1), + (new_sparse_primary_key(&["b", "x"], &metadata, 10, 0), 20, 2), + ]; + put_sparse_rows( + &engine, + region_id, + &row_schema, + expected + .iter() + .map(|(key, time, value)| (key.clone(), *time, *value)) + .collect(), + ) + .await; + test_util::flush_region(&engine, region_id, None).await; + + let request = ScanRequest { + distribution: Some(TimeSeriesDistribution::PerSeries), + preserve_pk_dictionary_encoding: true, + ..Default::default() + }; + let Scanner::Series(selected) = engine.scanner(region_id, request.clone()).await.unwrap() + else { + panic!("sparse PerSeries request must use SeriesScan"); + }; + assert_eq!("two_phase", selected.mode()); + let mut scanner = engine.handle_query(region_id, request).await.unwrap(); + let source = primary_key_label_source(&scanner.metadata()); + let ranges = scanner.properties().partitions.clone(); + scanner + .prepare(PrepareRequest { + ranges: Some(ranges.clone()), + target_partitions: Some(ranges.len()), + ..Default::default() + }) + .unwrap(); + let partitions = scanner.properties().num_partitions(); + assert!(partitions > 0, "prepared scan needs an output partition"); + let metrics = ExecutionPlanMetricsSet::default(); + let ctx = QueryScanContext::default(); + + // `scan_batch_in_partition` validates the index before it takes that partition's receiver, so + // the first index past the last partition must fail without disturbing scanner state. + let error = match scanner.scan_series_partition(&ctx, &metrics, partitions, &source) { + Ok(_) => { + panic!("partition {partitions} must be rejected by a {partitions}-partitions scan") + } + Err(error) => error, + }; + let message = error.to_string(); + assert!( + message.contains("out of range") && message.contains(&partitions.to_string()), + "expected a partition range error, got: {message}" + ); + + // The in-range partition must still read the complete data set. + let stream = scanner + .scan_series_partition(&ctx, &metrics, 0, &source) + .unwrap() + .expect("the raw series scan stays available after a rejected partition"); + let mut actual = HashMap::new(); + for chunk in stream.try_collect::>().await.unwrap() { + let times = chunk.time.as_buffer(); + for (index, time) in times.iter().enumerate() { + let SampleRef::Float(value) = chunk.fields.get(0).unwrap().sample(index) else { + panic!("raw fields are converted to float samples"); + }; + actual.insert((chunk.primary_key.to_vec(), *time), value); + } + } + assert_eq!( + expected.len(), + actual.len(), + "the rejected call must not consume rows" + ); + for (key, time, value) in expected { + assert_eq!(Some(&(value as f64)), actual.get(&(key, time))); + } + + // A partition can only be scanned once, so this proves the rejected index never took the + // in-range partition's receiver: the error above is raised by the range check alone. + let repeated = match scanner.scan_series_partition(&ctx, &metrics, 0, &source) { + Ok(_) => panic!("partition 0 was already scanned"), + Err(error) => error.to_string(), + }; + assert!( + repeated.contains("scan multiple times"), + "expected a repeated-scan error, got: {repeated}" + ); +} + +#[tokio::test] +async fn test_two_phase_raw_stream_stays_contiguous_across_disjoint_partition_assignments() { + use common_query::series_frame::{pack_raw_series, unpack_raw_series}; + use datatypes::series::SeriesOrdering; + use datatypes::series::raw::RawSeriesChunk; + use datatypes::series::samples::{SampleRef, SeriesSchema}; + use store_api::region_engine::QueryScanContext; + + let mut env = TestEnv::with_prefix("test_two_phase_raw_contiguous_assignments").await; + let engine = env + .create_engine(MitoConfig { + experimental_series_scan_v2: true, + // Automatic compaction could merge the two flushed SSTs before the scan pins its + // input and make the source-composition assertions below racy. + schedule_compaction_after_edit: false, + min_compaction_interval: Duration::from_secs(60 * 60), + ..Default::default() + }) + .await; + let region_id = RegionId::new(1, 1); + let (metadata, row_schema) = create_two_phase_sparse_region(&engine, region_id).await; + + // A sparse primary key carries the TSID, and `SeriesRange::partition_for` splits the u64 TSID + // domain over the output partitions, so the fixture splits into two disjoint groups of series: + // tsids below 2^63 own partition 0 and tsids at or above 2^63 own partition 1. + let series = [ + ("a0", 0_u64), + ("a1", 1), + ("b0", 1_u64 << 63), + ("b1", (1_u64 << 63) + 1), + ]; + let keys = series + .iter() + .map(|(tag, tsid)| new_sparse_primary_key(&[tag, "x"], &metadata, 10, *tsid)) + .collect::>(); + + // Three write rounds put every series into two SST sources (the first two rounds are flushed) + // plus one memtable source (the last round stays in memory), and the large rounds make a + // series span more than one read batch, so one series is emitted as multiple chunks instead + // of exactly one. The scan input assertions below check that composition instead of only + // checking that something was read. + const LARGE: i64 = 20_000; + const SMALL: i64 = 3; + let rounds: [&[(usize, i64, i64)]; 3] = [ + &[(0, 0, LARGE), (1, 0, SMALL), (2, 0, SMALL), (3, 0, LARGE)], + &[ + (0, LARGE, 5), + (1, SMALL, SMALL), + (2, SMALL, SMALL), + (3, LARGE, 5), + ], + &[ + (0, LARGE + 5, 2), + (1, 2 * SMALL, 2), + (2, 2 * SMALL, 2), + (3, LARGE + 5, 2), + ], + ]; + let mut expected_times = HashMap::, Vec>::new(); + for spec in rounds { + for &(index, first, count) in spec { + expected_times + .entry(keys[index].clone()) + .or_default() + .extend(first..first + count); + } + } + let mut round_rows = Vec::with_capacity(rounds.len()); + for (round, spec) in rounds.iter().enumerate() { + let rows = spec + .iter() + .flat_map(|&(index, first, count)| { + let key = keys[index].clone(); + (0..count).map(move |offset| { + let time = first + offset; + (key.clone(), time, (time + 1) as u64) + }) + }) + .collect::>(); + round_rows.push(rows.len()); + put_sparse_rows(&engine, region_id, &row_schema, rows).await; + if round + 1 < rounds.len() { + test_util::flush_region(&engine, region_id, None).await; + } + } + + let request = ScanRequest { + distribution: Some(TimeSeriesDistribution::PerSeries), + preserve_pk_dictionary_encoding: true, + ..Default::default() + }; + let selected = engine.scanner(region_id, request).await.unwrap(); + let Scanner::Series(mut scanner) = selected else { + panic!("sparse PerSeries request must use SeriesScan"); + }; + assert_eq!("two_phase", scanner.mode()); + assert_eq!(SeriesOrdering::Contiguous, scanner.raw_series_ordering()); + // Composition of the pinned scan input: the two flushes pin two SST sources and the third + // write round stays in one memtable source. Each source reports the rows its write round + // wrote, so this asserts the layout instead of only asserting that something was read. + { + let input = scanner.input(); + let mut file_rows = input + .files + .iter() + .map(|file| file.num_rows()) + .collect::>(); + file_rows.sort_unstable(); + let mut expected_file_rows = round_rows[..2].to_vec(); + expected_file_rows.sort_unstable(); + assert_eq!( + expected_file_rows, file_rows, + "each flushed write round must be one SST source" + ); + assert_eq!(2, input.num_files()); + assert_eq!( + 1, + input.num_memtables(), + "the last round is one memtable range" + ); + assert_eq!( + round_rows[2], + input + .memtables + .iter() + .map(|memtable| memtable.stats().num_rows) + .sum::(), + "the unflushed round must be the memtable's contribution" + ); + assert_eq!( + round_rows.iter().sum::(), + input.total_rows(), + "the sources must add up to every written row" + ); + } + let source = primary_key_label_source(&scanner.metadata()); + let ranges = scanner + .properties() + .partitions + .clone() + .into_iter() + .flatten() + .collect::>(); + assert!(!ranges.is_empty(), "region scan needs an input range"); + // Two output partitions make the candidate distributor create one reader assignment per + // partition, so the scan runs more than one assignment. + let partition_count = 2; + let mut partitions = vec![Vec::new(); partition_count]; + for (index, range) in ranges.into_iter().enumerate() { + partitions[index % partition_count].push(range); + } + scanner + .prepare(PrepareRequest { + ranges: Some(partitions), + target_partitions: Some(partition_count), + ..Default::default() + }) + .unwrap(); + assert_eq!(partition_count, scanner.properties().num_partitions()); + + let metrics = ExecutionPlanMetricsSet::default(); + let ctx = QueryScanContext::default(); + let mut per_partition = Vec::with_capacity(partition_count); + let mut schema: Option> = None; + for partition in 0..partition_count { + let stream = scanner + .scan_series_partition(&ctx, &metrics, partition, &source) + .unwrap() + .expect("two-phase raw scan is supported"); + assert_eq!(SeriesOrdering::Contiguous, stream.ordering()); + assert_eq!( + vec!["field_0"], + stream + .schema() + .fields + .iter() + .map(|field| field.name.as_ref()) + .collect::>() + ); + schema.get_or_insert_with(|| Arc::clone(stream.schema())); + let chunks = stream.try_collect::>().await.unwrap(); + assert!( + !chunks.is_empty(), + "partition {partition} must own a reader assignment" + ); + // Each partition owns exactly the series the TSID split assigned to it, so the two + // assignment streams are non-empty and disjoint. + let mut observed = chunks + .iter() + .map(|chunk| chunk.primary_key.to_vec()) + .collect::>(); + observed.sort(); + observed.dedup(); + let mut assigned = vec![keys[2 * partition].clone(), keys[2 * partition + 1].clone()]; + assigned.sort(); + assert_eq!( + assigned, observed, + "partition {partition} must own exactly its assigned series" + ); + per_partition.push(chunks); + } + + // The complete raw series stream of the two-partition scan: each partition's one assignment, + // concatenated in partition order. `test_two_phase_raw_same_partition_multi_round_assignment_ + // stays_contiguous` covers two assignments handed to the same partition. + let complete = per_partition + .into_iter() + .flatten() + .collect::>(); + let schema = schema.unwrap(); + let complete_rows = complete.iter().map(|chunk| chunk.time.len()).sum::(); + assert_eq!( + expected_times.values().map(Vec::len).sum::(), + complete_rows, + "the complete stream must carry every written row" + ); + + // Reuse the frame adapter's cross-chunk validator (private to + // `common_query::series_frame`) by packing and unpacking the complete stream. It rejects a key + // change that was not preceded by that key's final chunk, a key that recurs right after its + // final chunk, timestamps that do not increase across chunks, and a stream whose last chunk is + // not final. + let packed = pack_raw_series(Box::pin(ChainedRawSeriesStream::new( + Arc::clone(&schema), + source.clone(), + SeriesOrdering::Contiguous, + complete.clone(), + ))) + .unwrap(); + let unpacked = unpack_raw_series( + packed, + Arc::clone(&schema), + source, + SeriesOrdering::Contiguous, + ) + .unwrap(); + let validated = unpacked.try_collect::>().await.unwrap(); + assert_eq!( + complete_rows, + validated + .iter() + .map(|chunk| chunk.time.len()) + .sum::(), + "packing and unpacking must preserve the complete stream" + ); + + // `SequenceValidator` checks adjacent chunks only, so also reject non-adjacent recurrence: a + // series interrupted by another series and resumed later would break `Contiguous`. + let mut runs = Vec::>::new(); + let mut chunks_per_series = HashMap::, usize>::new(); + for chunk in &complete { + let key = chunk.primary_key.to_vec(); + *chunks_per_series.entry(key.clone()).or_default() += 1; + if runs.last() != Some(&key) { + assert!( + !runs.contains(&key), + "a series' chunks must stay contiguous across the concatenated assignments" + ); + runs.push(key); + } + } + assert_eq!(keys.len(), runs.len(), "every series appears exactly once"); + assert!( + chunks_per_series.values().copied().max().unwrap() > 1, + "the fixture must emit multiple chunks for one series, got {chunks_per_series:?}" + ); + assert_eq!(keys.len(), chunks_per_series.len()); + for (key, times) in &expected_times { + let observed = complete + .iter() + .filter(|chunk| chunk.primary_key.as_slice() == key.as_slice()) + .flat_map(|chunk| chunk.time.as_buffer().iter().copied()) + .collect::>(); + assert_eq!( + times, &observed, + "sample positions must increase per series" + ); + } + for (index, chunk) in complete.iter().enumerate() { + let next_is_same_series = complete + .get(index + 1) + .is_some_and(|next| next.primary_key.as_slice() == chunk.primary_key.as_slice()); + assert_eq!( + !next_is_same_series, chunk.is_last, + "is_last must mark the final chunk of a series" + ); + let times = chunk.time.as_buffer(); + for (sample, time) in times.iter().enumerate() { + let SampleRef::Float(value) = chunk.fields.get(0).unwrap().sample(sample) else { + panic!("raw fields are converted to float samples"); + }; + assert_eq!((*time + 1) as f64, value); + } + } +} + +/// Lowers the candidate-series assignment threshold and restores it when dropped. +/// +/// The override is process-global, so it is restored from `Drop` to keep a failing assertion or a +/// panic from leaking a weakened threshold into later tests of the same binary. `cargo nextest` +/// (the documented runner for this crate) additionally runs every test in its own process. +struct AssignmentThresholdScope; + +impl AssignmentThresholdScope { + fn lower_to(threshold: usize) -> Self { + crate::read::series_scan::set_candidate_series_assignment_threshold(threshold); + Self + } +} + +impl Drop for AssignmentThresholdScope { + fn drop(&mut self) { + crate::read::series_scan::set_candidate_series_assignment_threshold(0); + } +} + +/// One partition receiving two candidate assignments must still emit one contiguous stream. +/// +/// Without the threshold override a second assignment reaches the same partition only when a +/// single collection round holds `CANDIDATE_SERIES_ASSIGNMENT_THRESHOLD` (1,000,000) candidate +/// series, so this test lowers the threshold instead of writing a million series. +#[tokio::test] +async fn test_two_phase_raw_same_partition_multi_round_assignment_stays_contiguous() { + use common_query::series_frame::{pack_raw_series, unpack_raw_series}; + use datatypes::series::SeriesOrdering; + use datatypes::series::samples::SampleRef; + use store_api::region_engine::QueryScanContext; + + // The candidate distributor starts a new assignment as soon as the collected series reach the + // threshold, and the candidate stream hands series over in globally sorted batches of at most + // `METRIC_SERIES_ID_BATCH_SIZE` (500) entries, yielding a trailing batch for the remainder. + // With the threshold lowered to 1 every one of those batches becomes one assignment round, so + // 501 series produce exactly two non-empty rounds: the first 500 sorted series, then the last + // one. Both rounds are addressed to the single output partition, i.e. to one receiver. + const CANDIDATE_BATCH: usize = 500; + const SERIES: usize = CANDIDATE_BATCH + 1; + let _scope = AssignmentThresholdScope::lower_to(1); + + let mut env = TestEnv::with_prefix("test_two_phase_raw_same_partition_multi_round").await; + let engine = env + .create_engine(MitoConfig { + experimental_series_scan_v2: true, + // The source-composition assertions below pin the flushed SST until the scan opens. + schedule_compaction_after_edit: false, + min_compaction_interval: Duration::from_secs(60 * 60), + ..Default::default() + }) + .await; + let region_id = RegionId::new(1, 1); + let (metadata, row_schema) = create_two_phase_sparse_region(&engine, region_id).await; + + let tags = (0..SERIES) + .map(|index| format!("s{index}")) + .collect::>(); + let keys = tags + .iter() + .enumerate() + .map(|(index, tag)| { + new_sparse_primary_key(&[tag.as_str(), "x"], &metadata, 10, index as u64) + }) + .collect::>(); + // The round split below relies on the candidate stream ordering series by primary key, which + // is the ascending TSID order for this fixture. + let mut sorted_keys = keys.clone(); + sorted_keys.sort(); + assert_eq!(keys, sorted_keys, "primary keys must sort in TSID order"); + + // One flushed SST source plus one memtable source. The wide series spans more than one read + // batch, so a series is emitted as several chunks and the round boundary sits next to chunks + // that must stay grouped. + const WIDE: i64 = 20_000; + const SMALL: i64 = 3; + const SECOND_ROUND_FIRST: i64 = 100_000; + let mut expected_times = HashMap::, Vec>::new(); + expected_times.insert( + keys[0].clone(), + (0..WIDE) + .chain(SECOND_ROUND_FIRST..SECOND_ROUND_FIRST + 3) + .collect(), + ); + for key in &keys[1..] { + expected_times.insert( + key.clone(), + (0..SMALL) + .chain(SECOND_ROUND_FIRST..SECOND_ROUND_FIRST + 3) + .collect(), + ); + } + + let first_round = std::iter::once((keys[0].clone(), 0_i64, WIDE)) + .chain(keys[1..].iter().map(|key| (key.clone(), 0_i64, SMALL))) + .flat_map(|(key, first, count)| { + (0..count).map(move |offset| { + let time = first + offset; + (key.clone(), time, (time + 1) as u64) + }) + }) + .collect::>(); + let second_round = keys + .iter() + .flat_map(|key| { + (0..SMALL).map(move |offset| { + let time = SECOND_ROUND_FIRST + offset; + (key.clone(), time, (time + 1) as u64) + }) + }) + .collect::>(); + let (sst_rows, memtable_rows) = (first_round.len(), second_round.len()); + put_sparse_rows(&engine, region_id, &row_schema, first_round).await; + test_util::flush_region(&engine, region_id, None).await; + put_sparse_rows(&engine, region_id, &row_schema, second_round).await; + + let request = ScanRequest { + distribution: Some(TimeSeriesDistribution::PerSeries), + preserve_pk_dictionary_encoding: true, + ..Default::default() + }; + let selected = engine.scanner(region_id, request).await.unwrap(); + let Scanner::Series(mut scanner) = selected else { + panic!("sparse PerSeries request must use SeriesScan"); + }; + assert_eq!("two_phase", scanner.mode()); + assert_eq!(SeriesOrdering::Contiguous, scanner.raw_series_ordering()); + // The fixture must really be one SST source plus one memtable source, or the two rounds below + // would be explained by something else. + { + let input = scanner.input(); + assert_eq!(1, input.num_files()); + assert_eq!( + vec![sst_rows], + input + .files + .iter() + .map(|file| file.num_rows()) + .collect::>() + ); + assert_eq!(1, input.num_memtables()); + assert_eq!( + memtable_rows, + input + .memtables + .iter() + .map(|memtable| memtable.stats().num_rows) + .sum::() + ); + } + + let source = primary_key_label_source(&scanner.metadata()); + let ranges = scanner + .properties() + .partitions + .clone() + .into_iter() + .flatten() + .collect::>(); + assert!(!ranges.is_empty(), "region scan needs an input range"); + // One output partition means every series the distributor assigns belongs to this receiver, + // so both rounds are delivered to partition 0. + scanner + .prepare(PrepareRequest { + ranges: Some(vec![ranges]), + target_partitions: Some(1), + ..Default::default() + }) + .unwrap(); + assert_eq!(1, scanner.properties().num_partitions()); + + let metrics = ExecutionPlanMetricsSet::default(); + let ctx = QueryScanContext::default(); + let stream = scanner + .scan_series_partition(&ctx, &metrics, 0, &source) + .unwrap() + .expect("two-phase raw scan is supported"); + assert_eq!(SeriesOrdering::Contiguous, stream.ordering()); + let schema = Arc::clone(stream.schema()); + let chunks = stream.try_collect::>().await.unwrap(); + // Two collection rounds really happened; the fixture never reaches the production threshold. + let rounds = crate::read::series_scan::candidate_assignment_rounds(); + assert!( + rounds >= 2, + "the partition must consume more than one assignment round, got {rounds}" + ); + assert!( + !chunks.is_empty(), + "partition 0 must own reader assignments" + ); + + // Partition 0's single stream carries every series in one contiguous run each. `TwoPhase` + // declares only `Contiguous`, so the runs are not contracted to be sorted; the assignment + // composition is: the first assignment holds the first 500 candidate series (which includes + // `keys[0]`) and the trailing assignment holds the last candidate series. + let mut runs = Vec::>::new(); + let mut chunks_per_series = HashMap::, usize>::new(); + for chunk in &chunks { + let key = chunk.primary_key.to_vec(); + *chunks_per_series.entry(key.clone()).or_default() += 1; + if runs.last() != Some(&key) { + assert!( + !runs.contains(&key), + "a series must not resume after another series across a round boundary" + ); + runs.push(key); + } + } + assert_eq!(keys.len(), runs.len(), "every series appears in one run"); + assert!( + chunks_per_series.values().copied().max().unwrap() > 1, + "the fixture must emit multiple chunks for one series, got {chunks_per_series:?}" + ); + // The round boundary: the first run belongs to the first assignment, the last run to the + // second assignment, and both are chunks of the same partition stream. + let (first_assignment, second_assignment) = runs.split_at(SERIES - 1); + assert!( + first_assignment.contains(&keys[0]), + "the first assignment must stream the first key" + ); + assert!( + second_assignment.contains(&keys[SERIES - 1]), + "the second assignment must stream the last key" + ); + let last_round_chunks = chunks + .iter() + .filter(|chunk| chunk.primary_key.as_slice() == keys[SERIES - 1].as_slice()) + .count(); + assert!( + last_round_chunks > 0, + "the series of the second assignment must be streamed" + ); + + let stream_rows = chunks.iter().map(|chunk| chunk.time.len()).sum::(); + assert_eq!( + expected_times.values().map(Vec::len).sum::(), + stream_rows, + "the stream must carry every written row of both rounds" + ); + for (key, times) in &expected_times { + let observed = chunks + .iter() + .filter(|chunk| chunk.primary_key.as_slice() == key.as_slice()) + .flat_map(|chunk| chunk.time.as_buffer().iter().copied()) + .collect::>(); + assert_eq!(times, &observed, "timestamps must increase per series"); + assert!( + observed.windows(2).all(|window| window[0] < window[1]), + "timestamps must strictly increase within a key" + ); + } + for (index, chunk) in chunks.iter().enumerate() { + let next_is_same_series = chunks + .get(index + 1) + .is_some_and(|next| next.primary_key.as_slice() == chunk.primary_key.as_slice()); + assert_eq!( + !next_is_same_series, chunk.is_last, + "is_last must mark the final chunk of a series, including at the round boundary" + ); + let times = chunk.time.as_buffer(); + for (sample, time) in times.iter().enumerate() { + let SampleRef::Float(value) = chunk.fields.get(0).unwrap().sample(sample) else { + panic!("raw fields are converted to float samples"); + }; + assert_eq!((*time + 1) as f64, value); + } + } + + // Route the same partition stream through the frame codec so its `SequenceValidator` checks + // the chunks this reader produced, including the chunks the second assignment added. + let packed = pack_raw_series(Box::pin(ChainedRawSeriesStream::new( + Arc::clone(&schema), + source.clone(), + SeriesOrdering::Contiguous, + chunks.clone(), + ))) + .unwrap(); + let unpacked = unpack_raw_series( + packed, + Arc::clone(&schema), + source, + SeriesOrdering::Contiguous, + ) + .unwrap(); + let validated = unpacked.try_collect::>().await.unwrap(); + assert_eq!( + stream_rows, + validated + .iter() + .map(|chunk| chunk.time.len()) + .sum::(), + "packing and unpacking must preserve the multi-round stream" + ); + let mut schema_fields = schema + .fields + .iter() + .map(|field| field.name.as_ref()) + .collect::>(); + schema_fields.dedup(); + assert_eq!(vec!["field_0"], schema_fields); +} + +/// Creates the sparse-encoded flat region shared by the two-phase raw scan tests. +/// +/// Returns the region metadata and the row schema accepted by the sparse write path. +async fn create_two_phase_sparse_region( + engine: &crate::engine::MitoEngine, + region_id: RegionId, +) -> ( + Arc, + Vec, +) { + let metadata = Arc::new(sst_region_metadata_with_encoding( + PrimaryKeyEncoding::Sparse, + )); + let mut create = CreateRequestBuilder::new().build(); + create.column_metadatas = metadata.column_metadatas.clone(); + create.primary_key = metadata.primary_key.clone(); + create + .options + .insert(PRIMARY_KEY_ENCODING.to_string(), "sparse".to_string()); + create + .options + .insert("memtable.type".to_string(), "bulk".to_string()); + create + .options + .insert("sst_format".to_string(), "flat".to_string()); + let full_schema = test_util::rows_schema(&create); + let mut key_schema = full_schema[0].clone(); + key_schema.column_name = PRIMARY_KEY_COLUMN_NAME.to_string(); + key_schema.datatype = ColumnDataType::Binary.into(); + key_schema.semantic_type = SemanticType::Tag.into(); + let row_schema = vec![key_schema, full_schema[5].clone(), full_schema[4].clone()]; + engine + .handle_request(region_id, RegionRequest::Create(create)) + .await + .unwrap(); + (metadata, row_schema) +} + +/// Writes `(primary key, timestamp, field_0)` rows through the sparse write path. +async fn put_sparse_rows( + engine: &crate::engine::MitoEngine, + region_id: RegionId, + schema: &[api::v1::ColumnSchema], + rows: Vec<(Vec, i64, u64)>, +) { + let rows = rows + .into_iter() + .map(|(key, time, value)| { + row(vec![ + ValueData::BinaryValue(key), + ValueData::TimestampMillisecondValue(time), + ValueData::U64Value(value), + ]) + }) + .collect(); + engine + .handle_request( + region_id, + RegionRequest::Put(RegionPutRequest { + rows: Rows { + schema: schema.to_vec(), + rows, + }, + hint: Some(WriteHint { + primary_key_encoding: api::v1::PrimaryKeyEncoding::Sparse.into(), + }), + skip_wal: false, + partition_expr_version: None, + }), + ) + .await + .unwrap(); +} + +/// Binds every non-internal tag column of `metadata` as the raw label source. +fn primary_key_label_source( + metadata: &store_api::metadata::RegionMetadataRef, +) -> store_api::region_engine::RawLabelSource { + use store_api::region_engine::{BoundTag, RawLabelSource}; + + RawLabelSource::PrimaryKey { + metric_name: Arc::from("fixture_metric"), + physical_metadata: Arc::clone(metadata), + logical_tags: metadata + .column_metadatas + .iter() + .filter(|column| { + column.semantic_type == SemanticType::Tag + && !store_api::storage::consts::is_internal_column(&column.column_schema.name) + && !store_api::metric_engine_consts::is_metric_engine_internal_column( + &column.column_schema.name, + ) + }) + .map(|column| BoundTag { + column_id: column.column_id, + name: Arc::from(column.column_schema.name.as_str()), + }) + .collect::>() + .into(), + } +} + +/// Concatenates the raw chunks of every reader assignment of one scan, in partition order. +struct ChainedRawSeriesStream { + schema: Arc, + label_source: store_api::region_engine::RawLabelSource, + ordering: datatypes::series::SeriesOrdering, + chunks: std::vec::IntoIter, +} + +impl ChainedRawSeriesStream { + fn new( + schema: Arc, + label_source: store_api::region_engine::RawLabelSource, + ordering: datatypes::series::SeriesOrdering, + chunks: Vec, + ) -> Self { + Self { + schema, + label_source, + ordering, + chunks: chunks.into_iter(), + } + } +} + +impl futures::Stream for ChainedRawSeriesStream { + type Item = Result; + + fn poll_next( + mut self: std::pin::Pin<&mut Self>, + _cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + std::task::Poll::Ready(self.chunks.next().map(Ok)) + } +} + +impl store_api::region_engine::RawSeriesStream for ChainedRawSeriesStream { + fn schema(&self) -> &Arc { + &self.schema + } + + fn label_source(&self) -> &store_api::region_engine::RawLabelSource { + &self.label_source + } + + fn ordering(&self) -> datatypes::series::SeriesOrdering { + self.ordering + } +} + #[tokio::test] async fn test_mito_raw_series_int64_predicate_uses_original_precision() { use datatypes::series::samples::SampleRef; @@ -5199,7 +6342,7 @@ async fn test_mito_raw_series_int64_predicate_uses_original_precision() { ); let chunks = stream.try_collect::>().await.unwrap(); for chunk in chunks { - let axis = chunk.time.as_buffer().as_ref(); + let axis = chunk.time.as_buffer(); times.extend(axis.iter().copied()); for (index, &time) in axis.iter().enumerate() { output_values.push(( @@ -5214,6 +6357,115 @@ async fn test_mito_raw_series_int64_predicate_uses_original_precision() { } assert_eq!(vec![2], times); assert_eq!(vec![(2, 20.0)], output_values); + + // Validate the typed integer gate through RegionScanExec's actual physical consumer. + use common_query::series_frame::{raw_series_frame_ordering, unpack_raw_series}; + use datafusion::execution::TaskContext; + use datafusion::physical_plan::ExecutionPlan; + use datatypes::series::SeriesOrdering; + use table::table::scan::RegionScanExec; + let native_request = ScanRequest { + projection: Some(vec![2, 3]), + filters: vec![ + col("tag_0").eq(lit("series")), + col("field_0").gt(lit(boundary)), + ], + distribution: Some(TimeSeriesDistribution::PerSeries), + preserve_pk_dictionary_encoding: true, + ..Default::default() + }; + let native_scanner = engine + .handle_query(region_id, native_request.clone()) + .await + .unwrap(); + let native_metadata = native_scanner.metadata(); + let native_source = RawLabelSource::PrimaryKey { + metric_name: Arc::from("fixture_metric"), + physical_metadata: native_metadata.clone(), + logical_tags: Arc::from([BoundTag { + column_id: native_metadata.primary_key[0], + name: Arc::from("tag_0"), + }]), + }; + let native_plan = Arc::new(RegionScanExec::new(native_scanner, native_request, None).unwrap()) + .try_to_series_scan(native_source.clone()) + .unwrap() + .expect("typed int64 gate is supported"); + let native_schema = native_plan.series_schema().clone(); + let frames = native_plan + .execute(0, Arc::new(TaskContext::default())) + .unwrap(); + // Unpack under the frame's own declaration so the decoder checks the legacy key order. + let ordering = raw_series_frame_ordering(frames.schema().as_ref()).unwrap(); + assert_eq!( + SeriesOrdering::KeyThenTime, + ordering, + "the legacy producer must declare key-then-time frames" + ); + let chunks = unpack_raw_series(frames, native_schema, native_source, ordering) + .unwrap() + .try_collect::>() + .await + .unwrap(); + let native_rows = chunks + .iter() + .flat_map(|chunk| { + let times = chunk.time.as_buffer(); + (0..times.len()) + .map(|i| { + let SampleRef::Float(value) = chunk.fields.get(0).unwrap().sample(i) else { + panic!("expected float output field") + }; + (times[i], value) + }) + .collect::>() + }) + .collect::>(); + assert_eq!(vec![(2, 20.0)], native_rows); + + let empty_request = ScanRequest { + projection: Some(vec![2, 3]), + filters: vec![ + col("tag_0").eq(lit("series")), + col("field_0").gt(lit(boundary + 1)), + ], + distribution: Some(TimeSeriesDistribution::PerSeries), + preserve_pk_dictionary_encoding: true, + ..Default::default() + }; + let empty_scanner = engine + .handle_query(region_id, empty_request.clone()) + .await + .unwrap(); + let empty_metadata = empty_scanner.metadata(); + let empty_source = RawLabelSource::PrimaryKey { + metric_name: Arc::from("fixture_metric"), + physical_metadata: empty_metadata.clone(), + logical_tags: Arc::from([BoundTag { + column_id: empty_metadata.primary_key[0], + name: Arc::from("tag_0"), + }]), + }; + let empty_plan = Arc::new(RegionScanExec::new(empty_scanner, empty_request, None).unwrap()) + .try_to_series_scan(empty_source.clone()) + .unwrap() + .expect("statically empty scan still has native capability"); + let empty_schema = empty_plan.series_schema().clone(); + let empty_frames = empty_plan + .execute(0, Arc::new(TaskContext::default())) + .unwrap(); + let empty_ordering = raw_series_frame_ordering(empty_frames.schema().as_ref()).unwrap(); + assert_eq!( + SeriesOrdering::KeyThenTime, + empty_ordering, + "a statically empty legacy scan still declares key-then-time frames" + ); + let empty_chunks = unpack_raw_series(empty_frames, empty_schema, empty_source, empty_ordering) + .unwrap() + .try_collect::>() + .await + .unwrap(); + assert!(empty_chunks.is_empty()); } /// Invalid predicate bindings decline before consumption; explicit labels decline and legacy still reads. @@ -5304,6 +6556,11 @@ async fn test_mito_raw_series_invalid_binding_before_legacy_read() { name: Arc::from("tag_0"), }]), }; + assert_eq!( + supported, + scanner.raw_series_schema(&valid_source).unwrap().is_some(), + "schema preflight case: {name}" + ); let raw = scanner .scan_series_partition(&ctx, &metrics, 0, &valid_source) .unwrap(); @@ -5313,7 +6570,7 @@ async fn test_mito_raw_series_invalid_binding_before_legacy_read() { let tuples = chunks .iter() .map(|chunk| { - let times = chunk.time.as_buffer().as_ref(); + let times = chunk.time.as_buffer(); let SampleRef::Float(value) = chunk.fields.get(0).unwrap().sample(0) else { panic!("expected raw float sample"); }; @@ -5387,6 +6644,13 @@ async fn test_mito_raw_series_invalid_binding_before_legacy_read() { .unwrap() .is_none() ); + assert!( + scanner + .raw_series_schema(&RawLabelSource::Explicit) + .unwrap() + .is_none() + ); + assert!(scanner.raw_series_schema(&valid_shape_bad_tag).is_err()); assert!( scanner .scan_series_partition(&ctx, &metrics, 0, &valid_shape_bad_tag) @@ -5427,4 +6691,70 @@ async fn test_mito_raw_series_invalid_binding_before_legacy_read() { vec![("0".to_string(), 0.0, 0), ("1".to_string(), 1.0, 1000)], rows ); + + // Runtime pruning wrappers are deliberately ineligible for native planning, and + // wrappers installed after planning must be caught at execute time. + use datafusion::execution::TaskContext; + use datafusion::physical_plan::ExecutionPlan; + use table::table::scan::RegionScanExec; + let dynamic_request = ScanRequest { + distribution: Some(TimeSeriesDistribution::PerSeries), + preserve_pk_dictionary_encoding: true, + ..Default::default() + }; + let dynamic_source_for = |scanner: &dyn RegionScanner| { + let metadata = scanner.metadata(); + RawLabelSource::PrimaryKey { + metric_name: Arc::from("fixture_metric"), + physical_metadata: metadata.clone(), + logical_tags: Arc::from([BoundTag { + column_id: metadata.primary_key[0], + name: Arc::from("tag_0"), + }]), + } + }; + let make_dynamic = || { + Arc::new(DynamicFilterPhysicalExpr::new( + vec![Arc::new(Column::new("field_0", 1))], + physical_lit(true), + )) + }; + let before = engine + .handle_query(region_id, dynamic_request.clone()) + .await + .unwrap(); + let before_source = dynamic_source_for(before.as_ref()); + let before_scan = Arc::new(RegionScanExec::new(before, dynamic_request.clone(), None).unwrap()); + assert_eq!( + vec![true], + before_scan.add_dyn_filters_to_predicate(vec![make_dynamic()]) + ); + assert!( + before_scan + .clone() + .try_to_series_scan(before_source) + .unwrap() + .is_none() + ); + + let after = engine + .handle_query(region_id, dynamic_request.clone()) + .await + .unwrap(); + let after_source = dynamic_source_for(after.as_ref()); + let after_scan = Arc::new(RegionScanExec::new(after, dynamic_request, None).unwrap()); + let after_plan = after_scan + .clone() + .try_to_series_scan(after_source) + .unwrap() + .expect("native plan before dynamic wrapper installation"); + assert_eq!( + vec![true], + after_scan.add_dyn_filters_to_predicate(vec![make_dynamic()]) + ); + assert!( + after_plan + .execute(0, Arc::new(TaskContext::default())) + .is_err() + ); } diff --git a/src/mito2/src/read/series_candidate.rs b/src/mito2/src/read/series_candidate.rs index 3ba5512a7bd..82210e0ffa7 100644 --- a/src/mito2/src/read/series_candidate.rs +++ b/src/mito2/src/read/series_candidate.rs @@ -243,6 +243,10 @@ impl SeriesIndexCoverage { .version .series_indexes .values() + // Unimplemented formats cannot replace authoritative SST candidates. + .filter(|index| { + index.entry().format() == crate::series_index::artifact::ArtifactFormat::Parquet + }) .map(|index| { let files: HashSet<_> = uncovered .iter() @@ -778,6 +782,7 @@ mod tests { )); let store = env.access_layer.object_store().clone(); let entry = SeriesIndexEntry { + artifact: None, file_size: 0, index_uuid: FileId::random(), bucket_start: Timestamp::new_millisecond(0), @@ -964,6 +969,48 @@ mod tests { } } + #[tokio::test] + async fn index_merge_releases_permits_before_yielding() { + let (_env, scanner, _pruner) = indexed_scanner().await; + let semaphore = Arc::new(Semaphore::new(1)); + let context = scanner.stream_ctx.input.series_index.clone().unwrap(); + let streams = (0..3) + .map(|_| { + index_primary_key_stream( + scanner.stream_ctx.clone(), + context.clone(), + scanner.coverage.indexes[0].clone(), + semaphore.clone(), + ) + }) + .collect(); + let merged = merge_primary_key_streams( + streams, + scanner.memory_pool.clone(), + &scanner.metrics_set, + 0, + "permit-regression", + ) + .unwrap(); + let series = + decode_metric_series(merged, scanner.stream_ctx.input.region_metadata().clone()) + .unwrap(); + let batches = tokio::time::timeout( + std::time::Duration::from_secs(10), + series.try_collect::>(), + ) + .await + .expect("index streams must not hold permits across yield") + .unwrap(); + assert_eq!( + (0..1001) + .map(|tsid| MetricSeriesId { table_id: 1, tsid }) + .collect::>(), + batches.into_iter().flatten().collect::>() + ); + assert_eq!(1, semaphore.available_permits()); + } + #[tokio::test] async fn index_read_failure_after_output_is_propagated() { let (_env, scanner, _pruner) = indexed_scanner().await; diff --git a/src/mito2/src/read/series_scan.rs b/src/mito2/src/read/series_scan.rs index eb5c1048ab5..971ff93a742 100644 --- a/src/mito2/src/read/series_scan.rs +++ b/src/mito2/src/read/series_scan.rs @@ -31,13 +31,15 @@ use common_recordbatch::{RecordBatchStreamWrapper, SendableRecordBatchStream}; use common_telemetry::tracing::{self, Instrument}; use common_telemetry::{debug, warn}; use datafusion::physical_plan::metrics::ExecutionPlanMetricsSet; -use datafusion::physical_plan::{DisplayAs, DisplayFormatType}; +use datafusion::physical_plan::{DisplayAs, DisplayFormatType, PhysicalExpr}; use datafusion_common::tree_node::TreeNode; use datafusion_expr::{Expr, Volatility}; use datatypes::arrow::array::BinaryArray; use datatypes::arrow::datatypes::DataType as ArrowDataType; use datatypes::arrow::record_batch::RecordBatch; +use datatypes::prelude::ConcreteDataType; use datatypes::schema::SchemaRef; +use datatypes::series::SeriesOrdering; use datatypes::series::samples::{FieldSpec, SeriesSchema}; use datatypes::types::TimestampType; use futures::{StreamExt, TryStreamExt}; @@ -79,6 +81,49 @@ const SEND_TIMEOUT: Duration = Duration::from_micros(100); /// Maximum number of candidate series retained before distributing assignments. const CANDIDATE_SERIES_ASSIGNMENT_THRESHOLD: usize = 1_000_000; +/// Test-only override of [`CANDIDATE_SERIES_ASSIGNMENT_THRESHOLD`], where `0` means "use the +/// production constant". +/// +/// The production build compiles this static, the setter below and the private accessor out, so +/// the shipped threshold is always the constant above. +#[cfg(test)] +static CANDIDATE_SERIES_ASSIGNMENT_THRESHOLD_OVERRIDE: AtomicUsize = AtomicUsize::new(0); + +/// Test-only number of candidate assignment rounds sent since the override was last set. +/// +/// A round is one `send_assignments` call, so a partition that received more than one assignment +/// sees `>= 2` rounds here. Increments are compiled out of production builds. +#[cfg(test)] +static CANDIDATE_ASSIGNMENT_ROUNDS: AtomicUsize = AtomicUsize::new(0); + +/// Returns the candidate-series assignment threshold, honouring the test-only override. +fn candidate_series_assignment_threshold() -> usize { + #[cfg(test)] + { + let threshold = CANDIDATE_SERIES_ASSIGNMENT_THRESHOLD_OVERRIDE.load(Ordering::Relaxed); + if threshold > 0 { + return threshold; + } + } + CANDIDATE_SERIES_ASSIGNMENT_THRESHOLD +} + +/// Test-only: overrides the candidate-series assignment threshold and resets the round counter. +/// +/// Pass `0` to restore the production threshold. Tests must reset the override before they +/// return; a scope guard in `engine::scan_test` does that even on panic. +#[cfg(test)] +pub(crate) fn set_candidate_series_assignment_threshold(threshold: usize) { + CANDIDATE_SERIES_ASSIGNMENT_THRESHOLD_OVERRIDE.store(threshold, Ordering::Relaxed); + CANDIDATE_ASSIGNMENT_ROUNDS.store(0, Ordering::Relaxed); +} + +/// Test-only: returns the number of assignment rounds sent since the override was last set. +#[cfg(test)] +pub(crate) fn candidate_assignment_rounds() -> usize { + CANDIDATE_ASSIGNMENT_ROUNDS.load(Ordering::Relaxed) +} + /// Legacy data receivers for output partitions. type LegacyReceiverList = Vec>>>; @@ -88,6 +133,12 @@ type CandidateReceiverList = Vec>; /// Candidate assignment senders for output partitions. type CandidateSenderList = Vec>>>; +struct RawScanPreparation { + schema: Arc, + fields: Vec<(usize, ConcreteDataType)>, + predicates: Vec>, +} + struct CandidateReceiver { receiver: mpsc::UnboundedReceiver>, active_receivers: Arc, @@ -194,6 +245,189 @@ impl SeriesScan { } } + fn prepare_raw_scan( + &self, + label_source: &RawLabelSource, + ) -> Result, BoxedError> { + let input = &self.stream_ctx.input; + let metadata = input.mapper.metadata(); + match label_source { + RawLabelSource::PrimaryKey { + physical_metadata, + logical_tags, + .. + } => { + if !Arc::ptr_eq(metadata, physical_metadata) { + debug!( + "Raw series scan preflight rejected metadata snapshot: region_id={}, physical_region_id={}, schema_version={}, physical_schema_version={}", + metadata.region_id, + physical_metadata.region_id, + metadata.schema_version, + physical_metadata.schema_version, + ); + return Err(BoxedError::new( + InvalidRecordBatchSnafu { + reason: "raw label source metadata does not match scanner snapshot" + .to_string(), + } + .build(), + )); + } + let mut ids = HashSet::new(); + let mut names = HashSet::new(); + for tag in logical_tags.iter() { + let column = metadata.column_by_id(tag.column_id).filter(|column| { + column.semantic_type == SemanticType::Tag + && metadata.primary_key.contains(&tag.column_id) + }); + let valid = column.is_some_and(|column| { + column.column_schema.name == tag.name.as_ref() + && tag.name.as_ref() != "__name__" + && !is_internal_column(tag.name.as_ref()) + && !is_metric_engine_internal_column(tag.name.as_ref()) + && !ReservedColumnId::is_reserved(tag.column_id) + }); + let unique_id = valid && ids.insert(tag.column_id); + let unique_name = unique_id && names.insert(tag.name.as_ref()); + if !valid || !unique_id || !unique_name { + debug!( + "Raw series scan preflight rejected label binding: region_id={}, column_id={}, binding_valid={}, unique_id={}, unique_name={}", + metadata.region_id, tag.column_id, valid, unique_id, unique_name, + ); + return Err(BoxedError::new( + InvalidRecordBatchSnafu { + reason: "invalid raw label binding".to_string(), + } + .build(), + )); + } + } + } + RawLabelSource::Explicit => { + debug!( + "Raw series scan preflight rejected explicit labels: region_id={}", + metadata.region_id, + ); + return Ok(None); + } + } + let selected = input.mapper.output_schema(); + let mut fields = Vec::new(); + let mut specs = Vec::new(); + for output in selected.column_schemas() { + let Some(column) = metadata.column_metadatas.iter().find(|column| { + column.column_schema.name == output.name + && column.semantic_type == SemanticType::Field + }) else { + continue; + }; + let Some((index, (_, data_type))) = input + .mapper + .batch_schema() + .iter() + .enumerate() + .find(|(_, (id, _))| *id == column.column_id) + else { + debug!( + "Raw series scan preflight missing mapper field: region_id={}, column_id={}", + metadata.region_id, column.column_id, + ); + return Err(BoxedError::new( + InvalidRecordBatchSnafu { + reason: format!( + "selected field {} is missing from mapper batch schema", + output.name + ), + } + .build(), + )); + }; + if !data_type.is_numeric() && !is_native_histogram_value_type(data_type) { + debug!( + "Raw series scan preflight rejected field type: region_id={}, column_id={}", + metadata.region_id, column.column_id, + ); + return Ok(None); + } + fields.push((index, data_type.clone())); + specs.push(FieldSpec { + name: Arc::from(output.name.as_str()), + }); + } + let time_is_millis = matches!(metadata.time_index_type(), TimestampType::Millisecond(_)); + if fields.is_empty() + || input.append_mode + || input.series_row_selector.is_some() + || !time_is_millis + { + debug!( + "Raw series scan preflight rejected scan options: region_id={}, selected_fields={}, append_mode={}, row_selector={}, time_is_millis={}", + metadata.region_id, + fields.len(), + input.append_mode, + input.series_row_selector.is_some(), + time_is_millis, + ); + return Ok(None); + } + let schema = Arc::new(SeriesSchema { + fields: specs.into(), + eval_grid: None, + }); + let typed_schema = input.mapper.input_arrow_schema(false); + let mut predicates = Vec::new(); + if let Some(predicate) = input.predicate_group().predicate_without_region() { + if !predicate.dyn_filters().is_empty() { + debug!( + "Raw series scan preflight rejected dynamic predicate filters: region_id={}", + metadata.region_id, + ); + return Ok(None); + } + for expr in predicate.exprs() { + // Fresh execution properties cannot preserve Stable or query-dependent semantics. + if expr + .exists(|node| { + Ok(matches!(node, Expr::ScalarFunction(function) + if function.func.signature().volatility != Volatility::Immutable) + || matches!(node, Expr::ScalarVariable(..))) + }) + .unwrap_or(true) + { + debug!( + "Raw series scan preflight rejected predicate volatility or variable: region_id={}", + metadata.region_id, + ); + return Ok(None); + } + // Evaluate residuals with original types before raw conversion to Float64. + let Ok(physical) = Predicate::to_physical_expr(expr, &typed_schema) else { + debug!( + "Raw series scan preflight failed predicate compilation: region_id={}", + metadata.region_id, + ); + return Ok(None); + }; + if !matches!( + physical.data_type(&typed_schema), + Ok(ArrowDataType::Boolean | ArrowDataType::Null) + ) { + debug!( + "Raw series scan preflight rejected predicate type: region_id={}", + metadata.region_id, + ); + return Ok(None); + } + predicates.push(physical); + } + } + Ok(Some(RawScanPreparation { + schema, + fields, + predicates, + })) + } + fn supports_two_phase(input: &ScanInput) -> bool { if !is_sparse_metric_metadata(input.region_metadata()) { return false; @@ -578,6 +812,22 @@ impl RegionScanner for SeriesScan { .map_err(BoxedError::new) } + fn raw_series_ordering(&self) -> SeriesOrdering { + match self.mode { + SeriesScanMode::Legacy => SeriesOrdering::KeyThenTime, + SeriesScanMode::TwoPhase => SeriesOrdering::Contiguous, + } + } + + fn raw_series_schema( + &self, + label_source: &RawLabelSource, + ) -> Result>, BoxedError> { + Ok(self + .prepare_raw_scan(label_source)? + .map(|preparation| preparation.schema)) + } + fn scan_series_partition( &self, ctx: &QueryScanContext, @@ -585,171 +835,9 @@ impl RegionScanner for SeriesScan { partition: usize, label_source: &RawLabelSource, ) -> Result, BoxedError> { - let input = &self.stream_ctx.input; - let metadata = input.mapper.metadata(); - match label_source { - RawLabelSource::PrimaryKey { - physical_metadata, - logical_tags, - .. - } => { - if !Arc::ptr_eq(metadata, physical_metadata) { - debug!( - "Raw series scan preflight rejected metadata snapshot: region_id={}, physical_region_id={}, schema_version={}, physical_schema_version={}", - metadata.region_id, - physical_metadata.region_id, - metadata.schema_version, - physical_metadata.schema_version, - ); - return Err(BoxedError::new( - InvalidRecordBatchSnafu { - reason: "raw label source metadata does not match scanner snapshot" - .to_string(), - } - .build(), - )); - } - let mut ids = HashSet::new(); - let mut names = HashSet::new(); - for tag in logical_tags.iter() { - let column = metadata.column_by_id(tag.column_id).filter(|column| { - column.semantic_type == SemanticType::Tag - && metadata.primary_key.contains(&tag.column_id) - }); - let valid = column.is_some_and(|column| { - column.column_schema.name == tag.name.as_ref() - && tag.name.as_ref() != "__name__" - && !is_internal_column(tag.name.as_ref()) - && !is_metric_engine_internal_column(tag.name.as_ref()) - && !ReservedColumnId::is_reserved(tag.column_id) - }); - let unique_id = valid && ids.insert(tag.column_id); - let unique_name = unique_id && names.insert(tag.name.as_ref()); - if !valid || !unique_id || !unique_name { - debug!( - "Raw series scan preflight rejected label binding: region_id={}, column_id={}, binding_valid={}, unique_id={}, unique_name={}", - metadata.region_id, tag.column_id, valid, unique_id, unique_name, - ); - return Err(BoxedError::new( - InvalidRecordBatchSnafu { - reason: "invalid raw label binding".to_string(), - } - .build(), - )); - } - } - } - RawLabelSource::Explicit => { - debug!( - "Raw series scan preflight rejected explicit labels: region_id={}", - metadata.region_id, - ); - return Ok(None); - } - } - let selected = input.mapper.output_schema(); - let mut fields = Vec::new(); - let mut specs = Vec::new(); - for output in selected.column_schemas() { - let Some(column) = metadata.column_metadatas.iter().find(|column| { - column.column_schema.name == output.name - && column.semantic_type == SemanticType::Field - }) else { - continue; - }; - let Some((index, (_, data_type))) = input - .mapper - .batch_schema() - .iter() - .enumerate() - .find(|(_, (id, _))| *id == column.column_id) - else { - debug!( - "Raw series scan preflight missing mapper field: region_id={}, column_id={}", - metadata.region_id, column.column_id, - ); - return Err(BoxedError::new( - InvalidRecordBatchSnafu { - reason: format!( - "selected field {} is missing from mapper batch schema", - output.name - ), - } - .build(), - )); - }; - if !data_type.is_numeric() && !is_native_histogram_value_type(data_type) { - debug!( - "Raw series scan preflight rejected field type: region_id={}, column_id={}", - metadata.region_id, column.column_id, - ); - return Ok(None); - } - fields.push((index, data_type.clone())); - specs.push(FieldSpec { - name: Arc::from(output.name.as_str()), - }); - } - let time_is_millis = matches!(metadata.time_index_type(), TimestampType::Millisecond(_)); - if fields.is_empty() - || input.append_mode - || input.series_row_selector.is_some() - || !time_is_millis - { - debug!( - "Raw series scan preflight rejected scan options: region_id={}, selected_fields={}, append_mode={}, row_selector={}, time_is_millis={}", - metadata.region_id, - fields.len(), - input.append_mode, - input.series_row_selector.is_some(), - time_is_millis, - ); + let Some(preparation) = self.prepare_raw_scan(label_source)? else { return Ok(None); - } - let schema = Arc::new(SeriesSchema { - fields: specs.into(), - eval_grid: None, - }); - let typed_schema = input.mapper.input_arrow_schema(false); - let mut predicates = Vec::new(); - if let Some(predicate) = input.predicate_group().predicate_without_region() { - for expr in predicate.exprs() { - // Fresh execution properties cannot preserve Stable or query-dependent semantics. - if expr - .exists(|node| { - Ok(matches!(node, Expr::ScalarFunction(function) - if function.func.signature().volatility != Volatility::Immutable) - || matches!(node, Expr::ScalarVariable(..))) - }) - .unwrap_or(true) - { - debug!( - "Raw series scan preflight rejected predicate volatility or variable: region_id={}", - metadata.region_id, - ); - return Ok(None); - } - // Evaluate residuals with original types before raw conversion to Float64. - let Ok(physical) = Predicate::to_physical_expr(expr, &typed_schema) else { - debug!( - "Raw series scan preflight failed predicate compilation: region_id={}", - metadata.region_id, - ); - return Ok(None); - }; - if !matches!( - physical.data_type(&typed_schema), - Ok(ArrowDataType::Boolean | ArrowDataType::Null) - ) { - debug!( - "Raw series scan preflight rejected predicate type: region_id={}", - metadata.region_id, - ); - return Ok(None); - } - predicates.push(physical); - } - } + }; let metrics = new_partition_metrics( &self.stream_ctx, ctx.explain_verbose, @@ -760,13 +848,15 @@ impl RegionScanner for SeriesScan { let batches = self .scan_batch_in_partition(ctx, partition, metrics.clone(), metrics_set) .map_err(BoxedError::new)?; + let ordering = self.raw_series_ordering(); Ok(Some(raw::make_stream( batches, - schema, + preparation.schema, label_source.clone(), - fields, + preparation.fields, metrics, - predicates, + preparation.predicates, + ordering, ))) } @@ -1096,7 +1186,7 @@ impl SeriesCandidateDistributor { return Ok(()); } collector.push(batch); - if collector.len() >= CANDIDATE_SERIES_ASSIGNMENT_THRESHOLD { + if collector.len() >= candidate_series_assignment_threshold() { chunked = true; self.send_assignments(collector.finish(false), &partition_pruner); if !self.should_fetch_candidates() { @@ -1120,6 +1210,9 @@ impl SeriesCandidateDistributor { assignments: Vec, partition_pruner: &Arc, ) { + // One round of candidate collection; a partition that receives two assignments sees two. + #[cfg(test)] + CANDIDATE_ASSIGNMENT_ROUNDS.fetch_add(1, Ordering::Relaxed); for (partition, assigned_series) in assignments.into_iter().enumerate() { if assigned_series.series().is_empty() { continue; diff --git a/src/mito2/src/read/series_scan/raw.rs b/src/mito2/src/read/series_scan/raw.rs index 6ed41bf8c14..4adac4263ae 100644 --- a/src/mito2/src/read/series_scan/raw.rs +++ b/src/mito2/src/read/series_scan/raw.rs @@ -128,6 +128,7 @@ fn chunk( struct RawStream { schema: Arc, label_source: RawLabelSource, + ordering: SeriesOrdering, inner: BoxStream<'static, Result>, } @@ -146,7 +147,7 @@ impl RawSeriesStream for RawStream { &self.label_source } fn ordering(&self) -> SeriesOrdering { - SeriesOrdering::Contiguous + self.ordering } } @@ -157,15 +158,16 @@ pub(crate) fn make_stream( fields: Vec<(usize, ConcreteDataType)>, metrics: PartitionMetrics, predicates: Vec>, + ordering: SeriesOrdering, ) -> SendableRawSeriesStream { let output_schema = schema.clone(); let stream = async_stream::stream! { let mut input = inner; let mut pending: Option = None; while let Some(item) = input.next().await { - let (records, is_legacy) = match item { - Ok(ScanBatch::Series(SeriesBatch::Flat(flat))) => (flat.batches.into_vec(), true), - Ok(ScanBatch::RecordBatch(batch)) => (vec![batch], false), + let records = match item { + Ok(ScanBatch::Series(SeriesBatch::Flat(flat))) => flat.batches.into_vec(), + Ok(ScanBatch::RecordBatch(batch)) => vec![batch], Err(error) => { yield Err(BoxedError::new(error)); return; } }; for record in records { @@ -238,7 +240,7 @@ pub(crate) fn make_stream( }; metrics.inc_convert_batch_cost(begun.elapsed()); if let Some(old) = pending.as_ref() - && is_legacy + && ordering == SeriesOrdering::KeyThenTime && old.primary_key.as_slice() != current.primary_key.as_slice() && current.primary_key.as_slice() <= old.primary_key.as_slice() { @@ -265,6 +267,7 @@ pub(crate) fn make_stream( Box::pin(RawStream { schema, label_source, + ordering, inner: Box::pin(stream), }) } diff --git a/src/mito2/src/read/series_scan/raw/tests.rs b/src/mito2/src/read/series_scan/raw/tests.rs index 22dd3eefa15..1fdd703b00d 100644 --- a/src/mito2/src/read/series_scan/raw/tests.rs +++ b/src/mito2/src/read/series_scan/raw/tests.rs @@ -37,6 +37,7 @@ use datatypes::arrow::record_batch::RecordBatch; use datatypes::data_type::DataType as _; use datatypes::prelude::ConcreteDataType; use datatypes::schema::ColumnSchema; +use datatypes::series::SeriesOrdering; use datatypes::series::raw::RawSeriesChunk; use datatypes::series::samples::{FieldSpec, SampleRef, SeriesSchema}; use futures::{Stream, StreamExt, stream}; @@ -239,6 +240,7 @@ fn convert_with_source( schema: Arc, source: RawLabelSource, field_type: ConcreteDataType, + ordering: SeriesOrdering, ) -> SendableRawSeriesStream { make_stream( inner, @@ -247,6 +249,7 @@ fn convert_with_source( vec![(0, field_type)], metrics(), vec![], + ordering, ) } @@ -267,6 +270,17 @@ fn convert(inner: ScanBatchStream, schema: Arc) -> SendableRawSeri schema, primary_key_source(metadata()), ConcreteDataType::int64_datatype(), + SeriesOrdering::Contiguous, + ) +} + +fn convert_ordered(inner: ScanBatchStream, schema: Arc) -> SendableRawSeriesStream { + convert_with_source( + inner, + schema, + primary_key_source(metadata()), + ConcreteDataType::int64_datatype(), + SeriesOrdering::KeyThenTime, ) } @@ -370,6 +384,59 @@ async fn chunks_preserve_keys_values_times_and_final_flags_for_both_input_shapes } } +#[tokio::test] +async fn raw_stream_reports_and_enforces_declared_ordering() { + let stream = convert(input(vec![]), schema()); + assert_eq!(stream.ordering(), SeriesOrdering::Contiguous); + drop(stream); + let stream = convert_ordered(input(vec![]), schema()); + assert_eq!(stream.ordering(), SeriesOrdering::KeyThenTime); + drop(stream); + + let mut output = convert_ordered( + input(vec![ + Ok(ScanBatch::RecordBatch(dictionary_batch( + &[b"z"], + &[1], + &[Some(1)], + ))), + Ok(ScanBatch::RecordBatch(dictionary_batch( + &[b"a"], + &[1], + &[Some(2)], + ))), + ]), + schema(), + ); + assert!(output.next().await.unwrap().is_err()); + assert!(output.next().await.is_none()); + + let mut output = convert( + input(vec![ + Ok(ScanBatch::RecordBatch(dictionary_batch( + &[b"z"], + &[1], + &[Some(1)], + ))), + Ok(ScanBatch::RecordBatch(dictionary_batch( + &[b"a"], + &[1], + &[Some(2)], + ))), + ]), + schema(), + ); + assert_eq!( + output.next().await.unwrap().unwrap().primary_key.as_slice(), + b"z" + ); + assert_eq!( + output.next().await.unwrap().unwrap().primary_key.as_slice(), + b"a" + ); + assert!(output.next().await.is_none()); +} + #[tokio::test] async fn typed_predicate_filters_before_numeric_samples_are_cast() { let threshold = 1_i64 << 53; @@ -395,6 +462,7 @@ async fn typed_predicate_filters_before_numeric_samples_are_cast() { vec![(0, ConcreteDataType::int64_datatype())], metrics(), vec![predicate], + SeriesOrdering::Contiguous, ); let chunk = output.next().await.unwrap().unwrap(); assert!(chunk.is_last); @@ -424,6 +492,7 @@ async fn predicate_error_is_terminal() { vec![(0, ConcreteDataType::int64_datatype())], metrics(), vec![predicate], + SeriesOrdering::Contiguous, ); assert!(output.next().await.unwrap().is_err()); assert!(output.next().await.is_none()); @@ -515,7 +584,7 @@ async fn legacy_series_requires_increasing_keys_but_record_batches_do_not() { let mut batches = smallvec::SmallVec::new(); batches.push(dictionary_batch(&[b"b"], &[1], &[Some(1)])); batches.push(dictionary_batch(&[b"a"], &[2], &[Some(2)])); - let mut output = convert( + let mut output = convert_ordered( input(vec![Ok(ScanBatch::Series(SeriesBatch::Flat( FlatSeriesBatch { batches }, )))]), @@ -528,7 +597,7 @@ async fn legacy_series_requires_increasing_keys_but_record_batches_do_not() { batches.push(dictionary_batch(&[b"a"], &[1], &[Some(1)])); batches.push(dictionary_batch(&[b"b"], &[1], &[Some(2)])); batches.push(dictionary_batch(&[b"a"], &[2], &[Some(3)])); - let mut output = convert( + let mut output = convert_ordered( input(vec![Ok(ScanBatch::Series(SeriesBatch::Flat( FlatSeriesBatch { batches }, )))]), @@ -545,7 +614,7 @@ async fn legacy_series_requires_increasing_keys_but_record_batches_do_not() { let mut batches = smallvec::SmallVec::new(); batches.push(dictionary_batch(&[b"a"], &[1], &[Some(1)])); batches.push(dictionary_batch(&[b"b"], &[1], &[Some(2)])); - let mut output = convert( + let mut output = convert_ordered( input(vec![Ok(ScanBatch::Series(SeriesBatch::Flat( FlatSeriesBatch { batches }, )))]), @@ -636,6 +705,7 @@ async fn native_histogram_adapter_preserves_complete_payload() { native_histogram_value_type().clone(), )), native_histogram_value_type().clone(), + SeriesOrdering::Contiguous, ); assert!(matches!( raw.label_source(), diff --git a/src/mito2/src/series_index.rs b/src/mito2/src/series_index.rs index 3aa70256709..585120628e8 100644 --- a/src/mito2/src/series_index.rs +++ b/src/mito2/src/series_index.rs @@ -16,9 +16,13 @@ //! //! Under development. Index files are currently stored on the local filesystem. +pub(crate) mod artifact; mod bucket; mod builder; mod catalog; +pub(crate) mod format; +#[cfg(test)] +mod format_tests; mod maintenance; mod purger; mod searcher; diff --git a/src/mito2/src/series_index/artifact.rs b/src/mito2/src/series_index/artifact.rs new file mode 100644 index 00000000000..e1412920f6f --- /dev/null +++ b/src/mito2/src/series_index/artifact.rs @@ -0,0 +1,93 @@ +// 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. + +//! Immutable, potentially multi-object format outputs and reader-owned lifetimes. + +use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; + +use serde::{Deserialize, Serialize}; +use store_api::storage::FileId; + +use crate::series_index::purger::IndexFilePurger; +use crate::sst::file::RegionFileId; + +/// Persisted artifact identifiers. Concrete codecs are added with their implementations. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub(crate) enum ArtifactFormat { + #[serde(rename = "parquet")] + Parquet, + #[cfg(test)] + #[serde(rename = "fake-samples")] + FakeSamples, + #[cfg(test)] + #[serde(rename = "fake-index")] + FakeIndex, +} + +/// Identifies the exact bytes produced by one backend build. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub(crate) struct ArtifactMetadata { + pub(crate) format: ArtifactFormat, + pub(crate) generation: FileId, + /// Every object owned by this build, including metadata and auxiliary files. + pub(crate) objects: Vec, + pub(crate) file_size: u64, +} + +/// One samples artifact derived from an immutable Parquet SST. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub(crate) struct SamplesEntry { + pub(crate) file_id: FileId, + pub(crate) artifact: ArtifactMetadata, +} + +#[derive(Debug, Clone)] +pub(crate) struct SamplesFileHandle(Arc); + +#[derive(Debug)] +struct SamplesFileInner { + file_id: RegionFileId, + entry: SamplesEntry, + retired: AtomicBool, + purger: IndexFilePurger, +} + +impl SamplesFileHandle { + pub(crate) fn new(file_id: RegionFileId, entry: SamplesEntry, purger: IndexFilePurger) -> Self { + Self(Arc::new(SamplesFileInner { + file_id, + entry, + retired: AtomicBool::new(false), + purger, + })) + } + + pub(crate) fn entry(&self) -> &SamplesEntry { + &self.0.entry + } + + pub(crate) fn mark_deleted(&self) { + self.0.retired.store(true, Ordering::Release); + } +} + +impl Drop for SamplesFileInner { + fn drop(&mut self) { + if self.retired.load(Ordering::Acquire) { + self.purger + .purge_objects(self.file_id, self.entry.artifact.objects.clone()); + } + } +} diff --git a/src/mito2/src/series_index/bucket.rs b/src/mito2/src/series_index/bucket.rs index 446948838f6..470caeadd3b 100644 --- a/src/mito2/src/series_index/bucket.rs +++ b/src/mito2/src/series_index/bucket.rs @@ -334,6 +334,7 @@ impl SeriesBucket { .filter_map(|file| file.meta_ref().sequence.map(|sequence| sequence.get())) .min()?; Some(SeriesIndexEntry { + artifact: None, file_size: 0, index_uuid: FileId::random(), bucket_start: self.start, diff --git a/src/mito2/src/series_index/builder.rs b/src/mito2/src/series_index/builder.rs index 63a51badaeb..2a9db9503e3 100644 --- a/src/mito2/src/series_index/builder.rs +++ b/src/mito2/src/series_index/builder.rs @@ -35,6 +35,7 @@ use crate::series_index::bucket::SeriesBucket; use crate::series_index::catalog::{ RangeIndexEntry, SeriesIndexEntry, range_index_path, series_index_path, series_metadata, }; +use crate::series_index::format::SeriesIndexFormat; use crate::series_index::purger::{IndexFilePurger, IndexFileType, file_operation}; use crate::series_index::version::SeriesIndexFileHandle; use crate::series_index::{SeriesIndexWriter, SeriesIndexWriterOptions}; @@ -130,6 +131,7 @@ pub(crate) async fn build_series_index( version: &VersionRef, bucket: &SeriesBucket, entry: &SeriesIndexEntry, + backend: Option<&dyn SeriesIndexFormat>, purger: &IndexFilePurger, ) -> Result { let mut sources = Vec::::new(); @@ -180,6 +182,35 @@ pub(crate) async fn build_series_index( ) }; // TODO(yingwen): Deduplicate update-mode rows before series indexes are used by queries. + if let Some(backend) = backend { + let mut writer = backend + .writer(version.metadata.clone(), store.clone(), entry) + .await?; + let result: Result<()> = async { + while let Some(batch) = visible.try_next().await? { + writer.write(&batch).await?; + } + Ok(()) + } + .await; + if let Err(error) = result { + if let Err(cleanup_error) = writer.abort().await { + warn!(cleanup_error; "Failed to abort series-index artifact build"); + } + return Err(error); + } + let artifact = writer.finish().await?; + file_operation(IndexFileType::Series, "build", "success"); + return Ok(SeriesIndexFileHandle::new( + region.region_id, + SeriesIndexEntry { + file_size: artifact.file_size, + artifact: Some(artifact), + ..entry.clone() + }, + purger.clone(), + )); + } let path = series_index_path(region.region_id, entry.index_uuid); let mut writer = SeriesIndexWriter::try_new( version.metadata.clone(), @@ -214,6 +245,79 @@ pub(crate) async fn build_series_index( )) } +/// Converts one immutable SST into an independently rebuildable samples artifact. +pub(crate) async fn build_samples( + store: &ObjectStore, + region: &MitoRegionRef, + version: &VersionRef, + file: FileHandle, + backend: &dyn crate::series_index::format::SamplesFormat, + purger: &IndexFilePurger, +) -> Result> { + use crate::series_index::artifact::{SamplesEntry, SamplesFileHandle}; + let file_id = file.file_id(); + let Some((context, mut selection)) = region + .access_layer + .read_sst(file) + .build_reader_input(&mut ReaderMetrics::default()) + .await? + else { + return Ok(None); + }; + let context = Arc::new(context); + let mapper = FlatProjectionMapper::new( + &version.metadata, + 0..version.metadata.column_metadatas.len(), + )?; + let compat = FlatCompatBatch::try_new(&mapper, context.read_format(), false)?; + let generation = store_api::storage::FileId::random(); + let prefix = format!( + "{}/samples/{}/{generation}/", + region.region_id.as_u64(), + file_id.file_id() + ); + let mut writer = backend + .writer(version.metadata.clone(), store.clone(), &prefix, generation) + .await?; + let result: Result<()> = async { + while let Some((row_group, selection)) = selection.pop_first() { + let reader = context + .reader_builder() + .build(context.build_context(row_group, Some(selection), None)) + .await?; + let mut reader = FlatPruneReader::new_with_row_group_reader( + context.clone(), + FlatRowGroupReader::new(context.clone(), reader), + context.pre_filter_mode().skip_fields(), + ); + while let Some(batch) = reader.next_batch().await? { + let batch = match &compat { + Some(compat) => compat.compat(batch)?, + None => batch, + }; + writer.write(&batch).await?; + } + } + Ok(()) + } + .await; + if let Err(error) = result { + if let Err(cleanup) = writer.abort().await { + warn!(cleanup; "Failed to abort samples build"); + } + return Err(error); + } + let artifact = writer.finish().await?; + Ok(Some(SamplesFileHandle::new( + file_id, + SamplesEntry { + file_id: file_id.file_id(), + artifact, + }, + purger.clone(), + ))) +} + #[cfg(test)] mod tests { use std::collections::{BTreeMap, HashMap}; @@ -228,7 +332,8 @@ mod tests { use super::*; use crate::series_index::bucket::{group_files_into_series_buckets, plan_series_indexes}; use crate::series_index::catalog::{ - SeriesIndexCatalog, load_version_control, series_catalog_path, store_catalog, + SeriesIndexCatalog, load_version_control, series_catalog_path, series_index_path, + store_catalog, }; use crate::series_index::purger::series_index_channel; use crate::series_index::tests::prepare_region; @@ -321,6 +426,7 @@ mod tests { &version, &bucket, &entry, + None, &purger, ) .await; @@ -471,7 +577,7 @@ mod tests { .unwrap() .unwrap(); } - let _handle = build_series_index(&store, ®ion, &version, &bucket, &entry, &purger) + let _handle = build_series_index(&store, ®ion, &version, &bucket, &entry, None, &purger) .await .unwrap(); let bytes = store @@ -540,7 +646,7 @@ mod tests { }; completed.push(entry.file_id); } - build_series_index(&store, ®ion, &version, &bucket, &entry, &purger) + build_series_index(&store, ®ion, &version, &bucket, &entry, None, &purger) .await .map(Some) } @@ -746,7 +852,7 @@ mod tests { .map(|_| ()) } else { let (purger, _receiver) = series_index_channel(store.clone()); - build_series_index(&store, ®ion, &version, &bucket, &entry, &purger) + build_series_index(&store, ®ion, &version, &bucket, &entry, None, &purger) .await .map(|_| ()) }; diff --git a/src/mito2/src/series_index/catalog.rs b/src/mito2/src/series_index/catalog.rs index 95cf87f83d3..66355ace359 100644 --- a/src/mito2/src/series_index/catalog.rs +++ b/src/mito2/src/series_index/catalog.rs @@ -33,6 +33,7 @@ use crate::series_index::version::{ pub(crate) use crate::sst::range_index::range_index_path; const SERIES_DIR: &str = "series"; +const SAMPLES_CATALOG: &str = "samples.json"; const RANGE_CATALOG: &str = "range-index.json"; const SERIES_CATALOG: &str = "series-index.json"; const SERIES_METADATA_KEY: &str = "greptime.series_index"; @@ -60,6 +61,8 @@ pub(crate) struct WindowSequence { /// relies on this complete-coverage contract, not on `source_file_ids`. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub(crate) struct SeriesIndexEntry { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub(crate) artifact: Option, /// Completed size in the catalog; zero in the footer written before completion. pub(crate) file_size: u64, pub(crate) index_uuid: FileId, @@ -83,6 +86,14 @@ pub(crate) struct SeriesIndexEntry { } impl SeriesIndexEntry { + /// Entries written before pluggable formats describe the original Parquet layout. + pub(crate) fn format(&self) -> crate::series_index::artifact::ArtifactFormat { + self.artifact.as_ref().map_or( + crate::series_index::artifact::ArtifactFormat::Parquet, + |artifact| artifact.format, + ) + } + /// Whether this index completely covers an SST in the query's sequence domain. pub(crate) fn covers_file( &self, @@ -116,6 +127,18 @@ pub(crate) struct RangeIndexCatalog { pub(crate) indexes: Vec, } +#[derive(Debug, Default, Serialize, Deserialize)] +pub(crate) struct SamplesCatalog { + pub(crate) entries: Vec, + /// Once samples are enabled this single catalog commits all artifact references together. + pub(crate) series: Vec, + pub(crate) ranges: Vec, +} + +pub(crate) fn samples_catalog_path(region_id: RegionId) -> String { + format!("{}/{SAMPLES_CATALOG}", region_id.as_u64()) +} + pub(crate) fn range_catalog_path(region_id: RegionId) -> String { format!("{}/{RANGE_CATALOG}", region_id.as_u64()) } @@ -172,6 +195,7 @@ where pub(crate) async fn delete_catalogs(store: &ObjectStore, region_id: RegionId) { for path in [ series_catalog_path(region_id), + samples_catalog_path(region_id), range_catalog_path(region_id), ] { if let Err(error) = store.delete(&path).await @@ -194,14 +218,17 @@ pub(crate) async fn load_version_control( let series = load_catalog::(store, &series_catalog_path(region_id)) .await .unwrap_or_default(); - let version = SeriesIndexVersion::new( - range - .indexes + let samples = load_catalog::(store, &samples_catalog_path(region_id)).await; + let (ranges, indexes, sample_entries) = match samples { + Some(catalog) => (catalog.ranges, catalog.series, catalog.entries), + None => (range.indexes, series.indexes, Vec::new()), + }; + let mut version = SeriesIndexVersion::new( + ranges .into_iter() .map(|entry| (entry.file_id, entry)) .collect(), - series - .indexes + indexes .into_iter() .map(|entry| { ( @@ -211,6 +238,17 @@ pub(crate) async fn load_version_control( }) .collect(), ); + for entry in sample_entries { + let id = entry.file_id; + version.samples.insert( + id, + crate::series_index::artifact::SamplesFileHandle::new( + crate::sst::file::RegionFileId::new(region_id, id), + entry, + purger.clone(), + ), + ); + } let control = SeriesIndexVersionControl::default(); control.publish(std::sync::Arc::new(version)); control @@ -258,10 +296,37 @@ mod tests { } } + #[test] + fn legacy_series_catalog_fixture() { + use crate::series_index::artifact::ArtifactFormat; + let json = include_str!( + "../../../../tests/compatibility/fixtures/series_index/legacy_catalog.json" + ); + let catalog: SeriesIndexCatalog = serde_json::from_str(json).unwrap(); + assert_eq!(1, catalog.indexes.len()); + let entry = &catalog.indexes[0]; + assert!(entry.artifact.is_none()); + assert_eq!(ArtifactFormat::Parquet, entry.format()); + assert_eq!( + serde_json::from_str::(json).unwrap(), + serde_json::to_value(&catalog).unwrap() + ); + for (format, name) in [ + (ArtifactFormat::Parquet, "parquet"), + (ArtifactFormat::FakeSamples, "fake-samples"), + (ArtifactFormat::FakeIndex, "fake-index"), + ] { + let encoded = serde_json::to_value(format).unwrap(); + assert_eq!(name, encoded.as_str().unwrap()); + assert_eq!(format, serde_json::from_value(encoded).unwrap()); + } + } + #[test] fn coverage_uses_exclusive_time_end_and_inclusive_file_sequences() { let region_id = RegionId::new(1, 1); let entry = SeriesIndexEntry { + artifact: None, file_size: 0, index_uuid: FileId::random(), bucket_start: Timestamp::new_second(1), @@ -359,6 +424,7 @@ mod tests { let store = ObjectStore::new(Memory::default()).unwrap(); let region_id = RegionId::new(1, 1); let entry = SeriesIndexEntry { + artifact: None, file_size: 0, index_uuid: FileId::random(), bucket_start: Timestamp::new_second(0), diff --git a/src/mito2/src/series_index/format.rs b/src/mito2/src/series_index/format.rs new file mode 100644 index 00000000000..b59933f719a --- /dev/null +++ b/src/mito2/src/series_index/format.rs @@ -0,0 +1,72 @@ +// 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. + +//! Storage-format boundaries. Encodings stay outside reconciliation and source scheduling. + +use std::fmt::Debug; +use std::sync::Arc; + +use async_trait::async_trait; +use datatypes::arrow::record_batch::RecordBatch; +use object_store::ObjectStore; +use store_api::metadata::RegionMetadataRef; +use store_api::storage::FileId; + +use crate::error::Result; +use crate::series_index::artifact::{ArtifactFormat, ArtifactMetadata}; +use crate::series_index::catalog::SeriesIndexEntry; + +/// Writers own partial outputs until finish succeeds; abort removes every partial object. +/// A failed finish must clean up its output before returning; implementations also own +/// cleanup on cancellation while a writer is live. +#[async_trait] +pub(crate) trait FormatWriter: Send { + async fn write(&mut self, batch: &RecordBatch) -> Result<()>; + async fn finish(self: Box) -> Result; + async fn abort(self: Box) -> Result<()>; +} + +/// Builds series-index artifacts; read interfaces are deferred. +#[async_trait] +pub(crate) trait SeriesIndexFormat: Debug + Send + Sync { + fn format(&self) -> ArtifactFormat; + async fn writer( + &self, + metadata: RegionMetadataRef, + store: ObjectStore, + entry: &SeriesIndexEntry, + ) -> Result>; +} + +/// Builds samples artifacts; read interfaces are deferred. +#[async_trait] +pub(crate) trait SamplesFormat: Debug + Send + Sync { + fn format(&self) -> ArtifactFormat; + /// The caller supplies a unique prefix. Finish reports all objects owned below it. + async fn writer( + &self, + metadata: RegionMetadataRef, + store: ObjectStore, + prefix: &str, + generation: FileId, + ) -> Result>; +} + +/// Optional writers, captured once per reconciliation pass. +/// Without an index backend, builds use the existing Parquet writer. +#[derive(Debug, Clone, Default)] +pub(crate) struct SeriesFormats { + pub(crate) index: Option>, + pub(crate) samples: Option>, +} diff --git a/src/mito2/src/series_index/format_tests.rs b/src/mito2/src/series_index/format_tests.rs new file mode 100644 index 00000000000..a7e0c86069d --- /dev/null +++ b/src/mito2/src/series_index/format_tests.rs @@ -0,0 +1,442 @@ +// 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. + +//! Artifact lifecycle tests; fake formats have no codec. + +use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::time::Duration; + +use async_trait::async_trait; +use datatypes::arrow::record_batch::RecordBatch; +use futures::TryStreamExt; +use object_store::ObjectStore; +use object_store::layers::mock::{self, MockLayerBuilder, oio}; +use snafu::ResultExt; +use store_api::metadata::RegionMetadataRef; +use store_api::region_engine::RegionEngine; +use store_api::storage::FileId; + +use crate::config::MitoConfig; +use crate::error::{OpenDalSnafu, Result, UnexpectedSnafu}; +use crate::series_index::artifact::{ArtifactFormat, ArtifactMetadata}; +use crate::series_index::catalog::{SeriesIndexEntry, load_version_control}; +use crate::series_index::format::{FormatWriter, SamplesFormat, SeriesFormats, SeriesIndexFormat}; +use crate::series_index::maintenance::reconcile_series_indexes; +use crate::series_index::purger::series_index_channel; +use crate::series_index::tests::prepare_region_with_config; +use crate::test_util::TestEnv; + +#[derive(Debug, Default)] +struct FakeSamples { + fail: Arc, +} + +struct FakeWriter { + store: ObjectStore, + artifact: ArtifactMetadata, + fail: Arc, +} + +#[async_trait] +impl FormatWriter for FakeWriter { + async fn write(&mut self, _batch: &RecordBatch) -> Result<()> { + if self.fail.load(Ordering::Relaxed) { + return UnexpectedSnafu { + reason: "injected samples write failure", + } + .fail(); + } + Ok(()) + } + async fn finish(self: Box) -> Result { + Ok(self.artifact) + } + async fn abort(self: Box) -> Result<()> { + for path in &self.artifact.objects { + self.store.delete(path).await.context(OpenDalSnafu)?; + } + Ok(()) + } +} + +#[async_trait] +impl SamplesFormat for FakeSamples { + fn format(&self) -> ArtifactFormat { + ArtifactFormat::FakeSamples + } + async fn writer( + &self, + _metadata: RegionMetadataRef, + store: ObjectStore, + prefix: &str, + generation: FileId, + ) -> Result> { + let objects = vec![format!("{prefix}data"), format!("{prefix}directory")]; + for path in &objects { + store.write(path, "fake").await.context(OpenDalSnafu)?; + } + Ok(Box::new(FakeWriter { + store, + artifact: ArtifactMetadata { + format: self.format(), + generation, + objects, + file_size: 8, + }, + fail: self.fail.clone(), + })) + } +} + +#[tokio::test] +async fn format_skeleton_build_reopen_and_retire() { + let mut env = TestEnv::with_prefix("format-skeleton").await; + let (engine, region) = prepare_region_with_config( + &mut env, + &[1000, 2000], + MitoConfig { + experimental_enable_series_index: true, + experimental_series_index_maintenance_interval: Duration::from_secs(3600), + min_compaction_interval: Duration::from_secs(3600), + ..Default::default() + }, + ) + .await; + let fake = Arc::new(FakeSamples::default()); + *region.series_index_version_control.formats.write().unwrap() = Arc::new(SeriesFormats { + samples: Some(fake.clone()), + ..Default::default() + }); + let store = region.series_index_store.clone().unwrap(); + let (purger, mut purges) = series_index_channel(store.clone()); + let stats = reconcile_series_indexes( + 0, + store.clone(), + region.clone(), + Duration::from_secs(100), + 0, + purger.clone(), + false, + true, + ) + .await + .unwrap(); + assert_eq!(2, stats.built_samples); + let snapshot = region.series_index_version(); + assert_eq!(2, snapshot.samples.len()); + let reopened = load_version_control(&store, region.region_id, &purger).await; + assert_eq!(2, reopened.current().samples.len()); + assert!(snapshot.disk_usage() >= 16); + let stats = reconcile_series_indexes( + 0, + store.clone(), + region.clone(), + Duration::from_secs(100), + 0, + purger.clone(), + false, + true, + ) + .await + .unwrap(); + assert_eq!(0, stats.built_samples); + + // Rebuilding a missing sidecar replaces its generation but retains old readers' files. + let old = snapshot.samples.values().next().unwrap().clone(); + store + .delete(&old.entry().artifact.objects[0]) + .await + .unwrap(); + let stats = reconcile_series_indexes( + 0, + store.clone(), + region.clone(), + Duration::from_secs(100), + 0, + purger.clone(), + false, + true, + ) + .await + .unwrap(); + assert_eq!(1, stats.built_samples); + assert!(purges.try_recv().is_err()); + drop(snapshot); + assert!(purges.try_recv().is_err()); + drop(old); + let request = purges.try_recv().unwrap(); + assert_eq!(2, request.objects.unwrap().len()); + engine.stop().await.unwrap(); +} + +#[tokio::test] +async fn format_skeleton_abort_removes_all_objects() { + let mut env = TestEnv::with_prefix("format-abort").await; + let (engine, region) = crate::series_index::tests::prepare_region(&mut env).await; + let store = ObjectStore::new(object_store::services::Memory::default()).unwrap(); + let (purger, _receiver) = series_index_channel(store.clone()); + let version = region.version(); + let file = version + .ssts + .levels() + .iter() + .flat_map(|l| l.files()) + .next() + .unwrap() + .clone(); + let fake = FakeSamples::default(); + fake.fail.store(true, Ordering::Relaxed); + assert!( + crate::series_index::builder::build_samples( + &store, ®ion, &version, file, &fake, &purger + ) + .await + .is_err() + ); + assert!( + store + .list_with(&format!("{}/samples/", region.region_id.as_u64())) + .recursive(true) + .await + .unwrap() + .iter() + .all(|entry| entry.metadata().is_dir()) + ); + assert!(region.series_index_version().samples.is_empty()); + engine.stop().await.unwrap(); +} + +#[derive(Debug, Default)] +struct FakeIndex; + +#[async_trait] +impl SeriesIndexFormat for FakeIndex { + fn format(&self) -> ArtifactFormat { + ArtifactFormat::FakeIndex + } + + async fn writer( + &self, + metadata: RegionMetadataRef, + store: ObjectStore, + entry: &SeriesIndexEntry, + ) -> Result> { + let path = format!( + "{}/series/{}.fake", + metadata.region_id.as_u64(), + entry.index_uuid + ); + store.write(&path, "fake").await.context(OpenDalSnafu)?; + Ok(Box::new(FakeWriter { + store, + artifact: ArtifactMetadata { + format: self.format(), + generation: entry.index_uuid, + objects: vec![path], + file_size: 4, + }, + fail: Arc::new(AtomicBool::new(false)), + })) + } +} + +#[tokio::test] +async fn format_index_rebuild_is_scoped_and_scans_fall_back_to_ssts() { + let mut env = TestEnv::with_prefix("format-index").await; + // Index builds require four SSTs per bucket. + let timestamps = [1000, 2000, 3000, 4000, 101000, 102000, 103000, 104000]; + let (engine, region) = prepare_region_with_config( + &mut env, + ×tamps, + MitoConfig { + experimental_enable_series_index: true, + experimental_series_scan_v2: true, + experimental_series_index_maintenance_interval: Duration::from_secs(3600), + min_compaction_interval: Duration::from_secs(3600), + ..Default::default() + }, + ) + .await; + *region.series_index_version_control.formats.write().unwrap() = Arc::new(SeriesFormats { + index: Some(Arc::new(FakeIndex)), + ..Default::default() + }); + let store = region.series_index_store.clone().unwrap(); + let (purger, mut purges) = series_index_channel(store.clone()); + let reconcile = || { + reconcile_series_indexes( + 0, + store.clone(), + region.clone(), + Duration::from_secs(100), + 0, + purger.clone(), + false, + true, + ) + }; + assert_eq!(2, reconcile().await.unwrap().built_series); + let before = region.series_index_version(); + assert!( + before + .series_indexes + .values() + .all(|h| h.entry().format() == ArtifactFormat::FakeIndex) + ); + assert_eq!(0, reconcile().await.unwrap().built_series); + let scanner = engine + .scanner( + region.region_id, + store_api::storage::ScanRequest { + distribution: Some(store_api::storage::TimeSeriesDistribution::PerSeries), + ..Default::default() + }, + ) + .await + .unwrap(); + let batches = scanner + .scan() + .await + .unwrap() + .try_collect::>() + .await + .unwrap(); + assert_eq!( + timestamps.len(), + batches.iter().map(|batch| batch.num_rows()).sum::() + ); + drop(scanner); + + let missing = before.series_indexes.values().next().unwrap(); + store + .delete(&missing.entry().artifact.as_ref().unwrap().objects[0]) + .await + .unwrap(); + assert_eq!(1, reconcile().await.unwrap().built_series); + let after = region.series_index_version(); + assert_eq!( + 1, + after + .series_indexes + .keys() + .filter(|id| before.series_indexes.contains_key(id)) + .count() + ); + assert!(purges.try_recv().is_err()); + drop(before); + assert!(purges.try_recv().is_ok()); + + // Disabling the injected writer restores ordinary Parquet artifacts without a read adapter. + *region.series_index_version_control.formats.write().unwrap() = + Arc::new(SeriesFormats::default()); + assert_eq!(2, reconcile().await.unwrap().built_series); + assert!( + region + .series_index_version() + .series_indexes + .values() + .all(|h| h.entry().artifact.is_none()) + ); + assert_eq!(0, reconcile().await.unwrap().built_series); + engine.stop().await.unwrap(); +} + +struct FailedCatalogWriter(oio::Writer); + +impl mock::Write for FailedCatalogWriter { + async fn write(&mut self, buffer: mock::Buffer) -> mock::Result<()> { + self.0.write(buffer).await + } + async fn close(&mut self) -> mock::Result { + Err(mock::Error::new( + mock::ErrorKind::Unexpected, + "injected artifact catalog failure", + )) + } + async fn abort(&mut self) -> mock::Result<()> { + self.0.abort().await + } +} + +#[tokio::test] +async fn format_catalog_failure_preserves_published_artifacts() { + let mut env = TestEnv::with_prefix("format-catalog-failure").await; + let (engine, region) = crate::series_index::tests::prepare_region(&mut env).await; + *region.series_index_version_control.formats.write().unwrap() = Arc::new(SeriesFormats { + index: Some(Arc::new(FakeIndex)), + samples: Some(Arc::new(FakeSamples::default())), + }); + let store = ObjectStore::new(object_store::services::Memory::default()).unwrap(); + let (purger, mut purges) = series_index_channel(store.clone()); + reconcile_series_indexes( + 0, + store.clone(), + region.clone(), + Duration::from_secs(100), + 0, + purger.clone(), + false, + true, + ) + .await + .unwrap(); + let before = region.series_index_version(); + let catalog_path = crate::series_index::catalog::samples_catalog_path(region.region_id); + let catalog = store.read(&catalog_path).await.unwrap().to_bytes(); + let old = before.samples.values().next().unwrap(); + store + .delete(&old.entry().artifact.objects[0]) + .await + .unwrap(); + let layer = MockLayerBuilder::default() + .writer_factory(Arc::new(|path, _, inner| { + if path.ends_with("samples.json") { + Box::new(FailedCatalogWriter(inner)) + } else { + inner + } + })) + .build() + .unwrap(); + let error = reconcile_series_indexes( + 0, + store.clone().layer(layer), + region.clone(), + Duration::from_secs(100), + 0, + purger.clone(), + false, + true, + ) + .await + .unwrap_err(); + assert!(format!("{error:?}").contains("injected artifact catalog failure")); + assert!(Arc::ptr_eq(&before, ®ion.series_index_version())); + assert_eq!(catalog, store.read(&catalog_path).await.unwrap().to_bytes()); + let cleanup = purges.try_recv().unwrap().objects.unwrap(); + assert_eq!(2, cleanup.len()); + assert!( + cleanup + .iter() + .all(|path| !old.entry().artifact.objects.contains(path)) + ); + assert!(purges.try_recv().is_err()); + let reopened = load_version_control(&store, region.region_id, &purger).await; + assert_eq!( + old.entry(), + reopened.current().samples[&old.entry().file_id].entry() + ); + engine.stop().await.unwrap(); +} diff --git a/src/mito2/src/series_index/maintenance.rs b/src/mito2/src/series_index/maintenance.rs index f5221bb1ebe..232e17ac266 100644 --- a/src/mito2/src/series_index/maintenance.rs +++ b/src/mito2/src/series_index/maintenance.rs @@ -20,6 +20,7 @@ use std::time::{Duration, Instant}; use common_telemetry::{debug, info}; use object_store::ObjectStore; +use snafu::ResultExt; use store_api::storage::{FileId, RegionId}; use crate::error::Result; @@ -40,16 +41,23 @@ use crate::series_index::version::{SeriesIndexFileHandle, SeriesIndexVersion}; /// Retires newly completed series files unless their snapshot is published. #[derive(Default)] -struct UnpublishedSeriesFiles(Vec); +struct UnpublishedSeriesFiles( + Vec, + Vec, +); impl UnpublishedSeriesFiles { fn disarm(&mut self) { self.0.clear(); + self.1.clear(); } } impl Drop for UnpublishedSeriesFiles { fn drop(&mut self) { + for handle in &self.1 { + handle.mark_deleted(); + } for handle in &self.0 { handle.mark_deleted(); } @@ -58,6 +66,8 @@ impl Drop for UnpublishedSeriesFiles { #[derive(Debug, Default)] pub(crate) struct ReconcileStats { + pub(crate) built_samples: usize, + pub(crate) removed_samples: usize, pub(crate) source_files: usize, pub(crate) built_range: usize, pub(crate) built_series: usize, @@ -69,7 +79,13 @@ pub(crate) struct ReconcileStats { impl ReconcileStats { fn changed(&self) -> bool { - self.built_range + self.built_series + self.removed_range + self.removed_series > 0 + self.built_samples + + self.removed_samples + + self.built_range + + self.built_series + + self.removed_range + + self.removed_series + > 0 } } @@ -201,12 +217,43 @@ async fn build_index_version( } (false, _) => Vec::new(), }; - let plan = plan_series_indexes( - buckets, - current.index_buckets.clone(), - version.options.ttl, - now_ms, + let formats = region + .series_index_version_control + .formats + .read() + .unwrap() + .clone(); + let selected_format = formats.index.as_ref().map_or( + crate::series_index::artifact::ArtifactFormat::Parquet, + |backend| backend.format(), ); + let mut unusable = HashSet::new(); + for handle in current.series_indexes.values() { + let entry = handle.entry(); + let mut reusable = entry.format() == selected_format; + // Legacy Parquet indexes retain their existing maintenance behavior. + if reusable && let Some(artifact) = &entry.artifact { + for object in &artifact.objects { + reusable &= store + .exists(object) + .await + .context(crate::error::OpenDalSnafu)?; + } + } + if !reusable { + unusable.insert(entry.index_uuid); + } + } + let mut index_buckets = current.index_buckets.clone(); + for bucket in index_buckets.values_mut() { + if bucket.index_ids.iter().any(|id| unusable.contains(id)) { + // Force replacement only for the affected bucket. Retain its geometry and + // handles until a replacement is built and its catalog is published. + bucket.compaction_window_secs = 0; + bucket.window_sequences.clear(); + } + } + let plan = plan_series_indexes(buckets, index_buckets, version.options.ttl, now_ms); stats.computed_buckets = plan.computed_buckets; stats.skipped_buckets = plan.skipped_buckets; let mut next = prune_index_version(¤t, &visible, &plan.expired_index_ids, &mut stats); @@ -218,6 +265,7 @@ async fn build_index_version( } if plan.builds.is_empty() && next.is_none() + && formats.samples.is_none() && (!enable_range_index || current.range_indexes.len() == visible.len()) { return Ok((None, stats)); @@ -225,13 +273,50 @@ async fn build_index_version( let SeriesIndexVersion { mut range_indexes, mut series_indexes, + mut samples, .. } = next.unwrap_or_else(|| SeriesIndexVersion { + samples: current.samples.clone(), range_indexes: current.range_indexes.clone(), series_indexes: current.series_indexes.clone(), index_buckets: Default::default(), }); let index_buckets = plan.index_buckets; + if let Some(backend) = &formats.samples { + for file in &files { + let id = file.file_id().file_id(); + let reusable = if let Some(handle) = samples.get(&id) { + let artifact = &handle.entry().artifact; + let mut exists = artifact.format == backend.format(); + for path in &artifact.objects { + exists &= store + .exists(path) + .await + .context(crate::error::OpenDalSnafu)?; + } + exists + } else { + false + }; + if reusable { + continue; + } + if let Some(handle) = crate::series_index::builder::build_samples( + store, + region, + version, + file.clone(), + backend.as_ref(), + purger, + ) + .await? + { + unpublished.1.push(handle.clone()); + samples.insert(id, handle); + stats.built_samples += 1; + } + } + } for id in &plan.superseded_index_ids { series_indexes.remove(id); } @@ -246,8 +331,16 @@ async fn build_index_version( range_indexes.insert(entry.file_id, entry); } } - let series_handle = - build_series_index(store, region, version, &bucket, &expected, purger).await?; + let series_handle = build_series_index( + store, + region, + version, + &bucket, + &expected, + formats.index.as_deref(), + purger, + ) + .await?; unpublished.0.push(series_handle.clone()); stats.built_series += 1; series_indexes.insert(expected.index_uuid, series_handle); @@ -276,6 +369,7 @@ async fn build_index_version( return Ok((None, stats)); } let next = SeriesIndexVersion { + samples, range_indexes, series_indexes, index_buckets, @@ -293,7 +387,8 @@ fn prune_index_version( expired_index_ids: &[FileId], stats: &mut ReconcileStats, ) -> Option { - if current.range_indexes.keys().all(|id| visible.contains(id)) + if current.samples.keys().all(|id| visible.contains(id)) + && current.range_indexes.keys().all(|id| visible.contains(id)) && expired_index_ids .iter() .all(|id| !current.series_indexes.contains_key(id)) @@ -301,10 +396,13 @@ fn prune_index_version( return None; } let mut next = SeriesIndexVersion { + samples: current.samples.clone(), range_indexes: current.range_indexes.clone(), series_indexes: current.series_indexes.clone(), index_buckets: current.index_buckets.clone(), }; + next.samples.retain(|id, _| visible.contains(id)); + stats.removed_samples = current.samples.len() - next.samples.len(); next.range_indexes .retain(|file_id, _| visible.contains(file_id)); for id in expired_index_ids { @@ -322,6 +420,31 @@ async fn persist_index_catalogs( next: &SeriesIndexVersion, stats: &ReconcileStats, ) -> Result<()> { + let samples_path = crate::series_index::catalog::samples_catalog_path(region_id); + if !next.samples.is_empty() + || stats.removed_samples > 0 + || store + .exists(&samples_path) + .await + .context(crate::error::OpenDalSnafu)? + { + // A single object write commits all references. A failed write leaves the prior + // catalog usable while the unpublished-output guard retires the new artifacts. + return store_catalog( + store, + &samples_path, + &crate::series_index::catalog::SamplesCatalog { + entries: next.samples.values().map(|h| h.entry().clone()).collect(), + series: next + .series_indexes + .values() + .map(|h| h.entry().clone()) + .collect(), + ranges: next.range_indexes.values().copied().collect(), + }, + ) + .await; + } if stats.built_range + stats.removed_range > 0 { let mut range_entries = next.range_indexes.values().copied().collect::>(); range_entries @@ -364,6 +487,13 @@ async fn persist_index_catalogs( /// Publishes the snapshot and retires series files absent from the new version. fn publish_index_version(region: &MitoRegionRef, next: Arc) { let previous = region.series_index_version_control.publish(next.clone()); + for (id, handle) in &previous.samples { + if next.samples.get(id).is_none_or(|next| { + next.entry().artifact.generation != handle.entry().artifact.generation + }) { + handle.mark_deleted(); + } + } for (id, handle) in &previous.series_indexes { if !next.series_indexes.contains_key(id) { // Purge only after readers release their retained handles. @@ -405,6 +535,7 @@ mod tests { SeriesIndexFileHandle::new( RegionId::new(1, 1), SeriesIndexEntry { + artifact: None, index_uuid: series_id, file_size: 20, bucket_start: Timestamp::new_second(0), diff --git a/src/mito2/src/series_index/purger.rs b/src/mito2/src/series_index/purger.rs index a3eb6d89e38..66eda84c327 100644 --- a/src/mito2/src/series_index/purger.rs +++ b/src/mito2/src/series_index/purger.rs @@ -39,9 +39,10 @@ impl IndexFileType { } } -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone)] pub(crate) struct PurgeRequest { pub(crate) file_id: RegionFileId, + pub(crate) objects: Option>, } #[derive(Clone)] @@ -57,6 +58,13 @@ impl Debug for IndexFilePurger { } impl IndexFilePurger { + pub(crate) fn purge_objects(&self, file_id: RegionFileId, objects: Vec) { + self.purge(PurgeRequest { + file_id, + objects: Some(objects), + }); + } + pub(crate) fn purge(&self, request: PurgeRequest) { if let Err(error) = self.sender.send(request) { let store = self.store.clone(); @@ -87,17 +95,24 @@ pub(crate) async fn run_index_purge_task( } async fn purge_file(store: &ObjectStore, request: PurgeRequest) { - let path = series_index_path(request.file_id.region_id(), request.file_id.file_id()); - match store.delete(&path).await { - Ok(()) => { - file_operation(IndexFileType::Series, "delete", "success"); - } - Err(error) if error.kind() == ErrorKind::NotFound => { - file_operation(IndexFileType::Series, "delete", "success"); - } - Err(error) => { - file_operation(IndexFileType::Series, "delete", "failure"); - warn!(error; "Failed to delete series index, index_type: {}, path: {}, phase: deletion", IndexFileType::Series.as_str(), path); + let objects = request.objects.unwrap_or_else(|| { + vec![series_index_path( + request.file_id.region_id(), + request.file_id.file_id(), + )] + }); + for path in objects { + match store.delete(&path).await { + Ok(()) => { + file_operation(IndexFileType::Series, "delete", "success"); + } + Err(error) if error.kind() == ErrorKind::NotFound => { + file_operation(IndexFileType::Series, "delete", "success"); + } + Err(error) => { + file_operation(IndexFileType::Series, "delete", "failure"); + warn!(error; "Failed to delete series index, index_type: {}, path: {}, phase: deletion", IndexFileType::Series.as_str(), path); + } } } } @@ -129,7 +144,10 @@ mod tests { let path = series_index_path(file_id.region_id(), file_id.file_id()); store.write(&path, "index").await.unwrap(); paths.push(path); - purger.purge(PurgeRequest { file_id }); + purger.purge(PurgeRequest { + file_id, + objects: None, + }); } drop(purger); tokio::time::timeout( @@ -151,7 +169,10 @@ mod tests { let file_id = RegionFileId::new(RegionId::new(1, 1), FileId::random()); let path = series_index_path(file_id.region_id(), file_id.file_id()); store.write(&path, "index").await.unwrap(); - purger.purge(PurgeRequest { file_id }); + purger.purge(PurgeRequest { + file_id, + objects: None, + }); tokio::time::timeout(Duration::from_secs(10), async { while store.exists(&path).await.unwrap() { tokio::task::yield_now().await; diff --git a/src/mito2/src/series_index/searcher.rs b/src/mito2/src/series_index/searcher.rs index 0b9befe4b8a..a2589b3bda9 100644 --- a/src/mito2/src/series_index/searcher.rs +++ b/src/mito2/src/series_index/searcher.rs @@ -312,6 +312,7 @@ mod tests { ) -> (SeriesIndexFileHandle, UnboundedReceiver) { let (purger, receiver) = series_index_channel(store.clone()); let entry = SeriesIndexEntry { + artifact: None, file_size: 0, index_uuid: FileId::random(), bucket_start: common_time::Timestamp::new_second(0), diff --git a/src/mito2/src/series_index/tests.rs b/src/mito2/src/series_index/tests.rs index 631bdc75b87..145c2088cce 100644 --- a/src/mito2/src/series_index/tests.rs +++ b/src/mito2/src/series_index/tests.rs @@ -63,7 +63,7 @@ pub(crate) async fn prepare_region_with_timestamps( .await } -async fn prepare_region_with_config( +pub(crate) async fn prepare_region_with_config( env: &mut TestEnv, timestamps: &[i64], config: MitoConfig, @@ -726,7 +726,7 @@ async fn test_failed_reconcile_retires_only_unpublished_series() { .unwrap(); ranges.insert(entry.file_id, entry); } - let handle = build_series_index(store, ®ion, &version, bucket, entry, purger) + let handle = build_series_index(store, ®ion, &version, bucket, entry, None, purger) .await .unwrap(); let reused_path = series_index_path(region.region_id, entry.index_uuid); @@ -891,6 +891,7 @@ async fn test_drop_catalogs_and_retained_snapshot_after_task_stop() { let store = ObjectStore::new(Memory::default()).unwrap(); let region_id = RegionId::new(1, 1); let entry = SeriesIndexEntry { + artifact: None, file_size: 0, index_uuid: FileId::random(), bucket_start: Timestamp::new_second(0), diff --git a/src/mito2/src/series_index/version.rs b/src/mito2/src/series_index/version.rs index fa2115fc1f2..7d819a0df55 100644 --- a/src/mito2/src/series_index/version.rs +++ b/src/mito2/src/series_index/version.rs @@ -82,9 +82,15 @@ struct SeriesIndexFileHandleInner { impl Drop for SeriesIndexFileHandleInner { fn drop(&mut self) { if self.deleted.load(Ordering::Acquire) { - self.purger.purge(PurgeRequest { - file_id: self.file_id, - }); + if let Some(artifact) = &self.entry.artifact { + self.purger + .purge_objects(self.file_id, artifact.objects.clone()); + } else { + self.purger.purge(PurgeRequest { + file_id: self.file_id, + objects: None, + }); + } } } } @@ -94,6 +100,7 @@ impl Drop for SeriesIndexFileHandleInner { pub(crate) struct SeriesIndexVersion { /// Range indexes for visible SSTs; reconciliation removes IDs absent from its SST snapshot. /// Physical deletion is independently handled by the SST file purger. + pub(crate) samples: HashMap, pub(crate) range_indexes: HashMap, pub(crate) series_indexes: HashMap, pub(crate) index_buckets: BTreeMap, @@ -110,6 +117,7 @@ impl SeriesIndexVersion { IndexBucket::from_entry(handle.entry()).insert_into(&mut index_buckets); } Self { + samples: Default::default(), range_indexes, series_indexes, index_buckets, @@ -118,10 +126,15 @@ impl SeriesIndexVersion { /// Approximate installed usage; old snapshots and unpublished outputs are excluded. pub(crate) fn disk_usage(&self) -> u64 { - self.range_indexes + self.samples .values() - .map(|entry| entry.file_size) + .map(|handle| handle.entry().artifact.file_size) .sum::() + + self + .range_indexes + .values() + .map(|entry| entry.file_size) + .sum::() + self .series_indexes .values() @@ -130,6 +143,9 @@ impl SeriesIndexVersion { } fn mark_all_deleted(&self) { + self.samples + .values() + .for_each(|handle| handle.mark_deleted()); self.series_indexes .values() .for_each(SeriesIndexFileHandle::mark_deleted); @@ -140,6 +156,7 @@ impl SeriesIndexVersion { #[derive(Debug, Default)] pub(crate) struct SeriesIndexVersionControl { current: RwLock>, + pub(crate) formats: RwLock>, } impl SeriesIndexVersionControl { diff --git a/src/promql/AGENTS.md b/src/promql/AGENTS.md new file mode 100644 index 00000000000..dc90c5a0eec --- /dev/null +++ b/src/promql/AGENTS.md @@ -0,0 +1,110 @@ +# promql — Agent & Contributor Guide + +Navigation map for `src/promql`. Repo-wide invariants: +[`.agents/architecture-invariants.md`](../../.agents/architecture-invariants.md). + +`promql` owns PromQL evaluation: the series/evaluated data model, range and +instant evaluation, function kernels, vector matching, and the PromQL extension +operators. Statement-to-plan translation lives in `src/query/src/promql/`. + +## Module map + +| Area | Path | Entry point | +| --- | --- | --- | +| Data model | `src/promql/src/grid_series.rs`, `src/promql/src/evaluated.rs` | Grid series, evaluated chunks | +| Evaluation | `src/promql/src/range_eval.rs`, `src/promql/src/instant_eval.rs` | Range and instant evaluation | +| Kernels | `src/promql/src/functions.rs`, `src/promql/src/functions/` | PromQL functions | +| Matching | `src/promql/src/vector_match.rs` | Binary vector matching | +| Extension operators | `src/promql/src/extension_plan.rs`, `src/promql/src/extension_plan/` | Logical nodes and execution plans | +| Native model | `src/promql/src/native/` | Compiled tree, chunks, display, metrics | + +## Two evaluation paths, and their status + +The easiest thing to get wrong here. Both exist; only one runs. + +- **Extension operators are live.** `PromPlanner` builds `SeriesDivide`, + `SeriesNormalize`, `RangeManipulate`, `InstantManipulate`, `EmptyMetric` and + `UnionDistinctOn`; `PromExtensionPlanner` + (`src/promql/src/extension_plan/planner.rs`) maps each logical node to its + execution plan, one branch per node, `None` for anything else. +- **Nothing plans into the native model yet.** `src/query/src/promql/native_compile.rs` + compiles a statement into a `NativeTree`, but no planner calls it, so a tree + built here reaches no execution. `src/promql/src/native/` holds the tree, the + chunk protocol, display and metrics, and — once a runtime lands — the driver + that runs a tree node by node. Compiling and running are separate concerns + owned by separate files; do not assume a tree is executable because it + compiles. + +## Change coupling + +- **An extension node reaches a datanode only if three things line up**: a payload + branch in `src/common/substrait/src/extension_serializer.rs`, a physical branch + in `PromExtensionPlanner`, and a `Commutativity::Commutative` arm in + `check_extension_plan` (`src/query/src/dist_plan/commutativity.rs`). Missing the + third is silent: the node falls through to `Unsupported`, the distributed + boundary is placed above it, and it never reaches a region — with no error. +- A new logical node also needs its physical planner reachable from + `DfQueryPlanner::create_physical_plan`, assembled in + `src/query/src/query_engine/state.rs`. +- User-visible changes need planner coverage plus a sqlness case under + `tests/cases/standalone/common/promql/`. + +## Gotchas + +- **One operation, two implementations; build on the live one.** `InstantManipulate` + routes to `InstantManipulateExec`. The native alternative + `NativeInstantManipulateExec` is crate-private and + `InstantManipulate::try_to_native_execution_plan` has no caller on any branch — + its doc is written for a caller that does not exist. +- **A native window requires non-interleaved input.** Only `Contiguous` and + `KeyThenTime` are accepted; `Interleaved` is refused when compiled. +- **A series closes exactly once**, on the chunk that says so — per input series, + not per label set. A node that rewrites labels can hand two input series over + under one label set, and each closes on its own chunk, so a label set may be + closed more than once; a merge above them must not read the second close as a + defect. That closing chunk is the only way a consumer distinguishes "nothing more + here" from "no more input": dropping it leaves a series open, and a child that + merges regions must recompute finality globally rather than pass a region-local + flag through. +- **Raw-frame labels come from the binding, not the schema.** A raw series frame + carries an opaque encoded primary key while its schema metadata describes only + the format: `greptime.series.layout`, `.content`, `.labels` and `.time`, plus + the producer ordering the adapter adds. Nothing in that metadata binds the key + to a decode — the frame decoder compares those format keys and the field list, + and never a label identity. `unpack_raw_series` therefore requires a + caller-supplied `RawLabelSource`: decode with the source that was actually bound + when the stream was produced, because the frame schema does not replace that + binding. The metadata describes format, not label identity. +- **A wrong label binding is silently wrong, not an error.** Storage validates the + binding it is given (metadata identity, tag-ness, name matches, no duplicates) + but cannot tell whether that set is the one the query meant, and the metric name + is not part of that check. + +## Testing + +```bash +cargo nextest run -p promql +cargo nextest run -p query # planner and native-compile changes +cargo sqlness bare -t # PromQL cases live under tests/cases/standalone/common/promql/ +``` + +## Not wired up yet + +The storage side of the raw path exists; what is missing is planner wiring. Both +gaps below are structural, not bugs. + +- **The native tree has no planner entry point.** `native_compile::compile` has no + caller, and a `NativeTree` becomes a physical plan only once something builds it + while it still holds the AST and the selector binding — the point where the + matchers, `__field__` and the lookback are still available. Compiling later, from + a plan, loses them. +- **No planner reaches a raw scan.** mito2 implements + `RegionScanner::raw_series_schema` (`mito2/src/read/series_scan.rs`), so + `RegionScanExec::try_to_series_scan` can build `SeriesScanExec` from a prepared + scanner; the trait's `Ok(None)` default only refuses engines that have not + implemented it. What is missing is the planner side: nothing selects that + conversion, and `SeriesScanExec::children()` is empty, so wrapping a + `RegionScanExec` before partition preparation leaves `ParallelizeScan` unable to + assign partitions. Either give the partitioner a child it can see, or convert to + the native leaf only after partition preparation — the second is the contract + `RegionScanExec::try_to_series_scan` documents. diff --git a/src/promql/Cargo.toml b/src/promql/Cargo.toml index ad682f4b2a2..a3d8f5c4e71 100644 --- a/src/promql/Cargo.toml +++ b/src/promql/Cargo.toml @@ -28,6 +28,7 @@ prost.workspace = true regex.workspace = true snafu.workspace = true store-api.workspace = true +twox-hash.workspace = true [dev-dependencies] api.workspace = true diff --git a/src/promql/src/grid_series.rs b/src/promql/src/grid_series.rs index b5634b259e9..91d04b4c4b6 100644 --- a/src/promql/src/grid_series.rs +++ b/src/promql/src/grid_series.rs @@ -88,6 +88,17 @@ pub(crate) enum GridSample { const _: () = assert!(std::mem::size_of::>() == 16); +/// `sample` as an owned value; `None` when missing. +pub(crate) fn owned(sample: SampleRef<'_>) -> Result> { + Ok(match sample { + SampleRef::Missing => None, + SampleRef::Float(value) => Some(GridSample::Float(value)), + SampleRef::Histogram(view) => Some(GridSample::Histogram(Box::new( + FloatHistogram::try_from_view(&view)?, + ))), + }) +} + impl GridSeriesBuilder { /// Creates a builder for series of `schema`, which must have an evaluation grid. pub fn try_new(schema: Arc) -> Result { @@ -309,7 +320,8 @@ pub(crate) fn label_set(metric: &MetricName) -> LabelSet { } /// Merges series that share a label set into one, as Prometheus does after a function that -/// rewrites labels or drops `__name__`; two of them holding one field at one point fail. +/// rewrites labels or drops `__name__`; two of them holding one field at one point fail, whether +/// the samples are floats or native histograms. pub fn merge_same_labelset(schema: &Arc, series: Vec) -> Result> { let mut groups: HashMap> = HashMap::with_capacity(series.len()); let mut collided = false; @@ -335,8 +347,7 @@ pub fn merge_same_labelset(schema: &Arc, series: Vec) -> R // slot indexes `members`, so the merge below picks the member's cursor directly instead of // searching for the member per row. let mut rows: Vec<(usize, usize, usize)> = Vec::new(); - let mut values = vec![None; schema.fields.len()]; - let mut merged = vec![None; schema.fields.len()]; + let mut merged: Vec> = Vec::with_capacity(schema.fields.len()); for (labels, members) in groups { if let [member] = members[..] { output.extend(series[member].take()); @@ -361,7 +372,12 @@ pub fn merge_same_labelset(schema: &Arc, series: Vec) -> R .map(|field| field.cursor()) .collect(); for (point, row) in member_series.time.positions() { - if read_row(&mut cursors, row, &mut values)? { + if cursors.iter_mut().any(|cursor| { + !matches!( + cursor.at(row).expect("the axis and the fields are aligned"), + SampleRef::Missing + ) + }) { rows.push((point, member_slot, row)); } } @@ -389,11 +405,12 @@ pub fn merge_same_labelset(schema: &Arc, series: Vec) -> R let point = rows[start].0; let end = start + rows[start..].partition_point(|&(p, _, _)| p == point); // Rows of one point merge field by field; only a field present twice collides. - merged.iter_mut().for_each(|value| *value = None); + merged.clear(); + merged.resize_with(schema.fields.len(), || None); for &(_, member_slot, row) in &rows[start..end] { - read_row(&mut cursors[member_slot], row, &mut values)?; - for (slot, value) in merged.iter_mut().zip(&values) { - if value.is_some() { + for (slot, cursor) in merged.iter_mut().zip(&mut cursors[member_slot]) { + let sample = cursor.at(row).expect("the axis and the fields are aligned"); + if !matches!(sample, SampleRef::Missing) { if slot.is_some() { let labels: Vec<_> = labels .iter() @@ -404,11 +421,11 @@ pub fn merge_same_labelset(schema: &Arc, series: Vec) -> R labels.join(", ") ))); } - *slot = *value; + *slot = owned(sample)?; } } } - builder.push(point, &merged)?; + builder.push_samples(point, &merged)?; start = end; } let metric = series[members[0]].as_ref().unwrap().metric.clone(); @@ -417,24 +434,6 @@ pub fn merge_same_labelset(schema: &Arc, series: Vec) -> R Ok(output) } -/// Reads the float fields of `row` through the member's cursors; returns whether any is present. -fn read_row(cursors: &mut [RowCursor<'_>], row: usize, values: &mut [Option]) -> Result { - let mut present = false; - for (value, cursor) in values.iter_mut().zip(cursors) { - *value = match cursor.at(row).expect("the axis and the fields are aligned") { - SampleRef::Missing => None, - SampleRef::Float(value) => Some(value), - SampleRef::Histogram(_) => { - return Err(DataFusionError::NotImplemented( - "merging histogram series with the same label set".into(), - )); - } - }; - present |= value.is_some(); - } - Ok(present) -} - pub(crate) fn external(error: datatypes::error::Error) -> DataFusionError { DataFusionError::External(Box::new(error)) } diff --git a/src/promql/src/grid_series/tests.rs b/src/promql/src/grid_series/tests.rs index 59e833e3fd3..a3c7d0b3547 100644 --- a/src/promql/src/grid_series/tests.rs +++ b/src/promql/src/grid_series/tests.rs @@ -15,7 +15,7 @@ use std::mem::size_of; use std::sync::Arc; -use datatypes::series::samples::{DenseSamplesRef, FieldSpec, SampleRef, SeriesSchema}; +use datatypes::series::samples::{DenseSamplesRef, FieldSpec, SampleRef, Series, SeriesSchema}; use datatypes::series::time::{EvalGrid, GridSpec}; use datatypes::series::{Label, MetricName}; use rand::rngs::StdRng; @@ -25,7 +25,9 @@ use super::reference::{ Labels, Points, assert_pieces_eq, assert_result_eq, points_of, random_dense, run_ranges, series_of, stitch, }; -use super::{GridSeriesBuilder, merge_same_labelset}; +use super::{GridSample, GridSeriesBuilder, merge_same_labelset}; +use crate::grid_series::reference::OwnedHistogram; +use crate::histogram_eval::float_histogram::FloatHistogram; fn grid_schema(fields: usize, grid_len: u64) -> Arc { Arc::new(SeriesSchema { @@ -206,6 +208,93 @@ fn merge_combines_disjoint_fields_at_one_point() { ); } +/// Native histograms merge like floats: members of one label set combine by field and position, +/// histograms and floats keep their type, and a field held twice at one point collides whatever +/// the sample types. +#[test] +fn merge_keeps_histograms_and_rejects_any_doubled_field() { + #[derive(Clone, Copy)] + enum Value { + Float(f64), + Histogram(usize), + } + + let schema = grid_schema(2, 3); + let histograms = [ + OwnedHistogram::parse("0/0/0/3ff0000000000000/3ff0000000000000/0/1_1/3ff0000000000000///"), + OwnedHistogram::parse("0/0/0/4000000000000000/4008000000000000/1/0_1/4000000000000000///"), + ]; + let histogram = + |index: usize| FloatHistogram::try_from_view(&histograms[index].view()).unwrap(); + let sample = |value: Option| { + value.map(|value| match value { + Value::Float(float) => GridSample::Float(float), + Value::Histogram(index) => GridSample::Histogram(Box::new(histogram(index))), + }) + }; + let series = |rows: &[(usize, [Option; 2])]| { + let mut builder = GridSeriesBuilder::try_new(schema.clone()).unwrap(); + for &(point, row) in rows { + builder + .push_samples(point, &[sample(row[0]), sample(row[1])]) + .unwrap(); + } + builder.finish(metric(0)).unwrap().unwrap() + }; + let describe = |series: &Series, field: usize| -> Vec<(usize, String)> { + let samples = &series.fields.as_slice()[field]; + series + .time + .positions() + .filter_map(|(point, row)| match samples.sample(row) { + SampleRef::Missing => None, + SampleRef::Float(value) => Some((point, format!("{value}"))), + SampleRef::Histogram(view) => Some(( + point, + format!("{:?}", FloatHistogram::try_from_view(&view).unwrap()), + )), + }) + .collect() + }; + + let merged = merge_same_labelset( + &schema, + vec![ + series(&[ + (0, [Some(Value::Histogram(0)), None]), + (2, [Some(Value::Float(1.0)), None]), + ]), + series(&[ + (0, [None, Some(Value::Float(2.0))]), + (1, [Some(Value::Histogram(1)), None]), + ]), + ], + ) + .unwrap(); + assert_eq!(merged.len(), 1); + assert_eq!( + describe(&merged[0], 0), + [ + (0, format!("{:?}", histogram(0))), + (1, format!("{:?}", histogram(1))), + (2, "1".to_string()), + ] + ); + assert_eq!(describe(&merged[0], 1), [(0, "2".to_string())]); + + for second in [Value::Float(3.0), Value::Histogram(1)] { + let error = merge_same_labelset( + &schema, + vec![ + series(&[(1, [Some(Value::Histogram(0)), None])]), + series(&[(1, [Some(second), None])]), + ], + ) + .unwrap_err(); + assert!(error.to_string().contains("same labelset"), "{error}"); + } +} + /// Two values a million positions apart are one series with two rows, addressed by two runs: the /// positions between them cost neither a row nor a value buffer sized by the span. #[test] diff --git a/src/promql/src/map.rs b/src/promql/src/map.rs index f5a219cf987..cb3c3cc6f80 100644 --- a/src/promql/src/map.rs +++ b/src/promql/src/map.rs @@ -23,7 +23,10 @@ mod go_math; #[cfg(test)] mod tests; -pub use broadcast::{ScalarSeriesOperand, map_arithmetic_series, map_compare_series}; +pub use broadcast::{ + ScalarFunction, ScalarSeriesOperand, map_arithmetic_series, map_compare_series, + map_scalar_function, +}; pub use comparison::{Comparison, ComparisonOp, map_compare, scalar_arithmetic, scalar_compare}; use std::f64::consts::PI; diff --git a/src/promql/src/map/broadcast.rs b/src/promql/src/map/broadcast.rs index b94605a7c28..b6b1a4c161b 100644 --- a/src/promql/src/map/broadcast.rs +++ b/src/promql/src/map/broadcast.rs @@ -74,6 +74,58 @@ pub fn map_compare_series( }) } +/// `max_of(a, b)` and `min_of(a, b)`, which Prometheus evaluates with Go's `math.Max` and +/// `math.Min`. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ScalarFunction { + MaxOf, + MinOf, +} + +impl ScalarFunction { + /// Go's `math.Max` / `math.Min`: an infinity of the function's own sign wins over NaN, NaN + /// over any other value, and `+0` is greater than `-0`. + pub fn apply(self, a: f64, b: f64) -> f64 { + let (wins, signed_zero_wins) = match self { + Self::MaxOf => (f64::INFINITY, false), + Self::MinOf => (f64::NEG_INFINITY, true), + }; + if a == wins || b == wins { + wins + } else if a.is_nan() || b.is_nan() { + f64::NAN + } else if a == 0.0 && a == b { + if a.is_sign_negative() == signed_zero_wins { + a + } else { + b + } + } else if (a > b) == (self == Self::MaxOf) { + a + } else { + b + } + } +} + +/// Applies `function` to two time-varying scalars, aligned by absolute grid index; a point where +/// either has no value has no result. Both operands must be unlabelled single-field series. +pub fn map_scalar_function( + schema: &Arc, + function: ScalarFunction, + left: &Series, + right: &Series, +) -> Result> { + validate_scalar(schema, left)?; + map_grid_scalar( + schema, + left, + ScalarSeriesOperand::Right(right), + left.metric.clone(), + |left, right, _| Some(function.apply(left, right)), + ) +} + /// Calls `calculate(left, right, vector_sample)` at each index where both operands exist. fn map_grid_scalar( schema: &Arc, diff --git a/src/promql/src/map/max_min_cases.txt b/src/promql/src/map/max_min_cases.txt new file mode 100644 index 00000000000..ecd6c0dc738 --- /dev/null +++ b/src/promql/src/map/max_min_cases.txt @@ -0,0 +1,65 @@ +# Generated by promql-compat cmd/narthex-golden-scalarfn built for amd64: `a b math.Max(a,b) math.Min(a,b)` as hex bits. +0 0 0 0 +0 8000000000000000 0 8000000000000000 +0 3ff0000000000000 3ff0000000000000 0 +0 bff0000000000000 0 bff0000000000000 +0 4004000000000000 4004000000000000 0 +0 7ff0000000000000 7ff0000000000000 0 +0 fff0000000000000 0 fff0000000000000 +0 7ff8000000000001 7ff8000000000001 7ff8000000000001 +8000000000000000 0 0 8000000000000000 +8000000000000000 8000000000000000 8000000000000000 8000000000000000 +8000000000000000 3ff0000000000000 3ff0000000000000 8000000000000000 +8000000000000000 bff0000000000000 8000000000000000 bff0000000000000 +8000000000000000 4004000000000000 4004000000000000 8000000000000000 +8000000000000000 7ff0000000000000 7ff0000000000000 8000000000000000 +8000000000000000 fff0000000000000 8000000000000000 fff0000000000000 +8000000000000000 7ff8000000000001 7ff8000000000001 7ff8000000000001 +3ff0000000000000 0 3ff0000000000000 0 +3ff0000000000000 8000000000000000 3ff0000000000000 8000000000000000 +3ff0000000000000 3ff0000000000000 3ff0000000000000 3ff0000000000000 +3ff0000000000000 bff0000000000000 3ff0000000000000 bff0000000000000 +3ff0000000000000 4004000000000000 4004000000000000 3ff0000000000000 +3ff0000000000000 7ff0000000000000 7ff0000000000000 3ff0000000000000 +3ff0000000000000 fff0000000000000 3ff0000000000000 fff0000000000000 +3ff0000000000000 7ff8000000000001 7ff8000000000001 7ff8000000000001 +bff0000000000000 0 0 bff0000000000000 +bff0000000000000 8000000000000000 8000000000000000 bff0000000000000 +bff0000000000000 3ff0000000000000 3ff0000000000000 bff0000000000000 +bff0000000000000 bff0000000000000 bff0000000000000 bff0000000000000 +bff0000000000000 4004000000000000 4004000000000000 bff0000000000000 +bff0000000000000 7ff0000000000000 7ff0000000000000 bff0000000000000 +bff0000000000000 fff0000000000000 bff0000000000000 fff0000000000000 +bff0000000000000 7ff8000000000001 7ff8000000000001 7ff8000000000001 +4004000000000000 0 4004000000000000 0 +4004000000000000 8000000000000000 4004000000000000 8000000000000000 +4004000000000000 3ff0000000000000 4004000000000000 3ff0000000000000 +4004000000000000 bff0000000000000 4004000000000000 bff0000000000000 +4004000000000000 4004000000000000 4004000000000000 4004000000000000 +4004000000000000 7ff0000000000000 7ff0000000000000 4004000000000000 +4004000000000000 fff0000000000000 4004000000000000 fff0000000000000 +4004000000000000 7ff8000000000001 7ff8000000000001 7ff8000000000001 +7ff0000000000000 0 7ff0000000000000 0 +7ff0000000000000 8000000000000000 7ff0000000000000 8000000000000000 +7ff0000000000000 3ff0000000000000 7ff0000000000000 3ff0000000000000 +7ff0000000000000 bff0000000000000 7ff0000000000000 bff0000000000000 +7ff0000000000000 4004000000000000 7ff0000000000000 4004000000000000 +7ff0000000000000 7ff0000000000000 7ff0000000000000 7ff0000000000000 +7ff0000000000000 fff0000000000000 7ff0000000000000 fff0000000000000 +7ff0000000000000 7ff8000000000001 7ff0000000000000 7ff8000000000001 +fff0000000000000 0 0 fff0000000000000 +fff0000000000000 8000000000000000 8000000000000000 fff0000000000000 +fff0000000000000 3ff0000000000000 3ff0000000000000 fff0000000000000 +fff0000000000000 bff0000000000000 bff0000000000000 fff0000000000000 +fff0000000000000 4004000000000000 4004000000000000 fff0000000000000 +fff0000000000000 7ff0000000000000 7ff0000000000000 fff0000000000000 +fff0000000000000 fff0000000000000 fff0000000000000 fff0000000000000 +fff0000000000000 7ff8000000000001 7ff8000000000001 fff0000000000000 +7ff8000000000001 0 7ff8000000000001 7ff8000000000001 +7ff8000000000001 8000000000000000 7ff8000000000001 7ff8000000000001 +7ff8000000000001 3ff0000000000000 7ff8000000000001 7ff8000000000001 +7ff8000000000001 bff0000000000000 7ff8000000000001 7ff8000000000001 +7ff8000000000001 4004000000000000 7ff8000000000001 7ff8000000000001 +7ff8000000000001 7ff0000000000000 7ff0000000000000 7ff8000000000001 +7ff8000000000001 fff0000000000000 7ff8000000000001 fff0000000000000 +7ff8000000000001 7ff8000000000001 7ff8000000000001 7ff8000000000001 diff --git a/src/promql/src/map/tests.rs b/src/promql/src/map/tests.rs index e0eea771af1..1cc936287fd 100644 --- a/src/promql/src/map/tests.rs +++ b/src/promql/src/map/tests.rs @@ -551,3 +551,59 @@ fn math_drops_reserved_metadata_and_empty_labels_with_the_name() { assert_eq!(output.metric.name(), None); assert_eq!(names, ["__other__", "host"]); } + +/// `max_of` / `min_of` against Go's `math.Max` / `math.Min`, whose signed-zero, NaN and infinity +/// rules differ from `f64::max` / `f64::min`. +#[test] +fn max_of_and_min_of_follow_go() { + let bits = |text: &str| f64::from_bits(u64::from_str_radix(text, 16).unwrap()); + let same = |a: f64, b: f64| a.to_bits() == b.to_bits() || (a.is_nan() && b.is_nan()); + let mut checked = 0; + for line in include_str!("max_min_cases.txt") + .lines() + .filter(|line| !line.starts_with('#')) + { + let values: Vec = line.split_whitespace().map(bits).collect(); + let (a, b) = (values[0], values[1]); + assert!( + same(ScalarFunction::MaxOf.apply(a, b), values[2]), + "max_of({a}, {b})" + ); + assert!( + same(ScalarFunction::MinOf.apply(a, b), values[3]), + "min_of({a}, {b})" + ); + checked += 1; + } + assert_eq!(checked, 64); +} + +/// Time-varying scalars pair by absolute position; a point either one lacks has no result. +#[test] +fn scalar_functions_pair_scalars_by_position() { + let schema = Arc::new(schema(&["scalar"])); + let scalar = |start: usize, values: &[Option]| { + Series::try_new( + &schema, + MetricName::try_new(None, Vec::new()).unwrap(), + TimeAxis::Grid( + GridRange::try_new(schema.eval_grid.clone().unwrap(), start, values.len()).unwrap(), + ), + SeriesFields::try_from_vec(vec![floats(values)]).unwrap(), + ) + .unwrap() + }; + let left = scalar(2, &[Some(1.0), Some(5.0), None, Some(-0.0)]); + let right = scalar(3, &[Some(2.0), Some(7.0), Some(0.0), Some(9.0)]); + let result = map_scalar_function(&schema, ScalarFunction::MaxOf, &left, &right) + .unwrap() + .unwrap(); + assert_eq!( + run_floats(&result, 0), + vec![((3, 1), vec![Some(5.0)]), ((5, 1), vec![Some(0.0)])] + ); + assert!( + run_floats(&result, 0)[1].1[0].unwrap().is_sign_positive(), + "max_of(-0, +0) is +0" + ); +} diff --git a/src/promql/src/map_time.rs b/src/promql/src/map_time.rs index 5101cb7fbfa..c0a2c5b94c3 100644 --- a/src/promql/src/map_time.rs +++ b/src/promql/src/map_time.rs @@ -43,6 +43,37 @@ pub enum DateFunction { Year, } +/// `start()`, `end()`, `range()` and `step()`: constants of a query, which Prometheus folds into +/// number literals before evaluating it. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum QueryContextFunction { + Start, + End, + Range, + Step, +} + +impl QueryContextFunction { + /// The value for a query from `start_ms` to `end_ms` every `step_ms`. `step()` is 0 when the + /// start and end are equal, as for an instant query. + pub fn value(self, start_ms: i64, end_ms: i64, step_ms: i64) -> f64 { + match self { + Self::Start => start_ms as f64 / 1000.0, + Self::End => end_ms as f64 / 1000.0, + Self::Range => duration_seconds(end_ms.saturating_sub(start_ms)), + Self::Step if start_ms == end_ms => 0.0, + Self::Step => duration_seconds(step_ms), + } + } +} + +/// Go's `time.Duration.Seconds` of `ms` milliseconds: whole seconds plus the remainder over 1e9, +/// two roundings rather than one division. The nanosecond count saturates as `time.Time.Sub` does. +fn duration_seconds(ms: i64) -> f64 { + let nanos = ms.saturating_mul(1_000_000); + (nanos / 1_000_000_000) as f64 + (nanos % 1_000_000_000) as f64 / 1e9 +} + /// `time()`: the evaluation time in seconds at every point of the schema's grid, unlabelled. pub fn time(schema: &SeriesSchema) -> Result { eval_time_series(schema, |ms| ms as f64 / 1000.0) diff --git a/src/promql/src/map_time/duration_seconds_cases.txt b/src/promql/src/map_time/duration_seconds_cases.txt new file mode 100644 index 00000000000..851c93c2196 --- /dev/null +++ b/src/promql/src/map_time/duration_seconds_cases.txt @@ -0,0 +1,211 @@ +# Generated by promql-compat cmd/narthex-golden-scalarfn built for amd64: `ms time.Duration.Seconds()` for a millisecond difference, as hex bits. +0 0 +1 3f50624dd2f1a9fc +999 3feff7ced916872b +1000 3ff0000000000000 +1001 3ff004189374bc6a +4097 40106353f7ced917 +86400000 40f5180000000000 +9007199254740993 42012e0be826d695 +9223372036854 42012e0be826d4fe +9223372036855 42012e0be826d695 +2340891196754 41e170e5479820c5 +28653324801615 42012e0be826d695 +15150070723101 42012e0be826d695 +5203274071043 41f3623a9570b021 +6052846691025 41f68c71c6306666 +19267443321048 42012e0be826d695 +12341472526366 42012e0be826d695 +20530526999188 42012e0be826d695 +34395057096608 42012e0be826d695 +8401369033625 41ff4c2b7c9a0000 +31640814836996 42012e0be826d695 +29341436692007 42012e0be826d695 +34783771064630 42012e0be826d695 +35122158268884 42012e0be826d695 +1479560899851 41d60c13b0f676c9 +23204167308761 42012e0be826d695 +24788341754728 42012e0be826d695 +31429837266203 42012e0be826d695 +34244401504646 42012e0be826d695 +13068139990387 42012e0be826d695 +7761385230625 41fce9d570ea0000 +9735534642756 42012e0be826d695 +20240948685375 42012e0be826d695 +33614386955765 42012e0be826d695 +4944260193162 41f26b36c612978d +14479808172613 42012e0be826d695 +17797368697448 42012e0be826d695 +14261095223522 42012e0be826d695 +13428055133044 42012e0be826d695 +26352584022491 42012e0be826d695 +12325408233432 42012e0be826d695 +17010777739835 42012e0be826d695 +28430422903295 42012e0be826d695 +29753883088875 42012e0be826d695 +25998949474507 42012e0be826d695 +10226187168843 42012e0be826d695 +17769199632283 42012e0be826d695 +4363496320571 41f0415ab80922d1 +3287173645812 41e87dc941b9fbe7 +12725827928836 42012e0be826d695 +24795910633675 42012e0be826d695 +2828521278844 41e512f9e7db020c +4170359431716 41ef125450f6e979 +3589276087583 41eabe0036f2a7f0 +28524985356220 42012e0be826d695 +30320656727859 42012e0be826d695 +4256148642671 41efb5f5945578d5 +29566978976822 42012e0be826d695 +1648135601414 41d88f236c5a7efa +4780677690357 41f1cf35a3a5b646 +3390646811377 41e94325436c1062 +6912080800569 41f9bfdfba091aa0 +19744540834685 42012e0be826d695 +5048451373608 41f2ce9412d9ba5e +31511077099905 42012e0be826d695 +33060246075211 42012e0be826d695 +29446920826861 42012e0be826d695 +20868689814545 42012e0be826d695 +25732868269994 42012e0be826d695 +26023816056118 42012e0be826d695 +3312873794438 41e8aece284e0419 +8928671130029 4200a185acd03b64 +27201078145192 42012e0be826d695 +28161007422010 42012e0be826d695 +23548861302960 42012e0be826d695 +8403898590605 41ff4e950de9ae14 +2101319756957 41df4fe6133d3f7d +18153789976583 42012e0be826d695 +16078846218429 42012e0be826d695 +14439764018086 42012e0be826d695 +15717223852073 42012e0be826d695 +7594418227955 41fc4a9a033f47ae +1007547111184 41ce06f973978d50 +7653748532646 41fc832ef34a5604 +1542530612607 41d6fc498d26d917 +8959567520889 4200b04135071cac +1963454100123 41dd41fba507df3b +15423611227189 42012e0be826d695 +26921592371531 42012e0be826d695 +31560677483706 42012e0be826d695 +21024346859206 42012e0be826d695 +21411506917513 42012e0be826d695 +14299718455620 42012e0be826d695 +25966354044973 42012e0be826d695 +21480240207871 42012e0be826d695 +13383017194856 42012e0be826d695 +19694757267735 42012e0be826d695 +8329763775671 41ff07e1bbfabc6a +24204352256265 42012e0be826d695 +5166480575653 41f33f23cbfa72b0 +17311660727623 42012e0be826d695 +26768379971433 42012e0be826d695 +21452255664239 42012e0be826d695 +32930407231978 42012e0be826d695 +15551692227912 42012e0be826d695 +23123035545994 42012e0be826d695 +5875644708440 41f5e37392470a3d +18292969058316 42012e0be826d695 +1707825217332 41d972d610553f7d +23633727413655 42012e0be826d695 +24472694155897 42012e0be826d695 +32056439689975 42012e0be826d695 +25133237617897 42012e0be826d695 +10984619967605 42012e0be826d695 +12399994332458 42012e0be826d695 +17017307690557 42012e0be826d695 +24405803592380 42012e0be826d695 +34039839184523 42012e0be826d695 +12086900862154 42012e0be826d695 +4880216990563 41f22e2339e9020c +8291016285084 41fee2ede5d15810 +34263874447617 42012e0be826d695 +5219901505275 41f3721604146666 +3325751404254 41e8c75e0d8820c5 +10069265674283 42012e0be826d695 +2205986208125 41e06f95b4040000 +17331608106692 42012e0be826d695 +13164910887275 42012e0be826d695 +20165189440141 42012e0be826d695 +18100958213119 42012e0be826d695 +33252881487365 42012e0be826d695 +28493023658516 42012e0be826d695 +8853845580432 42007dd7b26374bc +34838873892001 42012e0be826d695 +20214329624262 42012e0be826d695 +31705600540751 42012e0be826d695 +31189001046116 42012e0be826d695 +29476027927210 42012e0be826d695 +26781011366842 42012e0be826d695 +22699855557780 42012e0be826d695 +17749079048291 42012e0be826d695 +30750407110207 42012e0be826d695 +24417907650311 42012e0be826d695 +10509573177644 42012e0be826d695 +1079378703273 41d01580c3d178d5 +34862833947828 42012e0be826d695 +24980588397686 42012e0be826d695 +11209991956240 42012e0be826d695 +8937655194575 4200a5ce5cd4999a +23978885618615 42012e0be826d695 +30065528340854 42012e0be826d695 +2628030736846 41e39492221b126f +2663938618407 41e3d90f474d0625 +34895264552379 42012e0be826d695 +15670995231869 42012e0be826d695 +4191047303785 41ef39c9d0f91eb8 +8437263482680 41ff6e66c7aae148 +3960463773594 41ed81fc33b3020c +4309293785322 41f00da9ad9526e9 +26649392599963 42012e0be826d695 +4157860955829 41eefa7d8b7a872b +22296599249089 42012e0be826d695 +32695131508279 42012e0be826d695 +30635397919632 42012e0be826d695 +15331538117365 42012e0be826d695 +11063348740606 42012e0be826d695 +32371941731295 42012e0be826d695 +13719859399284 42012e0be826d695 +7293417131828 41fb2b8b6abd3f7d +33407534042909 42012e0be826d695 +31297857606586 42012e0be826d695 +1097102363540 41d0591d06e28f5c +25332039849587 42012e0be826d695 +20041642732253 42012e0be826d695 +18752081044817 42012e0be826d695 +29442119239956 42012e0be826d695 +28855454932055 42012e0be826d695 +31958604071351 42012e0be826d695 +18688407702704 42012e0be826d695 +17871169582207 42012e0be826d695 +16003600945447 42012e0be826d695 +5284509633417 41f3afb37c16ac08 +11575618223404 42012e0be826d695 +23263846003261 42012e0be826d695 +14260376149051 42012e0be826d695 +20109100243330 42012e0be826d695 +16359587412236 42012e0be826d695 +34836697130263 42012e0be826d695 +23618221322728 42012e0be826d695 +16214535784284 42012e0be826d695 +8272436198602 41fed135be69a1cb +5401941921460 41f41fb17a175c29 +7235498080071 41faf44f060122d1 +14773466706669 42012e0be826d695 +34843449378732 42012e0be826d695 +23707829047074 42012e0be826d695 +4081628494438 41ee6916a9ce0419 +13573087199008 42012e0be826d695 +21542290482225 42012e0be826d695 +16883004474069 42012e0be826d695 +16172951872856 42012e0be826d695 +5849767460850 41f5cac5e24d999a +12771076879764 42012e0be826d695 +7080148168638 41fa6027cc8a353f +13469889734230 42012e0be826d695 +9296342191848 42012e0be826d695 +753418709293 41c67420eaa58106 +23411833150514 42012e0be826d695 +29448477422232 42012e0be826d695 +29413433948272 42012e0be826d695 diff --git a/src/promql/src/map_time/tests.rs b/src/promql/src/map_time/tests.rs index 05128523a65..0124b420129 100644 --- a/src/promql/src/map_time/tests.rs +++ b/src/promql/src/map_time/tests.rs @@ -24,7 +24,7 @@ use datatypes::series::samples::{ }; use datatypes::series::time::{EvalGrid, GridRange, GridSpan, GridSpec, TimeAxis}; -use super::{DateFunction, date_of_eval_time, map_date, time}; +use super::{DateFunction, QueryContextFunction, date_of_eval_time, map_date, time}; use crate::grid_series::merge_same_labelset; use crate::grid_series::reference::{ Labels, Points, canonical_bits, golden_cases, labels_of, metric_of, points_by_labels, @@ -289,3 +289,38 @@ fn map_date_reads_a_sparse_axis_by_its_positions() { ]) ); } + +/// `range()` and `step()` are Go `Duration.Seconds` of a millisecond count, which rounds twice +/// and so differs from `ms / 1000` in the last bit for most values. +#[test] +fn query_context_durations_match_go() { + let mut checked = 0; + for line in include_str!("duration_seconds_cases.txt") + .lines() + .filter(|line| !line.starts_with('#')) + { + let (ms, bits) = line.split_once(' ').unwrap(); + let ms: i64 = ms.parse().unwrap(); + let expected = u64::from_str_radix(bits, 16).unwrap(); + assert_eq!( + QueryContextFunction::Range.value(0, ms, 1).to_bits(), + expected, + "range() over {ms} ms" + ); + assert_eq!( + QueryContextFunction::Step.value(0, 1, ms).to_bits(), + expected, + "step() of {ms} ms" + ); + checked += 1; + } + assert!(checked > 200, "{checked} durations checked"); + // Two valid endpoints whose difference leaves `i64` saturate, as `time.Time.Sub` does. + assert_eq!( + QueryContextFunction::Range.value(i64::MIN / 2, i64::MAX / 2 + 1, 1), + QueryContextFunction::Range.value(0, i64::MAX, 1) + ); + assert_eq!(QueryContextFunction::Step.value(5_000, 5_000, 1_000), 0.0); + assert_eq!(QueryContextFunction::Start.value(-1_500, 0, 1), -1.5); + assert_eq!(QueryContextFunction::End.value(0, 2_250, 1), 2.25); +} diff --git a/src/promql/src/native/display.rs b/src/promql/src/native/display.rs index 7e0e82c2b5e..86d40946cc1 100644 --- a/src/promql/src/native/display.rs +++ b/src/promql/src/native/display.rs @@ -32,7 +32,7 @@ use crate::native::plan::{ NativeType, }; use crate::range_eval::RangeFunction; -use crate::vector_match::{BinaryOp, Cardinality, MatchingLabels}; +use crate::vector_match::{BinaryOp, Cardinality, FillValues, MatchingLabels}; /// The header line of every tree. const HEADER: &str = "NativeTree"; @@ -121,7 +121,7 @@ fn describe(node: &NativeNode) -> String { } ), NativeExpr::Binary { matching, .. } => format!( - "{}{} {} {}", + "{}{} {} {}{}", binary_op(matching.op), if matching.return_bool { " bool" } else { "" }, match &matching.labels { @@ -132,11 +132,25 @@ fn describe(node: &NativeNode) -> String { Cardinality::OneToOne => "one-to-one".to_string(), Cardinality::ManyToOne(labels) => format!("many-to-one [{}]", label_list(labels)), Cardinality::OneToMany(labels) => format!("one-to-many [{}]", label_list(labels)), - } + }, + fill(matching.fill) ), } } +fn fill(values: FillValues) -> String { + let side = |name: &str, value: Option| { + value + .map(|value| format!(" {name} {value}")) + .unwrap_or_default() + }; + format!( + "{}{}", + side("fill_left", values.lhs), + side("fill_right", values.rhs) + ) +} + /// A window: what it runs, over which range, and where it lands. fn window(op: &BoundWindow) -> String { format!( diff --git a/src/promql/src/native/display/tests.rs b/src/promql/src/native/display/tests.rs index 8166bad3c98..316f6a39a4e 100644 --- a/src/promql/src/native/display/tests.rs +++ b/src/promql/src/native/display/tests.rs @@ -24,7 +24,7 @@ use super::super::plan::{ }; use crate::map::MathOp; use crate::range_eval::{RangeFunction, RangeWindow}; -use crate::vector_match::{BinaryMatching, BinaryOp, Cardinality, MatchingLabels}; +use crate::vector_match::{BinaryMatching, BinaryOp, Cardinality, FillValues, MatchingLabels}; fn grid(start: i64, step: i64, len: u64) -> Arc { Arc::new(EvalGrid::try_new(GridSpec::try_new(start, step, len).unwrap()).unwrap()) @@ -324,11 +324,12 @@ fn an_aggregate_and_a_binary_name_their_binding() { return_bool: false, labels: MatchingLabels::Ignoring(Vec::new()), cardinality: Cardinality::OneToOne, + fill: FillValues::default(), }, }, output: evaluated(out), }; - let tree = NativeTree::try_new(binary).unwrap(); + let tree = NativeTree::try_new(binary.clone()).unwrap(); assert_eq!( tree.to_string(), shown(&[ @@ -339,6 +340,21 @@ fn an_aggregate_and_a_binary_name_their_binding() { " input Input raw samples", ]) ); + + // Fill values change the result, so a set one is part of the binding shown. + let mut filled = binary; + let NativeExpr::Binary { matching, .. } = &mut filled.expr else { + unreachable!() + }; + matching.fill.rhs = Some(0.5); + assert_eq!( + NativeTree::try_new(filled) + .unwrap() + .to_string() + .lines() + .nth(1), + Some(" root Binary + ignoring [] one-to-one fill_right 0.5") + ); } /// A `without` grouping reads as `without [...]`, not as `by [...]`. diff --git a/src/promql/src/native/metrics.rs b/src/promql/src/native/metrics.rs index bcae9ddcdd0..6e36153fe2e 100644 --- a/src/promql/src/native/metrics.rs +++ b/src/promql/src/native/metrics.rs @@ -452,6 +452,7 @@ mod tests { return_bool: false, labels: crate::vector_match::MatchingLabels::Ignoring(Vec::new()), cardinality: crate::vector_match::Cardinality::OneToOne, + fill: crate::vector_match::FillValues::default(), }, }, output: { diff --git a/src/promql/src/native/plan.rs b/src/promql/src/native/plan.rs index 6cca42134f3..f5715b21f72 100644 --- a/src/promql/src/native/plan.rs +++ b/src/promql/src/native/plan.rs @@ -122,8 +122,8 @@ pub struct AggregateStateSchema { /// A scan or exchange boundary bound as a node input. /// /// The binding records what the input was created for: the output a consumer type-checks against, -/// and the ordering it guarantees. A window reads the ordering, rejecting an interleaved input; no -/// other consumer reads it yet. +/// and the ordering it guarantees. An instant selection and a window both read the ordering, +/// rejecting an interleaved input; no other consumer reads it yet. #[derive(Clone, Debug)] pub struct BoundInput { /// What the input produces: raw samples from a scan, or state from an exchange boundary. @@ -329,14 +329,13 @@ impl NativeNode { ))); } } + ensure_not_interleaved(input)?; ensure_samples(&self.output, "an instant selection")?; // A selection is one source row per grid point: `select_instants` picks row // indices and every field is taken at them, so it carries one value per input // field. ensure_same_fields(&input.output, &self.output, "an instant selection")?; ensure_grid(&self.output, &op.output_grid, "an instant selection")?; - // `select_instants` reads one series at a time and holds no cross-series state, so - // an interleaved input is enough for it: no ordering check here. } NativeExpr::Window { input, op } => { input.validate()?; @@ -354,17 +353,7 @@ impl NativeNode { "a window reads an empty range: its range must be positive", )); } - // A range evaluator consumes one series to its last chunk before it starts the - // next, so interleaved chunks of different series cannot be fed to it. Only an - // input node carries an ordering, so only there is there one to check. - if let NativeExpr::Input(binding) = &input.expr - && binding.ordering == SeriesOrdering::Interleaved - { - return Err(invalid( - "a window reads one series to its last chunk before the next, so \ - interleaved chunks cannot be fed to it", - )); - } + ensure_not_interleaved(input)?; // What is checked here and just below is that the declared input grid equals the // input's grid, and that the output grid matches the declared one. The grid is // not checked to cover the window's read range @@ -624,6 +613,25 @@ fn ensure_grid(ty: &NativeType, expected: &Arc, what: &str) -> Result< } } +/// Checks that `input`, when it is a [`NativeExpr::Input`], does not declare interleaved series. +/// +/// A native evaluation reads one series to its last chunk before the next: a range evaluator folds +/// a series completely before it starts another, and an instant selection's consumer collects one +/// series' chunks into one buffer. Neither can be handed chunks of different series that +/// interleave, which is the one rule both consumers state. Only an input node carries an ordering, +/// so only there is there one to check. +fn ensure_not_interleaved(input: &NativeNode) -> Result<()> { + if let NativeExpr::Input(binding) = &input.expr + && binding.ordering == SeriesOrdering::Interleaved + { + return Err(invalid( + "a native evaluation reads one series to its last chunk before the next, so \ + interleaved chunks cannot be fed to it", + )); + } + Ok(()) +} + fn invalid(reason: &str) -> DataFusionError { // A rejected tree is a compiler defect, not a query error: the compiler only builds a node // after deciding that this combination is supported. `Internal` keeps that meaning on the wire diff --git a/src/promql/src/native/plan/tests.rs b/src/promql/src/native/plan/tests.rs index 85be7426a58..ba01eb53cdd 100644 --- a/src/promql/src/native/plan/tests.rs +++ b/src/promql/src/native/plan/tests.rs @@ -24,7 +24,7 @@ use super::{ }; use crate::map::MathOp; use crate::range_eval::{RangeFunction, RangeWindow}; -use crate::vector_match::{BinaryMatching, BinaryOp, Cardinality, MatchingLabels}; +use crate::vector_match::{BinaryMatching, BinaryOp, Cardinality, FillValues, MatchingLabels}; fn grid(start: i64, step: i64, len: u64) -> Arc { Arc::new(EvalGrid::try_new(GridSpec::try_new(start, step, len).unwrap()).unwrap()) @@ -96,6 +96,18 @@ fn input_with_fields(count: usize) -> NativeNode { } } +/// A scan of raw samples whose chunks of different series may interleave. +fn interleaved_input() -> NativeNode { + let output = raw(); + NativeNode { + expr: NativeExpr::Input(BoundInput { + output: output.clone(), + ordering: SeriesOrdering::Interleaved, + }), + output, + } +} + /// A constant scalar, which is a scalar type evaluated on the statement's grid. fn scalar_on(value: f64, grid: Arc) -> NativeNode { NativeNode { @@ -218,6 +230,7 @@ fn add() -> BinaryMatching { return_bool: false, labels: MatchingLabels::Ignoring(Vec::new()), cardinality: Cardinality::OneToOne, + fill: FillValues::default(), } } @@ -456,15 +469,35 @@ fn a_window_checks_its_declared_input_grid() { /// chunks of different series may not interleave. #[test] fn a_window_cannot_read_interleaved_input() { - let output = raw(); - let interleaved = NativeNode { - expr: NativeExpr::Input(BoundInput { - output: output.clone(), - ordering: SeriesOrdering::Interleaved, - }), - output, + let error = window_over(interleaved_input(), None) + .validate() + .unwrap_err(); + assert!( + error + .to_string() + .contains("interleaved chunks cannot be fed to it"), + "{error}" + ); +} + +/// An instant selection's consumer collects one series' chunks into one buffer, so it reads a +/// series to its last chunk before the next: it refuses an interleaved input for the reason a +/// window does. +#[test] +fn an_instant_selection_cannot_read_interleaved_input() { + let out = grid(0, 60_000, 4); + let node = NativeNode { + expr: NativeExpr::Instant { + input: Box::new(interleaved_input()), + op: BoundInstant { + lookback_ms: 300_000, + offset_ms: 0, + output_grid: Arc::clone(&out), + }, + }, + output: evaluated(Arc::new([]), out), }; - let error = window_over(interleaved, None).validate().unwrap_err(); + let error = node.validate().unwrap_err(); assert!( error .to_string() diff --git a/src/promql/src/range_eval.rs b/src/promql/src/range_eval.rs index 428f9f0f9af..1564fec0d0a 100644 --- a/src/promql/src/range_eval.rs +++ b/src/promql/src/range_eval.rs @@ -80,7 +80,8 @@ pub(crate) struct FloatWindows<'a> { pub resets: &'a [CounterReset], /// Absolute position of `timestamps[0]`: a reset position is `base` plus a sample index. pub base: u64, - /// One range per window, ascending and left-open at the start. + /// One range per window, ascending. Each is a half-open index range `[start, end)`; the time + /// window it was derived from is left-open, `(T - range_ms, T]`. pub windows: &'a [Range], /// The evaluation timestamp of each window. pub eval_ts: &'a [i64], @@ -90,8 +91,9 @@ pub(crate) struct FloatWindows<'a> { impl FloatWindows<'_> { /// The resets inside window `index`. /// - /// `windows` are absolute indices into `timestamps`, so a window whose start has already - /// been released still maps to the right reset. + /// `windows` index the retained arrays as they stand in this batch, not absolutely: each + /// span is relative to `timestamps` and `values`, which is why `base` plus a span start is + /// the absolute position of the sample the span starts at. pub(crate) fn resets_of(&self, index: usize) -> &[CounterReset] { let span = &self.windows[index]; resets_between( @@ -167,6 +169,10 @@ struct PendingBatch { /// Evaluation timestamp of each accumulated point. eval_ts: Vec, /// The retained-sample range of each point's window, one list per field. + /// + /// A span is relative to the retained arrays at the time it was recorded, not an absolute + /// index, so [`FieldSamples::advance`] invalidates it: that is why `evaluate` flushes the + /// batch before advancing. spans: Vec>>, /// One output column per field, written by the batch kernel. columns: Vec>>, @@ -202,9 +208,6 @@ impl PendingBatch { /// Records one point for the next batch call: its evaluation timestamp and, per field, the range /// of retained samples its window covers. -/// -/// The ranges are relative to the retained samples, so samples dropped before the batch flushes -/// would shift them; [`flush_batch`] therefore runs before any `advance`. fn accumulate(pending: &mut PendingBatch, index: usize, eval_ts: i64, fields: &[FieldSamples]) { debug_assert_eq!(pending.spans.len(), fields.len()); pending.indices.push(index); @@ -236,8 +239,6 @@ fn flush_batch( *batch_calls += 1; } for (index, field) in fields.iter().enumerate() { - // `pending.spans` index the retained arrays absolutely, so no adjustment is needed for - // samples `advance` has released: those sit before every span start. let batch = FloatWindows { timestamps: &field.timestamps, values: &field.values, @@ -1043,8 +1044,6 @@ impl RangeEvaluator { } state.cursor += 1; } - // The accumulated spans index the retained arrays, which `advance` below rewrites, so the - // batch has to flush first. if let Some(kernel) = self.batch.as_mut() { flush_batch( kernel, @@ -1073,6 +1072,11 @@ impl RangeEvaluator { } state.cursor = decided; } + debug_assert!( + self.pending.is_empty(), + "`evaluate` flushes the batch before `advance` rewrites the retained arrays a \ + pending span indexes" + ); for field in &mut self.fields { // After the last window, samples are still ingested so that the overflow and // histogram checks do not depend on how the series is chunked, but none are kept. diff --git a/src/promql/src/range_eval/function.rs b/src/promql/src/range_eval/function.rs index f2765d1a543..88888bb15f8 100644 --- a/src/promql/src/range_eval/function.rs +++ b/src/promql/src/range_eval/function.rs @@ -73,6 +73,11 @@ impl RangeFunction { /// the same as saying `IS_COUNTER` in `extrapolated_rate`: `rate` and `increase`. A kernel /// that corrects counters without the index being maintained would silently drop every /// correction, so a new counter function has to be added here as well. + /// + /// It governs both readers of the index: the per-window kernel above, and the batch kernel, + /// which reads [`FloatWindows::resets_of`]. When it returns `false`, `resets` stays empty and + /// `resets_of` yields nothing, so a batch kernel for a counter function would skip every + /// correction and report `delta`-like values. pub(crate) fn indexes_counter_resets(self) -> bool { matches!(self, Self::Rate | Self::Increase) } diff --git a/src/promql/src/range_eval/tests.rs b/src/promql/src/range_eval/tests.rs index c11af5e3c02..f3ea5d3d952 100644 --- a/src/promql/src/range_eval/tests.rs +++ b/src/promql/src/range_eval/tests.rs @@ -721,6 +721,185 @@ fn functions_match_range_udfs() { } } +/// Replaces the float sample of `field` at every row index in `rows` with a native histogram +/// sample, so the windows holding one take the mixed-window path. +fn mix_in_histograms(chunks: &mut [RawSeriesChunk], field: usize, rows: &BTreeSet) { + let histogram = "0/0/0/4014000000000000/4014000000000000/1/0_1/4014000000000000///"; + let view = OwnedHistogram::parse(histogram); + let mut start = 0; + for piece in chunks { + let len = piece.time.len(); + let local: Vec = rows + .range(start..start + len) + .map(|&row| row - start) + .collect(); + start += len; + if local.is_empty() { + continue; + } + let mut builder = HistogramBatchBuilder::new(); + let indices: UInt32Array = (0..len) + .map(|row| { + local + .binary_search(&row) + .is_ok() + .then(|| builder.push(view.view()).unwrap()) + }) + .collect(); + // A position is a float or a histogram, never both, so the float sample of a mixed-in row + // goes. + let values: Float64Array = piece.fields.as_slice()[field] + .iter() + .enumerate() + .map(|(row, sample)| match sample { + _ if local.binary_search(&row).is_ok() => None, + SampleRef::Float(value) => Some(value), + SampleRef::Missing => None, + SampleRef::Histogram(_) => unreachable!("histograms are mixed in once"), + }) + .collect(); + let histograms = HistogramSamples::try_new(indices, builder.finish().unwrap()).unwrap(); + let mixed = Samples::try_from_mixed(values, histograms).unwrap(); + let fields = piece + .fields + .as_slice() + .iter() + .enumerate() + .map(|(index, samples)| { + if index == field { + mixed.clone() + } else { + samples.clone() + } + }) + .collect(); + piece.fields = SeriesFields::try_from_vec(fields).unwrap(); + } +} + +/// Asserts two evaluator results are identical, including native histogram samples, which the +/// dense form [`expand`] rejects. +fn assert_same_series(actual: Option<&Series>, expected: Option<&Series>, context: &str) { + match (actual, expected) { + (None, None) => {} + (Some(actual), Some(expected)) => { + assert_eq!(actual.metric, expected.metric, "{context}: metric"); + let (TimeAxis::Grid(actual_range), TimeAxis::Grid(expected_range)) = + (&actual.time, &expected.time) + else { + panic!( + "{context}: expected grid series, got {:?} and {:?}", + actual.time, expected.time + ); + }; + assert_eq!(actual_range, expected_range, "{context}: grid range"); + for (field, (got, want)) in actual + .fields + .as_slice() + .iter() + .zip(expected.fields.as_slice()) + .enumerate() + { + for (row, (got, want)) in got.iter().zip(want.iter()).enumerate() { + let context = format!("{context}: field {field} row {row}"); + match (got, want) { + (SampleRef::Missing, SampleRef::Missing) => {} + (SampleRef::Float(got), SampleRef::Float(want)) => { + assert_eq!(got.to_bits(), want.to_bits(), "{context}: {got} vs {want}") + } + (SampleRef::Histogram(got), SampleRef::Histogram(want)) => { + let got = FloatHistogram::try_from_view(&got).unwrap(); + let want = FloatHistogram::try_from_view(&want).unwrap(); + // Both paths run the same histogram code over the same samples, so + // this is an exact comparison: an epsilon of zero. + assert_histogram_close(&got, &want, 0.0, &context); + } + (SampleRef::Missing, _) => { + panic!("{context}: only the per-window path produced a sample") + } + (_, SampleRef::Missing) => { + panic!("{context}: only the batch path produced a sample") + } + _ => panic!("{context}: a float on one path and a histogram on the other"), + } + } + } + } + (actual, expected) => panic!( + "{context}: one path produced a series, the other none: {actual:?} vs {expected:?}" + ), + } +} + +/// `rate` and `increase` over random input, computed twice: by the batch kernel and by the +/// per-window path. +/// +/// Both evaluators are built by `try_new` and differ in nothing but the dispatch shape, and both +/// kernels reach `extrapolated_rate_of`, so the results have to be bit-identical. A difference +/// means the batch read a different set of samples, evaluated at a different time, or corrected a +/// different reset: a window bound that disagrees with the per-window path. +/// +/// The rows mix gaps, stale markers, counter resets and native histogram samples, the last so +/// that some grid points take the mixed-window fallback, where a pending batch has to flush +/// before the point is decided on the per-window path. The random chunk splits are what make +/// `advance` drop samples mid-series, so spans that outlived one would read shifted samples. +#[test] +fn batch_counter_functions_match_the_per_window_path_on_random_input() { + let mut batch_calls = 0; + for seed in 0..200 { + let mut rng = StdRng::seed_from_u64(seed); + let fields = rng.random_range(1..=3); + let (grid, mut window) = random_window(&mut rng); + // An anchored selector reads one window and installs no batch kernel, so both paths would + // run the same code; every seed stays on the grid-driven batch path. + window.anchor_ms = None; + let function = if rng.random_bool(0.5) { + RangeFunction::Rate + } else { + RangeFunction::Increase + }; + let mut batched = + RangeEvaluator::try_new(&raw_schema(fields), grid.clone(), window, function).unwrap(); + let mut per_window = + RangeEvaluator::try_new(&raw_schema(fields), grid.clone(), window, function).unwrap(); + // The per-window path is the same evaluator without the batch kernel. `with_kernel`, + // which the other differential tests use, rejects native histogram samples, and these + // series carry some, so removing the batch form is the only switch that leaves the rest + // of the evaluation identical. + per_window.batch = None; + // One evaluator serves several series, so state must not leak between them. + for host in 0..3 { + let rows = random_rows(&mut rng, fields); + let histogram_field = rng.random_range(0..fields); + let histogram_rows: BTreeSet = + (0..rows.len()).filter(|_| rng.random_bool(0.1)).collect(); + let mut chunks = random_chunks(&mut rng, &rows, fields, host); + mix_in_histograms(&mut chunks, histogram_field, &histogram_rows); + // A push hands out the positions it decided, not only on the last chunk, so what each + // path produced is every chunk it handed out, folded back into one series. + let batched_chunks = collect_chunks(&mut batched, &chunks); + let per_window_chunks = collect_chunks(&mut per_window, &chunks); + + let batched_result = fold_chunks(&batched_chunks, batched.output_schema()); + let per_window_result = fold_chunks(&per_window_chunks, per_window.output_schema()); + + let context = format!("seed {seed} host {host}"); + assert_same_series( + batched_result.as_ref(), + per_window_result.as_ref(), + &context, + ); + } + batch_calls += batched.batch_call_count(); + } + // The comparison is worth nothing unless the batch kernel ran at all: counting the calls is + // what tells a batch-path comparison from one that silently fell back to per-window. + assert!( + batch_calls > 300, + "only {batch_calls} batch kernel calls: the batch path was barely reached" + ); +} + /// A chunk of entirely present float columns is appended in bulk, and that is the path the test /// asserts: the per-sample loop produces the same output, so comparing values alone could not /// tell whether the bulk path ran. @@ -786,31 +965,6 @@ fn a_chunk_with_histograms_is_not_appended_in_bulk() { assert_eq!(evaluator.bulk_ingest_count(), 0); } -/// `rate` over a float-only series takes the batch path. The per-window path produces the same -/// output, so a test comparing values alone could not tell which one ran. -#[test] -fn rate_over_floats_is_evaluated_in_batches() { - let rows: Vec = (0..20) - .map(|index| (index as i64 * 10_000, vec![Some(index as f64)])) - .collect(); - let mut evaluator = RangeEvaluator::try_new( - &raw_schema(1), - grid(0, 10_000, 20), - RangeWindow { - range_ms: 30_000, - offset_ms: 0, - anchor_ms: None, - }, - RangeFunction::Rate, - ) - .unwrap(); - evaluator.push(&chunk(&rows, 1, 0, true)).unwrap(); - assert!( - evaluator.batch_call_count() > 0, - "rate over floats must reach the batch kernel" - ); -} - /// A batch spanning more grid points than `EVALUATION_BATCH` flushes in the middle of one /// evaluation, and each field's windows come from that field's own samples. /// @@ -881,7 +1035,6 @@ fn a_batch_spanning_fields_and_the_flush_threshold_matches_the_per_window_path() .push(&chunk(&rows, fields, 0, true)) .unwrap() .expect("the series completes"); - assert_eq!(per_window.batch_call_count(), 0); // Fold the chunk back into one series first: a chunk's range reports the positions its push // decided, which can include leading and trailing positions holding no value, while the @@ -899,8 +1052,9 @@ fn a_batch_spanning_fields_and_the_flush_threshold_matches_the_per_window_path() /// A window holding a native histogram sample keeps its grid point on the per-window path, since /// the batch kernel reads the floats alone and cannot reproduce the mixed-window rules. /// -/// The series is laid out so every grid point's window contains the histogram: one point fewer -/// than the first test, so no point can fall before it. +/// The series is laid out so every grid point's window contains the histogram: three samples +/// 10 s apart with the histogram on the first, and a window reaching 30 s back, so a window +/// covering any grid point reaches timestamp 0. #[test] fn a_histogram_window_keeps_rate_on_the_per_window_path() { let histogram = "0/0/0/4014000000000000/4014000000000000/1/0_1/4014000000000000///"; diff --git a/src/promql/src/select.rs b/src/promql/src/select.rs index cc869b91a93..c65a7955c36 100644 --- a/src/promql/src/select.rs +++ b/src/promql/src/select.rs @@ -13,9 +13,9 @@ // limitations under the License. //! Operators that depend on the whole set of evaluated series at each grid point: `topk`, -//! `bottomk`, `scalar`, `absent` and the instant-result sorts. +//! `bottomk`, `limitk`, `limit_ratio`, `scalar`, `absent` and the instant-result sorts. //! -//! Semantics follow Prometheus (`aggregationK`, `funcScalar`, `funcAbsent`, +//! Semantics follow Prometheus (`aggregationK`, `HashRatioSampler`, `funcScalar`, `funcAbsent`, //! `createLabelsForAbsentFunction`, `funcSort*`). A scalar is a grid series without labels that //! has a value at every point, so `vector(s)` is `s` itself and has no kernel. @@ -29,7 +29,7 @@ use datatypes::series::time::union_positions; use datatypes::series::{Label, MetricName}; use crate::go_sort; -use crate::grid_series::{GridSeriesBuilder, external}; +use crate::grid_series::{GridSeriesBuilder, external, owned}; #[cfg(test)] mod tests; @@ -73,22 +73,9 @@ pub fn topk( // Validates the series against the schema and its grid; the walk below visits the carried // positions, not the whole grid. grid_points(schema, series)?; - // Prometheus returns nothing for k below 1, rejects NaN and values out of int64, then - // truncates k and caps it at the number of input series. - if k < 1.0 { + let Some(k) = k_param(k, series.len())? else { return Ok(Vec::new()); - } - if k.is_nan() { - return Err(DataFusionError::Execution("Parameter value is NaN".into())); - } - // Prometheus' `maxInt64`, the largest float64 below 2^63. - if k >= 9_223_372_036_854_774_784.0 { - return Err(DataFusionError::Execution(format!( - "Scalar value {k} overflows int64" - ))); - } - let k = k.trunc().min(series.len() as f64) as usize; - + }; let mut order: Vec = (0..series.len()).collect(); order.sort_by(|&a, &b| compare_labels(&series[a].metric, &series[b].metric)); let mut groups: HashMap> = HashMap::new(); @@ -149,6 +136,144 @@ pub fn topk( Ok(result) } +/// `limitk(k, ...)` with a constant `k`. +/// +/// At every point each group keeps the first `k` series in input order that hold a sample there, +/// float or histogram, with their labels and values. Prometheus takes them in the order its +/// storage returns series: label order once a query reads several queriers (the head and a +/// block), creation order when the TSDB head alone answers it. A caller that wants Prometheus' +/// result over persisted data passes the series in label order. +pub fn limitk( + k: f64, + grouping: &Grouping, + schema: &SeriesSchema, + series: &[Series], +) -> Result> { + single_field(schema, "limitk")?; + grid_points(schema, series)?; + let Some(k) = k_param(k, series.len())? else { + return Ok(Vec::new()); + }; + let mut groups: HashMap> = HashMap::new(); + for (index, item) in series.iter().enumerate() { + groups + .entry(group_key(&item.metric, grouping)) + .or_default() + .push(index); + } + let mut selected: Vec> = vec![Vec::new(); series.len()]; + for members in groups.values() { + let mut cursors: Vec> = members + .iter() + .map(|&member| series[member].fields.as_slice()[0].cursor()) + .collect(); + for point in union_positions(members.iter().map(|&member| &series[member].time)) { + let mut taken = 0; + for (slot, &member) in members.iter().enumerate() { + if taken == k { + break; + } + let Some(row) = series[member].time.row_of(point) else { + continue; + }; + if !matches!( + cursors[slot] + .at(row) + .expect("the axis and the fields are aligned"), + SampleRef::Missing + ) { + selected[member].push((point, row)); + taken += 1; + } + } + } + } + + let mut builder = GridSeriesBuilder::try_new(Arc::new(schema.clone()))?; + let mut result = Vec::new(); + for (index, points) in selected.into_iter().enumerate() { + if points.is_empty() { + continue; + } + builder.begin_series(); + // A series belongs to one group, whose points are visited in order. + let mut cursor = series[index].fields.as_slice()[0].cursor(); + for (point, row) in points { + let sample = cursor.at(row).expect("the axis and the fields are aligned"); + builder.push_samples(point, &[owned(sample)?])?; + } + result.extend(builder.finish(series[index].metric.clone())?); + } + Ok(result) +} + +/// `limit_ratio(r, ...)` with a constant `r`. +/// +/// Keeps whole series: Prometheus derives an offset in `[0, 1)` from the hash of a series' labels +/// and keeps the series when the offset is below `r`, or, for a negative `r`, at least `1 + r`. +/// Which series are kept does not depend on the grouping, so it is not a parameter. The hash is +/// the one of Prometheus' default `stringlabels` build; the other label builds hash differently. +pub fn limit_ratio(ratio: f64, schema: &SeriesSchema, series: &[Series]) -> Result> { + grid_points(schema, series)?; + if ratio.is_nan() { + return Err(DataFusionError::Execution("Ratio value is NaN".into())); + } + let ratio = ratio.clamp(-1.0, 1.0); + let mut builder = GridSeriesBuilder::try_new(Arc::new(schema.clone()))?; + let mut result = Vec::new(); + let mut row = Vec::with_capacity(schema.fields.len()); + for item in series { + let offset = sample_offset(&item.metric); + let kept = if ratio >= 0.0 { + offset < ratio + } else { + offset >= 1.0 + ratio + }; + if !kept { + continue; + } + // Rebuilt rather than cloned: an input may carry rows without a value, a result may not. + builder.begin_series(); + let mut cursors: Vec> = + item.fields.iter().map(|field| field.cursor()).collect(); + for (point, index) in item.time.positions() { + row.clear(); + for cursor in &mut cursors { + row.push(owned( + cursor + .at(index) + .expect("the axis and the fields are aligned"), + )?); + } + builder.push_samples(point, &row)?; + } + result.extend(builder.finish(item.metric.clone())?); + } + Ok(result) +} + +/// Prometheus `HashRatioSampler.SampleOffset`: the xxHash64 of the labels as `stringlabels` +/// encodes them, each name and value prefixed by its length, over `u64::MAX`. +fn sample_offset(metric: &MetricName) -> f64 { + let mut data = Vec::new(); + for (name, value) in labels_with_name(metric) { + // Prometheus label sets hold no empty value. + if value.is_empty() { + continue; + } + for text in [&name, &value] { + let len = text.len(); + if len < 255 { + data.push(len as u8); + } else { + data.extend([255, len as u8, (len >> 8) as u8, (len >> 16) as u8]); + } + data.extend_from_slice(text.as_bytes()); + } + } + twox_hash::XxHash64::oneshot(0, &data) as f64 / u64::MAX as f64 +} + /// `scalar(v)`: the value of the only series present at each point, NaN otherwise. pub fn scalar(schema: &SeriesSchema, series: &[Series]) -> Result { single_field(schema, "scalar")?; @@ -361,6 +486,25 @@ fn rank(a: f64, b: f64, top: bool) -> Ordering { } } +/// The `k` of `topk`, `bottomk` and `limitk` as Prometheus reads it: nothing for `k` below 1, an +/// error for NaN or a value out of int64, otherwise `k` truncated and capped at the number of +/// input series. +fn k_param(k: f64, series: usize) -> Result> { + if k < 1.0 { + return Ok(None); + } + if k.is_nan() { + return Err(DataFusionError::Execution("Parameter value is NaN".into())); + } + // Prometheus' `maxInt64`, the largest float64 below 2^63. + if k >= 9_223_372_036_854_774_784.0 { + return Err(DataFusionError::Execution(format!( + "Scalar value {k} overflows int64" + ))); + } + Ok(Some(k.trunc().min(series as f64) as usize)) +} + /// Label pairs identifying a group; empty values are absent, as in Prometheus. type GroupKey = Vec<(Arc, Arc)>; diff --git a/src/promql/src/select/limit_cases.txt b/src/promql/src/select/limit_cases.txt new file mode 100644 index 00000000000..a9ab4a27208 --- /dev/null +++ b/src/promql/src/select/limit_cases.txt @@ -0,0 +1,1372 @@ +# Generated by promql-compat cmd/narthex-golden-vector built for amd64 (Prometheus v3.15.0 +# engine, seed 1, 120 cases per kind, -experimental limit). Each case lists its operation, the grid, the +# input series and either the result series or the error. Points are `grid index:hex bits`. +case limit limit_ratio NaN by __name__,host,job,zone +grid 100000 10000 6 +series __name__=a,host=h2,job=y ; 1:7ff8000000000001 +series __name__=a,host=h1,job=y,zone=z1 ; 5:c079dc0000000000 +series __name__=a,host=h1,job=x,zone=z1 ; 1:4078980000000000 2:406ad00000000000 3:c08b8e0000000000 +series __name__=b,host=h1,job=y,zone=z2 ; 4:c0795c0000000000 5:7ff8000000000001 +error Ratio value is NaN +end +case limit limit_ratio 0.9 by __name__,job,zone +grid 100000 10000 1 +series __name__=a,host=h1,job=y ; 0:408ab00000000000 +series __name__=a,host=h2,job=x ; +series __name__=a,host=h2,job=y ; 0:4072120000000000 +series __name__=a,host=h1,job=x ; +series __name__=a,host=h2,job=x,zone=z1 ; +series __name__=b,host=h1,job=y ; 0:c08d650000000000 +result __name__=a,host=h1,job=y ; 0:408ab00000000000 +result __name__=a,host=h2,job=y ; 0:4072120000000000 +result __name__=b,host=h1,job=y ; 0:c08d650000000000 +end +case limit limit_ratio -0.5 without __name__,host +grid 100000 10000 6 +series __name__=a,host=h2,job=y,zone=z1 ; 4:407fa60000000000 +series __name__=a,host=h1,job=y,zone=z1 ; 5:fff0000000000000 +series __name__=a,host=h2,job=x ; 1:c0888a0000000000 3:407dfa0000000000 4:c089040000000000 5:c085460000000000 +series __name__=a,host=h1,job=x ; 0:4073480000000000 1:7e37e43c8800759c +series __name__=a,host=h2,job=x,zone=z1 ; +series __name__=b,host=h2,job=y ; 2:407ad60000000000 +series __name__=b,host=h1,job=x ; 0:c068f40000000000 1:bff4000000000000 3:c0740c0000000000 4:403a200000000000 +series __name__=b,host=h2,job=x,zone=z1 ; 3:7ff8000000000001 4:c08e530000000000 5:4072a60000000000 +result __name__=a,host=h2,job=x ; 1:c0888a0000000000 3:407dfa0000000000 4:c089040000000000 5:c085460000000000 +result __name__=b,host=h2,job=y ; 2:407ad60000000000 +end +case limit limit_ratio 0 by job,zone +grid 100000 10000 12 +series __name__=a,host=h2,job=x ; 1:c0631c0000000000 3:c08c910000000000 4:fff0000000000000 5:c077260000000000 6:c088fd0000000000 7:c07db40000000000 8:c0729c0000000000 +series __name__=a,host=h2,job=x,zone=z1 ; 4:c072a80000000000 5:4075700000000000 6:40833f0000000000 7:408c540000000000 8:4084b40000000000 9:c07c500000000000 +series __name__=a,host=h2,job=y ; 1:4051e80000000000 2:4063a40000000000 4:7ff0000000000000 6:c066a40000000000 9:c0886b0000000000 +series __name__=b,host=h2,job=y,zone=z2 ; 6:407f680000000000 7:c08e6b0000000000 8:c08c900000000000 +series __name__=b,host=h2,job=x,zone=z1 ; 10:407b8a0000000000 +series __name__=b,host=h2,job=y ; 0:406e800000000000 1:408bc00000000000 2:4052800000000000 4:4012000000000000 5:407e820000000000 +series __name__=b,host=h2,job=x ; +end +case limit limitk 0 without host +grid 100000 10000 9 +series __name__=a,host=h1,job=y,zone=z1 ; 5:c06a840000000000 6:fff0000000000000 7:7ff8000000000001 8:c074900000000000 +series __name__=a,host=h1,job=x ; 2:c08dde0000000000 3:c080f70000000000 5:c05b700000000000 7:7ff8000000000001 8:4080a70000000000 +series __name__=a,host=h2,job=y ; 5:c085a80000000000 8:c0616c0000000000 +series __name__=a,host=h2,job=x,zone=z1 ; 2:c087ad0000000000 3:c088cb0000000000 4:7e37e43c8800759c 5:c0893a0000000000 +series __name__=b,host=h1,job=x ; 6:c07bb20000000000 +end +case limit limit_ratio 1.5 by __name__,host,zone +grid 100000 10000 1 +series __name__=a,host=h2,job=y,zone=z1 ; 0:408db20000000000 +series __name__=a,host=h2,job=x,zone=z1 ; 0:7ff8000000000001 +series __name__=a,host=h1,job=x ; 0:c089610000000000 +series __name__=b,host=h2,job=x,zone=z1 ; 0:fff0000000000000 +series __name__=b,host=h2,job=y ; +series __name__=b,host=h2,job=y,zone=z1 ; 0:c052300000000000 +series __name__=b,host=h1,job=x,zone=z2 ; 0:c075de0000000000 +result __name__=a,host=h1,job=x ; 0:c089610000000000 +result __name__=a,host=h2,job=x,zone=z1 ; 0:7ff8000000000001 +result __name__=a,host=h2,job=y,zone=z1 ; 0:408db20000000000 +result __name__=b,host=h1,job=x,zone=z2 ; 0:c075de0000000000 +result __name__=b,host=h2,job=x,zone=z1 ; 0:fff0000000000000 +result __name__=b,host=h2,job=y,zone=z1 ; 0:c052300000000000 +end +case limit limitk -1 without __name__,host,job,zone +grid 100000 10000 10 +series __name__=a,host=h2,job=y,zone=z1 ; 0:c07e160000000000 2:c085d60000000000 3:406b840000000000 4:7ff0000000000000 5:c085140000000000 +series __name__=a,host=h2,job=x ; 9:4089a80000000000 +series __name__=a,host=h1,job=y ; 5:408dda0000000000 +series __name__=a,host=h1,job=x ; 3:c0872d0000000000 4:4083600000000000 +series __name__=b,host=h2,job=y ; 7:4054500000000000 +series __name__=b,host=h1,job=y ; 7:4072080000000000 8:4073880000000000 +end +case limit limitk 0 without host +grid 100000 10000 7 +series __name__=a,host=h1,job=x ; 3:c07b340000000000 4:406ba00000000000 +series __name__=a,host=h1,job=x,zone=z1 ; 4:4056f00000000000 +series __name__=a,host=h2,job=y,zone=z1 ; 4:c08ac20000000000 +series __name__=a,host=h1,job=y ; 6:c050300000000000 +series __name__=b,host=h2,job=x,zone=z1 ; 1:c047100000000000 2:c07cee0000000000 4:c08aab0000000000 5:c08cab0000000000 +series __name__=b,host=h1,job=y ; 0:c081500000000000 1:c080240000000000 2:c07ee00000000000 +series __name__=b,host=h1,job=x,zone=z1 ; 5:fff0000000000000 6:c0878c0000000000 +end +case limit limitk 0 none - +grid 100000 10000 5 +series __name__=a,host=h1,job=y ; 0:c075a00000000000 +series __name__=b,host=h1,job=y ; 2:4077a00000000000 +end +case limit limitk 1 by job +grid 100000 10000 3 +series __name__=a,host=h2,job=y ; 0:402ac00000000000 +series __name__=a,host=h1,job=y,zone=z1 ; 2:408f390000000000 +series __name__=a,host=h2,job=x,zone=z1 ; 1:408c220000000000 +series __name__=a,host=h1,job=y ; +series __name__=a,host=h2,job=x ; 1:c069080000000000 2:4071920000000000 +series __name__=b,host=h1,job=y,zone=z1 ; 0:4072a60000000000 1:408eae0000000000 2:c089030000000000 +series __name__=b,host=h2,job=x ; 2:407d1a0000000000 +series __name__=b,host=h2,job=x,zone=z1 ; 1:4080690000000000 2:408bb80000000000 +series __name__=b,host=h1,job=y ; 1:c08f1e0000000000 2:c086590000000000 +series __name__=b,host=h2,job=y,zone=z1 ; 0:7ff0000000000000 1:4081dd0000000000 +series __name__=b,host=h2,job=x,zone=z2 ; 0:408ece0000000000 1:4056880000000000 +result __name__=a,host=h1,job=y,zone=z1 ; 2:408f390000000000 +result __name__=a,host=h2,job=x ; 2:4071920000000000 +result __name__=a,host=h2,job=x,zone=z1 ; 1:408c220000000000 +result __name__=a,host=h2,job=y ; 0:402ac00000000000 +result __name__=b,host=h1,job=y,zone=z1 ; 1:408eae0000000000 +result __name__=b,host=h2,job=x,zone=z2 ; 0:408ece0000000000 +end +case limit limit_ratio 0.5 none - +grid 100000 10000 7 +series __name__=a,host=h2,job=y ; +series __name__=a,host=h2,job=y,zone=z1 ; 0:408d360000000000 1:4089930000000000 2:c083d70000000000 3:4078200000000000 +series __name__=b,host=h2,job=y,zone=z1 ; 6:c08df90000000000 +series __name__=b,host=h2,job=y ; 5:4084890000000000 6:c08cc40000000000 +result __name__=a,host=h2,job=y,zone=z1 ; 0:408d360000000000 1:4089930000000000 2:c083d70000000000 3:4078200000000000 +result __name__=b,host=h2,job=y,zone=z1 ; 6:c08df90000000000 +end +case limit limitk 1.5 by host,job +grid 100000 10000 1 +series __name__=a,host=h1,job=x ; 0:4081210000000000 +series __name__=a,host=h2,job=x ; 0:4081dd0000000000 +series __name__=b,host=h2,job=x ; +series __name__=b,host=h1,job=y ; +series __name__=b,host=h1,job=x ; 0:c083b90000000000 +result __name__=a,host=h1,job=x ; 0:4081210000000000 +result __name__=a,host=h2,job=x ; 0:4081dd0000000000 +end +case limit limitk 1.5 without __name__,zone +grid 100000 10000 8 +series __name__=a,host=h1,job=x ; 6:4081d10000000000 +series __name__=a,host=h2,job=y ; +series __name__=a,host=h2,job=x,zone=z1 ; 4:40793e0000000000 6:4087570000000000 +series __name__=b,host=h2,job=x,zone=z1 ; 0:c065fc0000000000 +series __name__=b,host=h1,job=x ; 6:40846d0000000000 7:7ff8000000000001 +series __name__=b,host=h1,job=x,zone=z1 ; +result __name__=a,host=h1,job=x ; 6:4081d10000000000 +result __name__=a,host=h2,job=x,zone=z1 ; 4:40793e0000000000 6:4087570000000000 +result __name__=b,host=h1,job=x ; 7:7ff8000000000001 +result __name__=b,host=h2,job=x,zone=z1 ; 0:c065fc0000000000 +end +case limit limit_ratio 0.9 none - +grid 100000 10000 11 +series __name__=a,host=h2,job=y ; 10:4073de0000000000 +series __name__=b,host=h2,job=y ; 7:406cc40000000000 +series __name__=b,host=h1,job=x ; 7:c06f500000000000 8:c08c310000000000 +result __name__=a,host=h2,job=y ; 10:4073de0000000000 +result __name__=b,host=h1,job=x ; 7:c06f500000000000 8:c08c310000000000 +result __name__=b,host=h2,job=y ; 7:406cc40000000000 +end +case limit limit_ratio 1 without zone +grid 100000 10000 2 +series __name__=a,host=h2,job=y ; 0:7e37e43c8800759c +series __name__=a,host=h1,job=y ; +series __name__=a,host=h2,job=x ; 1:407e2a0000000000 +series __name__=b,host=h2,job=x ; 0:7ff0000000000000 1:408da70000000000 +series __name__=b,host=h1,job=y ; 0:c088c60000000000 +series __name__=b,host=h2,job=y ; 1:4081880000000000 +result __name__=a,host=h2,job=x ; 1:407e2a0000000000 +result __name__=a,host=h2,job=y ; 0:7e37e43c8800759c +result __name__=b,host=h1,job=y ; 0:c088c60000000000 +result __name__=b,host=h2,job=x ; 0:7ff0000000000000 1:408da70000000000 +result __name__=b,host=h2,job=y ; 1:4081880000000000 +end +case limit limitk 0 without host,job,zone +grid 100000 10000 10 +series __name__=a,host=h2,job=x,zone=z1 ; 4:c087520000000000 5:4064540000000000 6:40879c0000000000 7:0 8:4082580000000000 +series __name__=a,host=h1,job=y ; 8:c088640000000000 +series __name__=a,host=h1,job=y,zone=z1 ; 0:c052f80000000000 1:c0709c0000000000 3:c08e060000000000 4:4079680000000000 5:c061200000000000 6:c077520000000000 7:40822a0000000000 9:4079ea0000000000 +series __name__=b,host=h1,job=y,zone=z1 ; 1:c071ec0000000000 +series __name__=b,host=h2,job=x ; 0:c08de50000000000 1:c08d6e0000000000 3:c08d530000000000 +end +case limit limit_ratio -0.5 by host +grid 100000 10000 12 +series __name__=a,host=h1,job=y ; 3:7ff8000000000001 4:c089990000000000 6:c066800000000000 +series __name__=a,host=h2,job=y ; 1:7ff0000000000000 3:407d4e0000000000 4:c07cfa0000000000 +series __name__=a,host=h2,job=y,zone=z1 ; 6:c086c20000000000 +series __name__=a,host=h1,job=x ; 8:4081ca0000000000 +series __name__=b,host=h1,job=x,zone=z2 ; 4:4085720000000000 5:c08a4a0000000000 +result __name__=a,host=h1,job=y ; 3:7ff8000000000001 4:c089990000000000 6:c066800000000000 +result __name__=a,host=h2,job=y ; 1:7ff0000000000000 3:407d4e0000000000 4:c07cfa0000000000 +end +case limit limitk -1 none - +grid 100000 10000 9 +series __name__=a,host=h1,job=x,zone=z1 ; 1:4089b40000000000 2:c07a7c0000000000 4:c041400000000000 5:4086be0000000000 7:4084730000000000 8:c06be80000000000 +series __name__=a,host=h1,job=y ; 0:0 +series __name__=b,host=h1,job=y ; +series __name__=b,host=h1,job=y,zone=z2 ; 3:4084c10000000000 4:c072220000000000 5:c06cb00000000000 6:7ff8000000000001 7:4055200000000000 +series __name__=b,host=h1,job=x,zone=z1 ; 3:7ff8000000000001 4:c04c300000000000 6:c0820b0000000000 +series __name__=b,host=h1,job=x,zone=z2 ; +end +case limit limitk 3 none - +grid 100000 10000 6 +series __name__=a,host=h2,job=x,zone=z1 ; 3:408b9d0000000000 4:fff0000000000000 +series __name__=b,host=h2,job=x,zone=z1 ; 1:40837b0000000000 2:40680c0000000000 4:404f600000000000 +series __name__=b,host=h2,job=x ; 2:4087400000000000 3:c083360000000000 4:c07fd40000000000 +result __name__=a,host=h2,job=x,zone=z1 ; 3:408b9d0000000000 4:fff0000000000000 +result __name__=b,host=h2,job=x ; 2:4087400000000000 3:c083360000000000 4:c07fd40000000000 +result __name__=b,host=h2,job=x,zone=z1 ; 1:40837b0000000000 2:40680c0000000000 4:404f600000000000 +end +case limit limit_ratio 0.5 without __name__ +grid 100000 10000 4 +series __name__=a,host=h1,job=x ; 2:407fae0000000000 +series __name__=b,host=h1,job=x ; 0:c072d40000000000 +result __name__=a,host=h1,job=x ; 2:407fae0000000000 +result __name__=b,host=h1,job=x ; 0:c072d40000000000 +end +case limit limit_ratio -0.5 without __name__,job +grid 100000 10000 2 +series __name__=a,host=h2,job=y ; 0:408d630000000000 +series __name__=b,host=h2,job=y ; 1:7ff0000000000000 +series __name__=b,host=h2,job=y,zone=z2 ; 1:4071840000000000 +series __name__=b,host=h1,job=y,zone=z1 ; 1:0 +result __name__=a,host=h2,job=y ; 0:408d630000000000 +result __name__=b,host=h1,job=y,zone=z1 ; 1:0 +result __name__=b,host=h2,job=y ; 1:7ff0000000000000 +end +case limit limit_ratio 0.3 none - +grid 100000 10000 4 +series __name__=a,host=h2,job=y ; +series __name__=a,host=h2,job=x ; 3:7e37e43c8800759c +series __name__=a,host=h1,job=x ; 2:c083650000000000 +series __name__=b,host=h2,job=x,zone=z1 ; 3:4081e50000000000 +series __name__=b,host=h1,job=x ; 0:40852b0000000000 1:4058780000000000 +series __name__=b,host=h2,job=x ; 3:7ff8000000000001 +series __name__=b,host=h2,job=x,zone=z2 ; 3:c0810c0000000000 +result __name__=b,host=h1,job=x ; 0:40852b0000000000 1:4058780000000000 +result __name__=b,host=h2,job=x ; 3:7ff8000000000001 +end +case limit limitk 1.5 none - +grid 100000 10000 10 +series __name__=a,host=h1,job=x,zone=z1 ; 7:7ff8000000000001 9:c076960000000000 +series __name__=a,host=h2,job=x ; 4:7ff0000000000000 5:407aaa0000000000 +series __name__=a,host=h2,job=y ; 6:c088eb0000000000 +series __name__=b,host=h2,job=x ; 4:c075060000000000 +series __name__=b,host=h2,job=y,zone=z1 ; 8:405dd80000000000 +result __name__=a,host=h1,job=x,zone=z1 ; 7:7ff8000000000001 9:c076960000000000 +result __name__=a,host=h2,job=x ; 4:7ff0000000000000 5:407aaa0000000000 +result __name__=a,host=h2,job=y ; 6:c088eb0000000000 +result __name__=b,host=h2,job=y,zone=z1 ; 8:405dd80000000000 +end +case limit limit_ratio 1.5 none - +grid 100000 10000 9 +series __name__=a,host=h2,job=y ; 4:c073ae0000000000 +series __name__=a,host=h2,job=x ; 6:fff0000000000000 7:c087ff0000000000 +series __name__=a,host=h1,job=x ; 3:4062fc0000000000 4:7ff8000000000001 5:c08bc10000000000 +series __name__=a,host=h2,job=x,zone=z1 ; 7:c08a6e0000000000 8:408a5d0000000000 +series __name__=b,host=h2,job=x ; +series __name__=b,host=h1,job=x ; 7:7e37e43c8800759c +result __name__=a,host=h1,job=x ; 3:4062fc0000000000 4:7ff8000000000001 5:c08bc10000000000 +result __name__=a,host=h2,job=x ; 6:fff0000000000000 7:c087ff0000000000 +result __name__=a,host=h2,job=x,zone=z1 ; 7:c08a6e0000000000 8:408a5d0000000000 +result __name__=a,host=h2,job=y ; 4:c073ae0000000000 +result __name__=b,host=h1,job=x ; 7:7e37e43c8800759c +end +case limit limit_ratio -0.5 none - +grid 100000 10000 11 +series __name__=a,host=h2,job=x ; 0:40802b0000000000 1:c087bb0000000000 3:c079d00000000000 +series __name__=b,host=h1,job=y,zone=z1 ; 9:408e5e0000000000 10:4087c30000000000 +series __name__=b,host=h2,job=x ; 5:4076820000000000 6:4051700000000000 7:7ff8000000000001 +result __name__=a,host=h2,job=x ; 0:40802b0000000000 1:c087bb0000000000 3:c079d00000000000 +result __name__=b,host=h1,job=y,zone=z1 ; 9:408e5e0000000000 10:4087c30000000000 +end +case limit limitk 2 without - +grid 100000 10000 2 +series __name__=a,host=h1,job=x ; 1:c081900000000000 +series __name__=a,host=h2,job=x,zone=z1 ; 0:fff0000000000000 +series __name__=b,host=h1,job=x ; 1:c084590000000000 +series __name__=b,host=h1,job=x,zone=z2 ; 1:c0825e0000000000 +series __name__=b,host=h1,job=y ; 1:c0773a0000000000 +series __name__=b,host=h2,job=x,zone=z1 ; 0:4084d70000000000 +result __name__=a,host=h1,job=x ; 1:c081900000000000 +result __name__=a,host=h2,job=x,zone=z1 ; 0:fff0000000000000 +result __name__=b,host=h1,job=x ; 1:c084590000000000 +result __name__=b,host=h1,job=x,zone=z2 ; 1:c0825e0000000000 +result __name__=b,host=h1,job=y ; 1:c0773a0000000000 +result __name__=b,host=h2,job=x,zone=z1 ; 0:4084d70000000000 +end +case limit limit_ratio 1.5 by job +grid 100000 10000 1 +series __name__=a,host=h2,job=y ; 0:c077d80000000000 +series __name__=a,host=h1,job=y,zone=z1 ; 0:c077a40000000000 +series __name__=a,host=h1,job=x ; 0:405d880000000000 +series __name__=b,host=h2,job=y ; 0:c0883e0000000000 +series __name__=b,host=h1,job=x,zone=z2 ; 0:c086890000000000 +result __name__=a,host=h1,job=x ; 0:405d880000000000 +result __name__=a,host=h1,job=y,zone=z1 ; 0:c077a40000000000 +result __name__=a,host=h2,job=y ; 0:c077d80000000000 +result __name__=b,host=h1,job=x,zone=z2 ; 0:c086890000000000 +result __name__=b,host=h2,job=y ; 0:c0883e0000000000 +end +case limit limit_ratio 0 none - +grid 100000 10000 3 +series __name__=a,host=h2,job=x,zone=z1 ; +series __name__=b,host=h2,job=x,zone=z2 ; 1:4076880000000000 +series __name__=b,host=h2,job=x,zone=z1 ; 1:c081500000000000 +series __name__=b,host=h2,job=x ; +end +case limit limit_ratio 0 without job +grid 100000 10000 8 +series __name__=a,host=h2,job=y ; 7:c07e260000000000 +series __name__=a,host=h1,job=y ; 2:c080820000000000 3:0 4:c082c60000000000 5:405e980000000000 +series __name__=a,host=h1,job=y,zone=z1 ; 4:c08e6a0000000000 +series __name__=b,host=h1,job=y,zone=z1 ; 1:40735e0000000000 +series __name__=b,host=h2,job=y ; 5:c06e2c0000000000 6:7ff0000000000000 +series __name__=b,host=h1,job=y ; 4:7ff8000000000001 +end +case limit limit_ratio NaN without host,job,zone +grid 100000 10000 11 +series __name__=a,host=h1,job=y,zone=z1 ; +series __name__=a,host=h2,job=x ; 0:4089af0000000000 1:c088900000000000 2:405aa80000000000 +series __name__=a,host=h2,job=x,zone=z1 ; 6:4078d20000000000 7:4084890000000000 8:40683c0000000000 9:4087510000000000 10:c086f20000000000 +series __name__=a,host=h2,job=y,zone=z1 ; 1:c063240000000000 2:7e37e43c8800759c 3:4063c80000000000 4:4072c60000000000 +series __name__=b,host=h2,job=x ; 10:4083e60000000000 +error Ratio value is NaN +end +case limit limit_ratio NaN without host,job,zone +grid 100000 10000 11 +series __name__=a,host=h2,job=y ; 8:4084880000000000 +series __name__=b,host=h2,job=y,zone=z1 ; 6:c06fb80000000000 7:c07ba40000000000 +series __name__=b,host=h2,job=y ; 7:7ff8000000000001 8:40853c0000000000 10:c0821e0000000000 +error Ratio value is NaN +end +case limit limit_ratio -1 without job +grid 100000 10000 7 +series __name__=a,host=h1,job=y,zone=z1 ; 3:7ff8000000000001 4:407cfa0000000000 5:407b9e0000000000 +series __name__=a,host=h2,job=x ; 6:0 +series __name__=a,host=h2,job=y,zone=z1 ; 2:40837e0000000000 +series __name__=a,host=h2,job=y ; 2:c082630000000000 3:c06bb80000000000 4:4082e10000000000 +series __name__=b,host=h2,job=y,zone=z1 ; +series __name__=b,host=h2,job=x ; 1:c0899f0000000000 3:40853d0000000000 4:4089390000000000 5:408e4a0000000000 6:7e37e43c8800759c +result __name__=a,host=h1,job=y,zone=z1 ; 3:7ff8000000000001 4:407cfa0000000000 5:407b9e0000000000 +result __name__=a,host=h2,job=x ; 6:0 +result __name__=a,host=h2,job=y ; 2:c082630000000000 3:c06bb80000000000 4:4082e10000000000 +result __name__=a,host=h2,job=y,zone=z1 ; 2:40837e0000000000 +result __name__=b,host=h2,job=x ; 1:c0899f0000000000 3:40853d0000000000 4:4089390000000000 5:408e4a0000000000 6:7e37e43c8800759c +end +case limit limitk NaN by __name__,host,job,zone +grid 100000 10000 1 +series __name__=a,host=h1,job=y ; 0:407a180000000000 +series __name__=a,host=h1,job=y,zone=z1 ; 0:c085b70000000000 +series __name__=b,host=h1,job=y,zone=z2 ; 0:4076400000000000 +series __name__=b,host=h1,job=y ; 0:4073d40000000000 +series __name__=b,host=h2,job=y ; +error Parameter value is NaN +end +case limit limit_ratio -0.3 by host +grid 100000 10000 6 +series __name__=a,host=h1,job=y,zone=z1 ; 0:7ff8000000000001 1:c088480000000000 2:4081130000000000 +series __name__=a,host=h2,job=y ; 0:4082da0000000000 1:4089250000000000 2:c08f260000000000 3:4081ba0000000000 4:c086560000000000 5:40868c0000000000 +series __name__=b,host=h2,job=y ; 3:c0858b0000000000 4:c085d50000000000 +series __name__=b,host=h1,job=y ; 4:c0870e0000000000 +series __name__=b,host=h1,job=y,zone=z1 ; 1:4084e20000000000 2:c084150000000000 +result __name__=b,host=h1,job=y,zone=z1 ; 1:4084e20000000000 2:c084150000000000 +end +case limit limit_ratio 0 none - +grid 100000 10000 9 +series __name__=a,host=h2,job=y,zone=z1 ; 8:4060000000000000 +series __name__=a,host=h1,job=x ; 6:c066380000000000 7:7ff8000000000001 +series __name__=a,host=h2,job=x ; 3:c067700000000000 4:fff0000000000000 6:408a7c0000000000 7:407d960000000000 +series __name__=a,host=h1,job=x,zone=z1 ; +series __name__=a,host=h2,job=x,zone=z1 ; 3:407d940000000000 4:7ff8000000000001 +series __name__=b,host=h2,job=y,zone=z1 ; +series __name__=b,host=h1,job=x,zone=z1 ; 2:c056080000000000 3:408e490000000000 4:c042c00000000000 5:c053a80000000000 +series __name__=b,host=h2,job=x,zone=z1 ; 6:c086520000000000 7:400a000000000000 8:c086fd0000000000 +series __name__=b,host=h1,job=x ; 8:c06ff40000000000 +end +case limit limitk 0 none - +grid 100000 10000 8 +series __name__=a,host=h2,job=x ; 6:4076cc0000000000 7:c066800000000000 +series __name__=b,host=h2,job=x ; 1:c0842b0000000000 2:c08df50000000000 3:4088430000000000 5:c08b9a0000000000 6:c0789e0000000000 +end +case limit limit_ratio 0.9 by __name__ +grid 100000 10000 8 +series __name__=a,host=h2,job=x ; 5:0 +series __name__=a,host=h2,job=y,zone=z1 ; 2:c063f00000000000 3:4077140000000000 4:4049d00000000000 5:c080600000000000 +series __name__=b,host=h2,job=x ; 1:4089270000000000 +series __name__=b,host=h2,job=y,zone=z1 ; 1:c055e00000000000 3:c067d40000000000 +result __name__=a,host=h2,job=x ; 5:0 +result __name__=a,host=h2,job=y,zone=z1 ; 2:c063f00000000000 3:4077140000000000 4:4049d00000000000 5:c080600000000000 +result __name__=b,host=h2,job=x ; 1:4089270000000000 +result __name__=b,host=h2,job=y,zone=z1 ; 1:c055e00000000000 3:c067d40000000000 +end +case limit limitk 3 none - +grid 100000 10000 8 +series __name__=a,host=h2,job=y,zone=z1 ; 7:408a630000000000 +series __name__=a,host=h2,job=y ; 5:c072620000000000 +series __name__=a,host=h1,job=x ; +series __name__=a,host=h1,job=y ; 0:4042000000000000 +series __name__=b,host=h1,job=x ; 1:408d840000000000 2:c082d80000000000 3:7ff8000000000001 +series __name__=b,host=h2,job=x ; 4:c07db20000000000 6:407a220000000000 +series __name__=b,host=h2,job=y,zone=z1 ; 2:407aba0000000000 3:c06ef40000000000 4:c083b40000000000 5:405c680000000000 6:c087080000000000 +result __name__=a,host=h1,job=y ; 0:4042000000000000 +result __name__=a,host=h2,job=y ; 5:c072620000000000 +result __name__=a,host=h2,job=y,zone=z1 ; 7:408a630000000000 +result __name__=b,host=h1,job=x ; 1:408d840000000000 2:c082d80000000000 3:7ff8000000000001 +result __name__=b,host=h2,job=x ; 4:c07db20000000000 6:407a220000000000 +result __name__=b,host=h2,job=y,zone=z1 ; 2:407aba0000000000 3:c06ef40000000000 4:c083b40000000000 5:405c680000000000 6:c087080000000000 +end +case limit limitk 3 none - +grid 100000 10000 2 +series __name__=a,host=h2,job=y ; 1:c062f40000000000 +series __name__=a,host=h2,job=y,zone=z1 ; +series __name__=a,host=h2,job=x,zone=z1 ; 0:c082e80000000000 1:c064680000000000 +series __name__=a,host=h2,job=x ; 0:c074e60000000000 +series __name__=a,host=h1,job=y ; 1:c0851b0000000000 +series __name__=a,host=h1,job=x ; +series __name__=b,host=h2,job=x,zone=z1 ; 0:c072ae0000000000 +series __name__=b,host=h2,job=y ; 1:c080e90000000000 +result __name__=a,host=h1,job=y ; 1:c0851b0000000000 +result __name__=a,host=h2,job=x ; 0:c074e60000000000 +result __name__=a,host=h2,job=x,zone=z1 ; 0:c082e80000000000 1:c064680000000000 +result __name__=a,host=h2,job=y ; 1:c062f40000000000 +result __name__=b,host=h2,job=x,zone=z1 ; 0:c072ae0000000000 +end +case limit limit_ratio -0.5 by host +grid 100000 10000 7 +series __name__=a,host=h2,job=x,zone=z1 ; 2:406aa00000000000 +series __name__=a,host=h1,job=y ; 3:4066a00000000000 4:c086b20000000000 5:407e0c0000000000 +series __name__=b,host=h1,job=y ; 0:407aac0000000000 1:c077080000000000 +series __name__=b,host=h2,job=y,zone=z1 ; 6:c065600000000000 +series __name__=b,host=h2,job=x,zone=z1 ; 3:4051380000000000 +series __name__=b,host=h1,job=y,zone=z2 ; +result __name__=a,host=h1,job=y ; 3:4066a00000000000 4:c086b20000000000 5:407e0c0000000000 +result __name__=a,host=h2,job=x,zone=z1 ; 2:406aa00000000000 +end +case limit limitk 2 none - +grid 100000 10000 9 +series __name__=a,host=h2,job=x ; 4:c08e180000000000 5:7e37e43c8800759c +series __name__=a,host=h1,job=x ; 6:c0830b0000000000 7:4089e80000000000 +series __name__=a,host=h2,job=y ; 0:4083010000000000 1:c066640000000000 +series __name__=a,host=h1,job=y,zone=z1 ; 6:40862c0000000000 +series __name__=a,host=h1,job=y ; 3:4077840000000000 +series __name__=b,host=h2,job=y,zone=z2 ; 6:406be00000000000 +series __name__=b,host=h1,job=y ; 7:4055a80000000000 +series __name__=b,host=h2,job=y ; 2:c073aa0000000000 3:c077f00000000000 4:4083630000000000 +result __name__=a,host=h1,job=x ; 6:c0830b0000000000 7:4089e80000000000 +result __name__=a,host=h1,job=y ; 3:4077840000000000 +result __name__=a,host=h1,job=y,zone=z1 ; 6:40862c0000000000 +result __name__=a,host=h2,job=x ; 4:c08e180000000000 5:7e37e43c8800759c +result __name__=a,host=h2,job=y ; 0:4083010000000000 1:c066640000000000 +result __name__=b,host=h1,job=y ; 7:4055a80000000000 +result __name__=b,host=h2,job=y ; 2:c073aa0000000000 3:c077f00000000000 4:4083630000000000 +end +case limit limitk 1.5 without __name__ +grid 100000 10000 1 +series __name__=a,host=h2,job=y ; 0:7ff8000000000001 +series __name__=b,host=h1,job=x ; 0:c08a990000000000 +series __name__=b,host=h2,job=y ; 0:c06b000000000000 +result __name__=a,host=h2,job=y ; 0:7ff8000000000001 +result __name__=b,host=h1,job=x ; 0:c08a990000000000 +end +case limit limitk -1 by __name__,host +grid 100000 10000 4 +series __name__=a,host=h1,job=y ; +series __name__=a,host=h2,job=x ; 1:c07ea20000000000 2:0 3:c086e10000000000 +series __name__=a,host=h1,job=x ; 2:7ff8000000000001 3:405e680000000000 +series __name__=a,host=h2,job=y ; 3:403cc00000000000 +series __name__=a,host=h2,job=y,zone=z1 ; 1:c052d00000000000 2:4087d80000000000 +series __name__=b,host=h2,job=y ; +series __name__=b,host=h2,job=x ; 1:407f180000000000 2:408b490000000000 +series __name__=b,host=h1,job=y,zone=z1 ; 0:4088240000000000 1:4084e00000000000 2:c07a960000000000 3:c069a00000000000 +series __name__=b,host=h1,job=x,zone=z1 ; 0:4044100000000000 1:c065880000000000 2:407e0e0000000000 +series __name__=b,host=h1,job=x ; 3:c052d80000000000 +end +case limit limit_ratio 1 by host,job +grid 100000 10000 2 +series __name__=a,host=h1,job=x ; 1:407dc40000000000 +series __name__=a,host=h1,job=y,zone=z1 ; 1:7ff8000000000001 +series __name__=b,host=h1,job=x ; 0:c087820000000000 +series __name__=b,host=h2,job=y ; 0:0 +series __name__=b,host=h1,job=y,zone=z1 ; 1:c075940000000000 +series __name__=b,host=h1,job=y ; 1:c058f00000000000 +result __name__=a,host=h1,job=x ; 1:407dc40000000000 +result __name__=a,host=h1,job=y,zone=z1 ; 1:7ff8000000000001 +result __name__=b,host=h1,job=x ; 0:c087820000000000 +result __name__=b,host=h1,job=y ; 1:c058f00000000000 +result __name__=b,host=h1,job=y,zone=z1 ; 1:c075940000000000 +result __name__=b,host=h2,job=y ; 0:0 +end +case limit limit_ratio -1 without - +grid 100000 10000 9 +series __name__=a,host=h1,job=y ; 8:40873f0000000000 +series __name__=a,host=h1,job=x,zone=z1 ; 3:c081420000000000 +series __name__=b,host=h1,job=x,zone=z1 ; +result __name__=a,host=h1,job=x,zone=z1 ; 3:c081420000000000 +result __name__=a,host=h1,job=y ; 8:40873f0000000000 +end +case limit limitk NaN none - +grid 100000 10000 3 +series __name__=a,host=h1,job=y,zone=z1 ; 1:c08b070000000000 +series __name__=a,host=h2,job=y ; 2:7ff0000000000000 +series __name__=a,host=h1,job=x ; 2:c082330000000000 +series __name__=a,host=h1,job=y ; 1:c08b810000000000 +series __name__=b,host=h2,job=y ; 1:7e37e43c8800759c 2:fff0000000000000 +series __name__=b,host=h1,job=y,zone=z1 ; 1:7ff8000000000001 2:4063080000000000 +series __name__=b,host=h1,job=y,zone=z2 ; 0:4075660000000000 +error Parameter value is NaN +end +case limit limitk 10 none - +grid 100000 10000 9 +series __name__=a,host=h2,job=y ; 7:405b400000000000 +series __name__=a,host=h1,job=y,zone=z1 ; 6:405dd00000000000 +series __name__=b,host=h2,job=x,zone=z1 ; 0:c077a60000000000 1:c08b2b0000000000 3:4089970000000000 +series __name__=b,host=h1,job=y ; 1:40833e0000000000 +series __name__=b,host=h2,job=y ; 4:407e480000000000 5:40752a0000000000 6:c020800000000000 7:c07bd60000000000 +series __name__=b,host=h1,job=y,zone=z2 ; 8:c08f0f0000000000 +result __name__=a,host=h1,job=y,zone=z1 ; 6:405dd00000000000 +result __name__=a,host=h2,job=y ; 7:405b400000000000 +result __name__=b,host=h1,job=y ; 1:40833e0000000000 +result __name__=b,host=h1,job=y,zone=z2 ; 8:c08f0f0000000000 +result __name__=b,host=h2,job=x,zone=z1 ; 0:c077a60000000000 1:c08b2b0000000000 3:4089970000000000 +result __name__=b,host=h2,job=y ; 4:407e480000000000 5:40752a0000000000 6:c020800000000000 7:c07bd60000000000 +end +case limit limit_ratio -1 without host,zone +grid 100000 10000 10 +series __name__=a,host=h1,job=x ; 5:c075400000000000 6:c072600000000000 7:c05a900000000000 9:4089c90000000000 +series __name__=b,host=h2,job=x,zone=z1 ; 7:406bd00000000000 +series __name__=b,host=h1,job=x ; 6:7e37e43c8800759c +result __name__=a,host=h1,job=x ; 5:c075400000000000 6:c072600000000000 7:c05a900000000000 9:4089c90000000000 +result __name__=b,host=h1,job=x ; 6:7e37e43c8800759c +result __name__=b,host=h2,job=x,zone=z1 ; 7:406bd00000000000 +end +case limit limit_ratio -0.5 by __name__ +grid 100000 10000 7 +series __name__=a,host=h2,job=x,zone=z1 ; 0:c08e130000000000 1:c064700000000000 2:4087fc0000000000 +series __name__=a,host=h2,job=x ; 6:4086a20000000000 +series __name__=a,host=h1,job=x,zone=z1 ; 3:7ff8000000000001 4:408a320000000000 +series __name__=a,host=h1,job=x ; 5:c047100000000000 +series __name__=a,host=h2,job=y,zone=z1 ; 3:c07aee0000000000 4:c082470000000000 +series __name__=b,host=h2,job=x,zone=z1 ; 5:c029000000000000 +series __name__=b,host=h1,job=x,zone=z1 ; 0:c050f00000000000 1:405ca80000000000 +series __name__=b,host=h2,job=x ; 6:c087b20000000000 +result __name__=a,host=h2,job=x ; 6:4086a20000000000 +result __name__=a,host=h2,job=x,zone=z1 ; 0:c08e130000000000 1:c064700000000000 2:4087fc0000000000 +end +case limit limit_ratio 0.3 none - +grid 100000 10000 1 +series __name__=a,host=h2,job=x ; 0:c089820000000000 +series __name__=b,host=h2,job=x,zone=z1 ; 0:c081140000000000 +series __name__=b,host=h1,job=y ; 0:7ff0000000000000 +series __name__=b,host=h2,job=x ; 0:c0721a0000000000 +result __name__=b,host=h2,job=x ; 0:c0721a0000000000 +end +case limit limit_ratio -1 none - +grid 100000 10000 3 +series __name__=a,host=h1,job=y,zone=z1 ; +series __name__=a,host=h1,job=y ; 1:7ff0000000000000 +series __name__=a,host=h1,job=x ; 1:c072800000000000 +series __name__=a,host=h2,job=y ; 2:7ff8000000000001 +series __name__=b,host=h2,job=y ; 1:c089740000000000 +series __name__=b,host=h1,job=x ; 0:c074c40000000000 1:c064b40000000000 +series __name__=b,host=h2,job=y,zone=z1 ; 2:4082940000000000 +result __name__=a,host=h1,job=x ; 1:c072800000000000 +result __name__=a,host=h1,job=y ; 1:7ff0000000000000 +result __name__=a,host=h2,job=y ; 2:7ff8000000000001 +result __name__=b,host=h1,job=x ; 0:c074c40000000000 1:c064b40000000000 +result __name__=b,host=h2,job=y ; 1:c089740000000000 +result __name__=b,host=h2,job=y,zone=z1 ; 2:4082940000000000 +end +case limit limitk 1.5 by host,zone +grid 100000 10000 6 +series __name__=a,host=h2,job=x ; 4:c0752c0000000000 +series __name__=a,host=h1,job=y,zone=z1 ; 0:0 +series __name__=a,host=h1,job=x ; 3:c07b360000000000 +series __name__=b,host=h1,job=x ; 4:7e37e43c8800759c +series __name__=b,host=h2,job=y ; 5:c062340000000000 +series __name__=b,host=h2,job=y,zone=z1 ; 5:7ff0000000000000 +result __name__=a,host=h1,job=x ; 3:c07b360000000000 +result __name__=a,host=h1,job=y,zone=z1 ; 0:0 +result __name__=a,host=h2,job=x ; 4:c0752c0000000000 +result __name__=b,host=h1,job=x ; 4:7e37e43c8800759c +result __name__=b,host=h2,job=y ; 5:c062340000000000 +result __name__=b,host=h2,job=y,zone=z1 ; 5:7ff0000000000000 +end +case limit limit_ratio -1.5 none - +grid 100000 10000 4 +series __name__=a,host=h2,job=x ; +series __name__=a,host=h2,job=y ; 1:4084200000000000 2:c086690000000000 3:c070ec0000000000 +series __name__=a,host=h2,job=y,zone=z1 ; 2:0 +series __name__=b,host=h2,job=y,zone=z1 ; 3:404c700000000000 +series __name__=b,host=h2,job=x ; 0:4089990000000000 1:c08e120000000000 2:407da40000000000 +series __name__=b,host=h1,job=y ; 1:c064e00000000000 2:c07e260000000000 3:4060600000000000 +result __name__=a,host=h2,job=y ; 1:4084200000000000 2:c086690000000000 3:c070ec0000000000 +result __name__=a,host=h2,job=y,zone=z1 ; 2:0 +result __name__=b,host=h1,job=y ; 1:c064e00000000000 2:c07e260000000000 3:4060600000000000 +result __name__=b,host=h2,job=x ; 0:4089990000000000 1:c08e120000000000 2:407da40000000000 +result __name__=b,host=h2,job=y,zone=z1 ; 3:404c700000000000 +end +case limit limit_ratio 1 without host,zone +grid 100000 10000 11 +series __name__=a,host=h2,job=y ; 10:c08d430000000000 +series __name__=a,host=h1,job=x,zone=z1 ; 1:c044500000000000 2:7ff0000000000000 3:4071940000000000 +series __name__=a,host=h2,job=y,zone=z1 ; 0:c063c80000000000 1:c08b5f0000000000 3:c086c20000000000 4:c071e40000000000 5:c089450000000000 6:4084b40000000000 8:c0889c0000000000 9:c08e350000000000 10:c0751e0000000000 +series __name__=a,host=h2,job=x ; 3:c080850000000000 4:c08c210000000000 5:c087fc0000000000 6:408d490000000000 8:c046f00000000000 +series __name__=b,host=h1,job=y ; 6:c07a260000000000 +series __name__=b,host=h1,job=x,zone=z1 ; 5:4073500000000000 +result __name__=a,host=h1,job=x,zone=z1 ; 1:c044500000000000 2:7ff0000000000000 3:4071940000000000 +result __name__=a,host=h2,job=x ; 3:c080850000000000 4:c08c210000000000 5:c087fc0000000000 6:408d490000000000 8:c046f00000000000 +result __name__=a,host=h2,job=y ; 10:c08d430000000000 +result __name__=a,host=h2,job=y,zone=z1 ; 0:c063c80000000000 1:c08b5f0000000000 3:c086c20000000000 4:c071e40000000000 5:c089450000000000 6:4084b40000000000 8:c0889c0000000000 9:c08e350000000000 10:c0751e0000000000 +result __name__=b,host=h1,job=x,zone=z1 ; 5:4073500000000000 +result __name__=b,host=h1,job=y ; 6:c07a260000000000 +end +case limit limitk 1 none - +grid 100000 10000 4 +series __name__=a,host=h1,job=y,zone=z1 ; 3:4083510000000000 +series __name__=a,host=h1,job=y ; 1:4072ca0000000000 +series __name__=a,host=h2,job=y ; 3:c0852b0000000000 +series __name__=b,host=h2,job=y ; 0:0 1:fff0000000000000 2:7ff8000000000001 +series __name__=b,host=h1,job=y,zone=z1 ; 2:c08e8d0000000000 3:7ff8000000000001 +series __name__=b,host=h1,job=y ; 2:c07d680000000000 3:c0863f0000000000 +result __name__=a,host=h1,job=y ; 1:4072ca0000000000 +result __name__=a,host=h1,job=y,zone=z1 ; 3:4083510000000000 +result __name__=b,host=h2,job=y ; 0:0 2:7ff8000000000001 +end +case limit limit_ratio 0 by __name__,host,zone +grid 100000 10000 9 +series __name__=a,host=h2,job=x ; 4:c08cd80000000000 +series __name__=a,host=h1,job=x,zone=z1 ; 2:c063540000000000 3:40754e0000000000 4:4086db0000000000 5:c05ba80000000000 6:c052200000000000 7:4084fd0000000000 8:c059800000000000 +series __name__=a,host=h2,job=x,zone=z1 ; 0:4071460000000000 2:4081220000000000 +series __name__=b,host=h2,job=x ; 2:4081560000000000 3:c07a380000000000 4:406b740000000000 5:407e520000000000 6:c08de10000000000 7:407b460000000000 +end +case limit limitk 1 without job +grid 100000 10000 8 +series __name__=a,host=h1,job=x ; 6:c04d200000000000 +series __name__=a,host=h2,job=y,zone=z1 ; 6:c086770000000000 7:4082900000000000 +series __name__=a,host=h2,job=y ; 1:4061080000000000 2:7ff8000000000001 3:c08c200000000000 4:4082a60000000000 +series __name__=a,host=h1,job=y ; 5:c08a560000000000 +series __name__=a,host=h1,job=y,zone=z1 ; 3:0 5:c07a8c0000000000 +series __name__=b,host=h1,job=y ; +series __name__=b,host=h1,job=y,zone=z1 ; 3:4071b60000000000 4:c046d00000000000 +series __name__=b,host=h1,job=x ; +series __name__=b,host=h1,job=y,zone=z2 ; 6:c077980000000000 7:c0778a0000000000 +result __name__=a,host=h1,job=x ; 6:c04d200000000000 +result __name__=a,host=h1,job=y ; 5:c08a560000000000 +result __name__=a,host=h1,job=y,zone=z1 ; 3:0 5:c07a8c0000000000 +result __name__=a,host=h2,job=y ; 1:4061080000000000 2:7ff8000000000001 3:c08c200000000000 4:4082a60000000000 +result __name__=a,host=h2,job=y,zone=z1 ; 6:c086770000000000 7:4082900000000000 +result __name__=b,host=h1,job=y,zone=z1 ; 4:c046d00000000000 +result __name__=b,host=h1,job=y,zone=z2 ; 6:c077980000000000 7:c0778a0000000000 +end +case limit limit_ratio 0.5 by host,job,zone +grid 100000 10000 4 +series __name__=a,host=h2,job=y ; 2:c075e40000000000 +series __name__=a,host=h1,job=y ; 2:4082130000000000 +series __name__=a,host=h2,job=y,zone=z1 ; 0:408ac60000000000 1:4042d00000000000 2:c08daf0000000000 3:40736c0000000000 +series __name__=a,host=h2,job=x ; 1:7ff8000000000001 2:4085af0000000000 3:c074820000000000 +series __name__=a,host=h2,job=x,zone=z1 ; 0:407d660000000000 1:40837d0000000000 +series __name__=b,host=h2,job=y ; 3:4084f00000000000 +result __name__=a,host=h2,job=y,zone=z1 ; 0:408ac60000000000 1:4042d00000000000 2:c08daf0000000000 3:40736c0000000000 +end +case limit limit_ratio 0.3 without host +grid 100000 10000 11 +series __name__=a,host=h2,job=y,zone=z1 ; 10:4077ba0000000000 +series __name__=a,host=h1,job=y ; 0:408cce0000000000 1:408bd00000000000 3:4084330000000000 5:407ff00000000000 6:c0770a0000000000 7:fff0000000000000 8:7ff0000000000000 9:406ec40000000000 +series __name__=a,host=h2,job=x ; 4:7ff8000000000001 5:c06e840000000000 6:c088c20000000000 7:40813f0000000000 +series __name__=b,host=h1,job=x ; 2:4088940000000000 3:4040500000000000 4:c06d680000000000 5:408dd70000000000 6:c0894b0000000000 7:7e37e43c8800759c 9:4075060000000000 10:4086f40000000000 +series __name__=b,host=h2,job=y,zone=z2 ; 9:40881f0000000000 10:4076bc0000000000 +series __name__=b,host=h1,job=x,zone=z1 ; 10:c085570000000000 +result __name__=b,host=h1,job=x ; 2:4088940000000000 3:4040500000000000 4:c06d680000000000 5:408dd70000000000 6:c0894b0000000000 7:7e37e43c8800759c 9:4075060000000000 10:4086f40000000000 +result __name__=b,host=h2,job=y,zone=z2 ; 9:40881f0000000000 10:4076bc0000000000 +end +case limit limit_ratio 1.5 none - +grid 100000 10000 8 +series __name__=a,host=h1,job=x,zone=z1 ; 3:4065f80000000000 +series __name__=a,host=h2,job=y,zone=z1 ; 5:c08bd20000000000 6:408c3d0000000000 +series __name__=a,host=h2,job=x,zone=z1 ; 4:c08bb40000000000 7:408ccd0000000000 +series __name__=b,host=h1,job=x,zone=z1 ; 4:c08cea0000000000 5:c08d320000000000 +series __name__=b,host=h2,job=x,zone=z1 ; 3:c08df70000000000 5:7ff8000000000001 6:c089d20000000000 +series __name__=b,host=h2,job=y,zone=z1 ; 2:c06d300000000000 3:40864f0000000000 5:407b0e0000000000 7:4089760000000000 +result __name__=a,host=h1,job=x,zone=z1 ; 3:4065f80000000000 +result __name__=a,host=h2,job=x,zone=z1 ; 4:c08bb40000000000 7:408ccd0000000000 +result __name__=a,host=h2,job=y,zone=z1 ; 5:c08bd20000000000 6:408c3d0000000000 +result __name__=b,host=h1,job=x,zone=z1 ; 4:c08cea0000000000 5:c08d320000000000 +result __name__=b,host=h2,job=x,zone=z1 ; 3:c08df70000000000 5:7ff8000000000001 6:c089d20000000000 +result __name__=b,host=h2,job=y,zone=z1 ; 2:c06d300000000000 3:40864f0000000000 5:407b0e0000000000 7:4089760000000000 +end +case limit limit_ratio 0.3 without - +grid 100000 10000 7 +series __name__=a,host=h2,job=x ; 3:c0864b0000000000 4:c06a080000000000 +series __name__=a,host=h2,job=x,zone=z1 ; 6:4081db0000000000 +series __name__=a,host=h2,job=y,zone=z1 ; 5:4078fc0000000000 +series __name__=a,host=h1,job=y,zone=z1 ; 3:c016000000000000 4:c06d140000000000 5:406bd40000000000 +series __name__=b,host=h2,job=x ; 0:7ff0000000000000 1:c083350000000000 2:c07be60000000000 3:408b590000000000 4:c083f50000000000 5:c088240000000000 +result __name__=b,host=h2,job=x ; 0:7ff0000000000000 1:c083350000000000 2:c07be60000000000 3:408b590000000000 4:c083f50000000000 5:c088240000000000 +end +case limit limitk NaN without __name__,job,zone +grid 100000 10000 5 +series __name__=a,host=h1,job=y ; +series __name__=a,host=h2,job=x,zone=z1 ; +series __name__=b,host=h2,job=x,zone=z1 ; 3:40754e0000000000 +error Parameter value is NaN +end +case limit limitk 0 none - +grid 100000 10000 11 +series __name__=a,host=h2,job=y ; 7:c04b400000000000 8:4085340000000000 9:c0742c0000000000 +series __name__=a,host=h2,job=x,zone=z1 ; 10:c08bc70000000000 +series __name__=a,host=h1,job=y ; 3:c088ea0000000000 4:4076660000000000 +series __name__=a,host=h2,job=y,zone=z1 ; 10:7ff8000000000001 +series __name__=a,host=h2,job=x ; 0:406b0c0000000000 1:4087650000000000 +series __name__=b,host=h2,job=y ; 1:40805c0000000000 2:c085350000000000 3:c08ab20000000000 +series __name__=b,host=h1,job=x ; 2:40750a0000000000 3:7e37e43c8800759c 5:c06d440000000000 7:c036600000000000 8:4060e80000000000 9:c0845c0000000000 +series __name__=b,host=h2,job=x ; 7:c0812f0000000000 8:40846e0000000000 9:c080930000000000 +series __name__=b,host=h1,job=y ; 2:c079b40000000000 +end +case limit limit_ratio -1 by host +grid 100000 10000 6 +series __name__=a,host=h2,job=y ; 4:407c360000000000 +series __name__=a,host=h2,job=x ; 0:c081410000000000 1:c068180000000000 +series __name__=a,host=h1,job=y ; 2:4061d80000000000 3:4078c20000000000 +series __name__=b,host=h2,job=y ; 1:c089f60000000000 +series __name__=b,host=h2,job=y,zone=z2 ; 1:c053d00000000000 2:4072180000000000 3:4072040000000000 +series __name__=b,host=h1,job=y ; 2:c082ac0000000000 3:c05e780000000000 +result __name__=a,host=h1,job=y ; 2:4061d80000000000 3:4078c20000000000 +result __name__=a,host=h2,job=x ; 0:c081410000000000 1:c068180000000000 +result __name__=a,host=h2,job=y ; 4:407c360000000000 +result __name__=b,host=h1,job=y ; 2:c082ac0000000000 3:c05e780000000000 +result __name__=b,host=h2,job=y ; 1:c089f60000000000 +result __name__=b,host=h2,job=y,zone=z2 ; 1:c053d00000000000 2:4072180000000000 3:4072040000000000 +end +case limit limitk 2 without host,job +grid 100000 10000 8 +series __name__=a,host=h2,job=y ; 3:c0879b0000000000 4:c07b4a0000000000 5:4085d90000000000 +series __name__=b,host=h2,job=y ; 4:7ff8000000000001 5:c063a80000000000 +result __name__=a,host=h2,job=y ; 3:c0879b0000000000 4:c07b4a0000000000 5:4085d90000000000 +result __name__=b,host=h2,job=y ; 4:7ff8000000000001 5:c063a80000000000 +end +case limit limit_ratio 0.9 none - +grid 100000 10000 3 +series __name__=a,host=h2,job=x,zone=z1 ; 1:7ff8000000000001 +series __name__=a,host=h1,job=y,zone=z1 ; +series __name__=a,host=h1,job=x ; 2:4083010000000000 +series __name__=b,host=h1,job=y,zone=z1 ; 0:7e37e43c8800759c 1:4085e60000000000 2:4074e00000000000 +series __name__=b,host=h1,job=x,zone=z1 ; +series __name__=b,host=h2,job=x,zone=z1 ; 1:c060ec0000000000 +series __name__=b,host=h1,job=x ; 1:408d5f0000000000 2:7ff8000000000001 +result __name__=a,host=h1,job=x ; 2:4083010000000000 +result __name__=a,host=h2,job=x,zone=z1 ; 1:7ff8000000000001 +result __name__=b,host=h1,job=x ; 1:408d5f0000000000 2:7ff8000000000001 +result __name__=b,host=h1,job=y,zone=z1 ; 0:7e37e43c8800759c 1:4085e60000000000 2:4074e00000000000 +result __name__=b,host=h2,job=x,zone=z1 ; 1:c060ec0000000000 +end +case limit limit_ratio 0.5 by host +grid 100000 10000 11 +series __name__=a,host=h1,job=x ; 8:0 +series __name__=b,host=h1,job=x ; +series __name__=b,host=h2,job=y,zone=z1 ; 8:c072360000000000 9:4084ff0000000000 +series __name__=b,host=h1,job=x,zone=z1 ; 8:7ff8000000000001 10:c086d20000000000 +series __name__=b,host=h2,job=x ; 8:7ff0000000000000 9:c08a000000000000 10:c077c80000000000 +result __name__=a,host=h1,job=x ; 8:0 +result __name__=b,host=h1,job=x,zone=z1 ; 8:7ff8000000000001 10:c086d20000000000 +result __name__=b,host=h2,job=x ; 8:7ff0000000000000 9:c08a000000000000 10:c077c80000000000 +result __name__=b,host=h2,job=y,zone=z1 ; 8:c072360000000000 9:4084ff0000000000 +end +case limit limit_ratio 0.5 without __name__ +grid 100000 10000 4 +series __name__=a,host=h2,job=x ; 2:c07bd20000000000 3:c0692c0000000000 +series __name__=a,host=h1,job=y ; 3:4084de0000000000 +series __name__=a,host=h2,job=y,zone=z1 ; 1:407c320000000000 2:408e5a0000000000 3:c07be00000000000 +series __name__=a,host=h1,job=x ; 2:c0807f0000000000 3:4052480000000000 +series __name__=b,host=h1,job=x,zone=z1 ; 0:c062280000000000 1:4081f80000000000 +series __name__=b,host=h1,job=y,zone=z1 ; 3:408bdb0000000000 +result __name__=a,host=h1,job=x ; 2:c0807f0000000000 3:4052480000000000 +result __name__=a,host=h2,job=y,zone=z1 ; 1:407c320000000000 2:408e5a0000000000 3:c07be00000000000 +result __name__=b,host=h1,job=x,zone=z1 ; 0:c062280000000000 1:4081f80000000000 +end +case limit limitk 3 by __name__ +grid 100000 10000 2 +series __name__=a,host=h1,job=y,zone=z1 ; 1:7e37e43c8800759c +series __name__=b,host=h1,job=y,zone=z1 ; 1:c0816c0000000000 +result __name__=a,host=h1,job=y,zone=z1 ; 1:7e37e43c8800759c +result __name__=b,host=h1,job=y,zone=z1 ; 1:c0816c0000000000 +end +case limit limit_ratio -1.5 by host +grid 100000 10000 6 +series __name__=a,host=h1,job=y ; +series __name__=a,host=h2,job=x ; 4:c0862b0000000000 +series __name__=b,host=h1,job=x ; 2:7ff0000000000000 +series __name__=b,host=h1,job=y,zone=z1 ; 0:4081a30000000000 +series __name__=b,host=h1,job=y ; 2:c080030000000000 +result __name__=a,host=h2,job=x ; 4:c0862b0000000000 +result __name__=b,host=h1,job=x ; 2:7ff0000000000000 +result __name__=b,host=h1,job=y ; 2:c080030000000000 +result __name__=b,host=h1,job=y,zone=z1 ; 0:4081a30000000000 +end +case limit limitk 3 none - +grid 100000 10000 11 +series __name__=a,host=h2,job=x,zone=z1 ; 7:c070be0000000000 8:4079560000000000 9:7e37e43c8800759c +series __name__=a,host=h1,job=x ; 5:c089950000000000 +series __name__=a,host=h1,job=x,zone=z1 ; 1:4075ce0000000000 2:408a680000000000 4:4043800000000000 5:c068ec0000000000 6:4044200000000000 7:fff0000000000000 8:407e900000000000 +series __name__=b,host=h1,job=x ; 7:c06a280000000000 +series __name__=b,host=h2,job=x,zone=z1 ; 1:407ed20000000000 3:c07a5e0000000000 4:7ff0000000000000 6:c08e910000000000 +result __name__=a,host=h1,job=x ; 5:c089950000000000 +result __name__=a,host=h1,job=x,zone=z1 ; 1:4075ce0000000000 2:408a680000000000 4:4043800000000000 5:c068ec0000000000 6:4044200000000000 7:fff0000000000000 8:407e900000000000 +result __name__=a,host=h2,job=x,zone=z1 ; 7:c070be0000000000 8:4079560000000000 9:7e37e43c8800759c +result __name__=b,host=h1,job=x ; 7:c06a280000000000 +result __name__=b,host=h2,job=x,zone=z1 ; 1:407ed20000000000 3:c07a5e0000000000 4:7ff0000000000000 6:c08e910000000000 +end +case limit limitk 2 none - +grid 100000 10000 4 +series __name__=a,host=h2,job=y ; +series __name__=a,host=h2,job=x,zone=z1 ; 0:4068d80000000000 2:c06c540000000000 3:c08f070000000000 +series __name__=a,host=h2,job=x ; 0:407e180000000000 +series __name__=a,host=h1,job=y ; 0:c086600000000000 +series __name__=b,host=h2,job=x,zone=z1 ; 1:408a2d0000000000 2:4064d00000000000 3:c087510000000000 +series __name__=b,host=h1,job=y,zone=z1 ; 3:4064f00000000000 +result __name__=a,host=h2,job=x ; 0:407e180000000000 +result __name__=a,host=h2,job=x,zone=z1 ; 0:4068d80000000000 2:c06c540000000000 3:c08f070000000000 +result __name__=b,host=h2,job=x,zone=z1 ; 1:408a2d0000000000 2:4064d00000000000 3:c087510000000000 +end +case limit limitk -1 by __name__,zone +grid 100000 10000 6 +series __name__=a,host=h2,job=x ; 4:4051e00000000000 +series __name__=a,host=h2,job=y,zone=z1 ; 0:408bf60000000000 1:407acc0000000000 +series __name__=b,host=h2,job=y,zone=z1 ; 5:4076c80000000000 +series __name__=b,host=h2,job=y ; 3:c063f00000000000 4:c07aa20000000000 +end +case limit limitk 3 without host,zone +grid 100000 10000 3 +series __name__=a,host=h1,job=y ; 0:4079fc0000000000 1:4087970000000000 2:407aaa0000000000 +series __name__=a,host=h1,job=x ; 2:4050c00000000000 +series __name__=b,host=h1,job=y ; 1:40888f0000000000 +series __name__=b,host=h2,job=y,zone=z1 ; 1:7ff8000000000001 2:c089030000000000 +result __name__=a,host=h1,job=x ; 2:4050c00000000000 +result __name__=a,host=h1,job=y ; 0:4079fc0000000000 1:4087970000000000 2:407aaa0000000000 +result __name__=b,host=h1,job=y ; 1:40888f0000000000 +result __name__=b,host=h2,job=y,zone=z1 ; 1:7ff8000000000001 2:c089030000000000 +end +case limit limitk 10 none - +grid 100000 10000 10 +series __name__=a,host=h1,job=x ; 3:406f140000000000 4:40723e0000000000 5:c05c300000000000 6:4085570000000000 +series __name__=a,host=h2,job=x,zone=z1 ; 0:4074080000000000 1:c075020000000000 2:c08def0000000000 4:0 5:7ff8000000000001 6:4068d00000000000 +series __name__=a,host=h2,job=y ; 9:c084020000000000 +series __name__=b,host=h2,job=y ; 6:40718c0000000000 7:c08efe0000000000 +series __name__=b,host=h1,job=x ; 3:c08df70000000000 4:404a500000000000 5:c08ed00000000000 6:4083640000000000 +result __name__=a,host=h1,job=x ; 3:406f140000000000 4:40723e0000000000 5:c05c300000000000 6:4085570000000000 +result __name__=a,host=h2,job=x,zone=z1 ; 0:4074080000000000 1:c075020000000000 2:c08def0000000000 4:0 5:7ff8000000000001 6:4068d00000000000 +result __name__=a,host=h2,job=y ; 9:c084020000000000 +result __name__=b,host=h1,job=x ; 3:c08df70000000000 4:404a500000000000 5:c08ed00000000000 6:4083640000000000 +result __name__=b,host=h2,job=y ; 6:40718c0000000000 7:c08efe0000000000 +end +case limit limitk 10 without host +grid 100000 10000 1 +series __name__=a,host=h1,job=y ; 0:40858b0000000000 +series __name__=a,host=h1,job=x ; 0:0 +series __name__=a,host=h2,job=x ; 0:c080310000000000 +series __name__=a,host=h2,job=y ; +series __name__=a,host=h2,job=x,zone=z1 ; 0:7e37e43c8800759c +series __name__=a,host=h2,job=y,zone=z1 ; 0:4083970000000000 +series __name__=b,host=h2,job=y,zone=z1 ; 0:4085320000000000 +series __name__=b,host=h2,job=x,zone=z1 ; 0:4085340000000000 +result __name__=a,host=h1,job=x ; 0:0 +result __name__=a,host=h1,job=y ; 0:40858b0000000000 +result __name__=a,host=h2,job=x ; 0:c080310000000000 +result __name__=a,host=h2,job=x,zone=z1 ; 0:7e37e43c8800759c +result __name__=a,host=h2,job=y,zone=z1 ; 0:4083970000000000 +result __name__=b,host=h2,job=x,zone=z1 ; 0:4085340000000000 +result __name__=b,host=h2,job=y,zone=z1 ; 0:4085320000000000 +end +case limit limitk 3 by __name__,job +grid 100000 10000 5 +series __name__=a,host=h1,job=y,zone=z1 ; 0:c075a20000000000 1:c082bc0000000000 +series __name__=b,host=h1,job=y,zone=z1 ; 4:c0878f0000000000 +result __name__=a,host=h1,job=y,zone=z1 ; 0:c075a20000000000 1:c082bc0000000000 +result __name__=b,host=h1,job=y,zone=z1 ; 4:c0878f0000000000 +end +case limit limitk 2 by __name__,zone +grid 100000 10000 11 +series __name__=a,host=h1,job=y,zone=z1 ; 5:40830d0000000000 6:407bfa0000000000 7:4085050000000000 +series __name__=a,host=h2,job=x ; 1:405fb00000000000 3:4083340000000000 5:c089590000000000 6:4081250000000000 7:7ff8000000000001 +series __name__=a,host=h1,job=x ; 2:4033400000000000 3:c074020000000000 4:4070220000000000 5:c085a90000000000 6:c08ccc0000000000 8:4029c00000000000 +series __name__=a,host=h1,job=y ; 8:c061c80000000000 9:4087c30000000000 10:4079940000000000 +series __name__=a,host=h2,job=y ; 9:c080c00000000000 10:7ff0000000000000 +series __name__=b,host=h1,job=x,zone=z1 ; 3:c07e520000000000 4:40771c0000000000 5:c072e00000000000 6:40891a0000000000 7:c073300000000000 +series __name__=b,host=h2,job=y,zone=z1 ; 2:c066780000000000 4:c08c940000000000 5:c080ca0000000000 6:40844f0000000000 7:4079d80000000000 +result __name__=a,host=h1,job=x ; 2:4033400000000000 3:c074020000000000 4:4070220000000000 5:c085a90000000000 6:c08ccc0000000000 8:4029c00000000000 +result __name__=a,host=h1,job=y ; 8:c061c80000000000 9:4087c30000000000 10:4079940000000000 +result __name__=a,host=h1,job=y,zone=z1 ; 5:40830d0000000000 6:407bfa0000000000 7:4085050000000000 +result __name__=a,host=h2,job=x ; 1:405fb00000000000 3:4083340000000000 5:c089590000000000 6:4081250000000000 7:7ff8000000000001 +result __name__=a,host=h2,job=y ; 9:c080c00000000000 10:7ff0000000000000 +result __name__=b,host=h1,job=x,zone=z1 ; 3:c07e520000000000 4:40771c0000000000 5:c072e00000000000 6:40891a0000000000 7:c073300000000000 +result __name__=b,host=h2,job=y,zone=z1 ; 2:c066780000000000 4:c08c940000000000 5:c080ca0000000000 6:40844f0000000000 7:4079d80000000000 +end +case limit limit_ratio NaN none - +grid 100000 10000 8 +series __name__=a,host=h1,job=x,zone=z1 ; 4:c027400000000000 5:c075cc0000000000 +series __name__=a,host=h1,job=x ; 5:c08a8f0000000000 6:4089760000000000 7:c08d7c0000000000 +series __name__=a,host=h1,job=y,zone=z1 ; 3:407f020000000000 4:0 6:c08d7d0000000000 +series __name__=a,host=h2,job=x ; 5:7ff0000000000000 6:408a740000000000 +series __name__=b,host=h1,job=y,zone=z1 ; +series __name__=b,host=h1,job=y ; +series __name__=b,host=h1,job=x ; 5:4073ce0000000000 +series __name__=b,host=h1,job=x,zone=z2 ; 6:40859b0000000000 +error Ratio value is NaN +end +case limit limitk 3 by __name__,host +grid 100000 10000 5 +series __name__=a,host=h1,job=y,zone=z1 ; 2:4082250000000000 3:4084e80000000000 +series __name__=a,host=h1,job=x,zone=z1 ; +series __name__=a,host=h2,job=y ; 1:7e37e43c8800759c 2:4086f20000000000 3:c076c20000000000 4:4087440000000000 +series __name__=a,host=h1,job=x ; 3:c07de20000000000 4:c076540000000000 +series __name__=b,host=h1,job=y ; 4:4077240000000000 +series __name__=b,host=h2,job=y ; 0:40746c0000000000 1:0 2:4076260000000000 3:c0824e0000000000 +series __name__=b,host=h1,job=y,zone=z2 ; 0:c042200000000000 1:4069ac0000000000 2:408d5a0000000000 3:c0746e0000000000 +series __name__=b,host=h1,job=y,zone=z1 ; 0:c067c80000000000 +series __name__=b,host=h1,job=x ; 4:7ff8000000000001 +result __name__=a,host=h1,job=x ; 3:c07de20000000000 4:c076540000000000 +result __name__=a,host=h1,job=y,zone=z1 ; 2:4082250000000000 3:4084e80000000000 +result __name__=a,host=h2,job=y ; 1:7e37e43c8800759c 2:4086f20000000000 3:c076c20000000000 4:4087440000000000 +result __name__=b,host=h1,job=x ; 4:7ff8000000000001 +result __name__=b,host=h1,job=y ; 4:4077240000000000 +result __name__=b,host=h1,job=y,zone=z1 ; 0:c067c80000000000 +result __name__=b,host=h1,job=y,zone=z2 ; 0:c042200000000000 1:4069ac0000000000 2:408d5a0000000000 3:c0746e0000000000 +result __name__=b,host=h2,job=y ; 0:40746c0000000000 1:0 2:4076260000000000 3:c0824e0000000000 +end +case limit limitk 1 without host +grid 100000 10000 7 +series __name__=a,host=h2,job=x ; 6:0 +series __name__=a,host=h2,job=y ; 5:c086680000000000 +series __name__=a,host=h2,job=x,zone=z1 ; 6:c087a30000000000 +series __name__=b,host=h2,job=x ; 2:7ff8000000000001 3:c080290000000000 4:c0890f0000000000 +series __name__=b,host=h2,job=x,zone=z1 ; 4:c065440000000000 +result __name__=a,host=h2,job=x ; 6:0 +result __name__=a,host=h2,job=x,zone=z1 ; 6:c087a30000000000 +result __name__=a,host=h2,job=y ; 5:c086680000000000 +result __name__=b,host=h2,job=x ; 2:7ff8000000000001 3:c080290000000000 4:c0890f0000000000 +result __name__=b,host=h2,job=x,zone=z1 ; 4:c065440000000000 +end +case limit limitk 0 without - +grid 100000 10000 3 +series __name__=a,host=h2,job=x ; 1:c061f80000000000 2:c07ada0000000000 +series __name__=a,host=h2,job=y ; 1:c048a00000000000 2:c06b2c0000000000 +series __name__=a,host=h2,job=y,zone=z1 ; +series __name__=a,host=h1,job=x,zone=z1 ; +series __name__=a,host=h2,job=x,zone=z1 ; 0:4063140000000000 +series __name__=b,host=h2,job=x ; 1:408d090000000000 2:7e37e43c8800759c +series __name__=b,host=h1,job=y ; 0:408e0f0000000000 1:c081700000000000 +series __name__=b,host=h1,job=x,zone=z1 ; 0:4076ac0000000000 +series __name__=b,host=h1,job=y,zone=z1 ; 1:407bda0000000000 2:c070240000000000 +series __name__=b,host=h2,job=x,zone=z1 ; 2:407e180000000000 +end +case limit limitk NaN by __name__,job +grid 100000 10000 5 +series __name__=a,host=h1,job=y,zone=z1 ; 3:7ff8000000000001 +series __name__=a,host=h1,job=x ; 2:c08d430000000000 3:c06a4c0000000000 4:7ff8000000000001 +series __name__=b,host=h1,job=x ; 3:fff0000000000000 +series __name__=b,host=h1,job=y ; +series __name__=b,host=h2,job=x ; 0:7ff8000000000001 +series __name__=b,host=h1,job=x,zone=z2 ; 1:407d200000000000 2:c08ea90000000000 +error Parameter value is NaN +end +case limit limit_ratio 1.5 without __name__,host,zone +grid 100000 10000 5 +series __name__=a,host=h2,job=y,zone=z1 ; 4:4065840000000000 +series __name__=a,host=h1,job=x ; 2:c060180000000000 3:7ff0000000000000 +series __name__=a,host=h1,job=y ; 0:4081400000000000 +series __name__=b,host=h1,job=x,zone=z2 ; 3:406d3c0000000000 +series __name__=b,host=h2,job=y,zone=z1 ; 2:4078ba0000000000 +result __name__=a,host=h1,job=x ; 2:c060180000000000 3:7ff0000000000000 +result __name__=a,host=h1,job=y ; 0:4081400000000000 +result __name__=a,host=h2,job=y,zone=z1 ; 4:4065840000000000 +result __name__=b,host=h1,job=x,zone=z2 ; 3:406d3c0000000000 +result __name__=b,host=h2,job=y,zone=z1 ; 2:4078ba0000000000 +end +case limit limitk 0 none - +grid 100000 10000 10 +series __name__=a,host=h1,job=x,zone=z1 ; 2:c05dd00000000000 5:0 7:4080880000000000 +series __name__=b,host=h2,job=y,zone=z1 ; 1:c080550000000000 +series __name__=b,host=h2,job=y ; 5:40888d0000000000 6:4078de0000000000 7:4084d60000000000 8:4071940000000000 +series __name__=b,host=h1,job=x,zone=z1 ; 2:404cf00000000000 +series __name__=b,host=h1,job=y ; 7:407e3c0000000000 8:40834f0000000000 +end +case limit limit_ratio -0.5 by host +grid 100000 10000 9 +series __name__=a,host=h1,job=y,zone=z1 ; 5:40824d0000000000 6:4080070000000000 +series __name__=a,host=h1,job=x,zone=z1 ; 7:fff0000000000000 +series __name__=a,host=h1,job=x ; +series __name__=a,host=h2,job=x ; 1:40839f0000000000 2:406d2c0000000000 3:408b070000000000 4:c045400000000000 5:7e37e43c8800759c 6:c074620000000000 +series __name__=b,host=h1,job=y ; 1:405f580000000000 2:c070e20000000000 +series __name__=b,host=h2,job=x ; 4:c0800e0000000000 5:4084af0000000000 6:408f0a0000000000 +series __name__=b,host=h1,job=x,zone=z2 ; 5:c0801e0000000000 +result __name__=a,host=h2,job=x ; 1:40839f0000000000 2:406d2c0000000000 3:408b070000000000 4:c045400000000000 5:7e37e43c8800759c 6:c074620000000000 +end +case limit limitk 1.5 without - +grid 100000 10000 9 +series __name__=a,host=h1,job=x,zone=z1 ; 6:c084e10000000000 7:40700e0000000000 8:c08e210000000000 +series __name__=b,host=h1,job=x,zone=z1 ; 7:c076fc0000000000 8:c057680000000000 +result __name__=a,host=h1,job=x,zone=z1 ; 6:c084e10000000000 7:40700e0000000000 8:c08e210000000000 +end +case limit limit_ratio -1.5 without __name__,job +grid 100000 10000 11 +series __name__=a,host=h2,job=x ; 9:408d0c0000000000 +series __name__=a,host=h2,job=y,zone=z1 ; 1:c054980000000000 +series __name__=b,host=h2,job=x ; +series __name__=b,host=h1,job=x,zone=z1 ; 2:4051500000000000 3:7e37e43c8800759c 4:407f160000000000 5:c063700000000000 8:c08efb0000000000 9:4062640000000000 10:7ff8000000000001 +result __name__=a,host=h2,job=x ; 9:408d0c0000000000 +result __name__=a,host=h2,job=y,zone=z1 ; 1:c054980000000000 +result __name__=b,host=h1,job=x,zone=z1 ; 2:4051500000000000 3:7e37e43c8800759c 4:407f160000000000 5:c063700000000000 8:c08efb0000000000 9:4062640000000000 10:7ff8000000000001 +end +case limit limit_ratio NaN without host +grid 100000 10000 3 +series __name__=a,host=h2,job=x ; 2:4061c00000000000 +series __name__=a,host=h1,job=x ; +series __name__=a,host=h2,job=y ; +series __name__=b,host=h1,job=x ; 2:4030800000000000 +series __name__=b,host=h2,job=y,zone=z2 ; 0:0 +series __name__=b,host=h2,job=y,zone=z1 ; 2:c075fa0000000000 +error Ratio value is NaN +end +case limit limitk 3 none - +grid 100000 10000 1 +series __name__=a,host=h1,job=x ; 0:c049000000000000 +series __name__=a,host=h1,job=y ; 0:c08e380000000000 +series __name__=a,host=h1,job=x,zone=z1 ; 0:0 +series __name__=a,host=h2,job=y ; 0:c05c080000000000 +series __name__=a,host=h2,job=y,zone=z1 ; 0:c0759e0000000000 +series __name__=b,host=h1,job=x,zone=z1 ; 0:4058200000000000 +series __name__=b,host=h2,job=y,zone=z1 ; +series __name__=b,host=h1,job=y,zone=z1 ; +series __name__=b,host=h2,job=x,zone=z1 ; 0:fff0000000000000 +series __name__=b,host=h1,job=x,zone=z2 ; +result __name__=a,host=h1,job=x ; 0:c049000000000000 +result __name__=a,host=h1,job=x,zone=z1 ; 0:0 +result __name__=a,host=h1,job=y ; 0:c08e380000000000 +end +case limit limit_ratio -0.3 by host,zone +grid 100000 10000 4 +series __name__=a,host=h2,job=x ; 3:c08e960000000000 +series __name__=a,host=h1,job=y ; +series __name__=a,host=h1,job=x ; 1:0 +series __name__=a,host=h2,job=y ; 1:c07e320000000000 +series __name__=a,host=h2,job=y,zone=z1 ; 2:c06a800000000000 +series __name__=b,host=h2,job=y,zone=z1 ; 2:c080d50000000000 +series __name__=b,host=h1,job=x,zone=z2 ; 2:40699c0000000000 +series __name__=b,host=h2,job=y ; 3:c066640000000000 +series __name__=b,host=h1,job=x ; 1:c07e960000000000 3:c08ad70000000000 +result __name__=a,host=h2,job=x ; 3:c08e960000000000 +end +case limit limit_ratio 0.3 by job +grid 100000 10000 1 +series __name__=a,host=h2,job=y,zone=z1 ; +series __name__=a,host=h2,job=x,zone=z1 ; +series __name__=b,host=h2,job=y,zone=z1 ; 0:4085180000000000 +series __name__=b,host=h2,job=x,zone=z1 ; +result __name__=b,host=h2,job=y,zone=z1 ; 0:4085180000000000 +end +case limit limit_ratio 0.5 without job,zone +grid 100000 10000 3 +series __name__=a,host=h1,job=x ; 0:406a480000000000 1:c0897d0000000000 +series __name__=a,host=h2,job=y ; +series __name__=a,host=h1,job=y ; +series __name__=b,host=h1,job=x,zone=z1 ; 0:c0619c0000000000 +series __name__=b,host=h2,job=y ; 0:4051100000000000 1:c078da0000000000 +series __name__=b,host=h1,job=y,zone=z1 ; 1:c081c70000000000 +series __name__=b,host=h2,job=x ; 1:4078f60000000000 2:0 +result __name__=a,host=h1,job=x ; 0:406a480000000000 1:c0897d0000000000 +result __name__=b,host=h1,job=x,zone=z1 ; 0:c0619c0000000000 +result __name__=b,host=h2,job=x ; 1:4078f60000000000 2:0 +end +case limit limitk 0 none - +grid 100000 10000 12 +series __name__=a,host=h1,job=y ; 8:7ff0000000000000 +series __name__=a,host=h2,job=y ; 5:7e37e43c8800759c +series __name__=b,host=h2,job=y,zone=z1 ; 10:4066a00000000000 11:4083ce0000000000 +end +case limit limit_ratio -1 without __name__,zone +grid 100000 10000 1 +series __name__=a,host=h2,job=y ; 0:7e37e43c8800759c +series __name__=a,host=h2,job=x ; 0:c08b560000000000 +series __name__=b,host=h1,job=y ; 0:4063c40000000000 +series __name__=b,host=h2,job=y ; 0:c08a2d0000000000 +series __name__=b,host=h2,job=x ; 0:0 +result __name__=a,host=h2,job=x ; 0:c08b560000000000 +result __name__=a,host=h2,job=y ; 0:7e37e43c8800759c +result __name__=b,host=h1,job=y ; 0:4063c40000000000 +result __name__=b,host=h2,job=x ; 0:0 +result __name__=b,host=h2,job=y ; 0:c08a2d0000000000 +end +case limit limitk 1 without - +grid 100000 10000 6 +series __name__=a,host=h2,job=x ; 5:c08dfc0000000000 +series __name__=a,host=h1,job=x ; 0:404b100000000000 1:c08b410000000000 4:c0663c0000000000 +series __name__=b,host=h2,job=x ; 1:c072c60000000000 2:4085340000000000 3:c07ff20000000000 +series __name__=b,host=h1,job=x ; 4:408aab0000000000 5:4087870000000000 +result __name__=a,host=h1,job=x ; 0:404b100000000000 1:c08b410000000000 4:c0663c0000000000 +result __name__=a,host=h2,job=x ; 5:c08dfc0000000000 +result __name__=b,host=h1,job=x ; 5:4087870000000000 +result __name__=b,host=h2,job=x ; 1:c072c60000000000 2:4085340000000000 3:c07ff20000000000 +end +case limit limitk 0 none - +grid 100000 10000 6 +series __name__=a,host=h1,job=y ; +series __name__=a,host=h1,job=y,zone=z1 ; 4:7ff0000000000000 +series __name__=b,host=h1,job=y,zone=z1 ; 4:7e37e43c8800759c +series __name__=b,host=h1,job=y ; 5:4085c70000000000 +series __name__=b,host=h1,job=y,zone=z2 ; 2:4084e10000000000 +end +case limit limitk NaN none - +grid 100000 10000 2 +series __name__=a,host=h2,job=y ; 1:4084a00000000000 +series __name__=a,host=h2,job=x ; 1:c061540000000000 +series __name__=a,host=h1,job=y ; +series __name__=a,host=h2,job=x,zone=z1 ; +series __name__=a,host=h1,job=y,zone=z1 ; 0:c0804c0000000000 +series __name__=b,host=h1,job=y ; 1:c084760000000000 +series __name__=b,host=h1,job=y,zone=z1 ; 1:c08be50000000000 +error Parameter value is NaN +end +case limit limitk 1 none - +grid 100000 10000 1 +series __name__=a,host=h2,job=x,zone=z1 ; +series __name__=a,host=h2,job=x ; 0:7ff8000000000001 +series __name__=a,host=h1,job=x,zone=z1 ; +series __name__=a,host=h1,job=y ; 0:c082570000000000 +series __name__=b,host=h1,job=x,zone=z1 ; 0:c0752e0000000000 +series __name__=b,host=h2,job=x ; 0:4089460000000000 +series __name__=b,host=h1,job=y,zone=z1 ; +series __name__=b,host=h2,job=x,zone=z1 ; 0:fff0000000000000 +result __name__=a,host=h2,job=x ; 0:7ff8000000000001 +end +case limit limit_ratio 1.5 none - +grid 100000 10000 11 +series __name__=a,host=h2,job=x ; +series __name__=a,host=h2,job=y ; +series __name__=a,host=h1,job=y ; 3:4087b10000000000 4:c08f300000000000 5:c081f90000000000 6:405c500000000000 7:c08aee0000000000 +series __name__=b,host=h1,job=y ; 3:c08e540000000000 4:4067f80000000000 5:0 +series __name__=b,host=h2,job=y,zone=z2 ; 0:7ff8000000000001 1:c085370000000000 3:40750a0000000000 4:c0617c0000000000 5:c076f20000000000 6:406f040000000000 7:7ff8000000000001 9:4055280000000000 10:406b7c0000000000 +series __name__=b,host=h2,job=x,zone=z1 ; 10:c085830000000000 +result __name__=a,host=h1,job=y ; 3:4087b10000000000 4:c08f300000000000 5:c081f90000000000 6:405c500000000000 7:c08aee0000000000 +result __name__=b,host=h1,job=y ; 3:c08e540000000000 4:4067f80000000000 5:0 +result __name__=b,host=h2,job=x,zone=z1 ; 10:c085830000000000 +result __name__=b,host=h2,job=y,zone=z2 ; 0:7ff8000000000001 1:c085370000000000 3:40750a0000000000 4:c0617c0000000000 5:c076f20000000000 6:406f040000000000 7:7ff8000000000001 9:4055280000000000 10:406b7c0000000000 +end +case limit limit_ratio 0.3 by __name__,job +grid 100000 10000 5 +series __name__=a,host=h1,job=y,zone=z1 ; +series __name__=a,host=h2,job=y ; 2:4089a70000000000 +series __name__=a,host=h2,job=y,zone=z1 ; 2:40723c0000000000 3:c086f30000000000 4:4077d20000000000 +series __name__=a,host=h1,job=x ; 1:c0804b0000000000 2:0 +series __name__=b,host=h2,job=x ; 3:c082f40000000000 +series __name__=b,host=h1,job=y,zone=z1 ; 4:4086160000000000 +series __name__=b,host=h1,job=x ; 3:7ff0000000000000 4:c070440000000000 +result __name__=b,host=h1,job=x ; 3:7ff0000000000000 4:c070440000000000 +result __name__=b,host=h2,job=x ; 3:c082f40000000000 +end +case limit limit_ratio -1.5 none - +grid 100000 10000 10 +series __name__=a,host=h2,job=x,zone=z1 ; 4:4052480000000000 5:c0896e0000000000 6:4068900000000000 7:c083a50000000000 +series __name__=b,host=h2,job=x ; 0:4083640000000000 1:c08a6b0000000000 2:405a480000000000 3:c08c510000000000 4:c085420000000000 5:4072a40000000000 +series __name__=b,host=h2,job=x,zone=z1 ; 0:4075800000000000 1:c06c7c0000000000 2:4089d70000000000 3:fff0000000000000 4:c070f60000000000 +series __name__=b,host=h2,job=y ; 1:4085a30000000000 2:4087760000000000 3:7e37e43c8800759c +result __name__=a,host=h2,job=x,zone=z1 ; 4:4052480000000000 5:c0896e0000000000 6:4068900000000000 7:c083a50000000000 +result __name__=b,host=h2,job=x ; 0:4083640000000000 1:c08a6b0000000000 2:405a480000000000 3:c08c510000000000 4:c085420000000000 5:4072a40000000000 +result __name__=b,host=h2,job=x,zone=z1 ; 0:4075800000000000 1:c06c7c0000000000 2:4089d70000000000 3:fff0000000000000 4:c070f60000000000 +result __name__=b,host=h2,job=y ; 1:4085a30000000000 2:4087760000000000 3:7e37e43c8800759c +end +case limit limit_ratio 0.9 by job +grid 100000 10000 12 +series __name__=a,host=h1,job=y ; 5:c069ec0000000000 6:7ff0000000000000 7:c041900000000000 +series __name__=a,host=h1,job=y,zone=z1 ; 11:c07a720000000000 +series __name__=b,host=h1,job=y,zone=z1 ; 1:40727c0000000000 3:406a940000000000 +series __name__=b,host=h1,job=y ; 4:40713e0000000000 5:c072660000000000 6:7e37e43c8800759c 7:7ff8000000000001 8:c072040000000000 +result __name__=a,host=h1,job=y ; 5:c069ec0000000000 6:7ff0000000000000 7:c041900000000000 +result __name__=a,host=h1,job=y,zone=z1 ; 11:c07a720000000000 +result __name__=b,host=h1,job=y ; 4:40713e0000000000 5:c072660000000000 6:7e37e43c8800759c 7:7ff8000000000001 8:c072040000000000 +result __name__=b,host=h1,job=y,zone=z1 ; 1:40727c0000000000 3:406a940000000000 +end +case limit limitk -1 none - +grid 100000 10000 5 +series __name__=a,host=h1,job=x,zone=z1 ; 1:4088e80000000000 2:40876c0000000000 3:c0878e0000000000 4:408b6e0000000000 +series __name__=a,host=h1,job=y ; 3:7ff8000000000001 +series __name__=a,host=h1,job=y,zone=z1 ; 1:407a980000000000 2:c04a900000000000 +series __name__=b,host=h1,job=y ; 2:405e280000000000 +series __name__=b,host=h1,job=x,zone=z1 ; +end +case limit limitk 0 without __name__,zone +grid 100000 10000 8 +series __name__=a,host=h2,job=x,zone=z1 ; 3:c087530000000000 4:408cad0000000000 +series __name__=a,host=h2,job=x ; 1:4035600000000000 2:40889f0000000000 3:4071880000000000 4:c0829f0000000000 5:406ae80000000000 +series __name__=a,host=h2,job=y ; 2:407f3c0000000000 3:c089c30000000000 4:c04e000000000000 5:7e37e43c8800759c 6:406f600000000000 7:c07d7e0000000000 +series __name__=a,host=h1,job=x,zone=z1 ; 0:4084450000000000 1:407f080000000000 +series __name__=b,host=h1,job=x ; 5:405e680000000000 6:4076a80000000000 7:fff0000000000000 +series __name__=b,host=h2,job=y ; 2:c021c00000000000 4:c084650000000000 5:c051c00000000000 6:c07c180000000000 7:4050c80000000000 +end +case limit limit_ratio 0.5 by host,zone +grid 100000 10000 12 +series __name__=a,host=h2,job=y,zone=z1 ; 7:4087260000000000 10:407c720000000000 +series __name__=a,host=h2,job=x ; 10:40797e0000000000 +series __name__=b,host=h2,job=y ; 11:c07f420000000000 +result __name__=a,host=h2,job=y,zone=z1 ; 7:4087260000000000 10:407c720000000000 +end +case limit limit_ratio 0.9 none - +grid 100000 10000 10 +series __name__=a,host=h1,job=x ; +series __name__=b,host=h1,job=x ; 6:c08dd70000000000 7:40753a0000000000 8:405d100000000000 9:4069f00000000000 +series __name__=b,host=h1,job=x,zone=z2 ; 1:c087a90000000000 2:c069640000000000 3:c087800000000000 4:408b400000000000 5:407d920000000000 6:c083600000000000 7:c07d3a0000000000 8:c081c90000000000 9:406c940000000000 +series __name__=b,host=h1,job=y,zone=z1 ; 6:4087230000000000 7:c060d40000000000 8:408b6a0000000000 9:40842a0000000000 +result __name__=b,host=h1,job=x ; 6:c08dd70000000000 7:40753a0000000000 8:405d100000000000 9:4069f00000000000 +result __name__=b,host=h1,job=x,zone=z2 ; 1:c087a90000000000 2:c069640000000000 3:c087800000000000 4:408b400000000000 5:407d920000000000 6:c083600000000000 7:c07d3a0000000000 8:c081c90000000000 9:406c940000000000 +result __name__=b,host=h1,job=y,zone=z1 ; 6:4087230000000000 7:c060d40000000000 8:408b6a0000000000 9:40842a0000000000 +end +case limit limitk -1 without job +grid 100000 10000 5 +series __name__=a,host=h1,job=x ; 3:c07e780000000000 4:c07fc80000000000 +series __name__=a,host=h2,job=x ; 1:c052000000000000 +series __name__=a,host=h1,job=y ; 1:c06e580000000000 2:408f0f0000000000 +series __name__=a,host=h1,job=x,zone=z1 ; 1:c07d7e0000000000 2:4063a40000000000 +series __name__=b,host=h1,job=x ; 3:c08bb80000000000 +series __name__=b,host=h1,job=y,zone=z1 ; +end +case limit limit_ratio -0.5 without __name__,zone +grid 100000 10000 8 +series __name__=a,host=h1,job=y,zone=z1 ; 3:c08abd0000000000 4:c08a940000000000 +series __name__=a,host=h1,job=y ; 0:4087470000000000 2:4088490000000000 3:4076f40000000000 +series __name__=b,host=h1,job=y ; 5:4087230000000000 +series __name__=b,host=h2,job=x ; 2:0 4:4045300000000000 +series __name__=b,host=h1,job=y,zone=z1 ; 3:7ff0000000000000 5:408b780000000000 7:c085310000000000 +result __name__=a,host=h1,job=y ; 0:4087470000000000 2:4088490000000000 3:4076f40000000000 +result __name__=b,host=h1,job=y,zone=z1 ; 3:7ff0000000000000 5:408b780000000000 7:c085310000000000 +end +case limit limitk 10 none - +grid 100000 10000 9 +series __name__=a,host=h1,job=x ; 3:c07f000000000000 4:c087f90000000000 5:407d620000000000 6:405f680000000000 7:4082420000000000 +series __name__=a,host=h2,job=x ; 1:406a880000000000 2:40872e0000000000 3:c071880000000000 4:40768c0000000000 +series __name__=a,host=h1,job=y ; 7:c03a800000000000 8:c0851c0000000000 +series __name__=a,host=h1,job=y,zone=z1 ; 7:4025800000000000 +series __name__=b,host=h2,job=x,zone=z1 ; 8:c063d00000000000 +series __name__=b,host=h1,job=x ; 6:c073c20000000000 7:c06aec0000000000 8:c080330000000000 +result __name__=a,host=h1,job=x ; 3:c07f000000000000 4:c087f90000000000 5:407d620000000000 6:405f680000000000 7:4082420000000000 +result __name__=a,host=h1,job=y ; 7:c03a800000000000 8:c0851c0000000000 +result __name__=a,host=h1,job=y,zone=z1 ; 7:4025800000000000 +result __name__=a,host=h2,job=x ; 1:406a880000000000 2:40872e0000000000 3:c071880000000000 4:40768c0000000000 +result __name__=b,host=h1,job=x ; 6:c073c20000000000 7:c06aec0000000000 8:c080330000000000 +result __name__=b,host=h2,job=x,zone=z1 ; 8:c063d00000000000 +end +case limit limit_ratio 0 without host +grid 100000 10000 10 +series __name__=a,host=h2,job=x,zone=z1 ; 8:40820b0000000000 +series __name__=a,host=h2,job=x ; 2:c082b20000000000 +series __name__=a,host=h1,job=x ; +series __name__=b,host=h2,job=x,zone=z1 ; 4:c068080000000000 5:4089be0000000000 +series __name__=b,host=h2,job=y ; 1:407c9e0000000000 2:408a140000000000 3:40848b0000000000 +series __name__=b,host=h2,job=x ; 9:40754e0000000000 +series __name__=b,host=h1,job=y,zone=z1 ; 5:4071720000000000 6:c085190000000000 7:c084510000000000 8:c082160000000000 +end +case limit limitk 10 none - +grid 100000 10000 6 +series __name__=a,host=h1,job=x,zone=z1 ; 3:40854d0000000000 4:4080900000000000 +series __name__=a,host=h2,job=y ; +series __name__=a,host=h1,job=x ; 1:c067280000000000 2:4066d00000000000 +series __name__=b,host=h2,job=y ; 3:0 +series __name__=b,host=h1,job=x ; 0:c07fc80000000000 +result __name__=a,host=h1,job=x ; 1:c067280000000000 2:4066d00000000000 +result __name__=a,host=h1,job=x,zone=z1 ; 3:40854d0000000000 4:4080900000000000 +result __name__=b,host=h1,job=x ; 0:c07fc80000000000 +result __name__=b,host=h2,job=y ; 3:0 +end +case limit limit_ratio -0.3 none - +grid 100000 10000 8 +series __name__=a,host=h2,job=y ; 7:0 +series __name__=a,host=h1,job=y,zone=z1 ; +series __name__=a,host=h2,job=x ; 3:408f0b0000000000 +series __name__=b,host=h1,job=y ; 2:c08dc40000000000 +series __name__=b,host=h2,job=x ; 3:4076540000000000 4:c088df0000000000 5:c059a80000000000 +series __name__=b,host=h2,job=x,zone=z2 ; 4:c08cd70000000000 5:4089ad0000000000 +result __name__=a,host=h2,job=x ; 3:408f0b0000000000 +end +case limit limit_ratio -1 without host,job,zone +grid 100000 10000 6 +series __name__=a,host=h2,job=x ; 5:c08ae60000000000 +series __name__=a,host=h1,job=x,zone=z1 ; 1:c055700000000000 2:0 +series __name__=a,host=h1,job=y ; +series __name__=a,host=h1,job=x ; 2:fff0000000000000 +series __name__=a,host=h2,job=y ; 3:40736a0000000000 +series __name__=b,host=h1,job=x,zone=z1 ; +result __name__=a,host=h1,job=x ; 2:fff0000000000000 +result __name__=a,host=h1,job=x,zone=z1 ; 1:c055700000000000 2:0 +result __name__=a,host=h2,job=x ; 5:c08ae60000000000 +result __name__=a,host=h2,job=y ; 3:40736a0000000000 +end +case limit limit_ratio 1.5 without zone +grid 100000 10000 4 +series __name__=a,host=h2,job=y ; 3:c0857a0000000000 +series __name__=a,host=h1,job=x ; 2:c084060000000000 +series __name__=a,host=h2,job=x ; 3:40854f0000000000 +series __name__=b,host=h2,job=x,zone=z1 ; 1:4076840000000000 2:c06f540000000000 3:0 +series __name__=b,host=h1,job=x ; 0:c08dcc0000000000 1:c07f840000000000 2:404d400000000000 +series __name__=b,host=h2,job=x ; 2:c054900000000000 3:7ff0000000000000 +series __name__=b,host=h1,job=x,zone=z1 ; 1:4086660000000000 2:c08e020000000000 +series __name__=b,host=h2,job=y ; +series __name__=b,host=h2,job=y,zone=z1 ; 3:c083a30000000000 +result __name__=a,host=h1,job=x ; 2:c084060000000000 +result __name__=a,host=h2,job=x ; 3:40854f0000000000 +result __name__=a,host=h2,job=y ; 3:c0857a0000000000 +result __name__=b,host=h1,job=x ; 0:c08dcc0000000000 1:c07f840000000000 2:404d400000000000 +result __name__=b,host=h1,job=x,zone=z1 ; 1:4086660000000000 2:c08e020000000000 +result __name__=b,host=h2,job=x ; 2:c054900000000000 3:7ff0000000000000 +result __name__=b,host=h2,job=x,zone=z1 ; 1:4076840000000000 2:c06f540000000000 3:0 +result __name__=b,host=h2,job=y,zone=z1 ; 3:c083a30000000000 +end +case limit limit_ratio NaN without __name__,job +grid 100000 10000 10 +series __name__=a,host=h1,job=y ; 2:4079280000000000 +series __name__=a,host=h2,job=x,zone=z1 ; 3:c085cc0000000000 4:c08ef40000000000 5:c05d800000000000 6:c08f260000000000 7:c06bfc0000000000 +series __name__=a,host=h2,job=y ; 1:407fec0000000000 2:c069a00000000000 +series __name__=b,host=h2,job=x ; 4:4075d60000000000 5:4070080000000000 +series __name__=b,host=h1,job=x ; 3:c07a480000000000 5:7ff8000000000001 +series __name__=b,host=h1,job=y,zone=z1 ; 1:7e37e43c8800759c 2:c05ab00000000000 3:c0798c0000000000 4:c072160000000000 5:40835a0000000000 +series __name__=b,host=h2,job=x,zone=z1 ; 5:c060d40000000000 6:c085260000000000 7:c071820000000000 +series __name__=b,host=h2,job=y ; 6:4086fa0000000000 7:c063bc0000000000 8:406ab40000000000 9:c086160000000000 +error Ratio value is NaN +end +case limit limit_ratio -1 without __name__,zone +grid 100000 10000 10 +series __name__=a,host=h1,job=y,zone=z1 ; 1:c08a660000000000 2:c082d60000000000 4:c051500000000000 5:406d040000000000 6:7ff8000000000001 7:c0612c0000000000 +series __name__=b,host=h1,job=y,zone=z1 ; 3:0 5:408b220000000000 6:c07e5e0000000000 +series __name__=b,host=h1,job=y ; 1:7e37e43c8800759c 2:4081920000000000 4:407ed40000000000 5:4067000000000000 7:c086ea0000000000 8:c084630000000000 +series __name__=b,host=h1,job=y,zone=z2 ; 0:7ff8000000000001 2:c0856a0000000000 3:c0759e0000000000 4:c074200000000000 5:7ff8000000000001 6:7ff8000000000001 7:c084a10000000000 8:c088830000000000 +result __name__=a,host=h1,job=y,zone=z1 ; 1:c08a660000000000 2:c082d60000000000 4:c051500000000000 5:406d040000000000 6:7ff8000000000001 7:c0612c0000000000 +result __name__=b,host=h1,job=y ; 1:7e37e43c8800759c 2:4081920000000000 4:407ed40000000000 5:4067000000000000 7:c086ea0000000000 8:c084630000000000 +result __name__=b,host=h1,job=y,zone=z1 ; 3:0 5:408b220000000000 6:c07e5e0000000000 +result __name__=b,host=h1,job=y,zone=z2 ; 0:7ff8000000000001 2:c0856a0000000000 3:c0759e0000000000 4:c074200000000000 5:7ff8000000000001 6:7ff8000000000001 7:c084a10000000000 8:c088830000000000 +end +case limit limit_ratio 1.5 by __name__,job +grid 100000 10000 1 +series __name__=a,host=h1,job=x ; 0:407f8a0000000000 +series __name__=a,host=h2,job=x,zone=z1 ; 0:7ff8000000000001 +series __name__=a,host=h1,job=y ; 0:7ff0000000000000 +series __name__=a,host=h2,job=y ; 0:fff0000000000000 +series __name__=b,host=h2,job=x,zone=z1 ; 0:c072320000000000 +series __name__=b,host=h1,job=y,zone=z1 ; +series __name__=b,host=h2,job=y ; 0:c067b80000000000 +result __name__=a,host=h1,job=x ; 0:407f8a0000000000 +result __name__=a,host=h1,job=y ; 0:7ff0000000000000 +result __name__=a,host=h2,job=x,zone=z1 ; 0:7ff8000000000001 +result __name__=a,host=h2,job=y ; 0:fff0000000000000 +result __name__=b,host=h2,job=x,zone=z1 ; 0:c072320000000000 +result __name__=b,host=h2,job=y ; 0:c067b80000000000 +end +case limit limitk 1 none - +grid 100000 10000 12 +series __name__=a,host=h2,job=y,zone=z1 ; 0:403a200000000000 1:408af00000000000 +series __name__=a,host=h2,job=x,zone=z1 ; 8:c066740000000000 +series __name__=b,host=h2,job=x,zone=z1 ; 6:c0633c0000000000 7:40758a0000000000 +series __name__=b,host=h2,job=x,zone=z2 ; +series __name__=b,host=h2,job=y,zone=z1 ; 0:408e220000000000 +series __name__=b,host=h2,job=y ; 0:408ce30000000000 1:c07f5e0000000000 2:c076880000000000 3:7ff0000000000000 4:c08a240000000000 5:fff0000000000000 7:4081450000000000 8:4061400000000000 9:407cec0000000000 +series __name__=b,host=h2,job=x ; 1:c073280000000000 2:4072240000000000 4:c05bc00000000000 5:408ab60000000000 6:40810e0000000000 9:c08cfc0000000000 10:40749e0000000000 +result __name__=a,host=h2,job=x,zone=z1 ; 8:c066740000000000 +result __name__=a,host=h2,job=y,zone=z1 ; 0:403a200000000000 1:408af00000000000 +result __name__=b,host=h2,job=x ; 10:40749e0000000000 +result __name__=b,host=h2,job=x,zone=z1 ; 6:c0633c0000000000 7:40758a0000000000 +result __name__=b,host=h2,job=y ; 2:c076880000000000 3:7ff0000000000000 4:c08a240000000000 5:fff0000000000000 9:407cec0000000000 +end +case limit limitk 1 none - +grid 100000 10000 7 +series __name__=a,host=h2,job=x ; 0:7ff8000000000001 1:4088b50000000000 +series __name__=a,host=h2,job=x,zone=z1 ; 0:408e740000000000 3:407a740000000000 4:7ff8000000000001 5:408f0d0000000000 +series __name__=a,host=h1,job=x,zone=z1 ; 4:7e37e43c8800759c +series __name__=a,host=h1,job=x ; 5:c0815e0000000000 6:40700a0000000000 +series __name__=b,host=h1,job=x,zone=z1 ; 1:c081b20000000000 +series __name__=b,host=h1,job=x ; 1:40724c0000000000 +result __name__=a,host=h1,job=x ; 6:40700a0000000000 +result __name__=a,host=h2,job=x ; 0:7ff8000000000001 1:4088b50000000000 +result __name__=a,host=h2,job=x,zone=z1 ; 3:407a740000000000 4:7ff8000000000001 5:408f0d0000000000 +end diff --git a/src/promql/src/select/tests.rs b/src/promql/src/select/tests.rs index 568ed4c4b5c..0cc2763bf64 100644 --- a/src/promql/src/select/tests.rs +++ b/src/promql/src/select/tests.rs @@ -24,14 +24,16 @@ use datatypes::series::time::{EvalGrid, GridRange, GridSpec, TimeAxis}; use datatypes::series::{Label, MetricName}; use super::{ - Grouping, LabelMatcher, MatchOp, SortOrder, absent, absent_labels, natural_less, scalar, - sort_instant, topk, + Grouping, LabelMatcher, MatchOp, SortOrder, absent, absent_labels, limit_ratio, limitk, + natural_less, sample_offset, scalar, sort_instant, topk, }; use crate::golden::fold_pieces; use crate::grid_series::reference::{ - Dense, Labels, Points, assert_pieces_eq, assert_result_eq, piece_ranges, run_ranges, - sparse_series, + Dense, Labels, OwnedHistogram, Points, assert_pieces_eq, assert_result_eq, piece_ranges, + run_ranges, sparse_series, }; +use crate::grid_series::{GridSample, GridSeriesBuilder}; +use crate::histogram_eval::float_histogram::FloatHistogram; fn schema(points: u64, fields: usize) -> SeriesSchema { SeriesSchema { @@ -117,42 +119,20 @@ fn names(list: &str) -> Vec> { /// header of `prometheus_cases.txt`. #[test] fn select_operators_match_prometheus() { - let text = include_str!("prometheus_cases.txt"); - let mut checked = 0; - for case in text.split("end\n").filter(|case| case.contains("case ")) { - let lines: Vec<&str> = case.lines().filter(|line| !line.starts_with('#')).collect(); - let spec: Vec<&str> = lines[0].split_whitespace().skip(1).collect(); - let points: usize = lines[1].split_whitespace().nth(3).unwrap().parse().unwrap(); - let schema = schema(points as u64, 1); - let mut inputs = Vec::new(); - let mut expected: BTreeMap = BTreeMap::new(); - for line in &lines[2..] { - let (kind, rest) = line.split_once(' ').unwrap(); - let (labels, values) = rest.split_once(" ;").unwrap(); - match kind { - "series" => inputs.push((metric(labels), dense(points, values))), - "result" => { - expected.insert(labels.to_string(), dense(points, values)); - } - other => panic!("unexpected line {other} in {case}"), - } - } - let series: Vec = inputs - .iter() - .filter_map(|(metric, dense)| build(&schema, metric.clone(), dense)) - .collect(); - - let actual: Vec = match spec[0] { + let checked = check_cases( + include_str!("prometheus_cases.txt"), + |spec, schema, series| match spec[0] { "topk" => { - let grouping = match spec[3] { - "without" => Grouping::Without(names(spec[4])), - "by" => Grouping::By(names(spec[4])), - _ => Grouping::By(Vec::new()), - }; let k: f64 = spec[2].parse().unwrap(); - topk(k, spec[1] == "topk", &grouping, &schema, &series).unwrap() + topk( + k, + spec[1] == "topk", + &grouping(spec[3], spec[4]), + schema, + series, + ) } - "scalar" => vec![scalar(&schema, &series).unwrap()], + "scalar" => Ok(vec![scalar(schema, series).unwrap()]), "absent" => { let matchers: Vec = spec .get(1) @@ -189,12 +169,156 @@ fn select_operators_match_prometheus() { }) .cloned() .collect(); - absent(&schema, &selected, absent_labels(&matchers).unwrap()) + Ok(absent(schema, &selected, absent_labels(&matchers).unwrap()) .unwrap() .into_iter() - .collect() + .collect()) } other => panic!("unknown case {other}"), + }, + ); + assert!(checked > 200, "{checked} result series checked"); +} + +/// `limitk` and `limit_ratio` evaluated by the Prometheus v3.15.0 engine; see the header of +/// `limit_cases.txt`. +#[test] +fn limit_operators_match_prometheus() { + let checked = check_cases(include_str!("limit_cases.txt"), |spec, schema, series| { + let param: f64 = spec[2].parse().unwrap(); + match spec[1] { + "limitk" => limitk(param, &grouping(spec[3], spec[4]), schema, series), + _ => limit_ratio(param, schema, series), + } + }); + assert!(checked > 200, "{checked} result series checked"); +} + +/// The labels of the fixtures are short; these need the long length prefix of `stringlabels`. +/// Prometheus' offsets were computed with promql-compat `cmd/narthex-golden-labelhash`. +#[test] +fn sample_offset_hashes_labels_like_prometheus() { + let cases = [ + ("__name__=a,job=x".to_string(), 0x3fe1c2f8b3f15aa8), + ( + format!("__name__=a,long={}", "v".repeat(300)), + 0x3fca51ed01e04434, + ), + ( + format!( + "__name__=a,{}={},zone={}", + "n".repeat(254), + "v".repeat(255), + "z".repeat(70000) + ), + 0x3fc3bf72b8300ba3, + ), + // A Prometheus label set has no empty value, so an empty label is no label. + ("__name__=a,empty=,job=x".to_string(), 0x3fe1c2f8b3f15aa8), + ]; + for (labels, offset) in cases { + assert_eq!(sample_offset(&metric(&labels)).to_bits(), offset); + } +} + +/// `limitk` and `limit_ratio` select series, so histogram samples pass through, and +/// `limit_ratio` keeps every field of the series it keeps. +#[test] +fn limit_operators_keep_histogram_samples() { + let histogram = + OwnedHistogram::parse("0/0/0/3ff0000000000000/3ff0000000000000/0/1_1/3ff0000000000000///"); + let sample = || { + GridSample::Histogram(Box::new( + FloatHistogram::try_from_view(&histogram.view()).unwrap(), + )) + }; + let kinds = |series: &Series, field: usize| -> Vec<(usize, &str)> { + let samples = &series.fields.as_slice()[field]; + series + .time + .positions() + .map(|(point, row)| { + let kind = match samples.sample(row) { + SampleRef::Missing => "missing", + SampleRef::Float(_) => "float", + SampleRef::Histogram(_) => "histogram", + }; + (point, kind) + }) + .collect() + }; + + let two_fields = schema(3, 2); + let mut builder = GridSeriesBuilder::try_new(Arc::new(two_fields.clone())).unwrap(); + builder + .push_samples(0, &[Some(GridSample::Float(1.0)), Some(sample())]) + .unwrap(); + builder.push_samples(2, &[None, Some(sample())]).unwrap(); + let series = builder.finish(metric("__name__=a")).unwrap().unwrap(); + let kept = limit_ratio(1.0, &two_fields, std::slice::from_ref(&series)).unwrap(); + assert_eq!(kept.len(), 1); + assert_eq!(kinds(&kept[0], 0), [(0, "float"), (2, "missing")]); + assert_eq!(kinds(&kept[0], 1), [(0, "histogram"), (2, "histogram")]); + + let one_field = schema(3, 1); + let mut builder = GridSeriesBuilder::try_new(Arc::new(one_field.clone())).unwrap(); + builder.push_samples(1, &[Some(sample())]).unwrap(); + let series = builder.finish(metric("__name__=a")).unwrap().unwrap(); + let kept = limitk(1.0, &Grouping::By(Vec::new()), &one_field, &[series]).unwrap(); + assert_eq!(kept.len(), 1); + assert_eq!(kinds(&kept[0], 0), [(1, "histogram")]); +} + +fn grouping(kind: &str, labels: &str) -> Grouping { + match kind { + "without" => Grouping::Without(names(labels)), + "by" => Grouping::By(names(labels)), + _ => Grouping::By(Vec::new()), + } +} + +/// Evaluates each case of a fixture with `eval` and returns how many result series matched; a +/// case with an `error` line must fail with that message. +fn check_cases( + text: &str, + eval: impl Fn(&[&str], &SeriesSchema, &[Series]) -> Result, DataFusionError>, +) -> usize { + let mut checked = 0; + for case in text.split("end\n").filter(|case| case.contains("case ")) { + let lines: Vec<&str> = case.lines().filter(|line| !line.starts_with('#')).collect(); + let spec: Vec<&str> = lines[0].split_whitespace().skip(1).collect(); + let points: usize = lines[1].split_whitespace().nth(3).unwrap().parse().unwrap(); + let schema = schema(points as u64, 1); + let mut inputs = Vec::new(); + let mut expected: BTreeMap = BTreeMap::new(); + let mut error = None; + for line in &lines[2..] { + if let Some(message) = line.strip_prefix("error ") { + error = Some(message); + continue; + } + let (kind, rest) = line.split_once(' ').unwrap(); + let (labels, values) = rest.split_once(" ;").unwrap(); + match kind { + "series" => inputs.push((metric(labels), dense(points, values))), + "result" => { + expected.insert(labels.to_string(), dense(points, values)); + } + other => panic!("unexpected line {other} in {case}"), + } + } + let series: Vec = inputs + .iter() + .filter_map(|(metric, dense)| build(&schema, metric.clone(), dense)) + .collect(); + + let actual = match (eval(&spec, &schema, &series), error) { + (Ok(actual), None) => actual, + (Err(actual), Some(expected)) => { + assert_eq!(actual.message(), expected, "{case}"); + continue; + } + (actual, expected) => panic!("{case}\ngot {actual:?}, expected error {expected:?}"), }; // A result arrives as one series over the runs of valued positions; the fixtures pin the @@ -213,7 +337,7 @@ fn select_operators_match_prometheus() { checked += 1; } } - assert!(checked > 200, "{checked} result series checked"); + checked } /// The fixture's label rendering: `name=value` pairs in name order, `-` when empty. diff --git a/src/promql/src/source/merge/tests.rs b/src/promql/src/source/merge/tests.rs index 4ad5d17cbb7..853da99e823 100644 --- a/src/promql/src/source/merge/tests.rs +++ b/src/promql/src/source/merge/tests.rs @@ -23,6 +23,7 @@ use std::task::{Context, Poll, Waker}; use api::v1::SemanticType; use common_error::ext::{BoxedError, ErrorExt, PlainError}; use common_error::status_code::StatusCode; +use common_query::series_frame::{pack_raw_series, raw_series_frame_ordering, unpack_raw_series}; use datatypes::arrow::array::Float64Array; use datatypes::arrow::buffer::{Buffer, ScalarBuffer}; use datatypes::data_type::ConcreteDataType; @@ -431,6 +432,67 @@ async fn merges_full_encoded_keys_and_interleaved_chunks_with_global_finality() assert!(series[2].3.last().unwrap()); } +/// Two inputs that each went through the frame codec merge like plain streams: unpacking keeps +/// the declared ordering, and the merge restores global key order and per-key finality across +/// the unpacked frames. +#[tokio::test] +async fn merges_two_unpacked_frames_through_the_frame_codec() { + let rack_a_sensor_1 = read_key("rack-a", "sensor-1"); + let rack_b_sensor_1 = read_key("rack-b", "sensor-1"); + let framed = |labels: RawLabelSource, chunks: Vec| { + let (stream, _, _) = stream( + labels, + SeriesOrdering::KeyThenTime, + chunks.into_iter().map(ready), + ); + let schema = Arc::clone(stream.schema()); + let binding = stream.label_source().clone(); + let packed = pack_raw_series(stream).unwrap(); + let ordering = raw_series_frame_ordering(packed.schema().as_ref()).unwrap(); + assert_eq!(SeriesOrdering::KeyThenTime, ordering); + unpack_raw_series(packed, schema, binding, ordering).unwrap() + }; + let left = framed( + label_source(1), + vec![ + chunk(rack_a_sensor_1.clone(), &[10, 30], &[1.0, 3.0], true), + chunk(rack_b_sensor_1.clone(), &[5], &[5.0], true), + ], + ); + let right = framed( + label_source(2), + vec![chunk(rack_a_sensor_1.clone(), &[20, 40], &[2.0, 4.0], true)], + ); + + let mut merged = merge_ordered_raw_series(vec![left, right]).unwrap(); + assert_eq!(SeriesOrdering::KeyThenTime, merged.ordering()); + let mut series = Vec::<(Buffer, Vec, Vec)>::new(); + while let Some(item) = merged.next().await { + let item = item.unwrap(); + if series + .last() + .is_none_or(|current| current.0 != item.primary_key) + { + series.push((item.primary_key.clone(), Vec::new(), Vec::new())); + } + let current = series.last_mut().unwrap(); + current.1.extend(item.time.as_buffer().to_vec()); + current.2.push(item.is_last); + } + assert_eq!(2, series.len()); + assert_eq!(rack_a_sensor_1, series[0].0); + assert_eq!(vec![10, 20, 30, 40], series[0].1); + assert!(*series[0].2.last().unwrap()); + assert!( + series[0].2[..series[0].2.len() - 1] + .iter() + .all(|last| !last) + ); + assert_eq!(rack_b_sensor_1, series[1].0); + assert_eq!(vec![5], series[1].1); + assert!(*series[1].2.last().unwrap()); +} + #[tokio::test] async fn rejects_equal_timestamp_from_separate_heads() { let key = read_key("rack-a", "sensor-1"); diff --git a/src/promql/src/vector_match.rs b/src/promql/src/vector_match.rs index 5e49080fb87..eab40a3f64e 100644 --- a/src/promql/src/vector_match.rs +++ b/src/promql/src/vector_match.rs @@ -25,12 +25,12 @@ use std::collections::{HashMap, HashSet}; use std::sync::Arc; use datafusion::error::{DataFusionError, Result}; -use datatypes::series::MetricName; use datatypes::series::histogram::{CounterResetHint, HistogramView}; use datatypes::series::samples::{RowCursor, SampleRef, Series, SeriesSchema}; use datatypes::series::time::{EvalGrid, GridRunPair, TimeAxis}; +use datatypes::series::{Label, MetricName}; -use crate::grid_series::{GridSample, GridSeriesBuilder, METADATA_LABELS}; +use crate::grid_series::{GridSample, GridSeriesBuilder, METADATA_LABELS, external, owned}; use crate::histogram_eval::float_histogram::FloatHistogram; #[cfg(test)] @@ -110,8 +110,30 @@ pub struct BinaryMatching { pub return_bool: bool, pub labels: MatchingLabels, pub cardinality: Cardinality, + pub fill: FillValues, } +/// The values of `fill`, `fill_left` and `fill_right`, which stand in for a sample that has no +/// match on the other side. +/// +/// Prometheus applies them after it swaps the operands of `group_right`, so there `fill_left` +/// stands in for a missing right-hand sample and `fill_right` for a missing left-hand one. +#[derive(Clone, Copy, Debug, Default)] +pub struct FillValues { + pub lhs: Option, + pub rhs: Option, +} + +// Compared by bits: a NaN fill equals itself, and `fill(-0)` differs from `fill(0)` (`1 / fill`). +impl PartialEq for FillValues { + fn eq(&self, other: &Self) -> bool { + let bits = |value: Option| value.map(f64::to_bits); + bits(self.lhs) == bits(other.lhs) && bits(self.rhs) == bits(other.rhs) + } +} + +impl Eq for FillValues {} + /// A PromQL set operator between two instant vectors. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum SetOp { @@ -135,7 +157,8 @@ pub fn binary_output_schema(lhs: &SeriesSchema, rhs: &SeriesSchema) -> Result (rhs, lhs, &include[..], true), }; let points = grid.timestamps().len(); - // Prometheus short-circuits a point where either side is empty, before any check. - let both_present: Vec = lhs + let fill = matching.fill; + // Prometheus short-circuits a point where either side is empty, before any check; with a fill + // value only a point where both are empty. + let filling = fill.lhs.is_some() || fill.rhs.is_some(); + let evaluated: Vec = lhs .presence(points) .into_iter() .zip(rhs.presence(points)) - .map(|(lhs, rhs)| lhs && rhs) + .map(|(lhs, rhs)| if filling { lhs || rhs } else { lhs && rhs }) .collect(); let one_groups = group(one, &matching.labels); let mut one_duplicate: Option<(usize, &GroupKey)> = None; for (key, members) in &one_groups { - if let Some(point) = overlap(one, members, |point| both_present[point]) + if let Some(point) = overlap(one, members, |point| evaluated[point]) && one_duplicate.is_none_or(|(first, _)| point < first) { one_duplicate = Some((point, key)); @@ -193,23 +219,23 @@ fn binary_field( } let drop_name = !matching.op.is_comparison() || matching.return_bool; - let mut results = Results::default(); - let group_keys: Vec<&GroupKey> = one_groups.keys().collect(); - let group_ids: HashMap<&GroupKey, usize> = group_keys - .iter() - .enumerate() - .map(|(id, key)| (*key, id)) - .collect(); - // One-to-one: a match group may be matched once per point; Prometheus counts a filtered - // comparison as a match too, but not an operation that failed. - let mut matched: HashSet<(usize, usize)> = HashSet::new(); - let mut implicit_many_to_one: Option<(usize, &GroupKey)> = None; + let mut pairs = Pairs { + matching, + one_is_left, + results: Results::default(), + group_keys: Vec::new(), + group_ids: HashMap::new(), + matched: (matching.cardinality == Cardinality::OneToOne || fill.lhs.is_some()) + .then(HashSet::new), + implicit_many_to_one: None, + }; for (many_index, many_series) in many.series.iter().enumerate() { let key = group_key(&many_series.metric, &matching.labels); - let Some(candidates) = one_groups.get(&key) else { + let candidates = one_groups.get(&key).map_or(&[][..], Vec::as_slice); + if candidates.is_empty() && fill.rhs.is_none() { continue; - }; - let group_id = group_ids[&key]; + } + let group = pairs.group_id(&key); for &one_index in candidates { let metric = result_metric( &many_series.metric, @@ -237,36 +263,65 @@ fn binary_field( .at(pair.other_row + offset) .expect("a row never goes back"), ); - if matches!(many_value, SampleRef::Missing) - || matches!(one_value, SampleRef::Missing) - { - continue; - } - let (lhs_value, rhs_value) = if one_is_left { - (one_value, many_value) - } else { - (many_value, one_value) - }; - let Some((result, keep)) = elem_binop(matching.op, lhs_value, rhs_value)? - else { - continue; - }; - if matching.cardinality == Cardinality::OneToOne - && !matched.insert((group_id, point)) - && implicit_many_to_one.is_none_or(|(first, _)| point < first) - { - implicit_many_to_one = Some((point, group_keys[group_id])); - } - let value = if matching.return_bool { - Some(GridSample::Float(if keep { 1.0 } else { 0.0 })) - } else { - keep.then_some(result) - }; - results.insert(&metric, group_id, point, value); + pairs.apply(group, &metric, point, many_value, one_value)?; + } + } + } + if let Some(value) = fill.rhs { + let metric = result_metric( + &many_series.metric, + &metric_of(&key)?, + matching, + include, + drop_name, + )?; + let mut cursor = many.cursor(many_index); + let mut candidate_cursors: Vec> = + candidates.iter().map(|&index| one.cursor(index)).collect(); + for (point, row) in many.positions(many_index) { + let sample = cursor.at(row).expect("a row never goes back"); + let matched = candidates + .iter() + .zip(&mut candidate_cursors) + .any(|(&index, cursor)| one.present(index, point, cursor)); + if !matched { + pairs.apply(group, &metric, point, sample, SampleRef::Float(value))?; } } } } + // Prometheus fills the many side after every match of the step, for the samples of the one + // side that no operation matched. + if let Some(value) = fill.lhs { + for (one_index, one_series) in one.series.iter().enumerate() { + let key = group_key(&one_series.metric, &matching.labels); + let group = pairs.group_id(&key); + let metric = result_metric( + &metric_of(&key)?, + &one_series.metric, + matching, + include, + drop_name, + )?; + let mut cursor = one.cursor(one_index); + for (point, row) in one.positions(one_index) { + if !pairs + .matched + .as_ref() + .is_some_and(|matched| matched.contains(&(group, point))) + { + let sample = cursor.at(row).expect("a row never goes back"); + pairs.apply(group, &metric, point, SampleRef::Float(value), sample)?; + } + } + } + } + let Pairs { + mut results, + group_keys, + implicit_many_to_one, + .. + } = pairs; // Prometheus fails at the first failing step. Within a step it checks the one side, then // matches, and only after the operator checks that result label sets are unique. if let Some((point, key)) = one_duplicate { @@ -283,7 +338,8 @@ fn binary_field( ), )); } - if let Some((point, key)) = implicit_many_to_one { + if let Some((point, group)) = implicit_many_to_one { + let key = &group_keys[group]; errors.push(( point, 1, @@ -319,6 +375,89 @@ fn binary_field( Ok(results) } +/// The matched pairs of one field and the state Prometheus checks them against. +struct Pairs<'a> { + matching: &'a BinaryMatching, + one_is_left: bool, + results: Results, + group_keys: Vec, + group_ids: HashMap, + /// The match groups an operation matched at a point, kept where Prometheus consults them: + /// one-to-one matching, and filling the many side. A filtered comparison counts as a match, + /// an operation that failed does not. + matched: Option>, + implicit_many_to_one: Option<(usize, usize)>, +} + +impl Pairs<'_> { + /// The id of the match group `key`, which stands for it in `matched` and in the results. + fn group_id(&mut self, key: &GroupKey) -> usize { + if let Some(&id) = self.group_ids.get(key) { + return id; + } + let id = self.group_keys.len(); + self.group_keys.push(key.clone()); + self.group_ids.insert(key.clone(), id); + id + } + + /// Applies the operator to a sample of each side at `point`; a missing sample is no pair. + fn apply( + &mut self, + group: usize, + metric: &MetricName, + point: usize, + many_value: SampleRef<'_>, + one_value: SampleRef<'_>, + ) -> Result<()> { + if matches!(many_value, SampleRef::Missing) || matches!(one_value, SampleRef::Missing) { + return Ok(()); + } + let (lhs_value, rhs_value) = if self.one_is_left { + (one_value, many_value) + } else { + (many_value, one_value) + }; + let Some((result, keep)) = elem_binop(self.matching.op, lhs_value, rhs_value)? else { + return Ok(()); + }; + // One-to-one: a match group may be matched once per point. + if let Some(matched) = &mut self.matched + && !matched.insert((group, point)) + && self.matching.cardinality == Cardinality::OneToOne + && self + .implicit_many_to_one + .is_none_or(|(first, _)| point < first) + { + self.implicit_many_to_one = Some((point, group)); + } + let value = if self.matching.return_bool { + Some(GridSample::Float(if keep { 1.0 } else { 0.0 })) + } else { + keep.then_some(result) + }; + self.results.insert(metric, group, point, value); + Ok(()) + } +} + +/// The labels Prometheus gives a fill sample: the match group of the sample it pairs with. +fn metric_of(key: &GroupKey) -> Result { + let mut name = None; + let mut labels = Vec::with_capacity(key.len()); + for (label, value) in key { + if &**label == "__name__" { + name = Some(value.clone()); + } else { + labels.push(Label { + name: label.clone(), + value: value.clone(), + }); + } + } + MetricName::try_new(name, labels).map_err(external) +} + fn earliest_error(errors: Vec<(usize, u8, String)>) -> Result<()> { match errors .into_iter() @@ -687,17 +826,6 @@ impl<'a> Operand<'a> { } } -/// `sample` as an owned value; `None` when missing. -fn owned(sample: SampleRef<'_>) -> Result> { - Ok(match sample { - SampleRef::Missing => None, - SampleRef::Float(value) => Some(GridSample::Float(value)), - SampleRef::Histogram(view) => Some(GridSample::Histogram(Box::new( - FloatHistogram::try_from_view(&view)?, - ))), - }) -} - /// Prometheus `vectorElemBinop` over present samples: the value and whether to keep it, or /// `None` when Prometheus reports the pair as an annotation and skips it without counting the /// match: an operator the sample types do not support, or an addition or subtraction of diff --git a/src/promql/src/vector_match/fill_cases.txt b/src/promql/src/vector_match/fill_cases.txt new file mode 100644 index 00000000000..3d6213c2c89 --- /dev/null +++ b/src/promql/src/vector_match/fill_cases.txt @@ -0,0 +1,2021 @@ +# Generated by promql-compat cmd/narthex-golden-vector built for amd64 (Prometheus v3.15.0 +# engine, seed 1, 200 cases per kind, -experimental fill). Each case lists its operation, the grid, the +# input series and either the result series or the error. Points are `grid index:hex bits`. +case binary - false ignoring zone one - 1 1 +grid 100000 10000 6 +series __name__=a,host=h2,job=y ; 1:7ff8000000000001 +series __name__=a,host=h1,job=y,zone=z1 ; 5:c079dc0000000000 +series __name__=a,host=h1,job=x,zone=z1 ; 1:4078980000000000 2:406ad00000000000 3:c08b8e0000000000 +series __name__=b,host=h1,job=y,zone=z2 ; 4:c0795c0000000000 5:7ff8000000000001 +result host=h1,job=x ; 1:4078880000000000 2:406ab00000000000 3:c08b960000000000 +result host=h1,job=y ; 4:40796c0000000000 5:7ff8000000000001 +result host=h2,job=y ; 1:7ff8000000000001 +end +case binary <= true on job,zone left - NaN NaN +grid 100000 10000 3 +series __name__=a,host=h1,job=y,zone=z1 ; 2:407a5e0000000000 +series __name__=a,host=h1,job=x,zone=z1 ; 0:c0763e0000000000 1:4075320000000000 +series __name__=a,host=h1,job=x ; 0:c07b560000000000 +series __name__=a,host=h2,job=y ; +series __name__=a,host=h2,job=x ; 0:c08f130000000000 1:4083b60000000000 +series __name__=b,host=h2,job=x,zone=z1 ; 1:407fa60000000000 +series __name__=b,host=h1,job=y,zone=z1 ; 1:4066580000000000 +series __name__=b,host=h2,job=x ; 2:c087190000000000 +series __name__=b,host=h1,job=x,zone=z1 ; 0:4053880000000000 +result host=h1,job=x ; 0:0 +result host=h1,job=x,zone=z1 ; 0:3ff0000000000000 1:3ff0000000000000 +result host=h1,job=y,zone=z1 ; 2:0 +result host=h2,job=x ; 0:0 1:0 +result job=x ; 2:0 +result job=y,zone=z1 ; 1:0 +end +case binary < true on host,zone,__name__ left job Inf Inf +grid 100000 10000 7 +series __name__=a,host=h2,job=x ; 3:c08ba10000000000 5:c0897e0000000000 6:4085ca0000000000 +series __name__=b,host=h2,job=x,zone=z2 ; 5:408c1e0000000000 6:c08e030000000000 +result host=h2 ; 3:3ff0000000000000 5:3ff0000000000000 6:3ff0000000000000 +result host=h2,job=x,zone=z2 ; 5:0 6:0 +end +case binary / false ignoring zone left job - NaN +grid 100000 10000 7 +series __name__=a,host=h1,job=y,zone=z1 ; 3:c08e530000000000 +series __name__=b,host=h2,job=y ; 5:7ff8000000000001 +series __name__=b,host=h1,job=y,zone=z1 ; 4:c08e150000000000 5:c060d40000000000 6:4073e00000000000 +series __name__=b,host=h1,job=y,zone=z2 ; 1:c0659c0000000000 +result host=h1,job=y,zone=z1 ; 3:7ff8000000000001 +end +case binary <= true none - one - - 1 +grid 100000 10000 4 +series __name__=a,host=h2,job=x ; 1:c025400000000000 2:c067180000000000 +series __name__=b,host=h2,job=x ; 3:4086d30000000000 +result host=h2,job=x ; 1:3ff0000000000000 2:3ff0000000000000 +end +case binary - false ignoring job left host,job 0 -2.5 +grid 100000 10000 4 +series __name__=a,host=h1,job=y ; 2:7ff8000000000001 3:4050b80000000000 +series __name__=a,host=h1,job=x,zone=z1 ; 2:4085500000000000 3:4088f60000000000 +series __name__=a,host=h2,job=x,zone=z1 ; 2:408cc80000000000 +series __name__=a,host=h1,job=x ; 1:c071360000000000 +series __name__=b,host=h1,job=y ; 3:c07f7a0000000000 +result host=h1 ; 1:c0710e0000000000 2:7ff8000000000001 +result host=h1,job=y ; 3:4081d40000000000 +result host=h1,zone=z1 ; 2:4085640000000000 3:40890a0000000000 +result host=h2,zone=z1 ; 2:408cdc0000000000 +end +case binary != true none - one - 1 1 +grid 100000 10000 5 +series __name__=a,host=h2,job=x ; 2:7ff8000000000001 +series __name__=a,host=h1,job=x,zone=z1 ; 3:fff0000000000000 +series __name__=a,host=h2,job=x,zone=z1 ; 0:c065ec0000000000 1:4085910000000000 2:7ff0000000000000 3:40883d0000000000 +series __name__=a,host=h1,job=y ; 3:406dc00000000000 +series __name__=b,host=h1,job=x,zone=z1 ; 3:c0708e0000000000 +series __name__=b,host=h2,job=x,zone=z1 ; 0:4083440000000000 +result host=h1,job=x,zone=z1 ; 3:3ff0000000000000 +result host=h1,job=y ; 3:3ff0000000000000 +result host=h2,job=x ; 2:3ff0000000000000 +result host=h2,job=x,zone=z1 ; 0:3ff0000000000000 1:3ff0000000000000 2:3ff0000000000000 3:3ff0000000000000 +end +case binary * false on zone one - Inf Inf +grid 100000 10000 12 +series __name__=a,host=h1,job=y ; 3:408ad50000000000 +series __name__=a,host=h2,job=x,zone=z1 ; 10:4047200000000000 +series __name__=b,host=h1,job=y ; 7:40700c0000000000 8:7ff8000000000001 9:c073260000000000 +result - ; 3:7ff0000000000000 7:7ff0000000000000 8:7ff8000000000001 9:fff0000000000000 +result zone=z1 ; 10:7ff0000000000000 +end +case binary + false ignoring - one - 0 - +grid 100000 10000 3 +series __name__=a,host=h2,job=x ; 1:401a000000000000 +series __name__=a,host=h2,job=y ; 0:c07e840000000000 +series __name__=b,host=h2,job=x ; 2:4051880000000000 +series __name__=b,host=h2,job=y ; 2:c052300000000000 +series __name__=b,host=h2,job=x,zone=z1 ; 1:c06d7c0000000000 +series __name__=b,host=h1,job=y ; 2:c0869d0000000000 +result host=h1,job=y ; 2:c0869d0000000000 +result host=h2,job=x ; 2:4051880000000000 +result host=h2,job=x,zone=z1 ; 1:c06d7c0000000000 +result host=h2,job=y ; 2:c052300000000000 +end +case binary != false on job,zone left - - Inf +grid 100000 10000 1 +series __name__=a,host=h1,job=x,zone=z1 ; 0:4086890000000000 +series __name__=a,host=h2,job=y,zone=z1 ; 0:408bc10000000000 +series __name__=a,host=h2,job=y ; 0:4049700000000000 +series __name__=a,host=h2,job=x ; 0:c08a530000000000 +series __name__=a,host=h1,job=y ; 0:c064dc0000000000 +series __name__=b,host=h2,job=x,zone=z1 ; 0:4054600000000000 +series __name__=b,host=h2,job=y ; +series __name__=b,host=h1,job=x ; 0:c08e020000000000 +series __name__=b,host=h1,job=y ; +series __name__=b,host=h1,job=x,zone=z1 ; 0:408aa10000000000 +error found duplicate series for the match group {job="x", zone="z1"} on the right hand-side of the operation: [{__name__="b", host="h1", job="x", zone="z1"}, {__name__="b", host="h2", job="x", zone="z1"}];many-to-many matching not allowed: matching labels must be unique on one side +end +case binary atan2 false on job right host -2.5 -2.5 +grid 100000 10000 9 +series __name__=a,host=h2,job=x,zone=z1 ; 6:c07a440000000000 7:c0852d0000000000 +series __name__=a,host=h1,job=x ; 1:408bd00000000000 2:c087690000000000 3:4053c00000000000 +series __name__=b,host=h1,job=x,zone=z2 ; 0:0 1:c08aae0000000000 2:4089660000000000 +result host=h1,job=x ; 3:3ff9a38f0b33076c +result host=h1,job=x,zone=z2 ; 1:4002aeeae3b4f170 2:bfe7d474561401df +result host=h2,job=x ; 6:bff93a59110050e1 7:bff93117e1c9f2e5 +result job=x,zone=z2 ; 0:bff921fb54442d18 +end +case binary / false on host,zone one - 1 1 +grid 100000 10000 7 +series __name__=a,host=h2,job=x ; 1:c07bb60000000000 3:7ff0000000000000 4:40835f0000000000 +series __name__=b,host=h2,job=x,zone=z2 ; 0:c087120000000000 1:7e37e43c8800759c 2:40775e0000000000 3:c0839a0000000000 4:407f120000000000 5:c0869e0000000000 +series __name__=b,host=h1,job=x ; 4:c08e610000000000 5:c05c000000000000 6:408c490000000000 +series __name__=b,host=h2,job=x ; 0:c067400000000000 1:7ff8000000000001 2:c0756e0000000000 +result host=h1 ; 4:bf50da929d696f5c 5:bf82492492492492 6:3f5219f333a70c7b +result host=h2 ; 0:bf76058160581606 1:7ff8000000000001 2:bf67e45ff1114409 3:7ff0000000000000 4:40835f0000000000 +result host=h2,zone=z2 ; 0:bf56316a061182fe 1:1a56e1fc2f8f359 2:3f65e93ba8036c71 3:bf5a1ed059c9ec35 4:3f607a8f8b9e6a36 5:bf56a33d1a9deed8 +end +case binary != true ignoring host left host,job,zone -2.5 1 +grid 100000 10000 7 +series __name__=a,host=h2,job=x ; 0:c05d580000000000 1:c08b240000000000 2:4082c00000000000 5:407d1a0000000000 6:408cb50000000000 +series __name__=a,host=h1,job=x ; 3:c0797e0000000000 4:0 5:40744e0000000000 6:40841e0000000000 +series __name__=b,host=h2,job=x,zone=z1 ; 3:7e37e43c8800759c 4:408b010000000000 5:7ff8000000000001 +series __name__=b,host=h1,job=x ; 5:408a370000000000 6:c078900000000000 +error multiple matches for labels: grouping labels must ensure unique matches +end +case binary < false ignoring host,job,zone right job 1 1 +grid 100000 10000 12 +series __name__=a,host=h2,job=y ; 2:7ff8000000000001 4:c07dc00000000000 5:408d360000000000 +series __name__=b,host=h2,job=y ; 10:40843c0000000000 11:4076e20000000000 +result __name__=b,host=h2 ; 10:3ff0000000000000 11:3ff0000000000000 +result job=y ; 4:c07dc00000000000 +end +case binary <= true none - one - - 1 +grid 100000 10000 11 +series __name__=a,host=h2,job=x,zone=z1 ; 2:c0837b0000000000 3:4079ae0000000000 4:0 5:7ff8000000000001 6:4083470000000000 +series __name__=a,host=h1,job=x ; 9:c0802b0000000000 +series __name__=a,host=h2,job=x ; 4:4081dd0000000000 5:408d5e0000000000 6:c0834d0000000000 7:7ff0000000000000 8:408af60000000000 9:c083ec0000000000 10:c07ac00000000000 +series __name__=a,host=h1,job=y,zone=z1 ; 4:4076d80000000000 5:4071320000000000 6:407c8a0000000000 +series __name__=a,host=h1,job=y ; 10:c053180000000000 +series __name__=b,host=h1,job=y,zone=z1 ; +result host=h1,job=x ; 9:3ff0000000000000 +result host=h1,job=y ; 10:3ff0000000000000 +result host=h1,job=y,zone=z1 ; 4:0 5:0 6:0 +result host=h2,job=x ; 4:0 5:0 6:3ff0000000000000 7:0 8:0 9:3ff0000000000000 10:3ff0000000000000 +result host=h2,job=x,zone=z1 ; 2:3ff0000000000000 3:0 4:3ff0000000000000 5:0 6:0 +end +case binary - false ignoring host left host,job -2.5 1 +grid 100000 10000 7 +series __name__=a,host=h2,job=x ; 3:4078760000000000 6:4082720000000000 +series __name__=a,host=h2,job=y ; 5:7ff8000000000001 +series __name__=b,host=h1,job=x ; 5:4087710000000000 6:4071c40000000000 +series __name__=b,host=h2,job=y ; 1:c06ef80000000000 2:c0811b0000000000 +series __name__=b,host=h2,job=x ; 3:c077000000000000 4:c053e80000000000 5:4077a20000000000 6:408b1b0000000000 +error found duplicate series for the match group {job="x"} on the right hand-side of the operation: [{__name__="b", host="h1", job="x"}, {__name__="b", host="h2", job="x"}];many-to-many matching not allowed: matching labels must be unique on one side +end +case binary == true on zone right job 1 -2.5 +grid 100000 10000 7 +series __name__=a,host=h1,job=y ; 2:40831e0000000000 3:7ff8000000000001 4:c084b80000000000 +series __name__=a,host=h2,job=y ; 0:c07aca0000000000 2:c0838f0000000000 4:c0769e0000000000 +series __name__=a,host=h1,job=x ; 1:c084be0000000000 2:0 3:4075260000000000 5:408cf10000000000 6:4081880000000000 +series __name__=a,host=h2,job=x ; 6:7ff8000000000001 +series __name__=a,host=h2,job=x,zone=z1 ; +series __name__=b,host=h2,job=y ; 2:4085920000000000 4:4086270000000000 +series __name__=b,host=h1,job=y,zone=z1 ; 6:c083820000000000 +series __name__=b,host=h1,job=x ; 4:fff0000000000000 5:c0709c0000000000 +error found duplicate series for the match group {} on the left hand-side of the operation: [{__name__="a", host="h1", job="y"}, {__name__="a", host="h2", job="y"}];many-to-many matching not allowed: matching labels must be unique on one side +end +case binary > false none - one - NaN NaN +grid 100000 10000 8 +series __name__=a,host=h1,job=x ; 1:4079ea0000000000 2:c086b40000000000 3:408c0e0000000000 4:407b780000000000 5:405a280000000000 6:c074320000000000 +series __name__=a,host=h2,job=y ; 5:c08c210000000000 7:7e37e43c8800759c +series __name__=a,host=h2,job=x,zone=z1 ; 5:4086300000000000 6:c080970000000000 +series __name__=a,host=h2,job=x ; 5:4080390000000000 7:407d4e0000000000 +series __name__=b,host=h2,job=x,zone=z1 ; 3:c0864b0000000000 5:c014000000000000 +series __name__=b,host=h2,job=y,zone=z1 ; 4:c080180000000000 +series __name__=b,host=h1,job=x ; 1:c07cf60000000000 2:40857f0000000000 4:4080340000000000 +result __name__=a,host=h1,job=x ; 1:4079ea0000000000 +result __name__=a,host=h2,job=x,zone=z1 ; 5:4086300000000000 +end +case binary / false ignoring job,zone right job -2.5 NaN +grid 100000 10000 1 +series __name__=a,host=h2,job=y,zone=z1 ; 0:4086be0000000000 +series __name__=b,host=h2,job=y,zone=z1 ; 0:c0898b0000000000 +series __name__=b,host=h1,job=y ; 0:c072680000000000 +result host=h1 ; 0:7ff8000000000001 +result host=h2,job=y,zone=z1 ; 0:bfec7dbfb7568f5e +end +case binary ^ false on job one - - 1 +grid 100000 10000 8 +series __name__=a,host=h1,job=x ; 2:408a630000000000 4:7ff8000000000001 5:4055200000000000 6:0 +series __name__=b,host=h1,job=x ; 2:c0820b0000000000 3:c079f00000000000 4:c076a20000000000 +series __name__=b,host=h1,job=x,zone=z1 ; 6:fff0000000000000 +series __name__=b,host=h2,job=x ; 1:4063340000000000 +series __name__=b,host=h2,job=x,zone=z1 ; 3:40881b0000000000 +error found duplicate series for the match group {job="x"} on the right hand-side of the operation: [{__name__="b", host="h1", job="x"}, {__name__="b", host="h2", job="x", zone="z1"}];many-to-many matching not allowed: matching labels must be unique on one side +end +case binary > true ignoring zone left job 1 1 +grid 100000 10000 11 +series __name__=a,host=h1,job=y ; 2:7ff8000000000001 3:4087400000000000 4:c083360000000000 5:c07fd40000000000 6:4072c80000000000 7:407ac00000000000 8:40786c0000000000 9:c083620000000000 +series __name__=b,host=h1,job=y ; 7:fff0000000000000 8:4070b00000000000 10:c043e00000000000 +result host=h1,job=y ; 2:0 3:3ff0000000000000 4:0 5:0 6:3ff0000000000000 7:3ff0000000000000 8:3ff0000000000000 9:0 10:3ff0000000000000 +end +case binary * false on zone one - Inf Inf +grid 100000 10000 8 +series __name__=a,host=h2,job=y ; 4:7ff8000000000001 +series __name__=a,host=h1,job=x,zone=z1 ; 1:7ff0000000000000 +series __name__=a,host=h1,job=y,zone=z1 ; 5:4062140000000000 +series __name__=a,host=h1,job=x ; 0:40613c0000000000 1:0 3:4064b80000000000 4:7ff8000000000001 5:408e320000000000 +series __name__=b,host=h1,job=y ; 6:c082940000000000 +error multiple matches for labels: many-to-one matching must be explicit (group_left/group_right) +end +case binary != true on zone one - 1 0 +grid 100000 10000 10 +series __name__=a,host=h2,job=y,zone=z1 ; 5:402e400000000000 6:c0700c0000000000 7:40852b0000000000 8:4058780000000000 +series __name__=a,host=h2,job=x ; 4:c033400000000000 5:4078820000000000 +series __name__=a,host=h1,job=y,zone=z1 ; 2:c083d60000000000 +series __name__=a,host=h2,job=y ; 5:405d680000000000 +series __name__=b,host=h2,job=x ; 8:c043500000000000 +error multiple matches for labels: many-to-one matching must be explicit (group_left/group_right) +end +case binary != true ignoring zone right zone Inf Inf +grid 100000 10000 9 +series __name__=a,host=h2,job=x ; 6:c083e60000000000 +series __name__=b,host=h1,job=x ; 0:c07c740000000000 1:c055e00000000000 2:4050900000000000 4:c075060000000000 6:c03e200000000000 +series __name__=b,host=h2,job=x,zone=z1 ; 2:c03c400000000000 4:405c580000000000 +series __name__=b,host=h2,job=x ; 4:c08a620000000000 6:fff0000000000000 7:c087ff0000000000 +error multiple matches for labels: grouping labels must ensure unique matches +end +case binary + false ignoring job right zone 0 Inf +grid 100000 10000 8 +series __name__=a,host=h2,job=x,zone=z1 ; 1:406cf80000000000 2:4071a40000000000 5:c08a6e0000000000 6:408a5d0000000000 +series __name__=a,host=h1,job=y ; 0:408eb30000000000 1:c085ad0000000000 2:4066e80000000000 3:407ea60000000000 5:c080320000000000 +series __name__=a,host=h2,job=y,zone=z1 ; +series __name__=b,host=h2,job=x ; 6:c07a260000000000 +result host=h1 ; 0:408eb30000000000 1:c085ad0000000000 2:4066e80000000000 3:407ea60000000000 5:c080320000000000 +result host=h2,job=x ; 6:7ff0000000000000 +result host=h2,zone=z1 ; 1:406cf80000000000 2:4071a40000000000 5:c08a6e0000000000 6:408a5d0000000000 +end +case binary / false ignoring job left zone - 1 +grid 100000 10000 7 +series __name__=a,host=h1,job=y,zone=z1 ; 6:7e37e43c8800759c +series __name__=b,host=h1,job=y ; 4:4051700000000000 +series __name__=b,host=h2,job=y ; 6:4080640000000000 +series __name__=b,host=h1,job=y,zone=z1 ; 5:4089570000000000 6:407f2a0000000000 +result host=h1,job=y,zone=z1 ; 6:7da8884c045da7ef +end +case binary ^ false ignoring zone left host NaN NaN +grid 100000 10000 11 +series __name__=a,host=h1,job=y ; 3:c088050000000000 4:c0825e0000000000 5:fff0000000000000 6:4071800000000000 7:4083ee0000000000 8:c0773a0000000000 9:c08e590000000000 +series __name__=a,host=h2,job=x ; 5:407f1a0000000000 7:c076240000000000 8:40754a0000000000 +series __name__=a,host=h1,job=x,zone=z1 ; +series __name__=a,host=h2,job=x,zone=z1 ; 9:c084030000000000 10:408c230000000000 +series __name__=a,host=h1,job=x ; 7:4071e60000000000 8:7ff0000000000000 9:c083990000000000 10:c059a80000000000 +series __name__=b,host=h2,job=x ; 1:40824b0000000000 5:7e37e43c8800759c 6:4078200000000000 7:c029000000000000 8:7ff8000000000001 9:408c9e0000000000 10:4083c80000000000 +series __name__=b,host=h1,job=y ; 3:4076080000000000 +series __name__=b,host=h2,job=y ; 7:40875b0000000000 8:c069d80000000000 9:402f400000000000 +series __name__=b,host=h1,job=x,zone=z1 ; 10:c046200000000000 +result host=h1,job=x ; 7:7ff8000000000001 8:7ff8000000000001 9:7ff8000000000001 10:7ff8000000000001 +result host=h1,job=y ; 3:7ff8000000000001 4:7ff8000000000001 5:7ff8000000000001 6:7ff8000000000001 7:7ff8000000000001 8:7ff8000000000001 9:7ff8000000000001 +result host=h2,job=x ; 1:7ff8000000000001 5:7ff0000000000000 6:7ff8000000000001 7:7ff8000000000001 8:7ff8000000000001 +result host=h2,job=x,zone=z1 ; 9:7ff8000000000001 10:7ff0000000000000 +result host=h2,job=y ; 7:7ff8000000000001 8:7ff8000000000001 9:7ff8000000000001 +end +case binary atan2 false ignoring host,zone one - Inf 0 +grid 100000 10000 10 +series __name__=a,host=h1,job=y ; 1:7ff8000000000001 +series __name__=a,host=h1,job=x ; 3:405e980000000000 4:0 5:c08e6a0000000000 6:4008000000000000 +series __name__=a,host=h2,job=x ; +series __name__=a,host=h2,job=x,zone=z1 ; 7:7ff8000000000001 +series __name__=a,host=h1,job=y,zone=z1 ; 6:40832c0000000000 7:c051b80000000000 +series __name__=b,host=h2,job=x ; 0:4076d20000000000 1:c085f00000000000 2:c070740000000000 +result job=x ; 0:3ff921fb54442d18 1:3ff921fb54442d18 2:3ff921fb54442d18 3:3ff921fb54442d18 4:0 5:bff921fb54442d18 6:3ff921fb54442d18 7:7ff8000000000001 +result job=y ; 1:7ff8000000000001 6:3ff921fb54442d18 7:bff921fb54442d18 +end +case binary atan2 false on job left host Inf Inf +grid 100000 10000 7 +series __name__=a,host=h1,job=y,zone=z1 ; 4:7e37e43c8800759c +series __name__=a,host=h2,job=x ; 6:c0779c0000000000 +series __name__=a,host=h2,job=y ; 2:4084890000000000 3:40683c0000000000 +series __name__=a,host=h1,job=y ; 4:7ff8000000000001 5:404f400000000000 6:c078e40000000000 +series __name__=a,host=h1,job=x ; 3:40759a0000000000 4:40802c0000000000 +series __name__=b,host=h2,job=y ; 2:c0756e0000000000 3:c087bb0000000000 4:7ff8000000000001 5:406f180000000000 +series __name__=b,host=h1,job=x ; 0:405c080000000000 1:c08a590000000000 2:c06ba80000000000 3:408b9d0000000000 4:c085fb0000000000 6:c071a80000000000 +series __name__=b,host=h2,job=x ; +series __name__=b,host=h1,job=y ; +result host=h1,job=x ; 0:3ff921fb54442d18 1:3ff921fb54442d18 2:3ff921fb54442d18 3:3fd7dcc2324c75d2 4:40040ee9aa8e1ec7 6:c001b40f1bbfa192 +result host=h2,job=y ; 2:400069ea22eca411 3:4007220bb590d0da 4:7ff8000000000001 5:3fcf823194bd196f +result host=h2,job=y,zone=z1 ; 4:7ff8000000000001 +result job=y ; 6:8000000000000000 +end +case binary * false on job,zone right host Inf -2.5 +grid 100000 10000 8 +series __name__=a,host=h2,job=y ; 4:406f000000000000 5:40837e0000000000 7:c06bbc0000000000 +series __name__=a,host=h1,job=x ; 7:c06bb80000000000 +series __name__=a,host=h1,job=x,zone=z1 ; 3:7ff8000000000001 4:40713e0000000000 5:4083990000000000 6:408e8a0000000000 +series __name__=b,host=h2,job=y,zone=z1 ; 2:c07a780000000000 3:406a400000000000 4:4056380000000000 +series __name__=b,host=h1,job=x ; 5:7ff8000000000001 6:4084bf0000000000 +series __name__=b,host=h1,job=x,zone=z1 ; 5:c06f5c0000000000 7:7ff8000000000001 +series __name__=b,host=h2,job=y ; 1:c0746e0000000000 2:40870f0000000000 3:4083c60000000000 +result host=h1,job=x ; 7:fff0000000000000 +result host=h1,job=x,zone=z1 ; 3:7ff8000000000001 4:7ff0000000000000 5:c103348fe0000000 6:7ff0000000000000 +result host=h2,job=y ; 4:7ff0000000000000 5:7ff0000000000000 7:fff0000000000000 +result job=x ; 5:7ff8000000000001 6:c099eec000000000 +result job=x,zone=z1 ; 7:7ff8000000000001 +result job=y ; 1:4089898000000000 2:c09cd2c000000000 3:c098b78000000000 +result job=y,zone=z1 ; 2:40908b0000000000 3:c080680000000000 4:c06bc60000000000 +end +case binary % false ignoring host,job,zone right host,zone NaN NaN +grid 100000 10000 12 +series __name__=a,host=h2,job=y ; 1:4085090000000000 2:408b8b0000000000 4:4076ba0000000000 5:c083b20000000000 7:c088480000000000 8:4081130000000000 9:c080580000000000 10:c07dd60000000000 +series __name__=a,host=h2,job=x,zone=z1 ; 5:c08de20000000000 6:7ff8000000000001 7:7ff0000000000000 8:40868c0000000000 9:4081ba0000000000 10:c076ce0000000000 +series __name__=a,host=h1,job=x ; 8:40756c0000000000 +series __name__=b,host=h1,job=x ; 4:c0870e0000000000 +series __name__=b,host=h2,job=x ; 2:c084150000000000 4:c078020000000000 5:c07e220000000000 +error multiple matches for labels: grouping labels must ensure unique matches +end +case binary + false ignoring - right host Inf - +grid 100000 10000 1 +series __name__=a,host=h2,job=y ; 0:c085f60000000000 +series __name__=a,host=h1,job=x ; 0:c067700000000000 +series __name__=b,host=h1,job=y,zone=z1 ; +series __name__=b,host=h1,job=x ; 0:4065d80000000000 +result host=h1,job=x ; 0:c029800000000000 +result host=h2,job=y ; 0:7ff0000000000000 +end +case binary > true on host,job,zone right - 0 - +grid 100000 10000 11 +series __name__=a,host=h1,job=x,zone=z1 ; 2:c0898e0000000000 3:408cb50000000000 4:c056080000000000 5:408e490000000000 6:c042c00000000000 7:c053a80000000000 8:fff0000000000000 +series __name__=a,host=h1,job=x ; 3:c069100000000000 +series __name__=a,host=h1,job=y ; 2:40828b0000000000 3:c0845d0000000000 4:c06ff40000000000 6:c01b800000000000 7:40846f0000000000 8:c0807f0000000000 +series __name__=b,host=h1,job=x,zone=z1 ; 7:4085780000000000 8:c085210000000000 9:c0700a0000000000 +series __name__=b,host=h1,job=x ; 5:c0789e0000000000 7:7ff8000000000001 +series __name__=b,host=h2,job=y ; 8:404d400000000000 +series __name__=b,host=h1,job=x,zone=z2 ; 10:40864f0000000000 +result host=h1,job=x ; 3:0 +result host=h1,job=x,zone=z1 ; 2:0 3:3ff0000000000000 4:0 5:3ff0000000000000 6:0 7:0 8:0 +result host=h1,job=y ; 2:3ff0000000000000 3:0 4:0 6:0 7:3ff0000000000000 8:0 +end +case binary < false ignoring job,zone right - - 1 +grid 100000 10000 4 +series __name__=a,host=h1,job=y,zone=z1 ; 0:4087720000000000 1:4082dd0000000000 2:0 3:c07b700000000000 +series __name__=b,host=h1,job=y,zone=z1 ; 0:c0705a0000000000 1:c0866e0000000000 +end +case binary + false none - one - NaN - +grid 100000 10000 12 +series __name__=a,host=h1,job=x ; 10:407b2c0000000000 +series __name__=a,host=h2,job=y ; 3:c08ea20000000000 +series __name__=a,host=h2,job=x ; 3:c08d240000000000 4:c07db20000000000 6:407a220000000000 7:fff0000000000000 8:4071e60000000000 9:4054b80000000000 +series __name__=b,host=h2,job=y,zone=z1 ; 5:c083b40000000000 6:405c680000000000 7:c087080000000000 8:4085ac0000000000 +series __name__=b,host=h2,job=y ; 7:c08a650000000000 9:c047200000000000 10:4060980000000000 +series __name__=b,host=h2,job=x,zone=z1 ; 9:c07d540000000000 10:4077dc0000000000 +series __name__=b,host=h2,job=y,zone=z2 ; 3:4083d10000000000 4:4057000000000000 5:c03a000000000000 6:4074a20000000000 7:4089040000000000 8:c0861f0000000000 +series __name__=b,host=h1,job=x ; 1:c08b870000000000 +result host=h1,job=x ; 1:7ff8000000000001 +result host=h2,job=x,zone=z1 ; 9:7ff8000000000001 10:7ff8000000000001 +result host=h2,job=y ; 7:7ff8000000000001 9:7ff8000000000001 10:7ff8000000000001 +result host=h2,job=y,zone=z1 ; 5:7ff8000000000001 6:7ff8000000000001 7:7ff8000000000001 8:7ff8000000000001 +result host=h2,job=y,zone=z2 ; 3:7ff8000000000001 4:7ff8000000000001 5:7ff8000000000001 6:7ff8000000000001 7:7ff8000000000001 8:7ff8000000000001 +end +case binary + false on - right host,job,zone NaN - +grid 100000 10000 1 +series __name__=a,host=h2,job=y,zone=z1 ; 0:0 +series __name__=a,host=h2,job=x,zone=z1 ; 0:c066d00000000000 +series __name__=b,host=h2,job=x,zone=z1 ; 0:c071840000000000 +error found duplicate series for the match group {} on the left hand-side of the operation: [{__name__="a", host="h2", job="x", zone="z1"}, {__name__="a", host="h2", job="y", zone="z1"}];many-to-many matching not allowed: matching labels must be unique on one side +end +case binary atan2 false on job one - 0 - +grid 100000 10000 8 +series __name__=a,host=h1,job=x ; 7:c089420000000000 +series __name__=a,host=h2,job=y ; 6:4082390000000000 7:7ff0000000000000 +series __name__=a,host=h2,job=x,zone=z1 ; 0:4073f80000000000 1:4058980000000000 2:c07ad20000000000 3:c083740000000000 4:c085fc0000000000 5:4074740000000000 6:c07a3c0000000000 +series __name__=a,host=h1,job=y ; 0:c087d90000000000 1:c06f380000000000 2:c086380000000000 3:7ff8000000000001 4:c089710000000000 +series __name__=b,host=h2,job=y ; 2:c085140000000000 3:40794a0000000000 4:407d500000000000 6:40862c0000000000 7:4072140000000000 +series __name__=b,host=h2,job=x,zone=z1 ; 2:408a6d0000000000 3:407f0a0000000000 4:406be00000000000 5:4077e40000000000 +series __name__=b,host=h1,job=y ; 5:c05aa80000000000 6:40764a0000000000 +error found duplicate series for the match group {job="y"} on the right hand-side of the operation: [{__name__="b", host="h1", job="y"}, {__name__="b", host="h2", job="y"}];many-to-many matching not allowed: matching labels must be unique on one side +end +case binary > false on host left zone 0 -2.5 +grid 100000 10000 1 +series __name__=a,host=h1,job=x ; 0:7ff0000000000000 +series __name__=a,host=h2,job=y ; 0:40853d0000000000 +series __name__=a,host=h2,job=x ; 0:7e37e43c8800759c +series __name__=b,host=h2,job=y ; 0:c058400000000000 +series __name__=b,host=h2,job=y,zone=z1 ; 0:c08a290000000000 +series __name__=b,host=h2,job=x ; 0:c065f00000000000 +error found duplicate series for the match group {host="h2"} on the right hand-side of the operation: [{__name__="b", host="h2", job="y", zone="z1"}, {__name__="b", host="h2", job="y"}];many-to-many matching not allowed: matching labels must be unique on one side +end +case binary / false on zone left host - 0 +grid 100000 10000 4 +series __name__=a,host=h1,job=y ; 3:c084bb0000000000 +series __name__=a,host=h2,job=x,zone=z1 ; 3:4088690000000000 +series __name__=a,host=h2,job=x ; +series __name__=b,host=h2,job=x,zone=z2 ; 0:4084610000000000 1:c063440000000000 +series __name__=b,host=h2,job=x ; 2:c0619c0000000000 +series __name__=b,host=h1,job=y ; 1:7ff8000000000001 2:c062d00000000000 +error found duplicate series for the match group {} on the right hand-side of the operation: [{__name__="b", host="h1", job="y"}, {__name__="b", host="h2", job="x"}];many-to-many matching not allowed: matching labels must be unique on one side +end +case binary >= true none - one - Inf Inf +grid 100000 10000 12 +series __name__=a,host=h1,job=x ; 1:c085100000000000 2:c0892e0000000000 3:c07b9c0000000000 4:c08eee0000000000 5:fff0000000000000 6:4085140000000000 8:7ff8000000000001 10:c07a040000000000 +series __name__=a,host=h1,job=y,zone=z1 ; +series __name__=b,host=h1,job=x ; 10:c087820000000000 +series __name__=b,host=h2,job=y ; 2:0 3:407ef60000000000 4:4066e80000000000 5:c082300000000000 6:c06d180000000000 7:4063040000000000 8:406e3c0000000000 9:c08b7b0000000000 +series __name__=b,host=h2,job=x ; +result host=h1,job=x ; 1:0 2:0 3:0 4:0 5:0 6:0 8:0 10:3ff0000000000000 +result host=h2,job=y ; 2:3ff0000000000000 3:3ff0000000000000 4:3ff0000000000000 5:3ff0000000000000 6:3ff0000000000000 7:3ff0000000000000 8:3ff0000000000000 9:3ff0000000000000 +end +case binary >= true none - one - 0 - +grid 100000 10000 8 +series __name__=a,host=h1,job=x,zone=z1 ; 4:c081a50000000000 5:40862a0000000000 +series __name__=b,host=h1,job=x,zone=z2 ; 4:408c480000000000 +series __name__=b,host=h1,job=x,zone=z1 ; +result host=h1,job=x,zone=z2 ; 4:0 +end +case binary * false ignoring host left host,zone Inf Inf +grid 100000 10000 5 +series __name__=a,host=h2,job=x ; 1:c062d80000000000 2:4082790000000000 3:4080010000000000 +series __name__=b,host=h2,job=x ; 0:fff0000000000000 3:4084d20000000000 +result host=h2,job=x ; 0:fff0000000000000 3:4114d34d20000000 +result job=x ; 1:fff0000000000000 2:7ff0000000000000 +end +case binary + false on job,zone right - - Inf +grid 100000 10000 1 +series __name__=a,host=h1,job=x ; 0:c074c20000000000 +series __name__=a,host=h2,job=x,zone=z1 ; 0:c0878a0000000000 +series __name__=a,host=h1,job=y ; 0:c0838a0000000000 +series __name__=b,host=h1,job=y ; +series __name__=b,host=h1,job=y,zone=z2 ; +end +case binary == true ignoring zone left job Inf Inf +grid 100000 10000 5 +series __name__=a,host=h2,job=y ; 2:c063e40000000000 4:4075380000000000 +series __name__=a,host=h1,job=x ; 4:c07ea40000000000 +series __name__=a,host=h2,job=x ; +series __name__=a,host=h1,job=y ; 2:408c550000000000 +series __name__=b,host=h1,job=x ; +series __name__=b,host=h2,job=y,zone=z1 ; 0:c087e00000000000 1:7ff0000000000000 2:0 +series __name__=b,host=h2,job=y ; 0:4060940000000000 1:c08b5e0000000000 +series __name__=b,host=h2,job=x ; 2:408c0a0000000000 3:c076120000000000 +error found duplicate series for the match group {host="h2", job="y"} on the right hand-side of the operation: [{__name__="b", host="h2", job="y", zone="z1"}, {__name__="b", host="h2", job="y"}];many-to-many matching not allowed: matching labels must be unique on one side +end +case binary / false ignoring host,job,zone one - -2.5 NaN +grid 100000 10000 11 +series __name__=a,host=h1,job=x ; 10:0 +series __name__=a,host=h1,job=x,zone=z1 ; 4:c08d580000000000 +series __name__=a,host=h2,job=y,zone=z1 ; 8:c06ef40000000000 9:4087360000000000 +series __name__=b,host=h1,job=x,zone=z1 ; 3:c067480000000000 4:7ff8000000000001 5:c07b1a0000000000 6:408e620000000000 7:c060d40000000000 +series __name__=b,host=h2,job=y,zone=z1 ; 8:c08bf80000000000 9:c08e620000000000 +series __name__=b,host=h1,job=x ; 5:4083bb0000000000 6:c084f70000000000 7:c0702c0000000000 8:c086e70000000000 9:c07fc20000000000 10:40840b0000000000 +error found duplicate series for the match group {} on the right hand-side of the operation: [{__name__="b", host="h1", job="x", zone="z1"}, {__name__="b", host="h1", job="x"}];many-to-many matching not allowed: matching labels must be unique on one side +end +case binary * false on job left zone 0 0 +grid 100000 10000 10 +series __name__=a,host=h2,job=x ; +series __name__=a,host=h2,job=x,zone=z1 ; 2:7ff0000000000000 4:c0806f0000000000 5:c081450000000000 +series __name__=a,host=h1,job=y,zone=z1 ; 5:7ff8000000000001 +series __name__=b,host=h2,job=x,zone=z1 ; 0:c069ac0000000000 1:0 2:7ff8000000000001 3:c074c40000000000 4:c064b40000000000 5:404ba00000000000 6:40773a0000000000 7:4074c00000000000 +series __name__=b,host=h2,job=x ; 5:c05f500000000000 +series __name__=b,host=h1,job=y,zone=z1 ; 7:c08b4e0000000000 +error found duplicate series for the match group {job="x"} on the right hand-side of the operation: [{__name__="b", host="h2", job="x", zone="z1"}, {__name__="b", host="h2", job="x"}];many-to-many matching not allowed: matching labels must be unique on one side +end +case binary < true ignoring job right zone - -2.5 +grid 100000 10000 11 +series __name__=a,host=h1,job=y ; 8:7e37e43c8800759c +series __name__=a,host=h2,job=y ; 6:c05e480000000000 7:c084800000000000 +series __name__=a,host=h2,job=y,zone=z1 ; 1:c08c670000000000 2:407bd00000000000 3:0 4:4078840000000000 5:40781a0000000000 6:40849c0000000000 8:408ea10000000000 9:40743e0000000000 10:4075fa0000000000 +series __name__=b,host=h1,job=y ; 7:c08a420000000000 9:405f180000000000 +result host=h1,job=y ; 7:0 9:3ff0000000000000 +end +case binary % false on job left zone NaN NaN +grid 100000 10000 1 +series __name__=a,host=h1,job=y,zone=z1 ; +series __name__=b,host=h2,job=x,zone=z1 ; +end +case binary == true on host left - 0 0 +grid 100000 10000 7 +series __name__=a,host=h1,job=y,zone=z1 ; 6:7ff8000000000001 +series __name__=a,host=h1,job=x ; 3:c07cd00000000000 4:405c100000000000 5:4038200000000000 6:c082350000000000 +series __name__=a,host=h1,job=y ; 3:7ff8000000000001 4:4086b90000000000 5:c05f800000000000 +series __name__=a,host=h2,job=x ; 2:4078320000000000 3:c07b060000000000 +series __name__=b,host=h1,job=y,zone=z1 ; 5:c071e40000000000 6:c089450000000000 +series __name__=b,host=h1,job=y ; +result host=h1,job=x ; 3:0 4:0 5:0 6:0 +result host=h1,job=y ; 3:0 4:0 5:0 +result host=h1,job=y,zone=z1 ; 6:0 +result host=h2,job=x ; 2:0 3:0 +end +case binary >= true none - one - - 0 +grid 100000 10000 1 +series __name__=a,host=h1,job=y,zone=z1 ; 0:c07d920000000000 +series __name__=a,host=h1,job=y ; +series __name__=b,host=h1,job=y ; 0:c086300000000000 +result host=h1,job=y,zone=z1 ; 0:0 +end +case binary * false ignoring - left host,job,zone - 1 +grid 100000 10000 6 +series __name__=a,host=h2,job=y,zone=z1 ; +series __name__=a,host=h2,job=x ; 1:406bc40000000000 2:c076f60000000000 4:408ab90000000000 +series __name__=a,host=h1,job=x ; 1:c085130000000000 +series __name__=a,host=h2,job=y ; 5:4063240000000000 +series __name__=b,host=h2,job=x ; 4:7e37e43c8800759c 5:406be00000000000 +result host=h1,job=x ; 1:c085130000000000 +result host=h2,job=x ; 1:406bc40000000000 2:c076f60000000000 4:7ed3f390ac72a237 +result host=h2,job=y ; 5:4063240000000000 +end +case binary * false none - one - - 1 +grid 100000 10000 11 +series __name__=a,host=h2,job=y ; +series __name__=a,host=h2,job=x,zone=z1 ; +series __name__=b,host=h2,job=x,zone=z1 ; 7:7e37e43c8800759c 10:c07d680000000000 +series __name__=b,host=h2,job=y ; 7:7ff8000000000001 8:405f200000000000 9:4085470000000000 +series __name__=b,host=h2,job=y,zone=z2 ; 2:c063540000000000 3:40754e0000000000 4:4086db0000000000 5:c05ba80000000000 6:c052200000000000 7:4084fd0000000000 8:c059800000000000 9:4083c20000000000 10:c08a990000000000 +end +case binary atan2 false ignoring job,zone,__name__ left - NaN Inf +grid 100000 10000 12 +series __name__=a,host=h1,job=x ; 4:408c340000000000 5:c071ea0000000000 7:c07d800000000000 +series __name__=a,host=h2,job=y ; 11:408ec80000000000 +series __name__=a,host=h2,job=x ; 2:c05a700000000000 3:407b000000000000 5:c078d40000000000 6:c084d50000000000 +series __name__=b,host=h2,job=y ; 6:c05e380000000000 8:c08c200000000000 9:4082a60000000000 +series __name__=b,host=h2,job=y,zone=z1 ; 8:4088d10000000000 10:c0854c0000000000 +series __name__=b,host=h1,job=x ; 10:7ff8000000000001 +error found duplicate series for the match group {host="h2"} on the right hand-side of the operation: [{__name__="b", host="h2", job="y", zone="z1"}, {__name__="b", host="h2", job="y"}];many-to-many matching not allowed: matching labels must be unique on one side +end +case binary < false ignoring zone right zone 0 Inf +grid 100000 10000 9 +series __name__=a,host=h2,job=y ; 5:4086fd0000000000 6:408ced0000000000 8:c07d6e0000000000 +series __name__=a,host=h1,job=x ; 4:407b060000000000 5:c068b40000000000 6:c07bee0000000000 +series __name__=b,host=h2,job=y ; 4:4059c00000000000 5:4083760000000000 +series __name__=b,host=h1,job=x ; 2:c084730000000000 3:408ac60000000000 4:4042d00000000000 5:c08daf0000000000 6:40736c0000000000 7:40785a0000000000 +result __name__=b,host=h1,job=x ; 6:c07bee0000000000 +result host=h2,job=y ; 8:c07d6e0000000000 +end +case binary / false none - one - - Inf +grid 100000 10000 4 +series __name__=a,host=h2,job=x ; 3:c0846b0000000000 +series __name__=a,host=h1,job=y ; 1:fff0000000000000 2:4081260000000000 +series __name__=a,host=h1,job=x ; +series __name__=b,host=h2,job=x ; 3:c052200000000000 +result host=h1,job=y ; 1:fff8000000000000 2:0 +result host=h2,job=x ; 3:4022062de74862de +end +case binary + false ignoring host left host - 1 +grid 100000 10000 2 +series __name__=a,host=h1,job=y ; 0:7e37e43c8800759c 1:c066000000000000 +series __name__=a,host=h2,job=x ; +series __name__=a,host=h1,job=y,zone=z1 ; 1:7ff8000000000001 +series __name__=a,host=h2,job=y ; 0:c06aa80000000000 +series __name__=a,host=h2,job=x,zone=z1 ; 0:408e4e0000000000 +series __name__=b,host=h1,job=y,zone=z1 ; 0:0 +series __name__=b,host=h2,job=x ; 0:c07a6c0000000000 +error multiple matches for labels: grouping labels must ensure unique matches +end +case binary == false ignoring job right host - 1 +grid 100000 10000 6 +series __name__=a,host=h2,job=y,zone=z1 ; 1:c08b920000000000 +series __name__=a,host=h1,job=x,zone=z1 ; 5:4065f80000000000 +series __name__=a,host=h2,job=y ; 2:c081b20000000000 3:7ff0000000000000 4:c08b780000000000 +series __name__=b,host=h1,job=x,zone=z1 ; 5:c053e80000000000 +series __name__=b,host=h1,job=y ; 3:c08cea0000000000 +series __name__=b,host=h2,job=y ; 4:40839f0000000000 +end +case binary == true none - one - 1 0 +grid 100000 10000 8 +series __name__=a,host=h2,job=x,zone=z1 ; +series __name__=b,host=h2,job=x,zone=z1 ; 3:407f220000000000 4:40823d0000000000 5:c077e00000000000 6:406da00000000000 +result host=h2,job=x,zone=z1 ; 3:0 4:0 5:0 6:0 +end +case binary atan2 false on - one - - Inf +grid 100000 10000 3 +series __name__=a,host=h2,job=y ; 0:408f3e0000000000 2:4045100000000000 +series __name__=a,host=h1,job=y ; 2:7ff0000000000000 +series __name__=a,host=h2,job=x ; 0:c07d6c0000000000 1:c083350000000000 2:c07be60000000000 +series __name__=a,host=h2,job=y,zone=z1 ; 1:c088240000000000 +series __name__=b,host=h2,job=x ; 1:c048e00000000000 +error multiple matches for labels: many-to-one matching must be explicit (group_left/group_right) +end +case binary > true ignoring - left job - 0 +grid 100000 10000 6 +series __name__=a,host=h2,job=x ; 3:406e580000000000 +series __name__=b,host=h2,job=x ; 3:c07e2c0000000000 4:4079080000000000 +result host=h2,job=x ; 3:3ff0000000000000 +end +case binary <= false none - one - - 1 +grid 100000 10000 11 +series __name__=a,host=h1,job=x ; 8:408c2c0000000000 9:7ff8000000000001 +series __name__=a,host=h2,job=y,zone=z1 ; 3:c08b430000000000 4:4081930000000000 +series __name__=a,host=h1,job=y ; 6:404a100000000000 7:406b0c0000000000 8:4087650000000000 +series __name__=b,host=h1,job=x ; 1:40805c0000000000 2:c085350000000000 3:c08ab20000000000 +series __name__=b,host=h2,job=x ; 10:c046b00000000000 +series __name__=b,host=h2,job=y,zone=z1 ; 6:c036600000000000 +result __name__=a,host=h2,job=y,zone=z1 ; 3:c08b430000000000 +end +case binary - false on - one - - Inf +grid 100000 10000 4 +series __name__=a,host=h1,job=y ; 2:4033200000000000 +series __name__=a,host=h2,job=x ; 3:4065300000000000 +series __name__=a,host=h1,job=x,zone=z1 ; 1:c079b40000000000 2:4067e40000000000 3:c07d680000000000 +series __name__=a,host=h1,job=y,zone=z1 ; +series __name__=b,host=h2,job=y ; 2:4083dc0000000000 3:405e300000000000 +series __name__=b,host=h1,job=y,zone=z1 ; 0:4061d80000000000 +series __name__=b,host=h2,job=x ; 1:c08a940000000000 2:c0711c0000000000 +error found duplicate series for the match group {} on the right hand-side of the operation: [{__name__="b", host="h2", job="x"}, {__name__="b", host="h2", job="y"}];many-to-many matching not allowed: matching labels must be unique on one side +end +case binary < false none - one - -2.5 - +grid 100000 10000 5 +series __name__=a,host=h2,job=x ; 2:c08bc10000000000 +series __name__=a,host=h1,job=x,zone=z1 ; 0:c085410000000000 +series __name__=a,host=h2,job=x,zone=z1 ; 4:c078c20000000000 +series __name__=a,host=h2,job=y ; 0:c0879b0000000000 1:c07b4a0000000000 2:4085d90000000000 +series __name__=b,host=h2,job=x ; +series __name__=b,host=h1,job=x,zone=z1 ; 2:4045000000000000 +result host=h1,job=x,zone=z1 ; 2:c004000000000000 +end +case binary - false on host,zone left job 0 - +grid 100000 10000 11 +series __name__=a,host=h1,job=x ; 7:c083f60000000000 8:408b790000000000 9:c08b180000000000 +series __name__=a,host=h2,job=x,zone=z1 ; 1:4081690000000000 2:c071360000000000 +series __name__=a,host=h2,job=y,zone=z1 ; 1:4074e00000000000 2:c080a20000000000 3:c087250000000000 4:c082b40000000000 5:7ff8000000000001 7:4031600000000000 8:406a1c0000000000 9:c066f80000000000 +series __name__=b,host=h1,job=y,zone=z1 ; 0:7ff8000000000001 +series __name__=b,host=h2,job=x,zone=z1 ; 4:c07bd60000000000 6:405c500000000000 +series __name__=b,host=h2,job=y ; 4:c06cd80000000000 5:4079080000000000 6:c08e010000000000 7:c087b80000000000 +result host=h1,job=y,zone=z1 ; 0:7ff8000000000001 +result host=h2,job=x,zone=z1 ; 4:c063240000000000 6:c05c500000000000 +result host=h2,job=y ; 4:406cd80000000000 5:c079080000000000 6:408e010000000000 7:4087b80000000000 +end +case binary + false ignoring job,zone right - - 0 +grid 100000 10000 7 +series __name__=a,host=h1,job=x ; 3:7ff8000000000001 +series __name__=a,host=h2,job=x,zone=z1 ; 0:0 1:40734a0000000000 2:c082680000000000 3:c051d00000000000 +series __name__=a,host=h2,job=y,zone=z1 ; 4:7ff8000000000001 +series __name__=a,host=h1,job=y,zone=z1 ; 4:c082710000000000 +series __name__=a,host=h1,job=x,zone=z1 ; 2:408e860000000000 4:4084ef0000000000 +series __name__=b,host=h1,job=x,zone=z1 ; 1:c07d940000000000 2:c0848b0000000000 +series __name__=b,host=h1,job=y,zone=z1 ; 2:c063c80000000000 3:7e37e43c8800759c +error found duplicate series for the match group {host="h1"} on the left hand-side of the operation: [{__name__="a", host="h1", job="x", zone="z1"}, {__name__="a", host="h1", job="y", zone="z1"}];many-to-many matching not allowed: matching labels must be unique on one side +end +case binary - false on host one - NaN - +grid 100000 10000 1 +series __name__=a,host=h2,job=x,zone=z1 ; 0:0 +series __name__=a,host=h2,job=y,zone=z1 ; 0:4081920000000000 +series __name__=b,host=h2,job=y,zone=z1 ; 0:c07cac0000000000 +series __name__=b,host=h1,job=y ; 0:c03da00000000000 +error multiple matches for labels: many-to-one matching must be explicit (group_left/group_right) +end +case binary * false on zone left - - -2.5 +grid 100000 10000 7 +series __name__=a,host=h1,job=y,zone=z1 ; 3:c081bd0000000000 4:fff0000000000000 5:c06e340000000000 +series __name__=a,host=h1,job=x ; 1:c080030000000000 3:c051780000000000 +series __name__=b,host=h1,job=y,zone=z1 ; 1:4083a50000000000 +result host=h1,job=x ; 1:409403c000000000 3:4065d60000000000 +result host=h1,job=y,zone=z1 ; 3:40962c4000000000 4:7ff0000000000000 5:4082e08000000000 +end +case binary / false on host left - Inf 0 +grid 100000 10000 8 +series __name__=a,host=h1,job=x ; 5:7ff8000000000001 6:c088ee0000000000 +series __name__=a,host=h1,job=y ; 1:40756a0000000000 +series __name__=a,host=h2,job=y ; 6:7ff8000000000001 7:c08e8b0000000000 +series __name__=a,host=h2,job=x ; 2:40679c0000000000 3:408b9b0000000000 6:407e900000000000 +series __name__=a,host=h1,job=x,zone=z1 ; 7:7e37e43c8800759c +series __name__=b,host=h1,job=x ; 1:4075760000000000 2:4080c00000000000 3:4047700000000000 +series __name__=b,host=h1,job=x,zone=z1 ; 7:c03e600000000000 +series __name__=b,host=h1,job=y,zone=z2 ; 1:c08e910000000000 +error found duplicate series for the match group {host="h1"} on the right hand-side of the operation: [{__name__="b", host="h1", job="x"}, {__name__="b", host="h1", job="y", zone="z2"}];many-to-many matching not allowed: matching labels must be unique on one side +end +case binary > false ignoring host,job,zone right - - -2.5 +grid 100000 10000 12 +series __name__=a,host=h2,job=x,zone=z1 ; 0:4068d80000000000 2:c06c540000000000 3:c08f070000000000 +series __name__=a,host=h2,job=x ; 0:407e180000000000 3:c040800000000000 4:c0813c0000000000 +series __name__=b,host=h2,job=x ; 7:4062340000000000 8:408a2d0000000000 9:4064d00000000000 +series __name__=b,host=h2,job=y ; 5:c062340000000000 7:c069900000000000 8:c0735e0000000000 +series __name__=b,host=h2,job=x,zone=z1 ; 5:7ff8000000000001 6:4077b00000000000 7:7ff8000000000001 8:c054480000000000 9:c080050000000000 10:c082160000000000 +error found duplicate series for the match group {} on the left hand-side of the operation: [{__name__="a", host="h2", job="x", zone="z1"}, {__name__="a", host="h2", job="x"}];many-to-many matching not allowed: matching labels must be unique on one side +end +case binary != false on host,job,zone one - 1 - +grid 100000 10000 10 +series __name__=a,host=h2,job=y,zone=z1 ; 3:0 4:c085c60000000000 5:406e540000000000 6:c058b00000000000 7:c043a00000000000 8:7e37e43c8800759c 9:4079fc0000000000 +series __name__=b,host=h1,job=y,zone=z1 ; 4:c0730a0000000000 5:408add0000000000 7:c07bc00000000000 +series __name__=b,host=h2,job=y,zone=z1 ; 2:7ff8000000000001 3:c077b20000000000 4:4084a40000000000 5:408dfc0000000000 6:7ff8000000000001 8:c069300000000000 +series __name__=b,host=h2,job=y ; 9:405fb00000000000 +result host=h1,job=y,zone=z1 ; 4:3ff0000000000000 5:3ff0000000000000 7:3ff0000000000000 +result host=h2,job=y ; 9:3ff0000000000000 +result host=h2,job=y,zone=z1 ; 2:3ff0000000000000 3:0 4:c085c60000000000 5:406e540000000000 6:c058b00000000000 8:7e37e43c8800759c +end +case binary == true ignoring - right - - 1 +grid 100000 10000 8 +series __name__=a,host=h2,job=y ; +series __name__=b,host=h2,job=x ; 1:40800c0000000000 2:40756e0000000000 +series __name__=b,host=h2,job=y ; 2:407a100000000000 5:c08efe0000000000 6:4051a00000000000 7:40750e0000000000 +result host=h2,job=x ; 1:0 2:0 +result host=h2,job=y ; 2:0 5:0 6:0 7:0 +end +case binary * false ignoring job one - Inf Inf +grid 100000 10000 10 +series __name__=a,host=h1,job=x ; 4:407e500000000000 5:4084930000000000 6:7ff8000000000001 7:c082e40000000000 8:0 9:c083b30000000000 +series __name__=a,host=h2,job=x ; 6:c086a90000000000 7:7ff8000000000001 +series __name__=a,host=h2,job=x,zone=z1 ; 7:7e37e43c8800759c 9:404c200000000000 +series __name__=a,host=h1,job=y ; 8:c081960000000000 9:c084c90000000000 +series __name__=a,host=h1,job=x,zone=z1 ; 4:408f260000000000 5:40818f0000000000 6:c071600000000000 +series __name__=b,host=h1,job=x ; 6:c08e400000000000 +series __name__=b,host=h2,job=x ; 7:c0743c0000000000 8:40725c0000000000 +series __name__=b,host=h2,job=y ; 7:408d9c0000000000 8:c088c20000000000 +error found duplicate series for the match group {host="h2"} on the right hand-side of the operation: [{__name__="b", host="h2", job="x"}, {__name__="b", host="h2", job="y"}];many-to-many matching not allowed: matching labels must be unique on one side +end +case binary + false on job,zone left host 1 - +grid 100000 10000 7 +series __name__=a,host=h2,job=x ; 6:405fb00000000000 +series __name__=a,host=h1,job=x ; 3:c089590000000000 4:4081250000000000 5:7ff8000000000001 +series __name__=b,host=h1,job=x,zone=z1 ; 3:4070220000000000 4:c085a90000000000 +series __name__=b,host=h2,job=x ; 0:c06cd80000000000 1:c087f90000000000 2:4081ae0000000000 3:c05ca00000000000 4:408ca50000000000 5:c082a30000000000 6:c080c00000000000 +series __name__=b,host=h1,job=x ; 4:4061500000000000 5:c075460000000000 +error found duplicate series for the match group {job="x"} on the right hand-side of the operation: [{__name__="b", host="h1", job="x"}, {__name__="b", host="h2", job="x"}];many-to-many matching not allowed: matching labels must be unique on one side +end +case binary <= true ignoring job right job Inf - +grid 100000 10000 10 +series __name__=a,host=h2,job=x ; 5:c08c940000000000 6:c080ca0000000000 7:40844f0000000000 8:4079d80000000000 9:7ff8000000000001 +series __name__=a,host=h2,job=y ; 3:7e37e43c8800759c 4:405dd00000000000 5:4073b40000000000 +series __name__=a,host=h1,job=x ; 5:4076b20000000000 7:c081800000000000 8:c069f00000000000 9:40806f0000000000 +series __name__=a,host=h1,job=y,zone=z1 ; +series __name__=a,host=h1,job=y ; 0:7ff0000000000000 1:c070a20000000000 4:408a740000000000 5:4052980000000000 6:0 +series __name__=b,host=h2,job=y,zone=z2 ; 8:4080d70000000000 +series __name__=b,host=h2,job=y ; 4:c078020000000000 6:40859b0000000000 8:c058d00000000000 9:408b000000000000 +series __name__=b,host=h2,job=x,zone=z1 ; +error found duplicate series for the match group {host="h2"} on the left hand-side of the operation: [{__name__="a", host="h2", job="x"}, {__name__="a", host="h2", job="y"}];many-to-many matching not allowed: matching labels must be unique on one side +end +case binary / false ignoring zone,__name__ right host,job - 0 +grid 100000 10000 6 +series __name__=a,host=h1,job=x,zone=z1 ; 5:407f040000000000 +series __name__=a,host=h2,job=x,zone=z1 ; +series __name__=a,host=h2,job=y ; 2:c076c20000000000 3:7e37e43c8800759c 4:c08c8b0000000000 5:c082a30000000000 +series __name__=a,host=h2,job=x ; +series __name__=b,host=h1,job=x ; 5:c077520000000000 +series __name__=b,host=h2,job=x,zone=z1 ; 3:40746c0000000000 4:0 5:4076260000000000 +result host=h1,job=x ; 5:bff5479c8412de20 +result host=h2,job=x,zone=z1 ; 3:0 4:fff8000000000000 5:0 +end +case binary == false none - one - - 0 +grid 100000 10000 4 +series __name__=a,host=h2,job=x ; 0:c076a40000000000 +series __name__=a,host=h1,job=x,zone=z1 ; 3:4082b80000000000 +series __name__=a,host=h2,job=x,zone=z1 ; 1:fff0000000000000 +series __name__=b,host=h2,job=x ; 1:c084d00000000000 2:7e37e43c8800759c +series __name__=b,host=h1,job=x,zone=z1 ; 0:4051c80000000000 1:40831f0000000000 +end +case binary / false ignoring host one - NaN 0 +grid 100000 10000 8 +series __name__=a,host=h1,job=x,zone=z1 ; 3:c04f800000000000 +series __name__=a,host=h1,job=x ; 2:7ff8000000000001 3:c081100000000000 4:c068f00000000000 6:c061f80000000000 7:c07ada0000000000 +series __name__=a,host=h2,job=y ; 5:c048a00000000000 +series __name__=b,host=h1,job=x ; 3:4080f40000000000 +series __name__=b,host=h1,job=x,zone=z1 ; 2:40820e0000000000 3:7ff0000000000000 6:4070be0000000000 +series __name__=b,host=h2,job=y ; 4:7ff8000000000001 +series __name__=b,host=h2,job=x ; +result job=x ; 2:7ff8000000000001 3:bff01a6d01a6d01a 4:fff0000000000000 6:fff0000000000000 7:fff0000000000000 +result job=x,zone=z1 ; 2:7ff8000000000001 3:8000000000000000 6:7ff8000000000001 +result job=y ; 4:7ff8000000000001 5:fff0000000000000 +end +case binary - false ignoring job,zone left job,zone 1 - +grid 100000 10000 8 +series __name__=a,host=h2,job=x,zone=z1 ; 7:408a990000000000 +series __name__=a,host=h1,job=x ; 4:40826d0000000000 5:4079e00000000000 6:c0884f0000000000 +series __name__=a,host=h1,job=y,zone=z1 ; 7:c083520000000000 +series __name__=b,host=h1,job=x ; 2:c07e100000000000 3:4089290000000000 +series __name__=b,host=h2,job=x,zone=z1 ; 0:c08bea0000000000 1:c0879e0000000000 +result host=h1,job=x ; 2:407e200000000000 3:c089210000000000 +result host=h2,job=x,zone=z1 ; 0:408bf20000000000 1:4087a60000000000 +end +case binary <= true ignoring - left - - 0 +grid 100000 10000 2 +series __name__=a,host=h2,job=x ; +series __name__=a,host=h1,job=y,zone=z1 ; 1:c08d500000000000 +series __name__=a,host=h2,job=x,zone=z1 ; 0:4087300000000000 1:7ff0000000000000 +series __name__=a,host=h2,job=y,zone=z1 ; 0:4079660000000000 +series __name__=b,host=h2,job=x ; +result host=h1,job=y,zone=z1 ; 1:3ff0000000000000 +result host=h2,job=x,zone=z1 ; 0:0 1:0 +result host=h2,job=y,zone=z1 ; 0:0 +end +case binary atan2 false ignoring host,zone left - - 0 +grid 100000 10000 10 +series __name__=a,host=h2,job=x ; 3:408ced0000000000 4:c08e850000000000 6:408c2a0000000000 7:40661c0000000000 8:4080b30000000000 9:40803f0000000000 +series __name__=a,host=h2,job=x,zone=z1 ; 5:0 7:4080880000000000 8:40787a0000000000 9:c089a60000000000 +series __name__=b,host=h2,job=x,zone=z1 ; 9:7e37e43c8800759c +series __name__=b,host=h1,job=x,zone=z1 ; 8:c089d10000000000 +series __name__=b,host=h2,job=x ; 8:408a550000000000 +error found duplicate series for the match group {job="x"} on the right hand-side of the operation: [{__name__="b", host="h1", job="x", zone="z1"}, {__name__="b", host="h2", job="x"}];many-to-many matching not allowed: matching labels must be unique on one side +end +case binary < false ignoring host left job,zone 1 0 +grid 100000 10000 2 +series __name__=a,host=h1,job=y ; 0:c0871e0000000000 +series __name__=b,host=h1,job=y ; 0:7e37e43c8800759c 1:c061e00000000000 +series __name__=b,host=h2,job=x,zone=z1 ; 0:c080ae0000000000 +result __name__=a,host=h1,job=y ; 0:c0871e0000000000 +end +case binary >= false ignoring host,job right host,zone NaN 1 +grid 100000 10000 6 +series __name__=a,host=h1,job=y,zone=z1 ; 2:c083350000000000 +series __name__=a,host=h2,job=x,zone=z1 ; 3:c065f80000000000 4:c066500000000000 5:408f3d0000000000 +series __name__=a,host=h1,job=x ; 4:c067a80000000000 +series __name__=a,host=h2,job=y ; 2:4072720000000000 +series __name__=b,host=h1,job=x ; 4:405fd00000000000 +series __name__=b,host=h2,job=x,zone=z1 ; 2:c088f70000000000 3:c08ed80000000000 +result __name__=b,host=h1,job=x,zone=z1 ; 2:c083350000000000 +result __name__=b,host=h2,job=x,zone=z1 ; 3:c065f80000000000 +end +case binary % false ignoring - left host,job,zone - 1 +grid 100000 10000 5 +series __name__=a,host=h2,job=x ; 0:c071020000000000 1:404ac00000000000 +series __name__=a,host=h1,job=y ; 2:7e37e43c8800759c +series __name__=a,host=h2,job=y,zone=z1 ; 2:c054980000000000 3:c067580000000000 4:c031a00000000000 +series __name__=a,host=h1,job=y,zone=z1 ; +series __name__=b,host=h1,job=y,zone=z2 ; 3:c081480000000000 +series __name__=b,host=h1,job=y ; 0:c08efb0000000000 +series __name__=b,host=h2,job=x ; 3:0 4:4048000000000000 +result host=h1,job=y ; 2:0 +result host=h2,job=x ; 0:bfc0000000000000 1:3fe0000000000000 +result host=h2,job=y,zone=z1 ; 2:bfd8000000000000 3:bfe8000000000000 4:bfe4000000000000 +end +case binary != true ignoring host,zone right job 1 1 +grid 100000 10000 8 +series __name__=a,host=h1,job=y ; 7:4071e00000000000 +series __name__=a,host=h1,job=x,zone=z1 ; 6:c0673c0000000000 7:c087720000000000 +series __name__=a,host=h1,job=x ; 5:4061c00000000000 6:7ff8000000000001 +series __name__=b,host=h1,job=x,zone=z1 ; 3:c075fa0000000000 +error found duplicate series for the match group {job="x"} on the left hand-side of the operation: [{__name__="a", host="h1", job="x", zone="z1"}, {__name__="a", host="h1", job="x"}];many-to-many matching not allowed: matching labels must be unique on one side +end +case binary > true on - left host 0 -2.5 +grid 100000 10000 7 +series __name__=a,host=h1,job=x ; 2:7ff0000000000000 +series __name__=a,host=h2,job=x ; 1:4089df0000000000 2:c066240000000000 3:4069580000000000 +series __name__=b,host=h2,job=x,zone=z1 ; 1:c0759e0000000000 2:c07ab40000000000 3:c05ef80000000000 +series __name__=b,host=h2,job=x ; 1:c021c00000000000 +series __name__=b,host=h1,job=y,zone=z1 ; 5:c080d60000000000 6:c080cd0000000000 +error found duplicate series for the match group {} on the right hand-side of the operation: [{__name__="b", host="h2", job="x", zone="z1"}, {__name__="b", host="h2", job="x"}];many-to-many matching not allowed: matching labels must be unique on one side +end +case binary >= true on job one - Inf - +grid 100000 10000 2 +series __name__=a,host=h2,job=y ; 1:c0893b0000000000 +series __name__=a,host=h2,job=y,zone=z1 ; 0:408c210000000000 +series __name__=a,host=h1,job=x ; 1:0 +series __name__=b,host=h2,job=y ; +series __name__=b,host=h2,job=y,zone=z1 ; 0:c065b00000000000 +series __name__=b,host=h1,job=x ; 0:c05ed80000000000 +result job=x ; 0:3ff0000000000000 +result job=y ; 0:3ff0000000000000 +end +case binary > true ignoring - left - 1 NaN +grid 100000 10000 7 +series __name__=a,host=h2,job=y ; 4:4057380000000000 +series __name__=a,host=h2,job=x ; 1:4089620000000000 +series __name__=b,host=h2,job=x,zone=z1 ; +series __name__=b,host=h2,job=x ; 2:c080570000000000 3:c083d20000000000 +series __name__=b,host=h2,job=y ; 1:4065f80000000000 3:c071ea0000000000 +series __name__=b,host=h1,job=x,zone=z1 ; 4:c030600000000000 5:c086ee0000000000 6:c051800000000000 +result host=h1,job=x,zone=z1 ; 4:3ff0000000000000 5:3ff0000000000000 6:3ff0000000000000 +result host=h2,job=x ; 1:0 2:3ff0000000000000 3:3ff0000000000000 +result host=h2,job=y ; 1:0 3:3ff0000000000000 4:0 +end +case binary atan2 false ignoring host right zone 1 1 +grid 100000 10000 11 +series __name__=a,host=h1,job=x ; 3:7ff0000000000000 4:4032800000000000 5:c084db0000000000 7:4045000000000000 8:c07eee0000000000 +series __name__=a,host=h1,job=y ; 0:408e680000000000 1:c074960000000000 2:7ff8000000000001 3:408e350000000000 5:c078da0000000000 7:c080c20000000000 8:7ff8000000000001 9:407c220000000000 +series __name__=a,host=h2,job=x ; 2:c089610000000000 4:406fa00000000000 6:c069700000000000 7:c075120000000000 8:406e980000000000 9:c082ff0000000000 10:c055480000000000 +series __name__=b,host=h2,job=x ; 2:4053600000000000 +series __name__=b,host=h1,job=x ; 3:4077600000000000 4:4087d20000000000 5:40664c0000000000 6:7ff8000000000001 7:407aae0000000000 8:c05d080000000000 9:4086a70000000000 10:c07e420000000000 +series __name__=b,host=h1,job=y,zone=z1 ; 7:c088c50000000000 8:c08e090000000000 +error found duplicate series for the match group {job="x"} on the left hand-side of the operation: [{__name__="a", host="h1", job="x"}, {__name__="a", host="h2", job="x"}];many-to-many matching not allowed: matching labels must be unique on one side +end +case binary >= true on host right zone NaN - +grid 100000 10000 8 +series __name__=a,host=h1,job=x ; 4:c07afe0000000000 5:4083ab0000000000 +series __name__=b,host=h1,job=x ; 2:c089f00000000000 +series __name__=b,host=h2,job=y ; 0:7ff0000000000000 1:4085c60000000000 +result host=h1 ; 4:0 5:0 +end +case binary >= false on host,job left zone - 1 +grid 100000 10000 3 +series __name__=a,host=h2,job=y ; +series __name__=a,host=h1,job=y ; 1:408aab0000000000 2:4087870000000000 +series __name__=b,host=h1,job=y ; 2:4083f80000000000 +series __name__=b,host=h2,job=y ; 1:7ff8000000000001 +result __name__=a,host=h1,job=y ; 1:408aab0000000000 2:4087870000000000 +end +case binary + false none - one - NaN - +grid 100000 10000 10 +series __name__=a,host=h2,job=y ; 4:4088500000000000 5:c08c5a0000000000 6:c07e140000000000 8:4008000000000000 +series __name__=a,host=h2,job=x ; 6:4084e10000000000 +series __name__=a,host=h1,job=x ; 1:7ff8000000000001 2:406a440000000000 3:7ff0000000000000 4:c080da0000000000 5:4084a00000000000 6:408c0d0000000000 7:c06c800000000000 +series __name__=b,host=h1,job=y ; 3:407aba0000000000 +series __name__=b,host=h2,job=y,zone=z1 ; 6:c08cbe0000000000 7:404cb00000000000 +series __name__=b,host=h2,job=y ; 9:4089120000000000 +result host=h1,job=y ; 3:7ff8000000000001 +result host=h2,job=y ; 9:7ff8000000000001 +result host=h2,job=y,zone=z1 ; 6:7ff8000000000001 7:7ff8000000000001 +end +case binary / false ignoring job right host,job NaN NaN +grid 100000 10000 11 +series __name__=a,host=h1,job=x,zone=z1 ; +series __name__=a,host=h1,job=y ; 7:c082570000000000 +series __name__=a,host=h2,job=x,zone=z1 ; 6:4057b00000000000 8:c086f30000000000 9:408e4a0000000000 +series __name__=b,host=h1,job=x,zone=z1 ; 4:c08ee70000000000 5:c08da00000000000 6:4084cc0000000000 7:4053e00000000000 +result host=h1,job=y ; 7:7ff8000000000001 +result host=h1,zone=z1 ; 4:7ff8000000000001 5:7ff8000000000001 6:7ff8000000000001 7:7ff8000000000001 +result host=h2,job=x,zone=z1 ; 6:7ff8000000000001 8:7ff8000000000001 9:7ff8000000000001 +end +case binary % false on host,job right - - 1 +grid 100000 10000 2 +series __name__=a,host=h1,job=x,zone=z1 ; 0:7ff8000000000001 1:c0854c0000000000 +series __name__=a,host=h1,job=y,zone=z1 ; +series __name__=a,host=h1,job=x ; 1:c08ae10000000000 +series __name__=a,host=h2,job=y ; 1:c032400000000000 +series __name__=a,host=h2,job=x,zone=z1 ; 1:407b320000000000 +series __name__=b,host=h1,job=x,zone=z2 ; +series __name__=b,host=h2,job=x,zone=z2 ; 0:c082480000000000 +series __name__=b,host=h1,job=x ; 1:fff0000000000000 +series __name__=b,host=h1,job=y,zone=z1 ; 1:40750a0000000000 +error found duplicate series for the match group {host="h1", job="x"} on the left hand-side of the operation: [{__name__="a", host="h1", job="x", zone="z1"}, {__name__="a", host="h1", job="x"}];many-to-many matching not allowed: matching labels must be unique on one side +end +case binary ^ false on host,__name__ one - Inf Inf +grid 100000 10000 11 +series __name__=a,host=h1,job=y,zone=z1 ; 6:406b7c0000000000 7:408ca10000000000 +series __name__=b,host=h1,job=x ; 8:c07f700000000000 9:7e37e43c8800759c +result host=h1 ; 6:7ff0000000000000 7:7ff0000000000000 8:0 9:7ff0000000000000 +end +case binary ^ false ignoring host,job,__name__ right host - -2.5 +grid 100000 10000 2 +series __name__=a,host=h2,job=y ; 1:c076740000000000 +series __name__=a,host=h2,job=x ; 0:4077d20000000000 +series __name__=a,host=h1,job=x,zone=z1 ; 1:40872e0000000000 +series __name__=b,host=h2,job=x,zone=z1 ; 1:408ba60000000000 +series __name__=b,host=h1,job=x,zone=z1 ; 0:4081050000000000 1:408e5c0000000000 +series __name__=b,host=h2,job=y ; 0:408c050000000000 +error multiple matches for labels: grouping labels must ensure unique matches +end +case binary - false none - one - NaN - +grid 100000 10000 5 +series __name__=a,host=h2,job=x,zone=z1 ; 3:c08b7a0000000000 +series __name__=a,host=h1,job=y,zone=z1 ; 1:c083a50000000000 2:408ccf0000000000 +series __name__=a,host=h1,job=x ; +series __name__=b,host=h1,job=y,zone=z1 ; 1:c08c510000000000 2:c085420000000000 3:4072a40000000000 +series __name__=b,host=h2,job=x ; 3:7ff8000000000001 +result host=h1,job=y,zone=z1 ; 1:4071580000000000 2:4099088000000000 3:7ff8000000000001 +result host=h2,job=x ; 3:7ff8000000000001 +end +case binary - false ignoring zone right - 1 -2.5 +grid 100000 10000 1 +series __name__=a,host=h1,job=x,zone=z1 ; 0:c08b460000000000 +series __name__=b,host=h1,job=x,zone=z1 ; 0:4071560000000000 +series __name__=b,host=h1,job=x ; 0:7e37e43c8800759c +result host=h1,job=x ; 0:fe37e43c8800759c +result host=h1,job=x,zone=z1 ; 0:c091f88000000000 +end +case binary < true on zone one - -2.5 - +grid 100000 10000 7 +series __name__=a,host=h2,job=y ; 2:7ff0000000000000 +series __name__=a,host=h1,job=y ; 0:c083860000000000 1:407b200000000000 2:40813f0000000000 3:c08c720000000000 +series __name__=a,host=h1,job=x,zone=z1 ; 1:c086b30000000000 2:c075040000000000 3:406e600000000000 +series __name__=a,host=h2,job=x,zone=z1 ; 4:40886f0000000000 5:7ff8000000000001 +series __name__=a,host=h1,job=y,zone=z1 ; 6:c08aec0000000000 +series __name__=a,host=h1,job=x ; 6:c081630000000000 +series __name__=b,host=h1,job=y ; 6:4087e80000000000 +series __name__=b,host=h1,job=x,zone=z1 ; 5:c057e80000000000 +series __name__=b,host=h1,job=x ; 1:4076e40000000000 +series __name__=b,host=h2,job=x,zone=z1 ; 3:4064f80000000000 4:c08dda0000000000 +result - ; 1:0 6:3ff0000000000000 +result zone=z1 ; 3:0 4:0 5:0 +end +case binary / false on job left zone Inf - +grid 100000 10000 10 +series __name__=a,host=h2,job=y ; 7:408cad0000000000 +series __name__=a,host=h2,job=x ; 3:4035600000000000 4:40889f0000000000 5:4071880000000000 6:c0829f0000000000 7:406ae80000000000 +series __name__=a,host=h1,job=y ; 8:7e37e43c8800759c +series __name__=b,host=h1,job=y ; 0:406f600000000000 1:c07d7e0000000000 2:c08cd50000000000 4:4084450000000000 5:407f080000000000 7:7ff8000000000001 8:4070940000000000 9:c087f40000000000 +series __name__=b,host=h2,job=x ; 4:c071080000000000 7:c07bf40000000000 8:c0698c0000000000 +result host=h1,job=y ; 8:7db70ef246f0470b +result host=h2,job=x ; 4:c0072159b7a99216 7:bfdecd335f28c856 +result host=h2,job=y ; 7:7ff8000000000001 +result job=x ; 8:fff0000000000000 +result job=y ; 0:7ff0000000000000 1:fff0000000000000 2:fff0000000000000 4:7ff0000000000000 5:7ff0000000000000 9:fff0000000000000 +end +case binary <= false on job,zone right - - Inf +grid 100000 10000 8 +series __name__=a,host=h2,job=x ; 5:40844e0000000000 +series __name__=a,host=h1,job=y,zone=z1 ; 3:c072a40000000000 4:4064a00000000000 5:40797e0000000000 6:c086d30000000000 +series __name__=b,host=h1,job=y,zone=z1 ; 5:c0805a0000000000 +series __name__=b,host=h2,job=x,zone=z1 ; 5:c076a80000000000 6:4088a20000000000 7:40833b0000000000 +end +case binary > false on - one - - NaN +grid 100000 10000 9 +series __name__=a,host=h2,job=x,zone=z1 ; 7:40669c0000000000 8:c06aa00000000000 +series __name__=b,host=h2,job=x ; 2:c064000000000000 3:c07ba00000000000 4:c0847f0000000000 +end +case binary - false ignoring job one - - 0 +grid 100000 10000 6 +series __name__=a,host=h1,job=x ; 0:c07f360000000000 +series __name__=a,host=h2,job=y ; +series __name__=b,host=h1,job=x ; 0:7ff0000000000000 +series __name__=b,host=h2,job=x,zone=z1 ; 4:c07d7e0000000000 5:4063a40000000000 +result host=h1 ; 0:fff0000000000000 +end +case binary atan2 false none - one - 0 - +grid 100000 10000 1 +series __name__=a,host=h1,job=x ; 0:4083940000000000 +series __name__=a,host=h2,job=y,zone=z1 ; 0:40870d0000000000 +series __name__=b,host=h2,job=y,zone=z1 ; +end +case binary atan2 false ignoring zone left job -2.5 -2.5 +grid 100000 10000 2 +series __name__=a,host=h2,job=x,zone=z1 ; 1:408cf40000000000 +series __name__=a,host=h1,job=x ; 0:407d0c0000000000 +series __name__=a,host=h2,job=y,zone=z1 ; 0:4045300000000000 +series __name__=a,host=h2,job=x ; 0:7ff8000000000001 +series __name__=b,host=h1,job=x ; 1:c07d480000000000 +result host=h1,job=x ; 0:3ff93803d00c19aa 1:c009170da933f265 +result host=h2,job=x ; 0:7ff8000000000001 +result host=h2,job=x,zone=z1 ; 1:3ff92d08b928356f +result host=h2,job=y,zone=z1 ; 0:3ffa135a985b725c +end +case binary * false ignoring host,__name__ one - NaN NaN +grid 100000 10000 6 +series __name__=a,host=h2,job=x ; 0:4050a80000000000 1:c059a00000000000 +series __name__=a,host=h1,job=y ; 0:4079920000000000 2:4056400000000000 3:c08bb40000000000 4:4086850000000000 5:c06b7c0000000000 +series __name__=a,host=h2,job=y,zone=z1 ; 5:4080d80000000000 +series __name__=b,host=h2,job=y,zone=z1 ; 1:40768c0000000000 2:4085530000000000 3:7e37e43c8800759c 4:c0851c0000000000 +result job=x ; 0:7ff8000000000001 1:7ff8000000000001 +result job=y ; 0:7ff8000000000001 2:7ff8000000000001 3:7ff8000000000001 4:7ff8000000000001 5:7ff8000000000001 +result job=y,zone=z1 ; 1:7ff8000000000001 2:7ff8000000000001 3:7ff8000000000001 4:7ff8000000000001 5:7ff8000000000001 +end +case binary * false ignoring host,job,zone left zone NaN 0 +grid 100000 10000 9 +series __name__=a,host=h1,job=y,zone=z1 ; 6:4088140000000000 7:c07fcc0000000000 +series __name__=b,host=h1,job=x ; 7:407db60000000000 +series __name__=b,host=h1,job=y,zone=z1 ; 6:c07d0c0000000000 +series __name__=b,host=h1,job=y ; 5:40831e0000000000 +series __name__=b,host=h1,job=x,zone=z1 ; 8:c08d650000000000 +result - ; 5:7ff8000000000001 +result host=h1,job=y ; 7:c10d85b840000000 +result host=h1,job=y,zone=z1 ; 6:c115db2780000000 +result zone=z1 ; 8:7ff8000000000001 +end +case binary - false on - left zone Inf 1 +grid 100000 10000 1 +series __name__=a,host=h2,job=y ; +series __name__=a,host=h1,job=x ; +series __name__=b,host=h1,job=x,zone=z1 ; 0:c05af00000000000 +series __name__=b,host=h1,job=x ; +series __name__=b,host=h2,job=y,zone=z2 ; 0:40819b0000000000 +series __name__=b,host=h1,job=x,zone=z2 ; +error found duplicate series for the match group {} on the right hand-side of the operation: [{__name__="b", host="h1", job="x", zone="z1"}, {__name__="b", host="h2", job="y", zone="z2"}];many-to-many matching not allowed: matching labels must be unique on one side +end +case binary + false ignoring host,zone left host Inf Inf +grid 100000 10000 4 +series __name__=a,host=h2,job=y,zone=z1 ; 0:c07d1e0000000000 1:404a600000000000 2:40833b0000000000 +series __name__=a,host=h1,job=y ; 1:40790a0000000000 3:c07b740000000000 +series __name__=a,host=h1,job=x ; 1:408a390000000000 +series __name__=b,host=h1,job=y,zone=z2 ; 0:c055f80000000000 1:404d200000000000 +result host=h1,job=y ; 1:407cae0000000000 +result host=h1,job=y,zone=z1 ; 0:c0814e0000000000 1:405bc00000000000 +result job=x ; 1:7ff0000000000000 +result job=y ; 3:7ff0000000000000 +result job=y,zone=z1 ; 2:7ff0000000000000 +end +case binary ^ false ignoring host,job left host Inf Inf +grid 100000 10000 8 +series __name__=a,host=h1,job=y ; 1:408eaf0000000000 2:c082bf0000000000 4:c084230000000000 +series __name__=b,host=h1,job=y,zone=z2 ; 4:408da20000000000 5:c039400000000000 +series __name__=b,host=h2,job=x ; +series __name__=b,host=h1,job=y ; 4:c06dac0000000000 5:40749e0000000000 6:c008000000000000 7:40897d0000000000 +series __name__=b,host=h1,job=x,zone=z1 ; 4:c08cd70000000000 5:4089ad0000000000 +result host=h1 ; 5:7ff0000000000000 6:0 7:7ff0000000000000 +result host=h1,job=y ; 4:7ff8000000000001 +result host=h1,zone=z1 ; 4:0 5:7ff0000000000000 +result host=h1,zone=z2 ; 4:7ff0000000000000 5:0 +result job=y ; 1:7ff0000000000000 2:7ff0000000000000 +end +case binary >= true ignoring job left job,zone NaN -2.5 +grid 100000 10000 5 +series __name__=a,host=h2,job=y,zone=z1 ; 4:c054a00000000000 +series __name__=a,host=h2,job=y ; 4:0 +series __name__=a,host=h1,job=y ; 1:c08a4c0000000000 2:c08ec30000000000 +series __name__=a,host=h1,job=x ; +series __name__=a,host=h1,job=y,zone=z1 ; 2:4068380000000000 +series __name__=a,host=h2,job=x ; 2:406dcc0000000000 +series __name__=b,host=h1,job=x ; 1:c049c00000000000 2:c0700c0000000000 3:4086740000000000 4:c084060000000000 +series __name__=b,host=h2,job=x ; 2:c021400000000000 3:404d200000000000 4:7ff8000000000001 +result host=h1,job=x ; 1:0 2:0 3:0 4:0 +result host=h1,zone=z1 ; 2:3ff0000000000000 +result host=h2,job=x ; 2:3ff0000000000000 3:0 4:0 +result host=h2,zone=z1 ; 4:0 +end +case binary - false ignoring job right zone 0 1 +grid 100000 10000 12 +series __name__=a,host=h1,job=x ; 3:c081730000000000 4:c06c480000000000 5:40804b0000000000 6:4076540000000000 7:c08d830000000000 8:406aa80000000000 9:c086930000000000 10:c06f980000000000 11:c08d840000000000 +series __name__=a,host=h2,job=y,zone=z1 ; 7:4034600000000000 10:c0689c0000000000 +series __name__=a,host=h2,job=y ; 7:c083a30000000000 9:4022c00000000000 10:c089150000000000 +series __name__=a,host=h2,job=x,zone=z1 ; +series __name__=a,host=h1,job=y ; 4:408b130000000000 5:c07ff80000000000 6:408bd20000000000 7:c040100000000000 8:7e37e43c8800759c +series __name__=b,host=h2,job=x,zone=z1 ; 3:4086080000000000 4:4052b00000000000 5:c089030000000000 6:c072660000000000 +series __name__=b,host=h1,job=x,zone=z2 ; 2:40823a0000000000 +series __name__=b,host=h1,job=x,zone=z1 ; +series __name__=b,host=h2,job=y,zone=z1 ; 9:4084130000000000 10:c089ac0000000000 +error found duplicate series for the match group {host="h1"} on the left hand-side of the operation: [{__name__="a", host="h1", job="x"}, {__name__="a", host="h1", job="y"}];many-to-many matching not allowed: matching labels must be unique on one side +end +case binary * false on job right host - Inf +grid 100000 10000 2 +series __name__=a,host=h2,job=x ; 0:c078860000000000 +series __name__=a,host=h2,job=x,zone=z1 ; 1:4078ba0000000000 +series __name__=a,host=h1,job=x,zone=z1 ; 0:407f460000000000 1:c071d60000000000 +series __name__=a,host=h1,job=x ; 1:4081040000000000 +series __name__=a,host=h2,job=y ; 0:4089140000000000 1:c08cd50000000000 +series __name__=b,host=h2,job=y ; +series __name__=b,host=h2,job=x ; 1:7e37e43c8800759c +series __name__=b,host=h1,job=y ; 0:408ece0000000000 +error found duplicate series for the match group {job="x"} on the left hand-side of the operation: [{__name__="a", host="h1", job="x", zone="z1"}, {__name__="a", host="h2", job="x"}];many-to-many matching not allowed: matching labels must be unique on one side +end +case binary >= true ignoring host,job left zone -2.5 - +grid 100000 10000 6 +series __name__=a,host=h2,job=y ; 1:c081f30000000000 +series __name__=b,host=h2,job=y,zone=z1 ; 3:407ed40000000000 4:4067000000000000 +result zone=z1 ; 3:0 4:0 +end +case binary == false on host,zone right job -2.5 - +grid 100000 10000 9 +series __name__=a,host=h1,job=y ; 2:408ccc0000000000 5:c0856a0000000000 6:c0759e0000000000 7:c074200000000000 8:7ff8000000000001 +series __name__=b,host=h1,job=y ; 2:c088bd0000000000 3:4060900000000000 4:40849e0000000000 5:404c600000000000 +series __name__=b,host=h1,job=y,zone=z1 ; 0:7ff8000000000001 1:c07f700000000000 3:7ff0000000000000 4:4028c00000000000 5:c08d9f0000000000 7:4081e50000000000 +end +case binary < false on job left - -2.5 - +grid 100000 10000 1 +series __name__=a,host=h2,job=y,zone=z1 ; 0:407b800000000000 +series __name__=a,host=h2,job=x,zone=z1 ; 0:c04b800000000000 +series __name__=b,host=h2,job=y ; +series __name__=b,host=h2,job=x,zone=z1 ; 0:408bc40000000000 +result __name__=a,host=h2,job=x,zone=z1 ; 0:c04b800000000000 +end +case binary <= true ignoring zone,__name__ right host,zone - 0 +grid 100000 10000 10 +series __name__=a,host=h2,job=y,zone=z1 ; +series __name__=b,host=h2,job=y,zone=z1 ; 9:c06bd40000000000 +result host=h2,job=y ; 9:0 +end +case binary > false on zone one - 0 - +grid 100000 10000 2 +series __name__=a,host=h2,job=x,zone=z1 ; 0:407cec0000000000 1:0 +series __name__=b,host=h2,job=x,zone=z1 ; 0:40828d0000000000 1:fff0000000000000 +result zone=z1 ; 1:0 +end +case binary > false on host,__name__ right job,zone 0 0 +grid 100000 10000 11 +series __name__=a,host=h1,job=y,zone=z1 ; 7:4080ff0000000000 8:404ba00000000000 +series __name__=a,host=h2,job=x,zone=z1 ; +series __name__=b,host=h1,job=y ; 8:fff0000000000000 +series __name__=b,host=h1,job=y,zone=z1 ; 6:4083800000000000 8:408f0d0000000000 9:4050380000000000 +series __name__=b,host=h2,job=x ; 9:4089a40000000000 10:7e37e43c8800759c +error multiple matches for labels: grouping labels must ensure unique matches +end +case binary atan2 false ignoring host,job right - NaN NaN +grid 100000 10000 11 +series __name__=a,host=h2,job=y ; 7:405fe80000000000 9:4073e80000000000 +series __name__=a,host=h1,job=x,zone=z1 ; 9:7ff8000000000001 +series __name__=a,host=h1,job=x ; 9:c080f60000000000 +series __name__=a,host=h2,job=x,zone=z1 ; 5:4089700000000000 7:c07d800000000000 8:fff0000000000000 +series __name__=b,host=h2,job=y ; 10:c0876d0000000000 +error found duplicate series for the match group {} on the left hand-side of the operation: [{__name__="a", host="h1", job="x"}, {__name__="a", host="h2", job="y"}];many-to-many matching not allowed: matching labels must be unique on one side +end +case binary atan2 false none - one - Inf - +grid 100000 10000 1 +series __name__=a,host=h1,job=x ; +series __name__=a,host=h1,job=y,zone=z1 ; 0:7ff8000000000001 +series __name__=b,host=h1,job=x ; +series __name__=b,host=h1,job=y,zone=z2 ; 0:c020400000000000 +series __name__=b,host=h2,job=y ; 0:c08e720000000000 +series __name__=b,host=h2,job=x ; 0:c067100000000000 +result host=h1,job=y,zone=z2 ; 0:3ff921fb54442d18 +result host=h2,job=x ; 0:3ff921fb54442d18 +result host=h2,job=y ; 0:3ff921fb54442d18 +end +case binary * false on - left host,job,zone NaN - +grid 100000 10000 2 +series __name__=a,host=h2,job=x,zone=z1 ; 0:406cc00000000000 1:fff0000000000000 +series __name__=a,host=h1,job=x ; +series __name__=a,host=h1,job=x,zone=z1 ; +series __name__=a,host=h2,job=y ; 1:c07b620000000000 +series __name__=b,host=h1,job=y ; +end +case binary / false none - one - -2.5 - +grid 100000 10000 7 +series __name__=a,host=h1,job=y,zone=z1 ; 5:408b300000000000 +series __name__=a,host=h2,job=x ; 1:408e830000000000 2:c081390000000000 +series __name__=a,host=h1,job=x ; 3:c07fda0000000000 4:4089050000000000 5:407f580000000000 +series __name__=a,host=h1,job=y ; 5:c078900000000000 +series __name__=b,host=h1,job=x ; +series __name__=b,host=h1,job=y,zone=z1 ; 0:c07e560000000000 +series __name__=b,host=h2,job=x ; 1:406e040000000000 3:c07ab40000000000 +series __name__=b,host=h2,job=y ; 6:c07f680000000000 +result host=h1,job=y,zone=z1 ; 0:3f7518dae1dee769 +result host=h2,job=x ; 1:401043b2b4f8efe0 3:3f77f79c8936602b +result host=h2,job=y ; 6:3f7460cbc7f5cf9a +end +case binary == false on zone one - - NaN +grid 100000 10000 2 +series __name__=a,host=h1,job=x,zone=z1 ; 0:408c160000000000 1:40748e0000000000 +series __name__=b,host=h1,job=y,zone=z1 ; 1:4048d00000000000 +series __name__=b,host=h1,job=x,zone=z1 ; 0:4088790000000000 1:4084db0000000000 +error found duplicate series for the match group {zone="z1"} on the right hand-side of the operation: [{__name__="b", host="h1", job="x", zone="z1"}, {__name__="b", host="h1", job="y", zone="z1"}];many-to-many matching not allowed: matching labels must be unique on one side +end +case binary - false on zone one - - -2.5 +grid 100000 10000 8 +series __name__=a,host=h1,job=x,zone=z1 ; 7:c065ec0000000000 +series __name__=a,host=h1,job=y ; 2:4082c30000000000 3:4073fa0000000000 4:c083b30000000000 +series __name__=a,host=h2,job=y ; 5:c073ac0000000000 +series __name__=b,host=h2,job=y ; 7:4076c60000000000 +series __name__=b,host=h1,job=y ; 5:c08bac0000000000 +series __name__=b,host=h1,job=y,zone=z2 ; 6:40816b0000000000 7:c085440000000000 +series __name__=b,host=h1,job=x,zone=z1 ; 4:40896b0000000000 +result - ; 2:4082d70000000000 3:4074220000000000 4:c0839f0000000000 5:4081d60000000000 +result zone=z1 ; 7:c0659c0000000000 +end +case binary > true ignoring - right job - Inf +grid 100000 10000 6 +series __name__=a,host=h2,job=x ; +series __name__=b,host=h2,job=x,zone=z1 ; 3:403ae00000000000 4:c08a2b0000000000 +series __name__=b,host=h2,job=y ; 5:c086880000000000 +result host=h2,job=x,zone=z1 ; 3:3ff0000000000000 4:3ff0000000000000 +result host=h2,job=y ; 5:3ff0000000000000 +end +case binary < false none - one - Inf Inf +grid 100000 10000 6 +series __name__=a,host=h1,job=y,zone=z1 ; 3:c07ba80000000000 4:407c2e0000000000 5:4071340000000000 +series __name__=a,host=h2,job=y,zone=z1 ; 1:406ac80000000000 2:c0783e0000000000 3:4085fb0000000000 4:c0661c0000000000 +series __name__=a,host=h2,job=x ; 1:c082670000000000 +series __name__=b,host=h1,job=y,zone=z2 ; 5:407e0c0000000000 +series __name__=b,host=h1,job=y,zone=z1 ; 3:40738a0000000000 +result __name__=a,host=h1,job=y,zone=z1 ; 3:c07ba80000000000 4:407c2e0000000000 5:4071340000000000 +result __name__=a,host=h2,job=x ; 1:c082670000000000 +result __name__=a,host=h2,job=y,zone=z1 ; 1:406ac80000000000 2:c0783e0000000000 3:4085fb0000000000 4:c0661c0000000000 +end +case binary != false ignoring zone one - 0 NaN +grid 100000 10000 5 +series __name__=a,host=h1,job=x,zone=z1 ; 2:c084d10000000000 3:7ff8000000000001 +series __name__=a,host=h1,job=x ; 1:c08d200000000000 2:4052c80000000000 3:c08d1c0000000000 +series __name__=a,host=h2,job=x ; 3:c084800000000000 4:4085df0000000000 +series __name__=a,host=h2,job=x,zone=z1 ; 1:4080070000000000 2:7ff8000000000001 +series __name__=b,host=h2,job=x,zone=z1 ; 4:c08e880000000000 +series __name__=b,host=h2,job=x ; 3:7ff8000000000001 +series __name__=b,host=h1,job=x,zone=z2 ; 3:c083d40000000000 +series __name__=b,host=h2,job=x,zone=z2 ; 2:c0833b0000000000 3:4082820000000000 +error multiple matches for labels: many-to-one matching must be explicit (group_left/group_right) +end +case binary * false ignoring host,job right job,zone 1 0 +grid 100000 10000 1 +series __name__=a,host=h1,job=x,zone=z1 ; +series __name__=a,host=h1,job=y,zone=z1 ; 0:7e37e43c8800759c +series __name__=a,host=h1,job=x ; 0:c082640000000000 +series __name__=a,host=h2,job=y ; 0:4075fc0000000000 +series __name__=b,host=h2,job=y,zone=z1 ; 0:c058700000000000 +series __name__=b,host=h2,job=y ; 0:7ff8000000000001 +series __name__=b,host=h1,job=x ; 0:4069b40000000000 +error found duplicate series for the match group {} on the left hand-side of the operation: [{__name__="a", host="h1", job="x"}, {__name__="a", host="h2", job="y"}];many-to-many matching not allowed: matching labels must be unique on one side +end +case binary * false ignoring job,zone right job,zone - Inf +grid 100000 10000 10 +series __name__=a,host=h1,job=x ; 9:c082210000000000 +series __name__=a,host=h2,job=y ; 0:40861c0000000000 1:c081f00000000000 2:c082a30000000000 3:c07aea0000000000 4:408ae80000000000 5:407dc40000000000 6:c068f00000000000 7:4089c30000000000 +series __name__=a,host=h2,job=y,zone=z1 ; 1:c081e50000000000 2:7e37e43c8800759c 3:0 4:408b4f0000000000 6:c012800000000000 7:7ff8000000000001 8:4079ba0000000000 9:c064780000000000 +series __name__=a,host=h1,job=x,zone=z1 ; 2:7ff8000000000001 4:c073b80000000000 +series __name__=b,host=h1,job=y,zone=z1 ; +error found duplicate series for the match group {host="h2"} on the left hand-side of the operation: [{__name__="a", host="h2", job="y", zone="z1"}, {__name__="a", host="h2", job="y"}];many-to-many matching not allowed: matching labels must be unique on one side +end +case binary == true ignoring zone one - Inf - +grid 100000 10000 11 +series __name__=a,host=h2,job=x ; 8:c086c90000000000 9:c03ec00000000000 +series __name__=a,host=h1,job=y ; 1:7ff0000000000000 2:fff0000000000000 3:40814a0000000000 4:406a800000000000 6:c08a8c0000000000 7:40823c0000000000 8:c08c7a0000000000 9:7ff8000000000001 10:408ea00000000000 +series __name__=a,host=h1,job=y,zone=z1 ; 1:408d470000000000 2:4059600000000000 3:4088ef0000000000 4:408cd90000000000 +series __name__=a,host=h1,job=x ; 9:7ff8000000000001 +series __name__=b,host=h1,job=x,zone=z2 ; 3:4076380000000000 4:c077080000000000 +result host=h1,job=x ; 3:0 4:0 +end +case binary == false ignoring - left - NaN - +grid 100000 10000 12 +series __name__=a,host=h2,job=x ; 11:4053d80000000000 +series __name__=a,host=h2,job=y ; 7:406cd00000000000 8:4088190000000000 9:40713a0000000000 10:7ff8000000000001 +series __name__=a,host=h1,job=y ; 9:c0745c0000000000 +series __name__=b,host=h1,job=x ; 7:c07e7c0000000000 8:c079ba0000000000 9:c08b860000000000 10:c082450000000000 +series __name__=b,host=h2,job=y ; 2:4077ea0000000000 3:fff0000000000000 4:408d780000000000 5:c07b4c0000000000 6:c083c90000000000 7:4089db0000000000 8:408a310000000000 9:4088510000000000 10:c088b60000000000 +end +case binary / false none - one - 0 Inf +grid 100000 10000 5 +series __name__=a,host=h1,job=y ; 0:0 1:c08d850000000000 +series __name__=b,host=h1,job=x ; 3:408cc60000000000 +series __name__=b,host=h1,job=y ; 0:4069d00000000000 1:40691c0000000000 2:4076040000000000 3:40875d0000000000 +series __name__=b,host=h1,job=y,zone=z2 ; 2:c05bc00000000000 +result host=h1,job=x ; 3:0 +result host=h1,job=y ; 0:0 1:c012cf699e301c0a 2:0 3:0 +result host=h1,job=y,zone=z2 ; 2:8000000000000000 +end +case binary != true ignoring - left job NaN 1 +grid 100000 10000 6 +series __name__=a,host=h2,job=x ; 3:407b800000000000 +series __name__=a,host=h2,job=y ; 5:c073880000000000 +series __name__=a,host=h1,job=x ; 5:4084650000000000 +series __name__=a,host=h2,job=x,zone=z1 ; 2:c07c3a0000000000 +series __name__=b,host=h1,job=x ; 2:408f370000000000 3:408d430000000000 4:407c000000000000 +series __name__=b,host=h2,job=x ; 1:c080730000000000 +result host=h1,job=x ; 2:3ff0000000000000 3:3ff0000000000000 4:3ff0000000000000 5:3ff0000000000000 +result host=h2,job=x ; 1:3ff0000000000000 3:3ff0000000000000 +result host=h2,job=x,zone=z1 ; 2:3ff0000000000000 +result host=h2,job=y ; 5:3ff0000000000000 +end +case binary ^ false ignoring host right host,zone - 0 +grid 100000 10000 6 +series __name__=a,host=h2,job=x ; 2:407a6a0000000000 +series __name__=a,host=h2,job=y ; 0:0 1:4080950000000000 2:4089ce0000000000 3:c079060000000000 +series __name__=a,host=h2,job=x,zone=z1 ; 5:4070680000000000 +series __name__=b,host=h2,job=y ; 1:4058480000000000 2:4080f50000000000 +result host=h2,job=y ; 1:76e18773d32422b0 2:7ff0000000000000 +end +case binary - false ignoring - right job,zone NaN - +grid 100000 10000 5 +series __name__=a,host=h2,job=y ; 4:7ff8000000000001 +series __name__=a,host=h1,job=y ; 4:408a210000000000 +series __name__=a,host=h2,job=x ; 2:c08bf50000000000 3:c07dea0000000000 +series __name__=b,host=h2,job=y,zone=z2 ; +series __name__=b,host=h2,job=x ; 3:4049600000000000 +series __name__=b,host=h2,job=x,zone=z2 ; 1:c089410000000000 +series __name__=b,host=h1,job=y,zone=z2 ; 1:4063e00000000000 +series __name__=b,host=h1,job=y ; 2:c0801b0000000000 3:c086a20000000000 4:4074960000000000 +result host=h1,job=y ; 4:407fac0000000000 +result host=h2,job=x ; 2:7ff8000000000001 3:c0808b0000000000 +result host=h2,job=y ; 4:7ff8000000000001 +end +case binary <= false ignoring host one - - 1 +grid 100000 10000 2 +series __name__=a,host=h2,job=x ; 1:40780a0000000000 +series __name__=a,host=h1,job=x,zone=z1 ; 0:7ff0000000000000 1:4062d80000000000 +series __name__=a,host=h2,job=y ; 1:408bb70000000000 +series __name__=b,host=h2,job=y ; 0:c08d110000000000 +series __name__=b,host=h1,job=x,zone=z1 ; 1:c037e00000000000 +series __name__=b,host=h2,job=x ; 1:7e37e43c8800759c +series __name__=b,host=h1,job=x,zone=z2 ; 0:4086520000000000 +series __name__=b,host=h2,job=x,zone=z1 ; 0:c088180000000000 +result __name__=a,job=x ; 1:40780a0000000000 +end +case binary % false ignoring job,__name__ right host - NaN +grid 100000 10000 5 +series __name__=a,host=h1,job=x,zone=z1 ; 1:7ff0000000000000 2:c03c800000000000 4:408d1e0000000000 +series __name__=a,host=h1,job=y ; +series __name__=a,host=h2,job=x ; 0:4046e00000000000 1:c081960000000000 2:40812b0000000000 3:c08f1c0000000000 4:4079d40000000000 +series __name__=b,host=h2,job=x,zone=z1 ; 3:4089100000000000 +series __name__=b,host=h1,job=x ; 1:c085590000000000 2:4089750000000000 3:4021800000000000 +result host=h1,job=x ; 1:7ff8000000000001 2:7ff8000000000001 3:7ff8000000000001 +result host=h2,job=x,zone=z1 ; 3:7ff8000000000001 +end +case binary - false on zone right job -2.5 0 +grid 100000 10000 5 +series __name__=a,host=h2,job=y,zone=z1 ; 1:407bf60000000000 2:408eff0000000000 3:7ff8000000000001 4:fff0000000000000 +series __name__=a,host=h1,job=x ; 4:40782a0000000000 +series __name__=b,host=h2,job=x,zone=z1 ; +series __name__=b,host=h2,job=y,zone=z1 ; 0:c0706a0000000000 1:4088500000000000 +series __name__=b,host=h1,job=x ; 3:c04cb00000000000 +series __name__=b,host=h2,job=x ; 4:7ff8000000000001 +result host=h1 ; 3:404cb00000000000 +result host=h2,job=x ; 4:7ff8000000000001 +result host=h2,job=y,zone=z1 ; 1:c074aa0000000000 +result host=h2,zone=z1 ; 0:40706a0000000000 +result job=y,zone=z1 ; 2:408f130000000000 3:7ff8000000000001 4:fff0000000000000 +end +case binary <= true ignoring host left - 0 1 +grid 100000 10000 1 +series __name__=a,host=h2,job=x,zone=z1 ; 0:c083b60000000000 +series __name__=a,host=h1,job=x ; 0:c08b840000000000 +series __name__=a,host=h2,job=y,zone=z1 ; +series __name__=b,host=h2,job=x,zone=z2 ; 0:c08df20000000000 +series __name__=b,host=h2,job=y,zone=z1 ; 0:c051780000000000 +series __name__=b,host=h1,job=x ; 0:c087c40000000000 +result host=h1,job=x ; 0:3ff0000000000000 +result host=h2,job=x,zone=z1 ; 0:3ff0000000000000 +result job=x,zone=z2 ; 0:0 +result job=y,zone=z1 ; 0:0 +end +case binary >= false on - one - 1 1 +grid 100000 10000 6 +series __name__=a,host=h1,job=x ; 5:fff0000000000000 +series __name__=a,host=h1,job=y ; 0:7ff8000000000001 1:4087460000000000 +series __name__=b,host=h1,job=y ; 0:7ff0000000000000 1:405de80000000000 2:c087a40000000000 3:40740e0000000000 +series __name__=b,host=h1,job=x,zone=z1 ; 4:c0857b0000000000 5:7e37e43c8800759c +series __name__=b,host=h1,job=x ; 5:c077da0000000000 +error found duplicate series for the match group {} on the right hand-side of the operation: [{__name__="b", host="h1", job="x", zone="z1"}, {__name__="b", host="h1", job="x"}];many-to-many matching not allowed: matching labels must be unique on one side +end +case binary ^ false ignoring host,zone one - 0 0 +grid 100000 10000 10 +series __name__=a,host=h2,job=y,zone=z1 ; 7:c070fc0000000000 +series __name__=a,host=h1,job=x ; 8:c0894b0000000000 +series __name__=a,host=h1,job=y ; 0:c07e2c0000000000 1:7ff8000000000001 2:c081460000000000 3:4057880000000000 4:c06c540000000000 5:40774e0000000000 6:c0722c0000000000 +series __name__=a,host=h2,job=y ; 9:c0805f0000000000 +series __name__=a,host=h1,job=y,zone=z1 ; 0:405d800000000000 1:c087570000000000 3:40860d0000000000 +series __name__=b,host=h1,job=y,zone=z1 ; 7:408bca0000000000 8:4047e00000000000 +series __name__=b,host=h1,job=x ; 4:4044200000000000 +series __name__=b,host=h2,job=y ; 7:c05d500000000000 8:c05c600000000000 +series __name__=b,host=h1,job=y ; 3:7ff8000000000001 +error multiple matches for labels: many-to-one matching must be explicit (group_left/group_right) +end +case binary >= false ignoring - left host,zone 0 0 +grid 100000 10000 3 +series __name__=a,host=h2,job=x ; 1:c069cc0000000000 +series __name__=a,host=h1,job=x,zone=z1 ; 1:4040a00000000000 2:40845b0000000000 +series __name__=a,host=h2,job=y ; 2:c079b20000000000 +series __name__=a,host=h1,job=x ; 0:7e37e43c8800759c 1:408a830000000000 2:fff0000000000000 +series __name__=b,host=h2,job=x ; 1:0 +series __name__=b,host=h1,job=x,zone=z1 ; 0:c075960000000000 +result __name__=a,host=h1,job=x ; 0:7e37e43c8800759c 1:408a830000000000 +result __name__=a,host=h1,job=x,zone=z1 ; 1:4040a00000000000 2:40845b0000000000 +result host=h1,job=x,zone=z1 ; 0:0 +end +case binary > false ignoring zone left job,zone -2.5 - +grid 100000 10000 5 +series __name__=a,host=h2,job=y ; 4:c0855a0000000000 +series __name__=b,host=h2,job=y ; 3:c072a40000000000 4:408cc30000000000 +series __name__=b,host=h2,job=x,zone=z1 ; 4:c05e380000000000 +series __name__=b,host=h2,job=x ; +result host=h2,job=x,zone=z1 ; 4:c004000000000000 +result host=h2,job=y ; 3:c004000000000000 +end +case binary != false ignoring job,zone one - - 0 +grid 100000 10000 2 +series __name__=a,host=h2,job=y ; 0:c0875b0000000000 +series __name__=a,host=h1,job=x ; 1:4071320000000000 +series __name__=a,host=h1,job=y ; 0:c078ce0000000000 1:c0833c0000000000 +series __name__=a,host=h1,job=x,zone=z1 ; 0:408aac0000000000 +series __name__=b,host=h2,job=y ; 0:fff0000000000000 +series __name__=b,host=h1,job=y ; 1:7ff0000000000000 +series __name__=b,host=h1,job=x,zone=z1 ; 1:c08cba0000000000 +error multiple matches for labels: many-to-one matching must be explicit (group_left/group_right) +end +case binary atan2 false ignoring host right host 1 - +grid 100000 10000 7 +series __name__=a,host=h2,job=y,zone=z1 ; 2:407e940000000000 3:4068180000000000 4:4066500000000000 5:408dfe0000000000 +series __name__=a,host=h2,job=x ; 2:4069ec0000000000 3:408b2a0000000000 4:fff0000000000000 +series __name__=a,host=h1,job=y ; 0:c081bf0000000000 +series __name__=a,host=h1,job=x ; 4:408c210000000000 5:c06b880000000000 +series __name__=a,host=h2,job=y ; 6:405bd00000000000 +series __name__=b,host=h2,job=x,zone=z2 ; 6:c079040000000000 +series __name__=b,host=h1,job=x ; 0:c074a60000000000 2:408c110000000000 4:4083b40000000000 +series __name__=b,host=h1,job=y ; 4:c083ab0000000000 5:c08bf20000000000 6:4080580000000000 +error found duplicate series for the match group {job="x"} on the left hand-side of the operation: [{__name__="a", host="h1", job="x"}, {__name__="a", host="h2", job="x"}];many-to-many matching not allowed: matching labels must be unique on one side +end +case binary atan2 false none - one - -2.5 Inf +grid 100000 10000 12 +series __name__=a,host=h1,job=x ; +series __name__=a,host=h2,job=y ; 4:4089320000000000 +series __name__=a,host=h1,job=y,zone=z1 ; 3:c0857a0000000000 4:7ff8000000000001 5:c086a20000000000 6:4076e20000000000 7:408b9a0000000000 8:c016800000000000 9:c08b740000000000 +series __name__=a,host=h2,job=x,zone=z1 ; 6:40844b0000000000 7:0 8:407ba40000000000 9:4054300000000000 10:c070e60000000000 11:7ff8000000000001 +series __name__=a,host=h2,job=x ; 7:40849f0000000000 9:c086060000000000 10:c068f40000000000 +series __name__=b,host=h1,job=x ; 10:c08f270000000000 11:fff0000000000000 +series __name__=b,host=h2,job=y ; 3:408e310000000000 4:406ef80000000000 5:403ec00000000000 6:c06ab40000000000 7:c08d660000000000 9:407f2a0000000000 10:40763e0000000000 +series __name__=b,host=h1,job=y,zone=z2 ; 9:c050480000000000 10:c074880000000000 +result host=h1,job=x ; 10:c0091cd880c9f449 11:c00921fb54442d18 +result host=h1,job=y,zone=z1 ; 3:8000000000000000 4:7ff8000000000001 5:8000000000000000 6:0 7:0 8:8000000000000000 9:8000000000000000 +result host=h1,job=y,zone=z2 ; 9:c008d366fcdc2a03 10:c00912656416c41a +result host=h2,job=x ; 7:0 9:8000000000000000 10:8000000000000000 +result host=h2,job=x,zone=z1 ; 6:0 7:0 8:0 9:0 10:8000000000000000 11:7ff8000000000001 +result host=h2,job=y ; 3:bf6532b29b492203 4:3ff45cd8f7d1314f 5:bfb4c46fe50bf1cf 6:c0090a03ff6e0fcd 7:c0091c8a0fa57e87 9:bf74894b2b15c02f 10:bf7cc60f8f0a99b8 +end +case binary <= true on host,zone one - NaN - +grid 100000 10000 1 +series __name__=a,host=h2,job=x ; +series __name__=a,host=h2,job=x,zone=z1 ; +series __name__=b,host=h2,job=x,zone=z2 ; +end +case binary != true on job,zone left - NaN NaN +grid 100000 10000 4 +series __name__=a,host=h1,job=y ; +series __name__=a,host=h1,job=x ; 0:c058580000000000 1:7ff8000000000001 +series __name__=a,host=h2,job=x,zone=z1 ; 2:c0805f0000000000 +series __name__=a,host=h1,job=x,zone=z1 ; 2:c0834e0000000000 +series __name__=b,host=h2,job=x ; 2:4070e40000000000 +series __name__=b,host=h1,job=y ; 3:4082320000000000 +series __name__=b,host=h2,job=y ; 0:407a2e0000000000 +result host=h1,job=x ; 0:3ff0000000000000 1:3ff0000000000000 +result host=h1,job=x,zone=z1 ; 2:3ff0000000000000 +result host=h2,job=x,zone=z1 ; 2:3ff0000000000000 +result job=x ; 2:3ff0000000000000 +result job=y ; 0:3ff0000000000000 3:3ff0000000000000 +end +case binary * false ignoring host,zone right job,zone - 1 +grid 100000 10000 1 +series __name__=a,host=h2,job=x ; 0:4070d80000000000 +series __name__=b,host=h2,job=y ; +end +case binary % false on - left host - -2.5 +grid 100000 10000 8 +series __name__=a,host=h1,job=y,zone=z1 ; 6:7e37e43c8800759c +series __name__=b,host=h2,job=y,zone=z1 ; 7:c055b80000000000 +series __name__=b,host=h1,job=x ; 1:7ff8000000000001 2:408ab40000000000 3:7ff8000000000001 4:406c200000000000 5:4084b40000000000 +result job=y,zone=z1 ; 6:0 +end +case binary atan2 false on job right host,zone Inf Inf +grid 100000 10000 10 +series __name__=a,host=h1,job=y ; 9:c081b70000000000 +series __name__=a,host=h1,job=x ; 9:40808e0000000000 +series __name__=b,host=h2,job=x ; 5:408a1e0000000000 6:c08b540000000000 7:4043100000000000 9:c078260000000000 +series __name__=b,host=h1,job=y,zone=z2 ; 2:c06ba00000000000 3:408ca40000000000 4:7ff8000000000001 5:c087b00000000000 7:c08dcd0000000000 8:4063780000000000 9:7e37e43c8800759c +result host=h1,job=x ; 9:40019b8cd194018d +result host=h1,job=y ; 9:8237ba1d4a6e81ed +result job=x ; 5:3ff921fb54442d18 6:3ff921fb54442d18 7:3ff921fb54442d18 +result job=y ; 2:3ff921fb54442d18 3:3ff921fb54442d18 4:7ff8000000000001 5:3ff921fb54442d18 7:3ff921fb54442d18 8:3ff921fb54442d18 +end +case binary == true on job,zone one - - 1 +grid 100000 10000 6 +series __name__=a,host=h2,job=x,zone=z1 ; 2:4075d80000000000 3:c070c60000000000 4:408df30000000000 +series __name__=a,host=h2,job=y ; 2:c0818a0000000000 4:405b080000000000 +series __name__=a,host=h1,job=x ; 4:c081cc0000000000 +series __name__=a,host=h1,job=x,zone=z1 ; 5:406e9c0000000000 +series __name__=b,host=h1,job=x ; 4:c074720000000000 +series __name__=b,host=h2,job=y ; 0:c041c00000000000 1:408d2a0000000000 2:c063440000000000 4:4066a00000000000 5:c089ee0000000000 +series __name__=b,host=h1,job=x,zone=z1 ; 0:40799c0000000000 1:407fbe0000000000 2:c064cc0000000000 3:4078aa0000000000 4:c07a480000000000 +series __name__=b,host=h1,job=y ; 2:4043000000000000 3:0 4:c034400000000000 +error found duplicate series for the match group {job="y"} on the right hand-side of the operation: [{__name__="b", host="h1", job="y"}, {__name__="b", host="h2", job="y"}];many-to-many matching not allowed: matching labels must be unique on one side +end +case binary <= false none - one - 1 1 +grid 100000 10000 6 +series __name__=a,host=h2,job=x ; 4:7ff8000000000001 5:c07fea0000000000 +series __name__=a,host=h1,job=x,zone=z1 ; +series __name__=a,host=h2,job=y ; 4:7ff0000000000000 5:c084ef0000000000 +series __name__=a,host=h1,job=y,zone=z1 ; 0:4082a20000000000 1:4085e90000000000 2:408f0e0000000000 3:7ff8000000000001 4:4087080000000000 5:4083310000000000 +series __name__=b,host=h2,job=x ; 5:4072c60000000000 +series __name__=b,host=h1,job=y,zone=z1 ; 0:4077a60000000000 1:c061ec0000000000 3:4062d40000000000 +result __name__=a,host=h2,job=x ; 5:c07fea0000000000 +result __name__=a,host=h2,job=y ; 5:c084ef0000000000 +end +case binary > false on host,__name__ right job NaN NaN +grid 100000 10000 6 +series __name__=a,host=h2,job=y,zone=z1 ; 4:405b400000000000 +series __name__=a,host=h2,job=y ; 2:fff0000000000000 +series __name__=a,host=h1,job=y ; 4:c073e80000000000 +series __name__=b,host=h2,job=y ; 5:c064580000000000 +series __name__=b,host=h1,job=y,zone=z1 ; 0:40801c0000000000 1:4075d20000000000 2:408c880000000000 3:c078300000000000 4:c08d1b0000000000 5:40740a0000000000 +series __name__=b,host=h2,job=x ; 2:4071ac0000000000 +series __name__=b,host=h1,job=y ; 2:4077500000000000 3:407cba0000000000 +end +case binary + false on host,zone one - Inf Inf +grid 100000 10000 12 +series __name__=a,host=h2,job=y ; +series __name__=a,host=h2,job=x ; 8:c087a80000000000 10:408be20000000000 11:7e37e43c8800759c +series __name__=a,host=h2,job=y,zone=z1 ; 10:407dc80000000000 +series __name__=b,host=h2,job=y,zone=z1 ; 11:c07c9e0000000000 +series __name__=b,host=h1,job=x ; 7:407c2a0000000000 8:fff0000000000000 9:403a600000000000 11:4051080000000000 +series __name__=b,host=h2,job=x ; 5:c0832c0000000000 6:407ede0000000000 +result host=h1 ; 7:7ff0000000000000 8:fff8000000000000 9:7ff0000000000000 11:7ff0000000000000 +result host=h2 ; 5:7ff0000000000000 6:7ff0000000000000 8:7ff0000000000000 10:7ff0000000000000 11:7ff0000000000000 +result host=h2,zone=z1 ; 10:7ff0000000000000 11:7ff0000000000000 +end +case binary == true on host right zone 1 Inf +grid 100000 10000 4 +series __name__=a,host=h1,job=x ; 1:c06e880000000000 +series __name__=a,host=h2,job=y ; +series __name__=b,host=h1,job=y,zone=z1 ; 0:c07c560000000000 1:407a740000000000 2:c08bbf0000000000 +series __name__=b,host=h1,job=x ; 3:4087c90000000000 +series __name__=b,host=h2,job=y ; 0:408c870000000000 1:c0831c0000000000 2:c07b880000000000 +result host=h1,job=x ; 3:0 +result host=h1,job=y ; 0:0 1:0 2:0 +result host=h2,job=y ; 0:0 1:0 2:0 +end +case binary / false ignoring host,job,zone one - 1 - +grid 100000 10000 5 +series __name__=a,host=h2,job=y ; 1:c08d630000000000 2:c07ad80000000000 +series __name__=a,host=h1,job=y ; 0:4087b00000000000 1:4089270000000000 2:4083e40000000000 3:c058d80000000000 4:408c400000000000 +series __name__=a,host=h1,job=x ; 0:c088860000000000 2:4069580000000000 3:c086c90000000000 4:404b600000000000 +series __name__=b,host=h1,job=x,zone=z1 ; 1:7ff0000000000000 3:407f600000000000 +error multiple matches for labels: many-to-one matching must be explicit (group_left/group_right) +end +case binary <= true ignoring host right job - 1 +grid 100000 10000 7 +series __name__=a,host=h1,job=x ; 4:c080f90000000000 +series __name__=a,host=h2,job=y ; 4:c08e420000000000 5:407ae20000000000 6:7ff8000000000001 +series __name__=a,host=h1,job=y ; 2:c08bf20000000000 +series __name__=b,host=h1,job=x ; 4:4051c00000000000 +result host=h1,job=x ; 4:3ff0000000000000 +end +case binary - false on zone left job NaN - +grid 100000 10000 3 +series __name__=a,host=h2,job=x ; 1:0 +series __name__=b,host=h2,job=x,zone=z1 ; 2:405fc00000000000 +series __name__=b,host=h1,job=x ; 2:405cd80000000000 +series __name__=b,host=h2,job=x ; 0:408d890000000000 2:c08f390000000000 +error found duplicate series for the match group {} on the right hand-side of the operation: [{__name__="b", host="h1", job="x"}, {__name__="b", host="h2", job="x"}];many-to-many matching not allowed: matching labels must be unique on one side +end +case binary % false on host right job Inf Inf +grid 100000 10000 12 +series __name__=a,host=h1,job=x ; 9:40802e0000000000 +series __name__=a,host=h2,job=x,zone=z1 ; 4:408e450000000000 5:4057600000000000 6:c0704a0000000000 7:4083470000000000 10:4088ad0000000000 11:4086ac0000000000 +series __name__=b,host=h2,job=x,zone=z1 ; 9:408d750000000000 10:40866a0000000000 +result host=h1,job=x ; 9:40802e0000000000 +result host=h2,job=x ; 4:408e450000000000 5:4057600000000000 6:c0704a0000000000 7:4083470000000000 11:4086ac0000000000 +result host=h2,job=x,zone=z1 ; 10:4052180000000000 +result host=h2,zone=z1 ; 9:7ff8000000000001 +end +case binary >= false ignoring - left - 0 - +grid 100000 10000 1 +series __name__=a,host=h2,job=y ; 0:408d180000000000 +series __name__=a,host=h1,job=y,zone=z1 ; 0:c06da40000000000 +series __name__=a,host=h1,job=y ; 0:408e670000000000 +series __name__=a,host=h2,job=x,zone=z1 ; 0:c089ef0000000000 +series __name__=b,host=h1,job=x ; 0:407d520000000000 +series __name__=b,host=h2,job=y,zone=z1 ; 0:c08f300000000000 +series __name__=b,host=h2,job=y ; 0:c07d6a0000000000 +result __name__=a,host=h2,job=y ; 0:408d180000000000 +result host=h2,job=y,zone=z1 ; 0:0 +end +case binary - false none - one - Inf - +grid 100000 10000 4 +series __name__=a,host=h1,job=x ; 3:7ff8000000000001 +series __name__=a,host=h1,job=y,zone=z1 ; 0:407a760000000000 1:c070240000000000 2:4041d00000000000 +series __name__=b,host=h1,job=y,zone=z1 ; 2:c087850000000000 3:0 +series __name__=b,host=h1,job=x ; 2:4084790000000000 +series __name__=b,host=h1,job=x,zone=z1 ; 0:c07cce0000000000 2:c043b00000000000 +result host=h1,job=x ; 2:7ff0000000000000 +result host=h1,job=x,zone=z1 ; 0:7ff0000000000000 2:7ff0000000000000 +result host=h1,job=y,zone=z1 ; 2:4088a20000000000 3:7ff0000000000000 +end +case binary atan2 false none - one - 1 1 +grid 100000 10000 11 +series __name__=a,host=h1,job=y,zone=z1 ; 2:c079aa0000000000 3:c0839f0000000000 +series __name__=a,host=h2,job=y,zone=z1 ; +series __name__=a,host=h2,job=y ; 3:408a080000000000 4:4067600000000000 5:c088d10000000000 6:0 +series __name__=a,host=h1,job=x ; 8:c087ef0000000000 9:4075060000000000 +series __name__=a,host=h1,job=y ; 10:4082810000000000 +series __name__=b,host=h1,job=y,zone=z1 ; 9:c08e960000000000 +result host=h1,job=x ; 8:bff91ca235567358 9:3ff915ce0e38cd2d +result host=h1,job=y ; 10:3ff91b107626535d +result host=h1,job=y,zone=z1 ; 2:bff91801b9770412 3:bff91b754a896035 9:40091fe3a889265a +result host=h2,job=y ; 3:3ff91d1088f92cb4 4:3ff90c14063a4079 5:bff91cd2e9bdcafb 6:0 +end +case binary > false on host,zone right - Inf - +grid 100000 10000 9 +series __name__=a,host=h2,job=y,zone=z1 ; 6:c08bff0000000000 7:c08d170000000000 8:4067500000000000 +series __name__=a,host=h2,job=y ; 2:7ff8000000000001 4:40821e0000000000 +series __name__=b,host=h2,job=y ; 7:c08a5d0000000000 +series __name__=b,host=h1,job=y ; 1:408bd70000000000 2:4056380000000000 3:fff0000000000000 +series __name__=b,host=h2,job=y,zone=z2 ; 7:4088e20000000000 8:4080ad0000000000 +end +case binary % false on job left - -2.5 -2.5 +grid 100000 10000 8 +series __name__=a,host=h2,job=x ; 5:7e37e43c8800759c +series __name__=a,host=h2,job=x,zone=z1 ; 5:40798a0000000000 +series __name__=a,host=h1,job=y ; 6:c055a80000000000 +series __name__=b,host=h1,job=y ; +series __name__=b,host=h2,job=y,zone=z1 ; 7:40745c0000000000 +series __name__=b,host=h2,job=x ; 1:406cd00000000000 3:fff0000000000000 +result host=h1,job=y ; 6:bffa000000000000 +result host=h2,job=x ; 5:0 +result host=h2,job=x,zone=z1 ; 5:3ff2000000000000 +result job=x ; 1:c004000000000000 3:c004000000000000 +result job=y ; 7:c004000000000000 +end +case binary / false on host,job,zone left - - 1 +grid 100000 10000 10 +series __name__=a,host=h2,job=x,zone=z1 ; 5:c085820000000000 6:c07dc00000000000 7:c03c600000000000 8:4053300000000000 9:406c680000000000 +series __name__=a,host=h1,job=x,zone=z1 ; 7:4088470000000000 +series __name__=a,host=h1,job=x ; 9:c089870000000000 +series __name__=a,host=h1,job=y ; 5:c072e40000000000 6:c08d2b0000000000 +series __name__=a,host=h2,job=y ; 7:40807a0000000000 +series __name__=b,host=h2,job=x,zone=z1 ; 7:404ce00000000000 8:fff0000000000000 +series __name__=b,host=h1,job=y ; 6:7e37e43c8800759c 7:c08a430000000000 +result host=h1,job=x ; 9:c089870000000000 +result host=h1,job=x,zone=z1 ; 7:4088470000000000 +result host=h1,job=y ; 5:c072e40000000000 6:82438898c35f9b0f +result host=h2,job=x,zone=z1 ; 5:c085820000000000 6:c07dc00000000000 7:bfdf7225adfdc897 8:8000000000000000 9:406c680000000000 +result host=h2,job=y ; 7:40807a0000000000 +end +case binary % false ignoring job right - NaN NaN +grid 100000 10000 9 +series __name__=a,host=h1,job=y,zone=z1 ; 5:c08c320000000000 6:c04ff00000000000 7:408cce0000000000 8:c086b20000000000 +series __name__=a,host=h1,job=x ; +series __name__=b,host=h1,job=x,zone=z1 ; 2:c086cb0000000000 3:4081070000000000 +series __name__=b,host=h1,job=x ; 4:4081e90000000000 5:7e37e43c8800759c 6:c040800000000000 +result host=h1,job=x ; 4:7ff8000000000001 5:7ff8000000000001 6:7ff8000000000001 +result host=h1,job=x,zone=z1 ; 2:7ff8000000000001 3:7ff8000000000001 +result host=h1,zone=z1 ; 5:7ff8000000000001 6:7ff8000000000001 7:7ff8000000000001 8:7ff8000000000001 +end +case binary < false none - one - Inf Inf +grid 100000 10000 3 +series __name__=a,host=h2,job=y ; 2:4081600000000000 +series __name__=b,host=h2,job=y,zone=z2 ; 0:c072f40000000000 1:c08e900000000000 +result __name__=a,host=h2,job=y ; 2:4081600000000000 +end +case binary % false none - one - - NaN +grid 100000 10000 5 +series __name__=a,host=h2,job=x,zone=z1 ; 4:408c220000000000 +series __name__=a,host=h1,job=x ; 3:fff0000000000000 +series __name__=b,host=h2,job=y ; 0:c08d5b0000000000 1:4083e50000000000 2:0 3:c070540000000000 4:4083960000000000 +result host=h1,job=x ; 3:7ff8000000000001 +result host=h2,job=x,zone=z1 ; 4:7ff8000000000001 +end +case binary <= false ignoring host,zone one - 1 0 +grid 100000 10000 6 +series __name__=a,host=h1,job=y,zone=z1 ; 3:c06d2c0000000000 +series __name__=a,host=h2,job=y,zone=z1 ; 3:408e380000000000 4:c087c20000000000 +series __name__=a,host=h2,job=x,zone=z1 ; 4:c069440000000000 5:c0807f0000000000 +series __name__=a,host=h2,job=x ; 3:7e37e43c8800759c +series __name__=b,host=h1,job=y ; 2:4080f00000000000 +series __name__=b,host=h1,job=x ; 3:4050080000000000 4:4086500000000000 +error multiple matches for labels: many-to-one matching must be explicit (group_left/group_right) +end +case binary ^ false ignoring - right job - NaN +grid 100000 10000 8 +series __name__=a,host=h2,job=x ; 4:4074440000000000 +series __name__=a,host=h2,job=y ; 6:c08dae0000000000 +series __name__=a,host=h1,job=x ; 2:c07c740000000000 3:c075120000000000 4:7ff8000000000001 5:4068440000000000 7:4088ac0000000000 +series __name__=a,host=h2,job=x,zone=z1 ; 1:4072040000000000 2:406e2c0000000000 3:408d9b0000000000 4:c081100000000000 5:c052800000000000 +series __name__=a,host=h1,job=y,zone=z1 ; 4:4080f70000000000 5:c058380000000000 +series __name__=b,host=h2,job=y ; 5:4084e80000000000 6:7ff8000000000001 7:40874b0000000000 +series __name__=b,host=h2,job=x,zone=z2 ; 0:4072760000000000 3:40809b0000000000 5:7ff8000000000001 +series __name__=b,host=h2,job=x ; 4:c064a00000000000 5:fff0000000000000 +result host=h2,job=x ; 4:0 5:7ff8000000000001 +result host=h2,job=x,zone=z2 ; 0:7ff8000000000001 3:7ff8000000000001 5:7ff8000000000001 +result host=h2,job=y ; 5:7ff8000000000001 6:7ff8000000000001 7:7ff8000000000001 +end +case binary - false ignoring host,zone left host,job,zone - 1 +grid 100000 10000 10 +series __name__=a,host=h1,job=x ; 3:40824a0000000000 4:4082910000000000 5:4084ce0000000000 +series __name__=a,host=h2,job=x ; 8:c0805d0000000000 9:c078fa0000000000 +series __name__=a,host=h2,job=x,zone=z1 ; 6:c080680000000000 7:c07c500000000000 +series __name__=b,host=h1,job=y ; 5:4065500000000000 6:c080380000000000 +series __name__=b,host=h2,job=x,zone=z1 ; 7:408d780000000000 8:4089a40000000000 +series __name__=b,host=h1,job=x ; 7:4080e70000000000 8:4071bc0000000000 +error found duplicate series for the match group {job="x"} on the right hand-side of the operation: [{__name__="b", host="h1", job="x"}, {__name__="b", host="h2", job="x", zone="z1"}];many-to-many matching not allowed: matching labels must be unique on one side +end +case binary + false none - one - 1 Inf +grid 100000 10000 8 +series __name__=a,host=h2,job=y ; 1:7e37e43c8800759c 2:c08c510000000000 3:4073d60000000000 4:407c880000000000 5:c082c20000000000 6:c075240000000000 7:4069cc0000000000 +series __name__=a,host=h1,job=y ; 1:40719e0000000000 2:7ff0000000000000 3:4078620000000000 +series __name__=b,host=h1,job=x ; 4:fff0000000000000 5:c075bc0000000000 +series __name__=b,host=h1,job=y ; 4:c066e40000000000 +series __name__=b,host=h2,job=y ; 3:4089e70000000000 +result host=h1,job=x ; 4:fff0000000000000 5:c075ac0000000000 +result host=h1,job=y ; 1:7ff0000000000000 2:7ff0000000000000 3:7ff0000000000000 4:c066c40000000000 +result host=h2,job=y ; 1:7ff0000000000000 2:7ff0000000000000 3:4091e90000000000 4:7ff0000000000000 5:7ff0000000000000 6:7ff0000000000000 7:7ff0000000000000 +end +case binary != true on host one - 0 -2.5 +grid 100000 10000 2 +series __name__=a,host=h2,job=x,zone=z1 ; +series __name__=b,host=h2,job=x,zone=z2 ; 1:4080e00000000000 +result host=h2 ; 1:3ff0000000000000 +end +case binary < true ignoring job one - -2.5 0 +grid 100000 10000 3 +series __name__=a,host=h1,job=y,zone=z1 ; 0:c072d20000000000 1:c08cc80000000000 +series __name__=b,host=h1,job=x,zone=z1 ; 0:408ea70000000000 +series __name__=b,host=h1,job=y,zone=z1 ; 0:7ff8000000000001 +error found duplicate series for the match group {host="h1", zone="z1"} on the right hand-side of the operation: [{__name__="b", host="h1", job="x", zone="z1"}, {__name__="b", host="h1", job="y", zone="z1"}];many-to-many matching not allowed: matching labels must be unique on one side +end +case binary ^ false ignoring job,zone left job 1 1 +grid 100000 10000 9 +series __name__=a,host=h2,job=y ; 5:c07bce0000000000 7:4079f00000000000 8:c08a8f0000000000 +series __name__=a,host=h1,job=x ; 7:408c0c0000000000 +series __name__=a,host=h2,job=x ; 2:4072000000000000 3:c08e720000000000 4:c085670000000000 +series __name__=b,host=h2,job=y,zone=z1 ; 3:4080750000000000 4:4083940000000000 6:c081ff0000000000 7:4077aa0000000000 +series __name__=b,host=h1,job=x ; 3:0 4:4080090000000000 5:4020000000000000 6:406bfc0000000000 +result host=h1 ; 7:408c0c0000000000 +result host=h1,job=x ; 3:3ff0000000000000 4:3ff0000000000000 5:3ff0000000000000 6:3ff0000000000000 +result host=h2 ; 2:4072000000000000 5:c07bce0000000000 8:c08a8f0000000000 +result host=h2,job=y ; 3:7ff8000000000001 4:7ff8000000000001 6:3ff0000000000000 7:7ff0000000000000 +end +case binary / false ignoring host right host,job -2.5 -2.5 +grid 100000 10000 8 +series __name__=a,host=h1,job=x ; 3:4083950000000000 4:407e500000000000 +series __name__=a,host=h1,job=y ; 2:7ff8000000000001 3:c086880000000000 5:4075040000000000 +series __name__=a,host=h2,job=y,zone=z1 ; 5:7ff8000000000001 6:c07f680000000000 +series __name__=b,host=h2,job=y,zone=z1 ; 2:4073860000000000 +series __name__=b,host=h1,job=x ; 7:40830f0000000000 +series __name__=b,host=h1,job=y ; 2:c022000000000000 3:c06f480000000000 4:407a0c0000000000 5:4065a40000000000 +result host=h1,job=x ; 3:c06f54cccccccccd 4:c068400000000000 +result host=h1,job=y ; 2:7ff8000000000001 3:40070c880e525983 5:3fff13686ee70c04 +result host=h2,job=y,zone=z1 ; 5:7ff8000000000001 6:4069200000000000 +result job=x ; 7:bf70ca52cc1b7e81 +result job=y ; 4:bf789232ad8885ea +result job=y,zone=z1 ; 2:bf8063fb7e1ad6ec +end +case binary % false on host,zone right - -2.5 -2.5 +grid 100000 10000 12 +series __name__=a,host=h2,job=y ; 7:4075e00000000000 +series __name__=a,host=h1,job=y ; 7:c0750e0000000000 8:c07d080000000000 +series __name__=b,host=h1,job=y,zone=z1 ; 2:407ae80000000000 3:c04a800000000000 4:c052f80000000000 5:c087f50000000000 6:4060b00000000000 7:c089420000000000 +series __name__=b,host=h1,job=x,zone=z1 ; 3:c0818d0000000000 4:c051780000000000 +series __name__=b,host=h1,job=y ; 9:4089cf0000000000 10:c075480000000000 +series __name__=b,host=h2,job=y ; 11:c08bf40000000000 +series __name__=b,host=h2,job=x,zone=z1 ; 10:fff0000000000000 11:c07d380000000000 +result host=h1 ; 7:bffe000000000000 8:c000000000000000 +result host=h1,job=x,zone=z1 ; 3:c004000000000000 4:c004000000000000 +result host=h1,job=y ; 9:c004000000000000 10:c004000000000000 +result host=h1,job=y,zone=z1 ; 2:c004000000000000 3:c004000000000000 4:c004000000000000 5:c004000000000000 6:c004000000000000 7:c004000000000000 +result host=h2 ; 7:0 +result host=h2,job=x,zone=z1 ; 10:c004000000000000 11:c004000000000000 +result host=h2,job=y ; 11:c004000000000000 +end +case binary == false ignoring job left - NaN NaN +grid 100000 10000 5 +series __name__=a,host=h2,job=y,zone=z1 ; 0:0 1:4088a70000000000 2:4067840000000000 +series __name__=a,host=h1,job=y ; 4:4073140000000000 +series __name__=a,host=h2,job=x,zone=z1 ; 3:c0852b0000000000 +series __name__=a,host=h2,job=x ; 0:4087550000000000 1:7ff8000000000001 2:404ab00000000000 3:c0817a0000000000 4:c04b600000000000 +series __name__=b,host=h1,job=y ; +series __name__=b,host=h2,job=y ; 4:7ff0000000000000 +series __name__=b,host=h2,job=x ; +end +case binary % false none - one - Inf NaN +grid 100000 10000 7 +series __name__=a,host=h1,job=y ; 4:7e37e43c8800759c 5:c05a000000000000 +series __name__=a,host=h2,job=x,zone=z1 ; +series __name__=a,host=h1,job=y,zone=z1 ; 1:405b280000000000 2:c08d3f0000000000 3:408b180000000000 4:4080400000000000 5:4064500000000000 6:4056780000000000 +series __name__=a,host=h2,job=x ; 5:405d080000000000 +series __name__=b,host=h1,job=x ; 2:408f180000000000 3:c029800000000000 4:c068f40000000000 5:c072760000000000 +series __name__=b,host=h1,job=x,zone=z1 ; 1:4087490000000000 2:c082120000000000 3:4067280000000000 4:fff0000000000000 5:c089fe0000000000 6:c05e780000000000 +series __name__=b,host=h1,job=y ; 1:4082600000000000 +series __name__=b,host=h2,job=x,zone=z1 ; 0:4073c00000000000 +result host=h1,job=x ; 2:7ff8000000000001 3:7ff8000000000001 4:7ff8000000000001 5:7ff8000000000001 +result host=h1,job=x,zone=z1 ; 1:7ff8000000000001 2:7ff8000000000001 3:7ff8000000000001 4:7ff8000000000001 5:7ff8000000000001 6:7ff8000000000001 +result host=h1,job=y ; 1:7ff8000000000001 4:7ff8000000000001 5:7ff8000000000001 +result host=h1,job=y,zone=z1 ; 1:7ff8000000000001 2:7ff8000000000001 3:7ff8000000000001 4:7ff8000000000001 5:7ff8000000000001 6:7ff8000000000001 +result host=h2,job=x ; 5:7ff8000000000001 +result host=h2,job=x,zone=z1 ; 0:7ff8000000000001 +end +case binary % false ignoring job right zone 0 0 +grid 100000 10000 3 +series __name__=a,host=h2,job=y ; 1:4080fc0000000000 +series __name__=a,host=h2,job=x ; 0:c077d60000000000 1:0 2:c08ee80000000000 +series __name__=b,host=h2,job=y ; 0:4078cc0000000000 1:c0786e0000000000 2:4061580000000000 +series __name__=b,host=h1,job=x ; +error found duplicate series for the match group {host="h2"} on the left hand-side of the operation: [{__name__="a", host="h2", job="x"}, {__name__="a", host="h2", job="y"}];many-to-many matching not allowed: matching labels must be unique on one side +end +case binary % false ignoring host,zone right host - 0 +grid 100000 10000 4 +series __name__=a,host=h1,job=x ; 2:7ff0000000000000 3:4064b40000000000 +series __name__=a,host=h1,job=y ; 0:fff0000000000000 +series __name__=a,host=h2,job=x ; 3:c07f8c0000000000 +series __name__=b,host=h1,job=y ; 1:c08d440000000000 +series __name__=b,host=h1,job=x,zone=z2 ; 0:c052400000000000 1:c079b40000000000 +series __name__=b,host=h2,job=x ; 1:0 2:c072620000000000 +error found duplicate series for the match group {job="x"} on the left hand-side of the operation: [{__name__="a", host="h1", job="x"}, {__name__="a", host="h2", job="x"}];many-to-many matching not allowed: matching labels must be unique on one side +end +case binary - false ignoring host,job,zone right job,zone 0 -2.5 +grid 100000 10000 1 +series __name__=a,host=h1,job=y ; 0:4089720000000000 +series __name__=a,host=h2,job=x ; 0:c077ce0000000000 +series __name__=a,host=h1,job=x ; +series __name__=b,host=h1,job=y ; 0:7ff8000000000001 +series __name__=b,host=h2,job=x ; 0:405a480000000000 +series __name__=b,host=h2,job=x,zone=z1 ; 0:c073be0000000000 +series __name__=b,host=h1,job=x ; 0:4081c70000000000 +error found duplicate series for the match group {} on the left hand-side of the operation: [{__name__="a", host="h1", job="y"}, {__name__="a", host="h2", job="x"}];many-to-many matching not allowed: matching labels must be unique on one side +end +case binary != false ignoring host,zone,__name__ one - - Inf +grid 100000 10000 5 +series __name__=a,host=h1,job=y,zone=z1 ; 1:4086f10000000000 2:4077b20000000000 4:7ff8000000000001 +series __name__=a,host=h2,job=y ; 4:408aac0000000000 +series __name__=b,host=h2,job=y ; 2:408e000000000000 3:403ec00000000000 +series __name__=b,host=h1,job=y,zone=z1 ; 2:c0699c0000000000 3:7ff8000000000001 +error found duplicate series for the match group {job="y"} on the right hand-side of the operation: [{__name__="b", host="h1", job="y", zone="z1"}, {__name__="b", host="h2", job="y"}];many-to-many matching not allowed: matching labels must be unique on one side +end +case binary * false ignoring zone left host,zone Inf 1 +grid 100000 10000 5 +series __name__=a,host=h2,job=x,zone=z1 ; 1:7e37e43c8800759c 2:c087e10000000000 3:c073540000000000 +series __name__=a,host=h2,job=x ; 0:c054b00000000000 1:c080310000000000 2:402a000000000000 3:408aca0000000000 +series __name__=a,host=h2,job=y,zone=z1 ; +series __name__=a,host=h2,job=y ; 4:c08de70000000000 +series __name__=b,host=h2,job=x,zone=z2 ; 3:4083020000000000 +series __name__=b,host=h2,job=y ; 1:fff0000000000000 2:406a740000000000 +series __name__=b,host=h2,job=y,zone=z1 ; 0:7ff8000000000001 1:408e280000000000 +series __name__=b,host=h2,job=x,zone=z1 ; 0:0 2:7ff0000000000000 3:40779e0000000000 4:408a270000000000 +error found duplicate series for the match group {host="h2", job="y"} on the right hand-side of the operation: [{__name__="b", host="h2", job="y", zone="z1"}, {__name__="b", host="h2", job="y"}];many-to-many matching not allowed: matching labels must be unique on one side +end +case binary <= true ignoring host,job right host,job,zone Inf Inf +grid 100000 10000 7 +series __name__=a,host=h2,job=y,zone=z1 ; 5:407fb20000000000 +series __name__=b,host=h2,job=y,zone=z1 ; +result host=h2,job=y,zone=z1 ; 5:3ff0000000000000 +end +case binary == false ignoring host,job right job,zone NaN 1 +grid 100000 10000 10 +series __name__=a,host=h1,job=x,zone=z1 ; 1:c07bbc0000000000 2:7ff8000000000001 3:c083fa0000000000 +series __name__=a,host=h1,job=y ; 7:c0603c0000000000 +series __name__=a,host=h2,job=y,zone=z1 ; 2:c08c610000000000 +series __name__=b,host=h2,job=y,zone=z1 ; 2:c03d000000000000 3:405fe80000000000 4:4077820000000000 5:408a060000000000 6:4089090000000000 7:7ff0000000000000 9:405e500000000000 +series __name__=b,host=h2,job=x,zone=z1 ; 1:c067d40000000000 2:4020c00000000000 3:c037600000000000 4:c083910000000000 5:c08b360000000000 +series __name__=b,host=h1,job=x,zone=z1 ; 1:c0876c0000000000 4:7ff8000000000001 5:407afa0000000000 6:c087160000000000 +error found duplicate series for the match group {zone="z1"} on the left hand-side of the operation: [{__name__="a", host="h1", job="x", zone="z1"}, {__name__="a", host="h2", job="y", zone="z1"}];many-to-many matching not allowed: matching labels must be unique on one side +end +case binary atan2 false on host,__name__ one - Inf - +grid 100000 10000 2 +series __name__=a,host=h1,job=y,zone=z1 ; +series __name__=a,host=h1,job=x,zone=z1 ; +series __name__=a,host=h2,job=x ; 0:c077ac0000000000 +series __name__=a,host=h1,job=x ; 1:408a780000000000 +series __name__=b,host=h2,job=x ; 1:4081d90000000000 +series __name__=b,host=h2,job=y ; 1:405e400000000000 +series __name__=b,host=h1,job=x ; 1:c08b8b0000000000 +error found duplicate series for the match group {__name__="b", host="h2"} on the right hand-side of the operation: [{__name__="b", host="h2", job="x"}, {__name__="b", host="h2", job="y"}];many-to-many matching not allowed: matching labels must be unique on one side +end +case binary - false ignoring host right host,zone - NaN +grid 100000 10000 3 +series __name__=a,host=h2,job=y ; 0:c070160000000000 +series __name__=a,host=h1,job=y ; 1:4045a00000000000 +series __name__=a,host=h1,job=x ; 2:0 +series __name__=a,host=h2,job=x ; 2:c078b00000000000 +series __name__=b,host=h1,job=y ; 1:408c3a0000000000 +series __name__=b,host=h1,job=x ; 0:7e37e43c8800759c +error found duplicate series for the match group {job="x"} on the left hand-side of the operation: [{__name__="a", host="h1", job="x"}, {__name__="a", host="h2", job="x"}];many-to-many matching not allowed: matching labels must be unique on one side +end +case binary <= true ignoring - right host,zone NaN 1 +grid 100000 10000 2 +series __name__=a,host=h1,job=y ; 0:7ff0000000000000 +series __name__=b,host=h2,job=y ; 0:c085090000000000 +result host=h1,job=y ; 0:0 +result host=h2,job=y ; 0:0 +end +case binary > true on - right - Inf NaN +grid 100000 10000 12 +series __name__=a,host=h1,job=x ; 0:c070e80000000000 +series __name__=a,host=h1,job=y ; 8:7ff8000000000001 9:408bb70000000000 +series __name__=a,host=h1,job=x,zone=z1 ; 6:c082ef0000000000 7:407a6c0000000000 +series __name__=a,host=h2,job=x ; 6:c082ec0000000000 7:c063480000000000 +series __name__=b,host=h1,job=x ; +error found duplicate series for the match group {} on the left hand-side of the operation: [{__name__="a", host="h1", job="x", zone="z1"}, {__name__="a", host="h2", job="x"}];many-to-many matching not allowed: matching labels must be unique on one side +end +case binary >= true ignoring job,zone one - -2.5 1 +grid 100000 10000 3 +series __name__=a,host=h2,job=y,zone=z1 ; 0:c08b540000000000 1:c05f100000000000 2:7ff0000000000000 +series __name__=a,host=h1,job=x ; 2:407fe80000000000 +series __name__=a,host=h1,job=x,zone=z1 ; 1:406ba40000000000 2:c07a1a0000000000 +series __name__=a,host=h1,job=y ; 1:c06e900000000000 +series __name__=b,host=h2,job=y,zone=z1 ; 1:408ebc0000000000 2:407d680000000000 +series __name__=b,host=h1,job=y,zone=z2 ; 2:c078580000000000 +error multiple matches for labels: many-to-one matching must be explicit (group_left/group_right) +end +case binary / false on host right zone - NaN +grid 100000 10000 8 +series __name__=a,host=h2,job=y ; 2:408bbe0000000000 3:4089960000000000 4:c070580000000000 5:c0705e0000000000 +series __name__=a,host=h2,job=x ; 7:7ff8000000000001 +series __name__=a,host=h2,job=x,zone=z1 ; 2:4087830000000000 3:7ff8000000000001 5:c085c90000000000 +series __name__=a,host=h1,job=x ; 5:4082470000000000 6:7e37e43c8800759c +series __name__=a,host=h2,job=y,zone=z1 ; +series __name__=b,host=h1,job=x ; +series __name__=b,host=h1,job=y ; 0:c083ed0000000000 1:7ff0000000000000 +error found duplicate series for the match group {host="h2"} on the left hand-side of the operation: [{__name__="a", host="h2", job="x", zone="z1"}, {__name__="a", host="h2", job="y"}];many-to-many matching not allowed: matching labels must be unique on one side +end +case binary >= true ignoring zone left host 1 - +grid 100000 10000 5 +series __name__=a,host=h1,job=y,zone=z1 ; 4:40864d0000000000 +series __name__=b,host=h1,job=y,zone=z1 ; 1:4079360000000000 2:4050580000000000 3:c0750a0000000000 +series __name__=b,host=h1,job=y ; 2:c077480000000000 3:40684c0000000000 +series __name__=b,host=h1,job=x,zone=z1 ; +error found duplicate series for the match group {host="h1", job="y"} on the right hand-side of the operation: [{__name__="b", host="h1", job="y", zone="z1"}, {__name__="b", host="h1", job="y"}];many-to-many matching not allowed: matching labels must be unique on one side +end +case binary < true ignoring host one - -2.5 -2.5 +grid 100000 10000 5 +series __name__=a,host=h2,job=y,zone=z1 ; 2:c078dc0000000000 3:407b280000000000 4:c0888c0000000000 +series __name__=b,host=h2,job=y ; 2:4088770000000000 3:c087ea0000000000 +series __name__=b,host=h2,job=y,zone=z1 ; 1:7ff0000000000000 +result job=y ; 2:3ff0000000000000 3:0 +result job=y,zone=z1 ; 1:3ff0000000000000 2:3ff0000000000000 3:0 4:3ff0000000000000 +end +case binary >= true none - one - NaN - +grid 100000 10000 12 +series __name__=a,host=h2,job=y ; 10:c08f3e0000000000 11:c08a890000000000 +series __name__=a,host=h2,job=x,zone=z1 ; 9:c082c70000000000 +series __name__=b,host=h2,job=x,zone=z1 ; 10:c082130000000000 11:40824e0000000000 +series __name__=b,host=h1,job=y ; 2:406f200000000000 4:c08ce30000000000 5:4079520000000000 6:c085e50000000000 7:c07a180000000000 8:c08f3c0000000000 9:7ff8000000000001 10:c067cc0000000000 +series __name__=b,host=h2,job=x,zone=z2 ; 1:4048b00000000000 2:4085430000000000 3:c082f60000000000 +result host=h1,job=y ; 2:0 4:0 5:0 6:0 7:0 8:0 9:0 10:0 +result host=h2,job=x,zone=z1 ; 10:0 11:0 +result host=h2,job=x,zone=z2 ; 1:0 2:0 3:0 +end +case binary >= false ignoring job one - - Inf +grid 100000 10000 2 +series __name__=a,host=h1,job=y ; 1:0 +series __name__=b,host=h1,job=y,zone=z1 ; 0:c05e080000000000 1:fff0000000000000 +series __name__=b,host=h1,job=x ; 1:408e280000000000 +series __name__=b,host=h1,job=y,zone=z2 ; 0:4042c00000000000 +end +case binary * false none - one - NaN - +grid 100000 10000 12 +series __name__=a,host=h2,job=y ; 2:7ff0000000000000 +series __name__=a,host=h1,job=x ; 5:4055f00000000000 6:407eba0000000000 7:c080390000000000 9:c087cc0000000000 10:c08baf0000000000 +series __name__=b,host=h2,job=y ; 11:4085e30000000000 +series __name__=b,host=h2,job=y,zone=z1 ; 10:fff0000000000000 11:40722e0000000000 +series __name__=b,host=h1,job=y,zone=z1 ; 6:407bf40000000000 7:4080390000000000 +result host=h1,job=y,zone=z1 ; 6:7ff8000000000001 7:7ff8000000000001 +result host=h2,job=y ; 11:7ff8000000000001 +result host=h2,job=y,zone=z1 ; 10:7ff8000000000001 11:7ff8000000000001 +end +case binary ^ false on job right - NaN NaN +grid 100000 10000 3 +series __name__=a,host=h1,job=x,zone=z1 ; 1:406de40000000000 2:c07c5a0000000000 +series __name__=a,host=h1,job=x ; 2:c06ad40000000000 +series __name__=a,host=h2,job=x,zone=z1 ; 0:c08c650000000000 +series __name__=b,host=h2,job=x,zone=z1 ; 2:c06f500000000000 +series __name__=b,host=h2,job=x,zone=z2 ; 2:c06f740000000000 +error found duplicate series for the match group {job="x"} on the left hand-side of the operation: [{__name__="a", host="h1", job="x", zone="z1"}, {__name__="a", host="h1", job="x"}];many-to-many matching not allowed: matching labels must be unique on one side +end +case binary % false on - right host,zone - NaN +grid 100000 10000 12 +series __name__=a,host=h2,job=y ; 5:408df90000000000 6:c074560000000000 7:c0832f0000000000 8:c076780000000000 10:c08c2d0000000000 +series __name__=a,host=h2,job=x,zone=z1 ; 10:4085020000000000 11:4075280000000000 +series __name__=b,host=h2,job=y ; 11:c070e80000000000 +error found duplicate series for the match group {} on the left hand-side of the operation: [{__name__="a", host="h2", job="x", zone="z1"}, {__name__="a", host="h2", job="y"}];many-to-many matching not allowed: matching labels must be unique on one side +end diff --git a/src/promql/src/vector_match/tests.rs b/src/promql/src/vector_match/tests.rs index 31a5f9bcc2c..3fac9bfb07e 100644 --- a/src/promql/src/vector_match/tests.rs +++ b/src/promql/src/vector_match/tests.rs @@ -23,7 +23,8 @@ use rand::rngs::StdRng; use rand::{Rng, SeedableRng}; use super::{ - BinaryMatching, BinaryOp, Cardinality, MatchingLabels, SetOp, binary_vectors, set_vectors, + BinaryMatching, BinaryOp, Cardinality, FillValues, MatchingLabels, SetOp, binary_vectors, + set_vectors, }; use crate::grid_series::GridSeriesBuilder; use crate::grid_series::reference::{ @@ -481,6 +482,7 @@ fn binary_vectors_match_prometheus_reference() { return_bool, labels, cardinality, + fill: FillValues::default(), }; let fields = lhs_fields.min(rhs_fields); let points = grid.timestamps().len(); @@ -555,6 +557,7 @@ fn rejects_operands_on_other_grids_and_unpaired_or_fields() { return_bool: false, labels: MatchingLabels::Ignoring(Vec::new()), cardinality: Cardinality::OneToOne, + fill: FillValues::default(), }; assert!(binary_vectors(&matching, &lhs.schema, &lhs.series, &schema(1, &other), &[]).is_err()); assert!(matches!( @@ -614,6 +617,7 @@ fn reports_the_earliest_failing_point() { return_bool: false, labels: MatchingLabels::On(vec![Arc::from("job")]), cardinality: Cardinality::ManyToOne(Vec::new()), + fill: FillValues::default(), }; let error = binary_vectors(&matching, &schema, &lhs, &schema, &rhs) .unwrap_err() @@ -655,6 +659,26 @@ fn canonical_bits(value: f64) -> u64 { /// `prometheus_cases.txt`. #[test] fn vector_operations_match_prometheus() { + let (matched, failed) = check_cases(include_str!("prometheus_cases.txt")); + assert!( + matched > 100 && failed > 20, + "{matched} matched, {failed} failed" + ); +} + +/// Binary operations with fill modifiers, evaluated by the Prometheus v3.15.0 engine; see the +/// header of `fill_cases.txt`. +#[test] +fn fill_modifiers_match_prometheus() { + let (matched, failed) = check_cases(include_str!("fill_cases.txt")); + assert!( + matched > 100 && failed > 20, + "{matched} matched, {failed} failed" + ); +} + +/// Checks each case of a fixture and returns how many matched results and how many failed. +fn check_cases(text: &str) -> (usize, usize) { let parse_labels = |labels: &str| -> Labels { labels .split(',') @@ -672,7 +696,6 @@ fn vector_operations_match_prometheus() { .collect() }; let (mut matched, mut failed) = (0, 0); - let text = include_str!("prometheus_cases.txt"); for case in text.split("end\n").filter(|case| case.contains("case ")) { let lines: Vec<&str> = case.lines().filter(|line| !line.starts_with('#')).collect(); let spec: Vec<&str> = lines[0].split_whitespace().skip(1).collect(); @@ -729,6 +752,10 @@ fn vector_operations_match_prometheus() { other => panic!("unexpected line {other}"), } } + let fill = |value: Option<&&str>| match value { + None | Some(&"-") => None, + Some(value) => Some(value.parse::().unwrap()), + }; let labels = |kind: &str, list: &str| match kind { "on" => MatchingLabels::On(names(list)), _ => MatchingLabels::Ignoring(names(list)), @@ -760,6 +787,10 @@ fn vector_operations_match_prometheus() { "right" => Cardinality::OneToMany(names(spec[6])), _ => Cardinality::OneToOne, }, + fill: FillValues { + lhs: fill(spec.get(7)), + rhs: fill(spec.get(8)), + }, }; binary_vectors(&matching, &schema, &lhs, &schema, &rhs) } @@ -806,10 +837,7 @@ fn vector_operations_match_prometheus() { (actual, expected) => panic!("{case}\ngot {actual:?}, expected error {expected:?}"), } } - assert!( - matched > 100 && failed > 20, - "{matched} matched, {failed} failed" - ); + (matched, failed) } /// Results that share a label set once `__type__` is dropped fail as a grouping violation within @@ -858,6 +886,7 @@ fn colliding_results_fail_by_where_they_collide() { return_bool: false, labels, cardinality: Cardinality::ManyToOne(Vec::new()), + fill: FillValues::default(), }; let error = binary_vectors(&matching, &schema, &lhs, &schema, rhs) .unwrap_err() @@ -1002,6 +1031,7 @@ fn histogram_operators_match_prometheus() { "right" => Cardinality::OneToMany(Vec::new()), _ => Cardinality::OneToOne, }, + fill: FillValues::default(), }; binary_vectors(&matching, &schema, &lhs, &schema, &rhs) } @@ -1127,12 +1157,66 @@ fn colliding_reset_hints_skip_the_pair() { return_bool: false, labels: MatchingLabels::On(vec![Arc::from("job")]), cardinality: Cardinality::OneToOne, + fill: FillValues::default(), }; let result = binary_vectors(&matching, &schema, &lhs, &schema, &rhs).unwrap(); assert_eq!(result.len(), 1, "{op:?}"); } } +/// A pair whose operation fails is not a match, so `fill_left` still fills for the right-hand +/// sample of that pair; the fixtures hold floats only. +#[test] +fn a_failed_pair_leaves_its_sample_to_the_fill() { + let grid = Arc::new(EvalGrid::try_new(GridSpec::try_new(0, 10_000, 1).unwrap()).unwrap()); + let schema = Arc::new(SeriesSchema { + fields: vec![FieldSpec { + name: Arc::from("value"), + }] + .into(), + eval_grid: Some(grid), + }); + let labels = |pairs: &[(&str, &str)]| -> Labels { + pairs + .iter() + .map(|(name, value)| (name.to_string(), value.to_string())) + .collect() + }; + let histogram = "0/0/0/4014000000000000/4014000000000000/0/0_1/4014000000000000///"; + let lhs = [mixed_series( + &schema, + &labels(&[("__name__", "a"), ("job", "x")]), + &BTreeMap::new(), + &BTreeMap::from([(0, histogram.to_string())]), + ) + .unwrap()]; + let rhs = [mixed_series( + &schema, + &labels(&[("__name__", "b"), ("job", "x")]), + &BTreeMap::from([(0, 2.0f64.to_bits())]), + &BTreeMap::new(), + ) + .unwrap()]; + // A histogram plus a float is skipped, so `b` is unmatched: 10 + 2 under the match labels. + let matching = BinaryMatching { + op: BinaryOp::Add, + return_bool: false, + labels: MatchingLabels::On(vec![Arc::from("job")]), + cardinality: Cardinality::OneToOne, + fill: FillValues { + lhs: Some(10.0), + rhs: None, + }, + }; + let result = binary_vectors(&matching, &schema, &lhs, &schema, &rhs).unwrap(); + assert_eq!(result.len(), 1); + assert_eq!(labels_of(&result[0].metric), labels(&[("job", "x")])); + assert!(matches!( + result[0].fields.as_slice()[0].sample(0), + SampleRef::Float(value) if value == 12.0 + )); +} + /// A binary pairing walks two operands of many single-position runs and still reports every /// position exactly once. /// @@ -1170,6 +1254,7 @@ fn a_pairing_reads_many_single_position_runs_once() { return_bool: false, labels: MatchingLabels::On(vec![Arc::from("job")]), cardinality: Cardinality::OneToOne, + fill: FillValues::default(), }; let result = binary_vectors( &matching, @@ -1218,6 +1303,7 @@ fn a_binary_pairing_of_sparse_operands_keeps_the_shared_positions() { return_bool: false, labels: MatchingLabels::Ignoring(Vec::new()), cardinality: Cardinality::OneToOne, + fill: FillValues::default(), }; let result = binary_vectors( &matching, @@ -1249,6 +1335,7 @@ fn a_binary_pairing_of_sparse_operands_keeps_the_shared_positions() { return_bool: false, labels: MatchingLabels::Ignoring(Vec::new()), cardinality: Cardinality::OneToOne, + fill: FillValues::default(), }; let result = binary_vectors( &filtered, @@ -1278,6 +1365,7 @@ fn a_binary_pairing_of_sparse_operands_keeps_the_shared_positions() { return_bool: true, labels: MatchingLabels::Ignoring(Vec::new()), cardinality: Cardinality::OneToOne, + fill: FillValues::default(), }; let result = binary_vectors( &booled, diff --git a/src/query/src/promql/native_compile.rs b/src/query/src/promql/native_compile.rs index cc5ff1bf8c0..56ea7445b76 100644 --- a/src/query/src/promql/native_compile.rs +++ b/src/query/src/promql/native_compile.rs @@ -45,7 +45,9 @@ //! The statement's grid, the selector's lookback, window, offset and anchor are the only numbers //! derived here; the legality of the tree is not checked here. [`NativeTree::try_new`] is the //! single authority on that, so a node added later cannot be legal in one place and illegal in -//! another. +//! another. What is refused before a tree exists is the scan's shape: no shape below can consume +//! chunks of different series that interleave, so an `Interleaved` ordering is refused once, at the +//! entry, and [`NativeNode::validate`] refuses the same ordering for a tree built some other way. use std::sync::Arc; use std::time::Duration; @@ -86,13 +88,12 @@ const SUPPORTED: &str = "a selector, a range function over a range selector — /// one. /// /// `ordering` is what the caller's scan guarantees, and the tree records it rather than assuming -/// one. Only `Contiguous` and `KeyThenTime` are accepted: a native evaluation reads one series to -/// its last chunk before the next, so chunks of different series may not interleave, and an -/// `Interleaved` declaration is refused here for every shape this compiler accepts. That is where -/// the refusal belongs: no accepted shape can consume interleaved chunks, so a tree declaring one -/// cannot be built here at all. [`NativeTree::validate`]'s window branch checks the same thing, so -/// if both land this refusal comes first and the tree check is a backstop for a tree built some -/// other way. +/// one. Only `Contiguous` and `KeyThenTime` are accepted: every shape this compiler accepts reads +/// one series to its last chunk before the next, so an `Interleaved` declaration is refused at the +/// entry, before any shape is built — the bare selector included, which returns before a window +/// exists — and a shape added later inherits the refusal instead of restating it. +/// [`NativeNode::validate`] refuses the same ordering in its `Instant` and `Window` branches, as a +/// backstop for a tree built some other way. /// /// The tree carries no metric name and no matchers, so `rate(x{job="a"}[5m])` and `rate(y[5m])` /// compile to identical trees, and an `EXPLAIN` built on this display cannot tell them apart. That diff --git a/src/query/src/promql/native_compile/tests.rs b/src/query/src/promql/native_compile/tests.rs index 055e50d42a0..f16bb74b072 100644 --- a/src/query/src/promql/native_compile/tests.rs +++ b/src/query/src/promql/native_compile/tests.rs @@ -689,11 +689,11 @@ fn the_numeric_edges_of_a_statement_are_bound_as_given() { SeriesOrdering::KeyThenTime, Expected::Rejected(Rejection::Plan), ), - // A range window reads one series to its last chunk before the next, so a scan whose - // chunks of different series interleave is refused instead of recorded onto an input the - // window would fail on. + // Every shape reads one series to its last chunk before the next, so a scan whose chunks + // of different series interleave is refused instead of recorded onto an input that shape + // would fail on. ( - "an interleaved ordering, which a window cannot consume to its last chunk", + "an interleaved ordering, which no shape can consume to its last chunk", eval_stmt("rate(x[5m])"), SeriesOrdering::Interleaved, Expected::Rejected(Rejection::NotImplemented), @@ -765,6 +765,46 @@ fn the_numeric_edges_of_a_statement_are_bound_as_given() { } } +/// An interleaved scan is refused whatever shape the statement has. The refusal sits at the entry +/// of the compiler rather than in one shape's branch, because a bare selector returns before any +/// window exists and would otherwise be accepted with an ordering it cannot be evaluated on. +#[test] +fn an_interleaved_scan_is_refused_whatever_the_shape() { + // The plain range function is the ordering table's case, which compiles `rate(x[5m])` under + // `Interleaved` and expects the same refusal; these cover the shapes that table cannot: the + // bare selector, a math function over one, and a parameterised range function. + for query in ["x", "abs(x)", "quantile_over_time(0.9, x[5m])"] { + let error = compile(&eval_stmt(query), raw_schema(), SeriesOrdering::Interleaved) + .expect_err(&format!("`{query}` compiled over an interleaved scan")); + assert!( + matches!(error, DataFusionError::NotImplemented(_)), + "`{query}` did not report NotImplemented: {error}" + ); + let message = error.to_string(); + assert!( + message.contains("reads one series to its last chunk before the next"), + "`{query}` did not say why the ordering is refused: {message}" + ); + } + // The same scan compiles a bare selector when its chunks are contiguous, and the ordering it + // was compiled under is recorded on the tree's input rather than dropped: that is what makes + // the refusals above about the ordering rather than about the shape. + let tree = compile(&eval_stmt("x"), raw_schema(), SeriesOrdering::Contiguous) + .expect("a contiguous scan compiles a bare selector"); + let (input, _) = root_instant(&tree); + let NativeExpr::Input(bound) = &input.expr else { + panic!( + "an instant selection's input is not an input: {:?}", + input.expr + ); + }; + assert_eq!( + bound.ordering, + SeriesOrdering::Contiguous, + "the instant selection dropped the scan's ordering" + ); +} + /// A shape outside the subset is refused, and the error says what was found so the query can be /// rewritten rather than guessed at. #[test] @@ -1119,28 +1159,3 @@ fn a_math_function_over_a_non_vector_argument_is_refused_and_names_it() { ); } } - -/// An interleaved scan is refused for every shape this compiler accepts, not only for the window -/// that first needed the guarantee: a native evaluation reads one series to its last chunk before -/// the next, so an instant over raw samples cannot consume interleaved chunks either. The refusal -/// sits at the entry so that a shape added later inherits it, and a contiguous scan still compiles -/// as the control that the refusal is about the ordering rather than about the queries. -#[test] -fn an_interleaved_scan_is_refused_whatever_the_shape() { - for query in ["x", "abs(x)", "rate(x[5m])"] { - let error = compile(&eval_stmt(query), raw_schema(), SeriesOrdering::Interleaved) - .expect_err(&format!("`{query}` compiled over an interleaved scan")); - assert!( - matches!(error, DataFusionError::NotImplemented(_)), - "`{query}` did not report NotImplemented: {error}" - ); - let message = error.to_string(); - assert!( - message.contains("interleave"), - "`{query}` did not say why an interleaved scan cannot be read: {message}" - ); - } - - compile(&eval_stmt("x"), raw_schema(), SeriesOrdering::Contiguous) - .expect("`x` did not compile over a contiguous scan"); -} diff --git a/src/query/src/query_engine.rs b/src/query/src/query_engine.rs index 51f7d7ce477..c512b9c1e5e 100644 --- a/src/query/src/query_engine.rs +++ b/src/query/src/query_engine.rs @@ -29,11 +29,9 @@ use common_function::handlers::{ FlowServiceHandlerRef, ProcedureServiceHandlerRef, TableMutationHandlerRef, }; use common_query::Output; -use common_query::request::{PreparedNamedRawSeries, RawSeriesRequest}; use datafusion::catalog::TableFunction; use datafusion::dataframe::DataFrame; use datafusion_expr::{AggregateUDF, LogicalPlan, WindowUDF}; -use datatypes::series::SeriesOrdering; pub use default_serializer::{DefaultPlanDecoder, DefaultSerializer}; use partition::manager::PartitionRuleManagerRef; use session::context::QueryContextRef; @@ -79,44 +77,6 @@ pub trait QueryEngine: Send + Sync { /// Execute the given [`LogicalPlan`]. async fn execute(&self, plan: LogicalPlan, query_ctx: QueryContextRef) -> Result; - /// Non-consumingly checks whether a native single-region raw-series scan is supported. - /// Returns its ordering on support, `None` when explicitly unsupported, and propagates errors. - /// This point-in-time check does not reserve a plan; execution revalidates support. - async fn prepare_raw_series( - &self, - _plan: LogicalPlan, - _query_ctx: QueryContextRef, - _binding: RawSeriesRequest, - ) -> Result> { - Ok(None) - } - - /// Non-consumingly checks whether a named raw-series preflight is supported. - /// - /// Returns the approved binding and ordering on support, or `None` when the plan is - /// explicitly unsupported. Errors propagate unchanged. This point-in-time check does not - /// reserve a plan; execution revalidates support. - async fn prepare_named_raw_series( - &self, - _plan: LogicalPlan, - _query_ctx: QueryContextRef, - ) -> Result> { - Ok(None) - } - - /// Execute a native single-region raw-series scan. - async fn execute_raw_series( - &self, - _plan: LogicalPlan, - _query_ctx: QueryContextRef, - _binding: RawSeriesRequest, - ) -> Result { - crate::error::UnimplementedSnafu { - operation: "native raw-series query", - } - .fail() - } - /// Register an aggregate function. /// /// # Panics diff --git a/src/query/src/region_query.rs b/src/query/src/region_query.rs index 96c843893bb..4d2dfb68d60 100644 --- a/src/query/src/region_query.rs +++ b/src/query/src/region_query.rs @@ -18,7 +18,7 @@ use api::v1::region::{RemoteDynFilterUnregister, RemoteDynFilterUpdate}; use async_trait::async_trait; use common_meta::node_manager::NodeManagerRef; use common_meta::peer::Peer; -use common_query::request::{PreparedNamedRawSeries, QueryRequest}; +use common_query::request::QueryRequest; use common_recordbatch::SendableRecordBatchStream; use partition::manager::PartitionRuleManagerRef; use session::ReadPreference; @@ -68,20 +68,6 @@ pub trait RegionQueryHandler: Send + Sync { request: QueryRequest, ) -> Result; - /// Non-consumingly checks whether a named raw-series preflight is supported. - /// - /// Returns the approved binding and ordering on support, or `None` when the request is - /// explicitly unsupported; failures that are not unsupported preparation are returned as - /// errors. This point-in-time check does not reserve a plan, and execution revalidates - /// support. - async fn prepare_named_raw_series_query( - &self, - _target: &RegionQueryTarget, - _request: QueryRequest, - ) -> Result> { - Ok(None) - } - async fn handle_remote_dyn_filter_update( &self, target: &RegionQueryTarget, diff --git a/tests/compatibility/fixtures/series_index/README.md b/tests/compatibility/fixtures/series_index/README.md new file mode 100644 index 00000000000..29c3000f838 --- /dev/null +++ b/tests/compatibility/fixtures/series_index/README.md @@ -0,0 +1,10 @@ +# Series-index catalog compatibility + +`legacy_catalog.json` is a synthetic fixture matching `SeriesIndexEntry` at +`3ad5f963285e51166fd8789920e4b3bf2cf60423`, before optional artifact metadata. +It is not captured from a released binary. Its entry must remain a reusable +Parquet index and serialize without adding an artifact field. + +Run `cargo nextest run -p mito2 -E 'test(legacy_series_catalog_fixture)'`. +The owning test also checks the persisted artifact format names. Ordinary +index-reader tests exercise Parquet files without a full-primary-key column. diff --git a/tests/compatibility/fixtures/series_index/legacy_catalog.json b/tests/compatibility/fixtures/series_index/legacy_catalog.json new file mode 100644 index 00000000000..e119e602a8d --- /dev/null +++ b/tests/compatibility/fixtures/series_index/legacy_catalog.json @@ -0,0 +1,27 @@ +{ + "indexes": [ + { + "file_size": 123, + "index_uuid": "00000000-0000-0000-0000-000000000001", + "bucket_start": { + "value": 0, + "unit": "Second" + }, + "bucket_end": { + "value": 100, + "unit": "Second" + }, + "source_file_ids": [], + "min_file_sequence": 1, + "max_file_sequence": 2, + "compaction_window_secs": 100, + "window_sequences": { + "0": { + "start": 0, + "end": 100, + "max_sequence": 2 + } + } + } + ] +} diff --git a/tests/compatibility/wire/named_raw_series_prepare/README.md b/tests/compatibility/wire/named_raw_series_prepare/README.md deleted file mode 100644 index 0175bdbeb3d..00000000000 --- a/tests/compatibility/wire/named_raw_series_prepare/README.md +++ /dev/null @@ -1,32 +0,0 @@ -# Named raw-series preflight contract v1 - -`v1_supported.json` and `v1_unsupported.json` are hand-authored, frozen JSON -bodies of the named raw-series preflight action -`greptime.prepare_raw_series.v1` (`PREPARE_RAW_SERIES_ACTION`). - -- `v1_supported.json` approves the request with the `contiguous` ordering. Its - nested `binding` value is the same version-1 request frozen in - [`../raw_series_request/v1_request.json`](../raw_series_request/v1_request.json) - (identical JSON text; the envelope embeds it without the source file's - trailing newline). -- `v1_unsupported.json` is the frozen unsupported shape, exactly - `{"version":1,"supported":null}`: the version-1 envelope with an explicit - `null` `supported` field. - -The envelope requires `version` (only `1` is supported) and a present-but-nullable -`supported` field. An approving `supported` object rejects unknown fields and -carries exactly the existing raw-series request plus an ordering of `contiguous` -or `read_key_then_time`; `interleaved`, unknown or missing orderings, duplicate -keys, unknown fields, and malformed bindings are rejected. The nested request -keeps its existing deserializer and validation rules; the nested physical -metadata is not checked for unknown fields by this contract. - -Run the focused check: - -```shell -cargo nextest run -p common-query named_raw_series -``` - -The fixture proves the version-1 envelope decodes as written and that the -unsupported shape is frozen. It does not prove interoperability with an old -binary; it records the contract this revision writes and reads. diff --git a/tests/compatibility/wire/named_raw_series_prepare/v1_supported.json b/tests/compatibility/wire/named_raw_series_prepare/v1_supported.json deleted file mode 100644 index e201cebe3b0..00000000000 --- a/tests/compatibility/wire/named_raw_series_prepare/v1_supported.json +++ /dev/null @@ -1 +0,0 @@ -{"version":1,"supported":{"binding":{"version":1,"physical_metadata":{"column_metadatas":[{"column_schema":{"name":"host","data_type":{"Int64":{}},"is_nullable":false,"is_time_index":false,"default_constraint":null,"metadata":{}},"semantic_type":"Tag","column_id":1},{"column_schema":{"name":"value","data_type":{"Int32":{}},"is_nullable":false,"is_time_index":false,"default_constraint":null,"metadata":{}},"semantic_type":"Field","column_id":2},{"column_schema":{"name":"aux","data_type":{"Int64":{}},"is_nullable":false,"is_time_index":false,"default_constraint":null,"metadata":{}},"semantic_type":"Field","column_id":3},{"column_schema":{"name":"ts","data_type":{"Timestamp":{"Millisecond":null}},"is_nullable":false,"is_time_index":true,"default_constraint":null,"metadata":{}},"semantic_type":"Timestamp","column_id":4}],"primary_key":[1],"region_id":4294967297,"schema_version":0,"primary_key_encoding":"dense"},"metric_name":"cpu","logical_tags":[{"column_id":1,"name":"host"}],"field_ids":[3,2]},"ordering":"contiguous"}} diff --git a/tests/compatibility/wire/named_raw_series_prepare/v1_unsupported.json b/tests/compatibility/wire/named_raw_series_prepare/v1_unsupported.json deleted file mode 100644 index cd2aeccb8a6..00000000000 --- a/tests/compatibility/wire/named_raw_series_prepare/v1_unsupported.json +++ /dev/null @@ -1 +0,0 @@ -{"version":1,"supported":null}