From 5b649daaeaf019616cb6e6b05798e73a54b73227 Mon Sep 17 00:00:00 2001 From: discord9 <55937128+discord9@users.noreply.github.com> Date: Sat, 17 Oct 2026 11:30:55 +0800 Subject: [PATCH 01/13] feat(mito2): produce typed raw series from RegionScanner with truthful ordering Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com> --- Cargo.lock | 1 + src/mito2/Cargo.toml | 1 + src/mito2/src/engine/scan_test.rs | 918 +++++++++++++++++++- src/mito2/src/read/series_scan.rs | 383 ++++---- src/mito2/src/read/series_scan/raw.rs | 13 +- src/mito2/src/read/series_scan/raw/tests.rs | 76 +- 6 files changed, 1211 insertions(+), 181 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ea2a530d6b5..43d65a03bd8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8742,6 +8742,7 @@ dependencies = [ "partition", "pin-project", "prometheus 0.14.0", + "promql", "prost 0.14.1", "puffin", "rand 0.9.4", diff --git a/src/mito2/Cargo.toml b/src/mito2/Cargo.toml index 02c134e964c..151694c40d1 100644 --- a/src/mito2/Cargo.toml +++ b/src/mito2/Cargo.toml @@ -101,6 +101,7 @@ dotenv.workspace = true log-store = { workspace = true, features = ["testing"] } mito-codec = { workspace = true, features = ["testing"] } object-store = { workspace = true, features = ["services-memory"] } +promql.workspace = true rskafka.workspace = true rstest.workspace = true rstest_reuse.workspace = true diff --git a/src/mito2/src/engine/scan_test.rs b/src/mito2/src/engine/scan_test.rs index a893a234adc..3ad13fac11e 100644 --- a/src/mito2/src/engine/scan_test.rs +++ b/src/mito2/src/engine/scan_test.rs @@ -4772,6 +4772,7 @@ async fn test_mito_handle_query_raw_series_candidate_projection_and_mvcc() { use datatypes::series::samples::SampleRef; use datatypes::value::Value as DatumValue; use mito_codec::row_converter::build_primary_key_codec; + use promql::source::materialize_raw_labels; use store_api::region_engine::{BoundTag, QueryScanContext, RawLabelSource}; use store_api::region_request::RegionDeleteRequest; use table::predicate::Predicate; @@ -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,133 @@ 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::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(); + let unpacked = unpack_raw_series( + native_frames, + native_schema, + native_source, + SeriesOrdering::Contiguous, + ) + .unwrap(); + let native_schema = unpacked.schema().clone(); + let materialized = materialize_raw_labels(unpacked).unwrap(); + assert!(matches!( + materialized.label_source(), + RawLabelSource::Explicit + )); + let mut native = materialized.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 metric = chunk.metric.as_ref().expect("materialized complete metric"); + assert_eq!(Some("fixture_metric"), metric.name()); + let labels = metric + .tags() + .iter() + .map(|label| (label.name.as_ref(), label.value.as_ref())) + .collect::>(); + assert_eq!(1, labels.len()); + let tag = labels["tag_0"].to_string(); + assert!(tag == "A" || tag == "B"); + let times = chunk.time.as_buffer(); + let key = chunk.primary_key.to_vec(); + 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.clone(), + *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 +5230,602 @@ async fn test_mito_handle_query_raw_series_candidate_projection_and_mvcc() { ); } +#[tokio::test] +async fn test_mito_regional_raw_merge_preserves_global_key_order_and_labels() { + use api::v1::{Row, Value}; + use common_query::series_frame::{pack_raw_series, unpack_raw_series}; + use datatypes::arrow::array::Array; + use datatypes::series::SeriesOrdering; + use datatypes::series::samples::{SampleRef, SeriesSchema}; + use datatypes::value::Value as DatumValue; + use mito_codec::row_converter::build_primary_key_codec; + use promql::source::{materialize_raw_labels, merge_ordered_raw_series}; + use store_api::region_engine::{BoundTag, QueryScanContext, RawLabelSource}; + use store_api::region_request::RegionDeleteRequest; + + let mut env = TestEnv::with_prefix("test_mito_regional_raw_merge").await; + let engine = env.create_engine(MitoConfig::default()).await; + let regions = [RegionId::new(1, 1), RegionId::new(1, 2)]; + let mut request = CreateRequestBuilder::new().field_num(3).build(); + request + .options + .insert("memtable.type".into(), "bulk".into()); + let schema = test_util::rows_schema(&request); + for region in regions { + engine + .handle_request(region, RegionRequest::Create(request.clone())) + .await + .unwrap(); + } + let make_row = |tag: &str, f0: Option, f1: f64, gate: f64, time: i64| Row { + values: vec![ + Some(ValueData::StringValue(tag.into())), + f0.map(ValueData::F64Value), + Some(ValueData::F64Value(f1)), + Some(ValueData::F64Value(gate)), + Some(ValueData::TimestampMillisecondValue(time)), + ] + .into_iter() + .map(|value_data| Value { value_data }) + .collect(), + }; + for (region, rows) in [ + ( + regions[0], + vec![ + make_row("A", Some(10.0), 1.0, 1.0, 10), + make_row("A", None, 3.0, 1.0, 30), + make_row("B", Some(-0.0), 5.0, 1.0, 10), + ], + ), + ( + regions[1], + vec![ + make_row("A", Some(20.0), 2.0, 1.0, 20), + make_row("A", Some(40.0), 4.0, 1.0, 40), + make_row("C", Some(60.0), 6.0, 1.0, 10), + ], + ), + ] { + test_util::put_rows( + &engine, + region, + Rows { + schema: schema.clone(), + rows, + }, + ) + .await; + } + test_util::flush_region(&engine, regions[0], None).await; + test_util::flush_region(&engine, regions[1], None).await; + test_util::put_rows( + &engine, + regions[0], + Rows { + schema: schema.clone(), + rows: vec![ + make_row("A", Some(11.0), 11.0, 1.0, 10), + make_row("A", Some(50.0), 5.0, 0.0, 50), + make_row("B", Some(60.0), 6.0, 1.0, 60), + ], + }, + ) + .await; + engine + .handle_request( + regions[0], + RegionRequest::Delete(RegionDeleteRequest { + rows: Rows { + schema: test_util::delete_rows_schema( + &CreateRequestBuilder::new().field_num(3).build(), + ), + rows: vec![row(vec![ + ValueData::StringValue("B".into()), + ValueData::TimestampMillisecondValue(60), + ])], + }, + hint: None, + partition_expr_version: None, + }), + ) + .await + .unwrap(); + let request = ScanRequest { + projection: Some(vec![2, 1, 4]), + filters: vec![col("field_2").gt(lit(0.0_f64))], + distribution: Some(TimeSeriesDistribution::PerSeries), + preserve_pk_dictionary_encoding: true, + ..Default::default() + }; + let mut streams = Vec::new(); + let mut metadata_for_codec = None; + let mut expected_schema: Option> = None; + let metrics = ExecutionPlanMetricsSet::default(); + for region in regions { + let selected = engine.scanner(region, request.clone()).await.unwrap(); + let mut scanner: Box = match selected { + Scanner::Series(series) => { + assert_eq!("legacy", series.mode()); + Box::new(series) + } + _ => panic!("PerSeries request must select a series scanner"), + }; + assert_eq!(SeriesOrdering::KeyThenTime, scanner.raw_series_ordering()); + let metadata = scanner.metadata(); + metadata_for_codec.get_or_insert_with(|| metadata.clone()); + 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() + .into_iter() + .flatten() + .collect::>(); + assert!(!ranges.is_empty(), "region scan needs an input range"); + 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(); + let partition_count = scanner.properties().num_partitions(); + assert!( + partition_count > 1, + "prepare must use multiple supplied native range partitions" + ); + for partition in 0..partition_count { + let stream = scanner + .scan_series_partition(&QueryScanContext::default(), &metrics, partition, &source) + .unwrap() + .expect("native raw input"); + assert_eq!(scanner.raw_series_ordering(), stream.ordering()); + assert_eq!(SeriesOrdering::KeyThenTime, stream.ordering()); + assert_eq!( + vec!["field_1", "field_0"], + stream + .schema() + .fields + .iter() + .map(|field| field.name.as_ref()) + .collect::>() + ); + if let Some(schema) = &expected_schema { + assert_eq!( + schema + .fields + .iter() + .map(|field| field.name.as_ref()) + .collect::>(), + stream + .schema() + .fields + .iter() + .map(|field| field.name.as_ref()) + .collect::>() + ); + } else { + expected_schema = Some(Arc::clone(stream.schema())); + } + streams.push(stream); + } + // Each scanner owns the same schema and explicitly bound source. + } + let merged = merge_ordered_raw_series(streams).unwrap(); + let merged_schema = Arc::clone(merged.schema()); + let binding = merged.label_source().clone(); + let ordering = merged.ordering(); + assert_eq!(SeriesOrdering::KeyThenTime, ordering); + let packed = pack_raw_series(merged).unwrap(); + let unpacked = unpack_raw_series(packed, merged_schema, binding, ordering).unwrap(); + let mut materialized = materialize_raw_labels(unpacked).unwrap(); + let mut actual = Vec::new(); + while let Some(chunk) = materialized.try_next().await.unwrap() { + let metric = chunk.metric.as_ref().unwrap(); + assert_eq!(Some("fixture_metric"), metric.name()); + let tag = metric.get("tag_0").unwrap().to_string(); + for index in 0..chunk.time.len() { + let SampleRef::Float(f1) = chunk.fields.get(0).unwrap().sample(index) else { + panic!("field_1 float") + }; + let f0 = match chunk.fields.get(1).unwrap().sample(index) { + SampleRef::Float(value) => Some(value.to_bits()), + SampleRef::Missing => None, + _ => panic!("unexpected field_0 sample type"), + }; + actual.push(( + chunk.primary_key.to_vec(), + tag.clone(), + chunk.time.timestamp(index).unwrap(), + f1.to_bits(), + f0, + chunk.is_last && index + 1 == chunk.time.len(), + )); + } + } + assert!( + actual + .windows(2) + .all(|pair| pair[0].0 < pair[1].0 || (pair[0].0 == pair[1].0 && pair[0].2 < pair[1].2)), + "merged stream must be ordered by full read key then time" + ); + let metadata = metadata_for_codec.unwrap(); + let codec = build_primary_key_codec(&metadata); + let mut expected = Vec::new(); + for (tag, time, f1, f0) in [ + ("A", 10, 11.0_f64, Some(11.0_f64)), + ("A", 20, 2.0, Some(20.0)), + ("A", 30, 3.0, None), + ("A", 40, 4.0, Some(40.0)), + ("B", 10, 5.0, Some(-0.0_f64)), + ("C", 10, 6.0, Some(60.0)), + ] { + let mut key = Vec::new(); + codec + .encode_values( + &[(metadata.primary_key[0], DatumValue::String(tag.into()))], + &mut key, + ) + .unwrap(); + expected.push((key, tag, time, f1.to_bits(), f0.map(f64::to_bits))); + } + let duplicate_key = expected[0].0.clone(); + assert_eq!( + expected + .iter() + .map(|(key, _, _, _, _)| key.clone()) + .collect::>(), + actual.iter().map(|row| row.0.clone()).collect::>() + ); + let visible = actual + .iter() + .map(|(_, tag, time, f1, f0, _)| (tag.as_str(), *time, *f1, *f0)) + .collect::>(); + assert_eq!( + expected + .iter() + .map(|(_, tag, time, f1, f0)| (*tag, *time, *f1, *f0)) + .collect::>(), + visible + ); + let mut legacy_batches = Vec::new(); + for region in regions { + let mut legacy_request = request.clone(); + legacy_request.projection = Some(vec![0, 1, 2, 3, 4]); + legacy_request.filters.clear(); + legacy_request.preserve_pk_dictionary_encoding = false; + let legacy = engine.handle_query(region, legacy_request).await.unwrap(); + for partition in 0..legacy.properties().num_partitions() { + legacy_batches.extend( + legacy + .scan_partition(&QueryScanContext::default(), &metrics, partition) + .unwrap() + .try_collect::>() + .await + .unwrap(), + ); + } + } + let mut legacy_visible = Vec::new(); + for batch in &legacy_batches { + let batch = batch.df_record_batch(); + let tags = batch + .column_by_name("tag_0") + .unwrap() + .as_any() + .downcast_ref::() + .unwrap(); + let f0 = batch + .column_by_name("field_0") + .unwrap() + .as_any() + .downcast_ref::() + .unwrap(); + let f1 = batch + .column_by_name("field_1") + .unwrap() + .as_any() + .downcast_ref::() + .unwrap(); + let times = batch + .column_by_name("ts") + .unwrap() + .as_any() + .downcast_ref::() + .unwrap(); + let gate = batch + .column_by_name("field_2") + .unwrap() + .as_any() + .downcast_ref::() + .unwrap(); + for row in 0..batch.num_rows() { + if gate.value(row) > 0.0 { + legacy_visible.push(( + tags.value(row).to_string(), + times.value(row), + f1.value(row).to_bits(), + if f0.is_null(row) { + None + } else { + Some(f0.value(row).to_bits()) + }, + )); + } + } + } + legacy_visible.sort_by(|left, right| left.0.cmp(&right.0).then(left.1.cmp(&right.1))); + let mut raw_visible = actual + .iter() + .map(|(_, tag, time, f1, f0, _)| (tag.clone(), *time, *f1, *f0)) + .collect::>(); + raw_visible.sort_by(|left, right| left.0.cmp(&right.0).then(left.1.cmp(&right.1))); + assert_eq!( + legacy_visible, raw_visible, + "raw candidates after field_2 predicate must match legacy scan" + ); + test_util::put_rows( + &engine, + regions[1], + Rows { + schema: schema.clone(), + rows: vec![make_row("A", Some(99.0), 99.0, 1.0, 30)], + }, + ) + .await; + let mut duplicate_inputs = Vec::new(); + for region in regions { + let scanner = engine.handle_query(region, request.clone()).await.unwrap(); + 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 mut scanner = scanner; + let ranges = scanner + .properties() + .partitions + .clone() + .into_iter() + .flatten() + .collect::>(); + 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(); + for partition in 0..scanner.properties().num_partitions() { + duplicate_inputs.push( + scanner + .scan_series_partition( + &QueryScanContext::default(), + &metrics, + partition, + &source, + ) + .unwrap() + .unwrap(), + ); + } + } + let mut duplicate_merge = merge_ordered_raw_series(duplicate_inputs).unwrap(); + let mut affected_key_last = Vec::new(); + let duplicate_error = loop { + match duplicate_merge.try_next().await { + Ok(Some(chunk)) => { + if chunk.primary_key.as_slice() == duplicate_key.as_slice() { + affected_key_last.push(chunk.is_last); + } + } + Ok(None) => panic!("same full key/timestamp across regions must fail"), + Err(error) => break error.to_string(), + } + }; + assert!( + duplicate_error.contains("timestamp") || duplicate_error.contains("time"), + "unexpected duplicate timestamp error: {duplicate_error}" + ); + assert!(!affected_key_last.is_empty()); + assert!(affected_key_last.iter().all(|is_last| !is_last)); + assert!( + duplicate_merge.try_next().await.unwrap().is_none(), + "merge error must fuse stream" + ); + for tag in ["A", "B", "C"] { + let key_rows = actual.iter().filter(|row| row.1 == tag).collect::>(); + assert_eq!( + 1, + key_rows.iter().filter(|row| row.5).count(), + "global finality for {tag}" + ); + assert!(key_rows.last().unwrap().5); + assert!(key_rows[..key_rows.len() - 1].iter().all(|row| !row.5)); + } +} + +#[tokio::test] +async fn test_two_phase_native_raw_stream_is_rejected_by_ordered_merge() { + use datatypes::series::SeriesOrdering; + 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_merge_rejection").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 streams = Vec::new(); + for partition in 0..ranges.len() { + let stream = scanner + .scan_series_partition(&QueryScanContext::default(), &metrics, partition, &source) + .unwrap() + .unwrap(); + assert_eq!(scanner.raw_series_ordering(), stream.ordering()); + assert_eq!(SeriesOrdering::Contiguous, stream.ordering()); + streams.push(stream); + } + let error = match promql::source::merge_ordered_raw_series(streams) { + Ok(_) => panic!("Contiguous two-phase input cannot satisfy ordered merge ordering"), + Err(error) => error, + }; + assert!( + error.to_string().contains("ordering") || error.to_string().contains("ordered"), + "expected ordering rejection, got: {error}" + ); +} + #[tokio::test] async fn test_mito_raw_series_int64_predicate_uses_original_precision() { use datatypes::series::samples::SampleRef; @@ -5199,7 +5923,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 +5938,112 @@ 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::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(); + let chunks = unpack_raw_series( + frames, + native_schema, + native_source, + SeriesOrdering::Contiguous, + ) + .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_chunks = unpack_raw_series( + empty_frames, + empty_schema, + empty_source, + SeriesOrdering::Contiguous, + ) + .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 +6134,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 +6148,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 +6222,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 +6269,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_scan.rs b/src/mito2/src/read/series_scan.rs index eb5c1048ab5..8b4ad2acf20 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}; @@ -88,6 +90,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 +202,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 +769,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 +792,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 +805,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, ))) } 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(), From 8e90db600d10ee226b42d66c64856766ddb259d2 Mon Sep 17 00:00:00 2001 From: discord9 <55937128+discord9@users.noreply.github.com> Date: Tue, 20 Oct 2026 20:27:44 +0800 Subject: [PATCH 02/13] test(mito2): cover raw partition bounds and contiguity across assignments The raw-series path had two untested behaviours. An out-of-range partition must fail without consuming the scanner's partition state: the range check runs before the receiver is taken, so a later in-bounds scan on the same scanner still reads its complete data, and a repeat scan of that partition then reports the one-shot violation. A two-phase stream must stay contiguous in the sense the raw contract advertises, including across candidate assignments: chunks are checked per partition for a non-empty, disjoint key set, the whole stream is routed through the frame codec so its own sequence validator runs, and a series may appear in only one run so a non-adjacent recurrence would fail. Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com> --- src/mito2/src/engine/scan_test.rs | 501 ++++++++++++++++++++++++++++++ 1 file changed, 501 insertions(+) diff --git a/src/mito2/src/engine/scan_test.rs b/src/mito2/src/engine/scan_test.rs index 3ad13fac11e..6fe07d63ecd 100644 --- a/src/mito2/src/engine/scan_test.rs +++ b/src/mito2/src/engine/scan_test.rs @@ -5826,6 +5826,507 @@ async fn test_two_phase_native_raw_stream_is_rejected_by_ordered_merge() { ); } +#[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_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, + ..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 ranges plus one memtable range, 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. + 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); + } + } + 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::>(); + 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 mut scanner = engine.handle_query(region_id, request).await.unwrap(); + assert_eq!(SeriesOrdering::Contiguous, scanner.raw_series_ordering()); + 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 multi-assignment scan: every assignment's chunks in + // partition order. + 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 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); + } + } +} + +/// 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; From 38f728ee3fa97e4405a1a4466022f7e63fe04b43 Mon Sep 17 00:00:00 2001 From: discord9 <55937128+discord9@users.noreply.github.com> Date: Tue, 20 Oct 2026 21:42:34 +0800 Subject: [PATCH 03/13] test(mito2): cover a second assignment reaching the same partition The cross-assignment test wrote four candidate series, gave each of two output partitions one assignment, and concatenated the two streams. It proved that two disjoint non-empty assignments stay contiguous once joined, but production only sends a second round to the *same* receiver when one collection round holds `CANDIDATE_SERIES_ASSIGNMENT_THRESHOLD` (1,000,000) candidates, so that boundary was never crossed. The new test lowers the threshold through a `#[cfg(test)]` override instead of writing a million series, then writes 501 series -- one candidate batch plus a trailing remainder -- into a single output partition and asserts on that partition's one stream: two or more assignment rounds were sent, every series appears exactly once in key order with no recurrence, timestamps increase within a key, `is_last` is correct at the boundary, and every value matches. The stream then goes through `pack_raw_series`/`unpack_raw_series` so `SequenceValidator` runs on it as well. Dropping a later round leaves a validator-clean stream, so the data assertions are what catch it. The layout assertions now check the composition rather than non-emptiness: the scan input reports two SST sources whose row counts are the two flushed write rounds and one memtable whose rows are the unflushed round. Compaction is disabled in both tests so a background merge cannot move that composition. The older test is renamed to say what it covers: two disjoint partitions, one assignment each, concatenated. Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com> --- src/mito2/src/engine/scan_test.rs | 369 +++++++++++++++++++++++++++++- src/mito2/src/read/series_scan.rs | 48 +++- 2 files changed, 408 insertions(+), 9 deletions(-) diff --git a/src/mito2/src/engine/scan_test.rs b/src/mito2/src/engine/scan_test.rs index 6fe07d63ecd..4a720ef73f2 100644 --- a/src/mito2/src/engine/scan_test.rs +++ b/src/mito2/src/engine/scan_test.rs @@ -5932,7 +5932,7 @@ async fn test_two_phase_raw_partition_out_of_range_keeps_scanner_usable() { } #[tokio::test] -async fn test_two_phase_raw_stream_stays_contiguous_across_assignments() { +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; @@ -5943,6 +5943,10 @@ async fn test_two_phase_raw_stream_stays_contiguous_across_assignments() { 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; @@ -5963,9 +5967,11 @@ async fn test_two_phase_raw_stream_stays_contiguous_across_assignments() { .map(|(tag, tsid)| new_sparse_primary_key(&[tag, "x"], &metadata, 10, *tsid)) .collect::>(); - // Three write rounds put every series into two SST ranges plus one memtable range, 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. + // 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] = [ @@ -5992,6 +5998,7 @@ async fn test_two_phase_raw_stream_stays_contiguous_across_assignments() { .extend(first..first + count); } } + let mut round_rows = Vec::with_capacity(rounds.len()); for (round, spec) in rounds.iter().enumerate() { let rows = spec .iter() @@ -6003,6 +6010,7 @@ async fn test_two_phase_raw_stream_stays_contiguous_across_assignments() { }) }) .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; @@ -6014,8 +6022,50 @@ async fn test_two_phase_raw_stream_stays_contiguous_across_assignments() { preserve_pk_dictionary_encoding: true, ..Default::default() }; - let mut scanner = engine.handle_query(region_id, request).await.unwrap(); + 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() @@ -6083,8 +6133,9 @@ async fn test_two_phase_raw_stream_stays_contiguous_across_assignments() { per_partition.push(chunks); } - // The complete raw series stream of the multi-assignment scan: every assignment's chunks in - // partition order. + // 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() @@ -6136,7 +6187,7 @@ async fn test_two_phase_raw_stream_stays_contiguous_across_assignments() { if runs.last() != Some(&key) { assert!( !runs.contains(&key), - "a series' chunks must stay contiguous across assignments" + "a series' chunks must stay contiguous across the concatenated assignments" ); runs.push(key); } @@ -6176,6 +6227,308 @@ async fn test_two_phase_raw_stream_stays_contiguous_across_assignments() { } } +/// 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 exactly once, in ascending key order, so + // the stream crosses the round boundary: series 0..500 come from the first assignment and + // series 500 (the last of the globally sorted candidate stream) from the second one. + 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, runs, "every series appears exactly once in key order"); + 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. + assert_eq!( + &keys[0], + runs.first().unwrap(), + "first round data is streamed" + ); + assert_eq!( + &keys[SERIES - 1], + runs.last().unwrap(), + "second round data is streamed" + ); + 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. diff --git a/src/mito2/src/read/series_scan.rs b/src/mito2/src/read/series_scan.rs index 8b4ad2acf20..971ff93a742 100644 --- a/src/mito2/src/read/series_scan.rs +++ b/src/mito2/src/read/series_scan.rs @@ -81,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>>>; @@ -1143,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() { @@ -1167,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; From e1dba11eea857c2b1b9825753262fece81fffcfa Mon Sep 17 00:00:00 2001 From: discord9 <55937128+discord9@users.noreply.github.com> Date: Tue, 20 Oct 2026 22:46:23 +0800 Subject: [PATCH 04/13] test(mito2): check the ordering the producer declares, and drop the promql dev-dep MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The three end-to-end sites unpacked every frame as `Contiguous` while the producer declared `KeyThenTime`, and `SequenceValidator` only checks key order under the ordering it is given. A producer that declared key-then-time and emitted keys out of order passed all three. Each site now reads the ordering the frame declares, asserts it is `KeyThenTime` — so a future producer that quietly downgrades its declaration fails instead of skipping the check — and unpacks under it. Reversing two series' key order in the fixture now fails the test; passing `Contiguous` for the same frames still passes, which is what the old code did. The two-phase test asserted key order for an ordering that declares only `Contiguous`: a reader that stayed contiguous and stopped sorting keys is within contract and failed the test for it. It now asserts one run per key and that the two assignments hold the first and last key, which is what `Contiguous` plus the assignment composition actually promise. `promql` leaves mito2's dev-dependencies. The storage contract needs only `pack`/`unpack_raw_series` from `common-query`, which mito2 already depends on; the cross-region merge composition moves to promql, where it is a stream-level test rather than a two-region fixture: `merges_two_unpacked_frames_through_the_frame_codec`. The merge rejection that the two-phase test used to exercise is already covered by `rejects_unsupported_ordering_and_empty_input_without_polling`, which the remaining test now cites instead of duplicating. Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com> --- Cargo.lock | 1 - src/mito2/Cargo.toml | 1 - src/mito2/src/engine/scan_test.rs | 637 +++++---------------------- src/promql/src/source/merge/tests.rs | 62 +++ 4 files changed, 164 insertions(+), 537 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 43d65a03bd8..ea2a530d6b5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8742,7 +8742,6 @@ dependencies = [ "partition", "pin-project", "prometheus 0.14.0", - "promql", "prost 0.14.1", "puffin", "rand 0.9.4", diff --git a/src/mito2/Cargo.toml b/src/mito2/Cargo.toml index 151694c40d1..02c134e964c 100644 --- a/src/mito2/Cargo.toml +++ b/src/mito2/Cargo.toml @@ -101,7 +101,6 @@ dotenv.workspace = true log-store = { workspace = true, features = ["testing"] } mito-codec = { workspace = true, features = ["testing"] } object-store = { workspace = true, features = ["services-memory"] } -promql.workspace = true rskafka.workspace = true rstest.workspace = true rstest_reuse.workspace = true diff --git a/src/mito2/src/engine/scan_test.rs b/src/mito2/src/engine/scan_test.rs index 4a720ef73f2..c0b155f7311 100644 --- a/src/mito2/src/engine/scan_test.rs +++ b/src/mito2/src/engine/scan_test.rs @@ -4772,7 +4772,6 @@ async fn test_mito_handle_query_raw_series_candidate_projection_and_mvcc() { use datatypes::series::samples::SampleRef; use datatypes::value::Value as DatumValue; use mito_codec::row_converter::build_primary_key_codec; - use promql::source::materialize_raw_labels; use store_api::region_engine::{BoundTag, QueryScanContext, RawLabelSource}; use store_api::region_request::RegionDeleteRequest; use table::predicate::Predicate; @@ -5093,7 +5092,7 @@ async fn test_mito_handle_query_raw_series_candidate_projection_and_mvcc() { assert_eq!(raw_visible, visible); // Exercise the production table physical consumer over a fresh real Mito scanner. - use common_query::series_frame::unpack_raw_series; + 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; @@ -5125,20 +5124,18 @@ async fn test_mito_handle_query_raw_series_candidate_projection_and_mvcc() { let native_frames = native_plan .execute(0, Arc::new(TaskContext::default())) .unwrap(); - let unpacked = unpack_raw_series( - native_frames, - native_schema, - native_source, - SeriesOrdering::Contiguous, - ) - .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 materialized = materialize_raw_labels(unpacked).unwrap(); - assert!(matches!( - materialized.label_source(), - RawLabelSource::Explicit - )); - let mut native = materialized.try_collect::>().await.unwrap(); + let mut native = unpacked.try_collect::>().await.unwrap(); assert_eq!( vec!["field_1", "field_0"], native_schema @@ -5150,18 +5147,17 @@ async fn test_mito_handle_query_raw_series_candidate_projection_and_mvcc() { let mut native_visible = native .drain(..) .flat_map(|chunk| { - let metric = chunk.metric.as_ref().expect("materialized complete metric"); - assert_eq!(Some("fixture_metric"), metric.name()); - let labels = metric - .tags() - .iter() - .map(|label| (label.name.as_ref(), label.value.as_ref())) - .collect::>(); - assert_eq!(1, labels.len()); - let tag = labels["tag_0"].to_string(); - assert!(tag == "A" || tag == "B"); - let times = chunk.time.as_buffer(); 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() { @@ -5175,7 +5171,7 @@ async fn test_mito_handle_query_raw_series_candidate_projection_and_mvcc() { }; rows.push(( key.clone(), - tag.clone(), + tag.to_string(), *time, f1, f0, @@ -5230,478 +5226,22 @@ 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_mito_regional_raw_merge_preserves_global_key_order_and_labels() { - use api::v1::{Row, Value}; - use common_query::series_frame::{pack_raw_series, unpack_raw_series}; - use datatypes::arrow::array::Array; - use datatypes::series::SeriesOrdering; - use datatypes::series::samples::{SampleRef, SeriesSchema}; - use datatypes::value::Value as DatumValue; - use mito_codec::row_converter::build_primary_key_codec; - use promql::source::{materialize_raw_labels, merge_ordered_raw_series}; - use store_api::region_engine::{BoundTag, QueryScanContext, RawLabelSource}; - use store_api::region_request::RegionDeleteRequest; - - let mut env = TestEnv::with_prefix("test_mito_regional_raw_merge").await; - let engine = env.create_engine(MitoConfig::default()).await; - let regions = [RegionId::new(1, 1), RegionId::new(1, 2)]; - let mut request = CreateRequestBuilder::new().field_num(3).build(); - request - .options - .insert("memtable.type".into(), "bulk".into()); - let schema = test_util::rows_schema(&request); - for region in regions { - engine - .handle_request(region, RegionRequest::Create(request.clone())) - .await - .unwrap(); - } - let make_row = |tag: &str, f0: Option, f1: f64, gate: f64, time: i64| Row { - values: vec![ - Some(ValueData::StringValue(tag.into())), - f0.map(ValueData::F64Value), - Some(ValueData::F64Value(f1)), - Some(ValueData::F64Value(gate)), - Some(ValueData::TimestampMillisecondValue(time)), - ] - .into_iter() - .map(|value_data| Value { value_data }) - .collect(), +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, }; - for (region, rows) in [ - ( - regions[0], - vec![ - make_row("A", Some(10.0), 1.0, 1.0, 10), - make_row("A", None, 3.0, 1.0, 30), - make_row("B", Some(-0.0), 5.0, 1.0, 10), - ], - ), - ( - regions[1], - vec![ - make_row("A", Some(20.0), 2.0, 1.0, 20), - make_row("A", Some(40.0), 4.0, 1.0, 40), - make_row("C", Some(60.0), 6.0, 1.0, 10), - ], - ), - ] { - test_util::put_rows( - &engine, - region, - Rows { - schema: schema.clone(), - rows, - }, - ) - .await; - } - test_util::flush_region(&engine, regions[0], None).await; - test_util::flush_region(&engine, regions[1], None).await; - test_util::put_rows( - &engine, - regions[0], - Rows { - schema: schema.clone(), - rows: vec![ - make_row("A", Some(11.0), 11.0, 1.0, 10), - make_row("A", Some(50.0), 5.0, 0.0, 50), - make_row("B", Some(60.0), 6.0, 1.0, 60), - ], - }, - ) - .await; - engine - .handle_request( - regions[0], - RegionRequest::Delete(RegionDeleteRequest { - rows: Rows { - schema: test_util::delete_rows_schema( - &CreateRequestBuilder::new().field_num(3).build(), - ), - rows: vec![row(vec![ - ValueData::StringValue("B".into()), - ValueData::TimestampMillisecondValue(60), - ])], - }, - hint: None, - partition_expr_version: None, - }), - ) - .await - .unwrap(); - let request = ScanRequest { - projection: Some(vec![2, 1, 4]), - filters: vec![col("field_2").gt(lit(0.0_f64))], - distribution: Some(TimeSeriesDistribution::PerSeries), - preserve_pk_dictionary_encoding: true, - ..Default::default() - }; - let mut streams = Vec::new(); - let mut metadata_for_codec = None; - let mut expected_schema: Option> = None; - let metrics = ExecutionPlanMetricsSet::default(); - for region in regions { - let selected = engine.scanner(region, request.clone()).await.unwrap(); - let mut scanner: Box = match selected { - Scanner::Series(series) => { - assert_eq!("legacy", series.mode()); - Box::new(series) - } - _ => panic!("PerSeries request must select a series scanner"), - }; - assert_eq!(SeriesOrdering::KeyThenTime, scanner.raw_series_ordering()); - let metadata = scanner.metadata(); - metadata_for_codec.get_or_insert_with(|| metadata.clone()); - 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() - .into_iter() - .flatten() - .collect::>(); - assert!(!ranges.is_empty(), "region scan needs an input range"); - 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(); - let partition_count = scanner.properties().num_partitions(); - assert!( - partition_count > 1, - "prepare must use multiple supplied native range partitions" - ); - for partition in 0..partition_count { - let stream = scanner - .scan_series_partition(&QueryScanContext::default(), &metrics, partition, &source) - .unwrap() - .expect("native raw input"); - assert_eq!(scanner.raw_series_ordering(), stream.ordering()); - assert_eq!(SeriesOrdering::KeyThenTime, stream.ordering()); - assert_eq!( - vec!["field_1", "field_0"], - stream - .schema() - .fields - .iter() - .map(|field| field.name.as_ref()) - .collect::>() - ); - if let Some(schema) = &expected_schema { - assert_eq!( - schema - .fields - .iter() - .map(|field| field.name.as_ref()) - .collect::>(), - stream - .schema() - .fields - .iter() - .map(|field| field.name.as_ref()) - .collect::>() - ); - } else { - expected_schema = Some(Arc::clone(stream.schema())); - } - streams.push(stream); - } - // Each scanner owns the same schema and explicitly bound source. - } - let merged = merge_ordered_raw_series(streams).unwrap(); - let merged_schema = Arc::clone(merged.schema()); - let binding = merged.label_source().clone(); - let ordering = merged.ordering(); - assert_eq!(SeriesOrdering::KeyThenTime, ordering); - let packed = pack_raw_series(merged).unwrap(); - let unpacked = unpack_raw_series(packed, merged_schema, binding, ordering).unwrap(); - let mut materialized = materialize_raw_labels(unpacked).unwrap(); - let mut actual = Vec::new(); - while let Some(chunk) = materialized.try_next().await.unwrap() { - let metric = chunk.metric.as_ref().unwrap(); - assert_eq!(Some("fixture_metric"), metric.name()); - let tag = metric.get("tag_0").unwrap().to_string(); - for index in 0..chunk.time.len() { - let SampleRef::Float(f1) = chunk.fields.get(0).unwrap().sample(index) else { - panic!("field_1 float") - }; - let f0 = match chunk.fields.get(1).unwrap().sample(index) { - SampleRef::Float(value) => Some(value.to_bits()), - SampleRef::Missing => None, - _ => panic!("unexpected field_0 sample type"), - }; - actual.push(( - chunk.primary_key.to_vec(), - tag.clone(), - chunk.time.timestamp(index).unwrap(), - f1.to_bits(), - f0, - chunk.is_last && index + 1 == chunk.time.len(), - )); - } - } - assert!( - actual - .windows(2) - .all(|pair| pair[0].0 < pair[1].0 || (pair[0].0 == pair[1].0 && pair[0].2 < pair[1].2)), - "merged stream must be ordered by full read key then time" - ); - let metadata = metadata_for_codec.unwrap(); - let codec = build_primary_key_codec(&metadata); - let mut expected = Vec::new(); - for (tag, time, f1, f0) in [ - ("A", 10, 11.0_f64, Some(11.0_f64)), - ("A", 20, 2.0, Some(20.0)), - ("A", 30, 3.0, None), - ("A", 40, 4.0, Some(40.0)), - ("B", 10, 5.0, Some(-0.0_f64)), - ("C", 10, 6.0, Some(60.0)), - ] { - let mut key = Vec::new(); - codec - .encode_values( - &[(metadata.primary_key[0], DatumValue::String(tag.into()))], - &mut key, - ) - .unwrap(); - expected.push((key, tag, time, f1.to_bits(), f0.map(f64::to_bits))); - } - let duplicate_key = expected[0].0.clone(); - assert_eq!( - expected - .iter() - .map(|(key, _, _, _, _)| key.clone()) - .collect::>(), - actual.iter().map(|row| row.0.clone()).collect::>() - ); - let visible = actual - .iter() - .map(|(_, tag, time, f1, f0, _)| (tag.as_str(), *time, *f1, *f0)) - .collect::>(); - assert_eq!( - expected - .iter() - .map(|(_, tag, time, f1, f0)| (*tag, *time, *f1, *f0)) - .collect::>(), - visible - ); - let mut legacy_batches = Vec::new(); - for region in regions { - let mut legacy_request = request.clone(); - legacy_request.projection = Some(vec![0, 1, 2, 3, 4]); - legacy_request.filters.clear(); - legacy_request.preserve_pk_dictionary_encoding = false; - let legacy = engine.handle_query(region, legacy_request).await.unwrap(); - for partition in 0..legacy.properties().num_partitions() { - legacy_batches.extend( - legacy - .scan_partition(&QueryScanContext::default(), &metrics, partition) - .unwrap() - .try_collect::>() - .await - .unwrap(), - ); - } - } - let mut legacy_visible = Vec::new(); - for batch in &legacy_batches { - let batch = batch.df_record_batch(); - let tags = batch - .column_by_name("tag_0") - .unwrap() - .as_any() - .downcast_ref::() - .unwrap(); - let f0 = batch - .column_by_name("field_0") - .unwrap() - .as_any() - .downcast_ref::() - .unwrap(); - let f1 = batch - .column_by_name("field_1") - .unwrap() - .as_any() - .downcast_ref::() - .unwrap(); - let times = batch - .column_by_name("ts") - .unwrap() - .as_any() - .downcast_ref::() - .unwrap(); - let gate = batch - .column_by_name("field_2") - .unwrap() - .as_any() - .downcast_ref::() - .unwrap(); - for row in 0..batch.num_rows() { - if gate.value(row) > 0.0 { - legacy_visible.push(( - tags.value(row).to_string(), - times.value(row), - f1.value(row).to_bits(), - if f0.is_null(row) { - None - } else { - Some(f0.value(row).to_bits()) - }, - )); - } - } - } - legacy_visible.sort_by(|left, right| left.0.cmp(&right.0).then(left.1.cmp(&right.1))); - let mut raw_visible = actual - .iter() - .map(|(_, tag, time, f1, f0, _)| (tag.clone(), *time, *f1, *f0)) - .collect::>(); - raw_visible.sort_by(|left, right| left.0.cmp(&right.0).then(left.1.cmp(&right.1))); - assert_eq!( - legacy_visible, raw_visible, - "raw candidates after field_2 predicate must match legacy scan" - ); - test_util::put_rows( - &engine, - regions[1], - Rows { - schema: schema.clone(), - rows: vec![make_row("A", Some(99.0), 99.0, 1.0, 30)], - }, - ) - .await; - let mut duplicate_inputs = Vec::new(); - for region in regions { - let scanner = engine.handle_query(region, request.clone()).await.unwrap(); - 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 mut scanner = scanner; - let ranges = scanner - .properties() - .partitions - .clone() - .into_iter() - .flatten() - .collect::>(); - 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(); - for partition in 0..scanner.properties().num_partitions() { - duplicate_inputs.push( - scanner - .scan_series_partition( - &QueryScanContext::default(), - &metrics, - partition, - &source, - ) - .unwrap() - .unwrap(), - ); - } - } - let mut duplicate_merge = merge_ordered_raw_series(duplicate_inputs).unwrap(); - let mut affected_key_last = Vec::new(); - let duplicate_error = loop { - match duplicate_merge.try_next().await { - Ok(Some(chunk)) => { - if chunk.primary_key.as_slice() == duplicate_key.as_slice() { - affected_key_last.push(chunk.is_last); - } - } - Ok(None) => panic!("same full key/timestamp across regions must fail"), - Err(error) => break error.to_string(), - } - }; - assert!( - duplicate_error.contains("timestamp") || duplicate_error.contains("time"), - "unexpected duplicate timestamp error: {duplicate_error}" - ); - assert!(!affected_key_last.is_empty()); - assert!(affected_key_last.iter().all(|is_last| !is_last)); - assert!( - duplicate_merge.try_next().await.unwrap().is_none(), - "merge error must fuse stream" - ); - for tag in ["A", "B", "C"] { - let key_rows = actual.iter().filter(|row| row.1 == tag).collect::>(); - assert_eq!( - 1, - key_rows.iter().filter(|row| row.5).count(), - "global finality for {tag}" - ); - assert!(key_rows.last().unwrap().5); - assert!(key_rows[..key_rows.len() - 1].iter().all(|row| !row.5)); - } -} - -#[tokio::test] -async fn test_two_phase_native_raw_stream_is_rejected_by_ordered_merge() { 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_merge_rejection").await; + 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, @@ -5806,23 +5346,47 @@ async fn test_two_phase_native_raw_stream_is_rejected_by_ordered_merge() { }) .unwrap(); let metrics = ExecutionPlanMetricsSet::default(); - let mut streams = Vec::new(); + 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()); - streams.push(stream); + schema.get_or_insert_with(|| Arc::clone(stream.schema())); + chunks.extend(stream.try_collect::>().await.unwrap()); } - let error = match promql::source::merge_ordered_raw_series(streams) { - Ok(_) => panic!("Contiguous two-phase input cannot satisfy ordered merge ordering"), - Err(error) => error, - }; - assert!( - error.to_string().contains("ordering") || error.to_string().contains("ordered"), - "expected ordering rejection, got: {error}" + 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::() ); } @@ -6418,9 +5982,10 @@ async fn test_two_phase_raw_same_partition_multi_round_assignment_stays_contiguo "partition 0 must own reader assignments" ); - // Partition 0's single stream carries every series exactly once, in ascending key order, so - // the stream crosses the round boundary: series 0..500 come from the first assignment and - // series 500 (the last of the globally sorted candidate stream) from the second one. + // 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 { @@ -6434,22 +5999,21 @@ async fn test_two_phase_raw_same_partition_multi_round_assignment_stays_contiguo runs.push(key); } } - assert_eq!(keys, runs, "every series appears exactly once in key order"); + 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. - assert_eq!( - &keys[0], - runs.first().unwrap(), - "first round data is streamed" + 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_eq!( - &keys[SERIES - 1], - runs.last().unwrap(), - "second round data is streamed" + assert!( + second_assignment.contains(&keys[SERIES - 1]), + "the second assignment must stream the last key" ); let last_round_chunks = chunks .iter() @@ -6794,7 +6358,7 @@ async fn test_mito_raw_series_int64_predicate_uses_original_precision() { assert_eq!(vec![(2, 20.0)], output_values); // Validate the typed integer gate through RegionScanExec's actual physical consumer. - use common_query::series_frame::unpack_raw_series; + 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; @@ -6830,16 +6394,18 @@ async fn test_mito_raw_series_int64_predicate_uses_original_precision() { let frames = native_plan .execute(0, Arc::new(TaskContext::default())) .unwrap(); - let chunks = unpack_raw_series( - frames, - native_schema, - native_source, - SeriesOrdering::Contiguous, - ) - .unwrap() - .try_collect::>() - .await - .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| { @@ -6887,16 +6453,17 @@ async fn test_mito_raw_series_int64_predicate_uses_original_precision() { let empty_frames = empty_plan .execute(0, Arc::new(TaskContext::default())) .unwrap(); - let empty_chunks = unpack_raw_series( - empty_frames, - empty_schema, - empty_source, - SeriesOrdering::Contiguous, - ) - .unwrap() - .try_collect::>() - .await - .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()); } 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"); From 420255ed3950a0a0801ec8551816582e9c00af54 Mon Sep 17 00:00:00 2001 From: dennis zhuang Date: Wed, 21 Oct 2026 11:21:36 +0800 Subject: [PATCH 05/13] feat(promql): add limitk, limit_ratio and binary fill modifiers to native kernels (#9805) Signed-off-by: Dennis Zhuang --- Cargo.lock | 1 + Cargo.toml | 1 + src/promql/Cargo.toml | 1 + src/promql/src/grid_series.rs | 11 + src/promql/src/native/display.rs | 20 +- src/promql/src/native/display/tests.rs | 20 +- src/promql/src/native/metrics.rs | 1 + src/promql/src/native/plan/tests.rs | 3 +- src/promql/src/select.rs | 180 +- src/promql/src/select/limit_cases.txt | 1372 +++++++++++++ src/promql/src/select/tests.rs | 204 +- src/promql/src/vector_match.rs | 246 ++- src/promql/src/vector_match/fill_cases.txt | 2021 ++++++++++++++++++++ src/promql/src/vector_match/tests.rs | 100 +- 14 files changed, 4052 insertions(+), 129 deletions(-) create mode 100644 src/promql/src/select/limit_cases.txt create mode 100644 src/promql/src/vector_match/fill_cases.txt 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/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..ce24cae6af4 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 { 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/tests.rs b/src/promql/src/native/plan/tests.rs index 85be7426a58..1c42c3b32af 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()) @@ -218,6 +218,7 @@ fn add() -> BinaryMatching { return_bool: false, labels: MatchingLabels::Ignoring(Vec::new()), cardinality: Cardinality::OneToOne, + fill: FillValues::default(), } } 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/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, From 66f055e186a79913848eceea125800922e74737e Mon Sep 17 00:00:00 2001 From: Dennis Zhuang Date: Wed, 21 Oct 2026 11:39:40 +0800 Subject: [PATCH 06/13] feat(promql): merge native histogram series that share a label set Signed-off-by: Dennis Zhuang --- src/promql/src/grid_series.rs | 44 +++++--------- src/promql/src/grid_series/tests.rs | 93 ++++++++++++++++++++++++++++- 2 files changed, 107 insertions(+), 30 deletions(-) diff --git a/src/promql/src/grid_series.rs b/src/promql/src/grid_series.rs index ce24cae6af4..91d04b4c4b6 100644 --- a/src/promql/src/grid_series.rs +++ b/src/promql/src/grid_series.rs @@ -320,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; @@ -346,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()); @@ -372,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)); } } @@ -400,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() @@ -415,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(); @@ -428,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] From b16cffd5c618f0a0aa2eb900109e4e6183154227 Mon Sep 17 00:00:00 2001 From: Dennis Zhuang Date: Wed, 21 Oct 2026 12:08:42 +0800 Subject: [PATCH 07/13] feat(promql): add max_of, min_of and query context functions to native kernels Signed-off-by: Dennis Zhuang --- src/promql/src/map.rs | 5 +- src/promql/src/map/broadcast.rs | 52 +++++ src/promql/src/map/max_min_cases.txt | 65 ++++++ src/promql/src/map/tests.rs | 56 +++++ src/promql/src/map_time.rs | 31 +++ .../src/map_time/duration_seconds_cases.txt | 211 ++++++++++++++++++ src/promql/src/map_time/tests.rs | 32 ++- 7 files changed, 450 insertions(+), 2 deletions(-) create mode 100644 src/promql/src/map/max_min_cases.txt create mode 100644 src/promql/src/map_time/duration_seconds_cases.txt diff --git a/src/promql/src/map.rs b/src/promql/src/map.rs index ffc7383f37e..2d2c4b80a7b 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..afb9fd419aa 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 - 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..3dba9472832 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,33 @@ 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"); + 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); +} From 9158f6ccba3ce00b61f25c200cc7b2c3a2e826ed Mon Sep 17 00:00:00 2001 From: Dennis Zhuang Date: Wed, 21 Oct 2026 15:43:20 +0800 Subject: [PATCH 08/13] fix(promql): saturate range() when the endpoint difference leaves i64 Signed-off-by: Dennis Zhuang --- src/promql/src/map_time.rs | 2 +- src/promql/src/map_time/tests.rs | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/promql/src/map_time.rs b/src/promql/src/map_time.rs index afb9fd419aa..c0a2c5b94c3 100644 --- a/src/promql/src/map_time.rs +++ b/src/promql/src/map_time.rs @@ -60,7 +60,7 @@ impl QueryContextFunction { match self { Self::Start => start_ms as f64 / 1000.0, Self::End => end_ms as f64 / 1000.0, - Self::Range => duration_seconds(end_ms - start_ms), + 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), } diff --git a/src/promql/src/map_time/tests.rs b/src/promql/src/map_time/tests.rs index 3dba9472832..0124b420129 100644 --- a/src/promql/src/map_time/tests.rs +++ b/src/promql/src/map_time/tests.rs @@ -315,6 +315,11 @@ fn query_context_durations_match_go() { 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); From 8543cbf401db9dc4fa7e1ca9aa347f254c6cb83d Mon Sep 17 00:00:00 2001 From: Yingwen Date: Wed, 21 Oct 2026 16:38:37 +0800 Subject: [PATCH 09/13] feat(mito): scaffold series format writers and artifact lifecycle (#9788) * feat(mito): add pluggable series artifact reconciliation Signed-off-by: evenyag * feat(mito): prepare independent series reads from SST artifacts Signed-off-by: evenyag * feat(mito): prepare mixed sidecar SST and memtable series reads Signed-off-by: evenyag * refactor(mito): make prepared series sources replaceable Signed-off-by: evenyag * fix(mito): narrow series formats to artifact lifecycle integration Signed-off-by: evenyag * test(mito): populate artifact regression buckets above build threshold Signed-off-by: evenyag --------- Signed-off-by: evenyag --- src/mito2/AGENTS.md | 3 +- src/mito2/src/engine/scan_test.rs | 1 + src/mito2/src/read/series_candidate.rs | 47 ++ src/mito2/src/series_index.rs | 4 + src/mito2/src/series_index/artifact.rs | 93 ++++ src/mito2/src/series_index/bucket.rs | 1 + src/mito2/src/series_index/builder.rs | 114 ++++- src/mito2/src/series_index/catalog.rs | 76 ++- src/mito2/src/series_index/format.rs | 72 +++ src/mito2/src/series_index/format_tests.rs | 442 ++++++++++++++++++ src/mito2/src/series_index/maintenance.rs | 151 +++++- src/mito2/src/series_index/purger.rs | 49 +- src/mito2/src/series_index/searcher.rs | 1 + src/mito2/src/series_index/tests.rs | 5 +- src/mito2/src/series_index/version.rs | 27 +- .../fixtures/series_index/README.md | 10 + .../fixtures/series_index/legacy_catalog.json | 27 ++ 17 files changed, 1082 insertions(+), 41 deletions(-) create mode 100644 src/mito2/src/series_index/artifact.rs create mode 100644 src/mito2/src/series_index/format.rs create mode 100644 src/mito2/src/series_index/format_tests.rs create mode 100644 tests/compatibility/fixtures/series_index/README.md create mode 100644 tests/compatibility/fixtures/series_index/legacy_catalog.json 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 c0b155f7311..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), 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/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/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 + } + } + } + ] +} From ee4c06d49815bfc60dbe67404c50cd0217ccce45 Mon Sep 17 00:00:00 2001 From: discord9 Date: Wed, 21 Oct 2026 16:57:22 +0800 Subject: [PATCH 10/13] test(promql): hold the batch path to the per-window one on random input (#9739) * test(promql): hold the batch path to the per-window one on random input Two comments claimed the opposite of what the code relies on: a batch's spans were described as absolute indices that survive a release, but a span is recorded against the retained arrays as they stand, and `advance` drains and shifts them. Nothing enforces the difference -- the batch is correct only because `evaluate` flushes it before advancing, so a later change that let a batch outlive one call would return wrong counter values with no error. Both now say that, and `PendingBatch::spans` carries the invariant where a reader looks for it. The batch path had no randomized comparison at all: the differential tests drive a probe kernel and the function tests cover no counter function, so only fixed cases exercised it. A new differential test runs rate and increase over 200 seeds of one to three fields, three series through one evaluator, random chunk splits and mixed histogram samples, comparing the batch path against the same evaluator with its batch form removed. Two assertions that could only have failed on a broken helper are gone, and the counter-reset flag is documented as governing both readers of the index, not just the per-window one. Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com> * test(promql): assert the batch is flushed before the sample drain `PendingBatch.spans` index the retained sample arrays, which `FieldSamples::advance` drains and shifts. The batch is correct only because `evaluate` flushes it just above that drain, and nothing enforced the ordering: a future change that let a batch outlive the call would compute counter values against shifted samples with no error. The assertion holds on every tested batch path. It is skipped when no batch kernel is installed, since `pending` is only ever filled on the kernel branch. Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com> * docs(promql): state the flush-before-advance rule once, and assert it plainly The batch assert could not fail on its `batch.is_none()` disjunct: `accumulate` is reached only from inside `if let Some(kernel) = self.batch.as_mut()`, and `batch` is never cleared, so a non-empty `pending` implies a kernel. Drop the disjunct and name who guarantees the order in the message instead. The rule that a batch must flush before `advance` was restated in six places. Keep it on `PendingBatch::spans`, where a reader asks why a span dies with the batch, and in the assertion message. The other four now state only the site-specific fact each needs. Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com> * docs(promql): drop the last restatement of flush-before-advance The rule now lives only on PendingBatch::spans and in the debug_assert message, as the review asked; the comment above the final flush restated it a third time. Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com> --------- Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com> --- src/promql/src/range_eval.rs | 24 +-- src/promql/src/range_eval/function.rs | 5 + src/promql/src/range_eval/tests.rs | 210 ++++++++++++++++++++++---- 3 files changed, 201 insertions(+), 38 deletions(-) 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///"; From 77a7d099e6a7dfcea7706c28798dde267a2b3a11 Mon Sep 17 00:00:00 2001 From: discord9 Date: Wed, 21 Oct 2026 17:02:18 +0800 Subject: [PATCH 11/13] docs(promql): add a map for src/promql (#9796) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs(promql): add a map for src/promql `src/promql` has no guide of its own, and the two things that cost the most to learn the hard way are not visible from any one file: which of the two evaluation paths is live, and what has to line up for a new extension node to reach a datanode at all. The map records the module layout, that coupling (substrait payload branch, physical planner branch, `Commutativity` arm), the native model's reachability, and the gotchas that fail silently: a missing commutativity arm, a label set that closes twice under one label, a raw frame decoded with a binding other than the one it was written with. It describes mechanisms and where they live, not which branch or PR adds what, so it stays true as the surrounding work lands. Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com> * docs(promql): correct what the frame schema does and does not say Two claims in the map were wrong about the code it describes. The guide said a raw frame's schema metadata explains how to decode the primary key. It does not: the metadata carries the format keys only — layout, content, labels, time — and the decoder compares those and the field list, never a label identity. `unpack_raw_series` still requires the caller's `RawLabelSource`, so the guide now says to decode with the binding the stream was produced under, which is the mistake it exists to prevent. A future decode descriptor can be described here once it is in the base and this text can describe the API that actually exists. The raw-scan gap named one way past the partition problem as though it were the only one. `RegionScanExec::try_to_series_scan` documents the other — finish partition preparation before converting — so both are stated now, with the scanner-side override kept as the actual requirement. Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com> * docs(promql): say the storage producer exists and name what is left The "cannot be planned" bullet listed the scanner-side override as missing, but mito2 implements `RegionScanner::raw_series_schema` — #9669 landed it, so the trait's `Ok(None)` default only refuses engines that have not implemented it. Listing an implemented override as a gap misreads how far the raw path actually reaches. Separate the two things: the storage producer is available, and what is missing is planner wiring plus the partition-preparation order. Merge current narthex in so the paragraph describes the branch it will land on rather than a stale base. Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com> --------- Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com> --- AGENTS.md | 1 + src/promql/AGENTS.md | 110 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 111 insertions(+) create mode 100644 src/promql/AGENTS.md 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/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. From 1156cfbe0cc54cbce6d4550ae999dd28a33d91f8 Mon Sep 17 00:00:00 2001 From: discord9 Date: Wed, 21 Oct 2026 17:02:37 +0800 Subject: [PATCH 12/13] chore(query): drop the prepared raw-series RPC skeleton (#9797) The raw-series preparation RPC was merged into `narthex` for a plan that never landed: no server or frontend dispatches `greptime.prepare_raw_series.v1`, and the three `Datanode` methods, `bind_raw_series_query` and the `QueryEngine` trio have no call sites and no implementors. `PreparedNamedRawSeries` had no user outside them. The wire envelope never shipped -- no tag contains the commit that added it -- so the compatibility fixtures go with it. The harness drives `wire/` fixtures from the Rust tests that own their context conversion rather than from SQLness discovery, and has no retirement mechanism to record instead. Left in place: `RawSeriesRequest` and its extension key, `raw_series_frame_ordering`, `unpack_raw_series` and `RawLabelSource`, all of which the surviving series-frame path still uses. Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com> --- src/common/meta/src/node_manager.rs | 120 +----- src/common/query/src/request.rs | 5 - .../query/src/request/named_raw_series.rs | 387 ------------------ src/query/src/query_engine.rs | 40 -- src/query/src/region_query.rs | 16 +- .../wire/named_raw_series_prepare/README.md | 32 -- .../v1_supported.json | 1 - .../v1_unsupported.json | 1 - 8 files changed, 3 insertions(+), 599 deletions(-) delete mode 100644 src/common/query/src/request/named_raw_series.rs delete mode 100644 tests/compatibility/wire/named_raw_series_prepare/README.md delete mode 100644 tests/compatibility/wire/named_raw_series_prepare/v1_supported.json delete mode 100644 tests/compatibility/wire/named_raw_series_prepare/v1_unsupported.json 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/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/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} From ee54ded60fce10cb9295383735aedb63a72a1cfc Mon Sep 17 00:00:00 2001 From: discord9 Date: Wed, 21 Oct 2026 17:24:07 +0800 Subject: [PATCH 13/13] docs(promql): state the real reason an instant selection needs no ordering check (#9785) * docs(promql): state the real reason an instant selection needs no ordering check `select_instants` reads one series' complete axis, so the executor must collect a series to its last chunk before evaluating it: a single buffer closed by `is_last` and keyed by nothing else. That needs each series' chunks contiguous, which `Contiguous` and `KeyThenTime` both guarantee, and `Interleaved` never reaches it because `unpack_raw_series` rejects that ordering at the raw-frame boundary. The comment said the opposite: that interleaved input was enough because `select_instants` holds no cross-series state. The conclusion stood, but the stated reason named a property the executor does not have. Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com> * docs(promql): stop deriving the instant arm's ordering check from another path The comment justified skipping an ordering check here by pointing at `unpack_raw_series`, which rejects `Interleaved` at the raw-frame boundary. This validator never runs through that call, so the rejection was never a premise it could use, and the conclusion did not follow. The requirement it was reaching for does exist, one layer down: `NativeInstantManipulateExec` collects a series to its `is_last` chunk into one buffer keyed by nothing else, so it needs each series' chunks contiguous. This validator does not check that. `Instant` has no implementation in the native tree yet and `try_to_native_execution_plan` has no callers, so the requirement is unenforced rather than met in advance; it is settled when `Instant` lands. Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com> * feat(promql): refuse an interleaved scan at the compiler entry, and in the validator The `Instant` validation arm replaced an ordering check with a comment saying no check was needed. The argument was that `unpack_raw_series` rejects `Interleaved`, which holds only for the legacy `InstantManipulate` path and says nothing about this tree. Meanwhile the refusal in `compile` sat after the bare-selector early return, so `compile(eval_stmt("x"), schema, Interleaved)` returned `Ok` against `compile`'s own rustdoc, and the same hole was open to every nested shape. The requirement is real for every shape: a native evaluation reads one series to its last chunk before the next. The refusal now runs before `compile` dispatches on the shape, so the bare selector, the range function and its parameterised form all inherit it, and a shape added later inherits it too. `NativeNode::validate` keeps a check of its own, for a tree built some other way: `Instant` and `Window` now share `ensure_not_interleaved` instead of one branch having the check and the other a comment about it. Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com> * docs(promql): link the validator by the node that has it `NativeTree` has no `validate`; the method lives on `NativeNode`. Both doc comments linked the tree, so rustdoc reports an unresolved link and the published API docs lose the reference. Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com> * test(promql): assert the control records the scan's ordering The contiguous control in the interleaved-refusal test only checked that the bare selector compiled, while its comment said the ordering is recorded. Assert it on the tree's input instead. Drop the plain range case from the loop, which the ordering table already compiles under `Interleaved`. Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com> --------- Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com> --- src/promql/src/native/plan.rs | 38 ++++++++++------ src/promql/src/native/plan/tests.rs | 48 ++++++++++++++++---- src/query/src/promql/native_compile.rs | 39 +++++++++------- src/query/src/promql/native_compile/tests.rs | 48 ++++++++++++++++++-- 4 files changed, 129 insertions(+), 44 deletions(-) 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 1c42c3b32af..ba01eb53cdd 100644 --- a/src/promql/src/native/plan/tests.rs +++ b/src/promql/src/native/plan/tests.rs @@ -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 { @@ -457,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/query/src/promql/native_compile.rs b/src/query/src/promql/native_compile.rs index 88e5023aa42..1be936d7980 100644 --- a/src/query/src/promql/native_compile.rs +++ b/src/query/src/promql/native_compile.rs @@ -34,7 +34,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; @@ -72,12 +74,12 @@ const SUPPORTED: &str = "a bare selector, a range function over a 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 range window consumes one series -/// to its last chunk before the next, so chunks of different series may not interleave, and an -/// `Interleaved` declaration is refused here — which is where the refusal belongs, since a tree that -/// declares it under a window cannot be built by this compiler 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 @@ -88,6 +90,17 @@ pub fn compile( input: Arc, ordering: SeriesOrdering, ) -> Result { + // Every shape this compiler accepts reads one series to its last chunk before the next, so raw + // samples whose chunks of different series interleave cannot be fed to any of them. The refusal + // is here rather than in a shape's branch so that a shape added later inherits it, and it comes + // before the early return below for a bare selector. + if ordering == SeriesOrdering::Interleaved { + return Err(not_implemented( + "raw samples whose series interleave, which this compiler cannot consume: a native \ + evaluation reads one series to its last chunk before the next, so chunks of different \ + series may not interleave", + )); + } // A bare selector is the other shape: an instant selection over the same raw input. if let PromExpr::VectorSelector(selector) = unparen(&stmt.expr) { return instant_selection(stmt, selector, input, ordering); @@ -126,16 +139,8 @@ pub fn compile( func.name, argument ))); }; - // A native range window reads one series to its last chunk before it reads the next, so raw - // samples whose chunks of different series interleave cannot be folded by it. The declaration - // is refused here rather than recorded onto an input the tree is built over and fails on. - if ordering == SeriesOrdering::Interleaved { - return Err(not_implemented( - "raw samples whose series interleave, which this compiler cannot consume: its range \ - window reads one series to its last chunk before the next, so chunks of different \ - series may not interleave", - )); - } + // The ordering was refused at the entry above, so every shape below is built over a scan whose + // series' chunks do not interleave. window_selection(stmt, function, vs, range, input, ordering) } diff --git a/src/query/src/promql/native_compile/tests.rs b/src/query/src/promql/native_compile/tests.rs index 5e375858111..05b4d07ead8 100644 --- a/src/query/src/promql/native_compile/tests.rs +++ b/src/query/src/promql/native_compile/tests.rs @@ -631,11 +631,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), @@ -707,6 +707,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 two cover the shapes that table cannot, + // because one returns before a window exists and the other takes a parameter. + for query in ["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]