1use promql_parser::label::{MatchOp, Matcher, Matchers};
18use promql_parser::parser::{
19 AggregateExpr, BinaryExpr, Expr, LabelModifier, VectorMatchCardinality, token,
20};
21
22const MATCH_ALL_REGEX: &str = "^(?:.*)$";
25
26const LABEL_PRESERVING_RANGE_FUNCTIONS: [&str; 20] = [
30 "avg_over_time",
31 "changes",
32 "count_over_time",
33 "delta",
34 "deriv",
35 "double_exponential_smoothing",
36 "idelta",
37 "increase",
38 "irate",
39 "last_over_time",
40 "max_over_time",
41 "min_over_time",
42 "predict_linear",
43 "present_over_time",
44 "quantile_over_time",
45 "rate",
46 "resets",
47 "stddev_over_time",
48 "stdvar_over_time",
49 "sum_over_time",
50];
51
52pub(super) fn propagate(
74 binary: &BinaryExpr,
75 left_tags: &[String],
76 right_tags: &[String],
77 left_field_labels: &[String],
78 right_field_labels: &[String],
79) -> Option<BinaryExpr> {
80 match try_propagate(
81 binary,
82 left_tags,
83 right_tags,
84 left_field_labels,
85 right_field_labels,
86 ) {
87 Ok(rewritten) => Some(rewritten),
88 Err(reason) => {
89 common_telemetry::debug!("Matching filter not propagated ({reason}): {binary}");
90 None
91 }
92 }
93}
94
95fn try_propagate(
97 binary: &BinaryExpr,
98 left_tags: &[String],
99 right_tags: &[String],
100 left_field_labels: &[String],
101 right_field_labels: &[String],
102) -> Result<BinaryExpr, &'static str> {
103 if !matches!(
104 binary.op.id(),
105 token::T_ADD | token::T_SUB | token::T_MUL | token::T_DIV | token::T_MOD | token::T_POW
106 ) {
107 return Err("operator is not arithmetic");
108 }
109 if let Some(modifier) = &binary.modifier
110 && (modifier.fill_values.lhs.is_some() || modifier.fill_values.rhs.is_some())
111 {
112 return Err("operand carries a fill modifier");
113 }
114 let matching = binary
115 .modifier
116 .as_ref()
117 .and_then(|modifier| modifier.matching.as_ref());
118
119 let is_matching_label = |name: &String| {
120 !name.starts_with("__")
122 && left_tags.contains(name)
123 && right_tags.contains(name)
124 && !left_field_labels.contains(name)
126 && !right_field_labels.contains(name)
127 && match matching {
128 None => true,
129 Some(LabelModifier::Include(on)) => on.labels.contains(name),
130 Some(LabelModifier::Exclude(ignoring)) => !ignoring.labels.contains(name),
131 }
132 };
133 if let Some(modifier) = &binary.modifier {
134 let unique_on_matching_labels = |expr: &Expr, tags: &[String]| {
135 has_unique_aggregate_output(expr) && tags.iter().all(&is_matching_label)
136 };
137 let supported_cardinality = match &modifier.card {
138 VectorMatchCardinality::OneToOne => true,
139 VectorMatchCardinality::ManyToOne(_) => {
140 unique_on_matching_labels(&binary.rhs, right_tags)
141 }
142 VectorMatchCardinality::OneToMany(_) => {
143 unique_on_matching_labels(&binary.lhs, left_tags)
144 }
145 VectorMatchCardinality::ManyToMany => false,
146 };
147 if !supported_cardinality {
148 return Err("one-side uniqueness is not proven");
149 }
150 }
151 let mut rewritten = binary.clone();
152 let left = selector_matchers(&mut rewritten.lhs).ok_or("left operand is not a selector")?;
153 let right = selector_matchers(&mut rewritten.rhs).ok_or("right operand is not a selector")?;
154 if !left.or_matchers.is_empty() || !right.or_matchers.is_empty() {
155 return Err("selector has an or matcher group");
156 }
157
158 let constraints = left
159 .matchers
160 .iter()
161 .chain(&right.matchers)
162 .filter(|matcher| is_matching_label(&matcher.name) && !matches_every_value(matcher))
163 .cloned()
164 .collect::<Vec<_>>();
165
166 let mut changed = false;
167 for matcher in constraints {
168 for (target, operand) in [
169 (&mut left.matchers, &binary.lhs),
170 (&mut right.matchers, &binary.rhs),
171 ] {
172 if !target.contains(&matcher) && preserves_filter(operand, &matcher.name) {
173 target.push(matcher.clone());
174 changed = true;
175 }
176 }
177 }
178 if !changed {
179 return Err("operands already carry the same matching-label matchers");
180 }
181 Ok(rewritten)
182}
183
184fn matches_every_value(matcher: &Matcher) -> bool {
185 matches!(&matcher.op, MatchOp::Re(re) if re.as_str() == MATCH_ALL_REGEX)
186}
187
188fn partitions_by_grouping_labels(aggregate: &AggregateExpr) -> bool {
194 matches!(
195 aggregate.op.id(),
196 token::T_SUM
197 | token::T_AVG
198 | token::T_COUNT
199 | token::T_MIN
200 | token::T_MAX
201 | token::T_GROUP
202 | token::T_STDDEV
203 | token::T_STDVAR
204 | token::T_QUANTILE
205 )
206}
207
208fn ranks_by_grouping_labels(aggregate: &AggregateExpr) -> bool {
210 matches!(aggregate.op.id(), token::T_TOPK | token::T_BOTTOMK)
211}
212
213fn has_unique_aggregate_output(expr: &Expr) -> bool {
218 match expr {
219 Expr::Paren(paren) => has_unique_aggregate_output(&paren.expr),
220 Expr::Aggregate(aggregate) if partitions_by_grouping_labels(aggregate) => true,
221 Expr::Aggregate(aggregate) if ranks_by_grouping_labels(aggregate) => {
222 has_unique_aggregate_output(&aggregate.expr)
223 }
224 _ => false,
225 }
226}
227
228fn vector_operand_is_lhs(binary: &BinaryExpr) -> Option<bool> {
229 if !matches!(
230 binary.op.id(),
231 token::T_ADD | token::T_SUB | token::T_MUL | token::T_DIV | token::T_MOD | token::T_POW
232 ) || binary.modifier.is_some()
233 {
234 return None;
235 }
236 if matches!(binary.lhs.as_ref(), Expr::NumberLiteral(_)) {
237 Some(false)
238 } else if matches!(binary.rhs.as_ref(), Expr::NumberLiteral(_)) {
239 Some(true)
240 } else {
241 None
242 }
243}
244
245fn preserves_filter(expr: &Expr, label: &str) -> bool {
250 match expr {
251 Expr::VectorSelector(_) => true,
252 Expr::Paren(paren) => preserves_filter(&paren.expr, label),
253 Expr::Aggregate(aggregate) => {
254 let partition_label = match &aggregate.modifier {
255 Some(LabelModifier::Include(labels)) => labels.labels.iter().any(|x| x == label),
256 Some(LabelModifier::Exclude(labels)) => labels.labels.iter().all(|x| x != label),
257 None => false,
258 };
259 partition_label && preserves_filter(&aggregate.expr, label)
260 }
261 Expr::Binary(binary) => match vector_operand_is_lhs(binary) {
262 Some(true) => preserves_filter(&binary.lhs, label),
263 Some(false) => preserves_filter(&binary.rhs, label),
264 None => false,
265 },
266 Expr::Call(call) if LABEL_PRESERVING_RANGE_FUNCTIONS.contains(&call.func.name) => true,
268 _ => false,
269 }
270}
271
272fn selector_matchers(expr: &mut Expr) -> Option<&mut Matchers> {
275 match expr {
276 Expr::VectorSelector(selector) => Some(&mut selector.matchers),
277 Expr::Paren(paren) => selector_matchers(&mut paren.expr),
278 Expr::Aggregate(aggregate)
279 if partitions_by_grouping_labels(aggregate) || ranks_by_grouping_labels(aggregate) =>
280 {
281 selector_matchers(&mut aggregate.expr)
282 }
283 Expr::Binary(binary) => match vector_operand_is_lhs(binary) {
284 Some(true) => selector_matchers(&mut binary.lhs),
285 Some(false) => selector_matchers(&mut binary.rhs),
286 None => None,
287 },
288 Expr::Call(call) if LABEL_PRESERVING_RANGE_FUNCTIONS.contains(&call.func.name) => {
289 let matrix = single_matrix_argument(&call.args.args)?;
291 match call.args.args[matrix].as_mut() {
292 Expr::MatrixSelector(selector) => Some(&mut selector.vs.matchers),
293 _ => None,
294 }
295 }
296 _ => None,
298 }
299}
300
301fn single_matrix_argument(args: &[Box<Expr>]) -> Option<usize> {
302 let mut found = None;
303 for (index, arg) in args.iter().enumerate() {
304 if matches!(arg.as_ref(), Expr::MatrixSelector(_)) {
305 if found.is_some() {
306 return None;
307 }
308 found = Some(index);
309 }
310 }
311 found
312}
313
314#[cfg(test)]
315mod tests {
316 use promql_parser::parser::parse;
317
318 use super::*;
319
320 fn rewrite_with(query: &str, left_tags: &[&str], right_tags: &[&str]) -> Option<Expr> {
321 rewrite_labels_with(query, left_tags, &[], right_tags, &[])
322 }
323
324 fn rewrite_labels_with(
327 query: &str,
328 left_tags: &[&str],
329 left_field_labels: &[&str],
330 right_tags: &[&str],
331 right_field_labels: &[&str],
332 ) -> Option<Expr> {
333 let Expr::Binary(binary) = parse(query).unwrap() else {
334 panic!("expected binary")
335 };
336 let owned = |tags: &[&str]| tags.iter().map(|tag| tag.to_string()).collect::<Vec<_>>();
337 propagate(
338 &binary,
339 &owned(left_tags),
340 &owned(right_tags),
341 &owned(left_field_labels),
342 &owned(right_field_labels),
343 )
344 .map(Expr::Binary)
345 }
346
347 fn rewrite(query: &str) -> Option<Expr> {
348 rewrite_with(query, &["host", "zone"], &["host", "zone"])
349 }
350
351 #[track_caller]
352 fn assert_rewrite(query: &str, expected: &str) {
353 assert_eq!(rewrite(query).unwrap(), parse(expected).unwrap(), "{query}");
354 assert!(rewrite(expected).is_none(), "{expected}");
356 }
357
358 #[test]
359 fn propagates_only_explicit_matching_labels() {
360 assert_rewrite(
361 r#"a / on(host) b{host="x",zone="y"}"#,
362 r#"a{host="x"} / on(host) b{host="x",zone="y"}"#,
363 );
364 }
365
366 #[test]
367 fn propagates_labels_not_named_in_ignoring() {
368 assert_rewrite(
369 r#"a{zone="y"} / ignoring(zone) b{host="x"}"#,
370 r#"a{zone="y",host="x"} / ignoring(zone) b{host="x"}"#,
371 );
372 }
373
374 #[test]
375 fn propagates_default_matching_without_metric_name() {
376 assert_rewrite(
377 r#"a / {host="x",__name__="b"}"#,
378 r#"a{host="x"} / {host="x",__name__="b"}"#,
379 );
380 assert!(rewrite(r#"a / {__name__="b"}"#).is_none());
381 }
382
383 #[test]
384 fn propagates_only_names_that_are_tags_on_both_sides() {
385 for query in [r#"a / b{value="2"}"#, r#"a / on(host,value) b{value="2"}"#] {
387 assert!(rewrite(query).is_none(), "{query}");
388 }
389 assert_rewrite(
390 r#"a / b{host="x",value="2"}"#,
391 r#"a{host="x"} / b{host="x",value="2"}"#,
392 );
393 assert!(rewrite_with(r#"a / b{zone="y"}"#, &["host"], &["host", "zone"]).is_none());
395 }
396
397 #[test]
398 fn propagates_matchers_the_join_enforces_anyway() {
399 for (query, expected) in [
400 (
401 r#"a / b{host=~"x.*"}"#,
402 r#"a{host=~"x.*"} / b{host=~"x.*"}"#,
403 ),
404 (r#"a / b{host!="x"}"#, r#"a{host!="x"} / b{host!="x"}"#),
405 (
406 r#"a / b{host!~"x.*"}"#,
407 r#"a{host!~"x.*"} / b{host!~"x.*"}"#,
408 ),
409 (r#"a / b{host=""}"#, r#"a{host=""} / b{host=""}"#),
410 ] {
411 assert_rewrite(query, expected);
412 }
413 assert!(rewrite(r#"a / b{host=~".*"}"#).is_none());
414 }
415
416 #[test]
417 fn propagates_through_partitioning_aggregations() {
418 assert_rewrite(
419 r#"sum by(host) (a) / on(host) max by(host) (b{host="x"})"#,
420 r#"sum by(host) (a{host="x"}) / on(host) max by(host) (b{host="x"})"#,
421 );
422 assert_rewrite(
423 r#"avg without(zone) (rate(a[5m])) / b{host="x"}"#,
424 r#"avg without(zone) (rate(a{host="x"}[5m])) / b{host="x"}"#,
425 );
426 }
427
428 #[test]
429 fn does_not_propagate_grouping_labels_that_are_value_fields() {
430 assert!(
433 rewrite_labels_with(
434 r#"count by(status) (a) / on(status) count by(status) (b{status="ready"})"#,
435 &["status"],
436 &["status"],
437 &["status"],
438 &["status"],
439 )
440 .is_none()
441 );
442 assert!(
444 rewrite_labels_with(
445 r#"sum by(host, status) (a) / on(status) sum by(host, status) (b{status="ready"})"#,
446 &["host", "status"],
447 &["status"],
448 &["host", "status"],
449 &["status"],
450 )
451 .is_none()
452 );
453 }
454
455 #[test]
456 fn propagates_grouping_labels_of_aggregated_operands() {
457 assert_rewrite(
458 r#"count by(host) (a) / on(host) count by(host) (b{host="x"})"#,
459 r#"count by(host) (a{host="x"}) / on(host) count by(host) (b{host="x"})"#,
460 );
461 assert_rewrite(
462 r#"sum by(host) (rate(a[5m])) / on(host) sum by(host) (b{host="x"})"#,
463 r#"sum by(host) (rate(a{host="x"}[5m])) / on(host) sum by(host) (b{host="x"})"#,
464 );
465 }
466
467 #[test]
468 fn keeps_matchers_outside_the_grouping_labels_on_their_own_operand() {
469 assert_rewrite(
470 r#"sum by(host) (a) / on(host) sum by(host) (b{host="x",status="ready"})"#,
471 r#"sum by(host) (a{host="x"}) / on(host) sum by(host) (b{host="x",status="ready"})"#,
472 );
473 assert!(rewrite(r#"avg without(host) (a) / on(host) b{host="x"}"#).is_none());
475 assert_rewrite(
476 r#"avg without(zone) (a) / on(host) b{host="x"}"#,
477 r#"avg without(zone) (a{host="x"}) / on(host) b{host="x"}"#,
478 );
479 }
480
481 #[test]
482 fn leaves_selecting_aggregations_alone() {
483 for query in [
484 r#"topk(3, a) / on(host) b{host="x"}"#,
485 r#"bottomk(3, a) / on(host) b{host="x"}"#,
486 r#"count_values("v", a) / on(host) b{host="x"}"#,
487 ] {
488 assert!(rewrite(query).is_none(), "{query}");
489 }
490 }
491
492 #[test]
493 fn finds_the_matrix_argument_of_multi_argument_rollups() {
494 assert_rewrite(
495 r#"quantile_over_time(0.9, a[5m]) / on(host) b{host="x"}"#,
496 r#"quantile_over_time(0.9, a{host="x"}[5m]) / on(host) b{host="x"}"#,
497 );
498 assert_rewrite(
499 r#"predict_linear(a[5m], 60) / on(host) b{host="x"}"#,
500 r#"predict_linear(a{host="x"}[5m], 60) / on(host) b{host="x"}"#,
501 );
502 assert_rewrite(
503 r#"double_exponential_smoothing(a[5m], 0.5, 0.5) / on(host) b{host="x"}"#,
504 r#"double_exponential_smoothing(a{host="x"}[5m], 0.5, 0.5) / on(host) b{host="x"}"#,
505 );
506 }
507
508 #[test]
509 fn preserves_windows_offsets_and_conflicting_matchers() {
510 assert_eq!(
511 rewrite(
512 r#"rate(a{host="x"}[5m] offset 1h) / on(host) count_over_time(b{host="y"}[1m])"#
513 )
514 .unwrap(),
515 parse(
516 r#"rate(a{host="x",host="y"}[5m] offset 1h) / on(host) count_over_time(b{host="y",host="x"}[1m])"#
517 )
518 .unwrap()
519 );
520 }
521
522 #[test]
523 fn propagates_through_scalar_arithmetic_and_ranking() {
524 assert_rewrite(
525 r#"topk(1, a{host="x"}) / on(host) b"#,
526 r#"topk(1, a{host="x"}) / on(host) b{host="x"}"#,
527 );
528 assert_rewrite(
529 r#"a / on(host) bottomk(1, b{host="x"})"#,
530 r#"a{host="x"} / on(host) bottomk(1, b{host="x"})"#,
531 );
532 assert_rewrite(
533 r#"(8 * rate(a{host="x"}[5m])) / on(host) topk by(host)(1, b)"#,
534 r#"(8 * rate(a{host="x"}[5m])) / on(host) topk by(host)(1, b{host="x"})"#,
535 );
536 assert_rewrite(
537 r#"bottomk without(zone)(2, a / 8) / on(host) b{host="x"}"#,
538 r#"bottomk without(zone)(2, a{host="x"} / 8) / on(host) b{host="x"}"#,
539 );
540 }
541
542 #[test]
543 fn propagates_grouped_matching_only_with_a_unique_one_side() {
544 let cases = [
545 (
546 r#"(8 * rate(a{host="x"}[5m])) / on(host) group_left topk by(host)(1, max by(host)(b))"#,
547 r#"(8 * rate(a{host="x"}[5m])) / on(host) group_left topk by(host)(1, max by(host)(b{host="x"}))"#,
548 vec!["host", "zone"],
549 vec!["host"],
550 ),
551 (
552 r#"sum by(host)(a) / on(host) group_right b{host!="x"}"#,
553 r#"sum by(host)(a{host!="x"}) / on(host) group_right b{host!="x"}"#,
554 vec!["host"],
555 vec!["host", "zone"],
556 ),
557 (
558 r#"a{host=""} / on(host) group_left max by(host)(b)"#,
559 r#"a{host=""} / on(host) group_left max by(host)(b{host=""})"#,
560 vec!["host", "zone"],
561 vec!["host"],
562 ),
563 ];
564 for (query, expected, left_tags, right_tags) in cases {
565 assert_eq!(
566 rewrite_with(query, &left_tags, &right_tags).unwrap(),
567 parse(expected).unwrap(),
568 "{query}"
569 );
570 assert!(rewrite_with(expected, &left_tags, &right_tags).is_none());
571 }
572 }
573
574 #[test]
575 fn preserves_ranking_candidates_and_unproven_cardinality() {
576 for query in [
577 r#"a{host="x"} / on(host) group_left max by(host,zone)(b)"#,
578 r#"a{host="x"} / on(host) group_left topk by(host)(1, b)"#,
579 r#"a{host="x"} / on(host) group_left topk(1, max by(host)(b))"#,
580 r#"topk by(zone)(1, a) / on(host) b{host="x"}"#,
581 r#"topk by(host)(1, topk(1, a)) / on(host) b{host="x"}"#,
582 r#"topk by(host)(1, sum by(zone)(a)) / on(host) b{host="x"}"#,
583 r#"(a + vector(8)) / on(host) b{host="x"}"#,
584 ] {
585 assert!(rewrite(query).is_none(), "{query}");
586 }
587 assert!(
589 rewrite_with(
590 r#"a{host="x"} / on(host) group_left topk(1, max by(host)(b))"#,
591 &["host", "zone"],
592 &["host"]
593 )
594 .is_none()
595 );
596 }
597
598 #[test]
599 fn leaves_unproven_semantics_unchanged() {
600 for query in [
601 r#"a or on(host) b{host="x"}"#,
602 r#"a / on(host) group_left b{host="x"}"#,
603 r#"sum by(host,zone)(a) / on(host) group_right b{host="x"}"#,
604 r#"label_replace(a,"host","x","zone",".*") / on(host) b{host="x"}"#,
605 r#"a > on(host) b{host="x"}"#,
606 r#"a / on(host) absent_over_time(b{host="x"}[5m])"#,
607 r#"a / on(host) (b{host="x"} + b)"#,
608 ] {
609 assert!(rewrite(query).is_none(), "{query}");
610 }
611 }
612}