use std::collections::{HashMap, HashSet};
use crate::promql::ast::{
BinaryOp, Expr, PromQLValue, Series, VectorMatching, VectorMatchingCardinality,
};
use super::{EvalError, PromQLEvaluator, QueryParams};
impl PromQLEvaluator {
pub(crate) fn eval_binary(
&self,
op: BinaryOp,
lhs: &Expr,
rhs: &Expr,
bool_mod: bool,
matching: Option<&VectorMatching>,
params: &QueryParams,
) -> Result<PromQLValue, EvalError> {
let lval = self.eval(lhs, params)?;
let rval = self.eval(rhs, params)?;
match (lval, rval) {
(PromQLValue::Scalar(a), PromQLValue::Scalar(b)) => {
let (result, _) = apply_binary_op(op, a, b, bool_mod);
Ok(PromQLValue::Scalar(result))
}
(PromQLValue::Vector(series), PromQLValue::Scalar(s)) => {
let drop_name = op.is_arithmetic() || (op.is_comparison() && bool_mod);
let result: Vec<_> = series
.into_iter()
.map(|mut ser| {
if drop_name {
ser.labels.retain(|(k, _)| k != "__name__");
}
ser.samples.retain_mut(|sample| {
let (val, keep) = apply_binary_op(op, sample.value, s, bool_mod);
sample.value = val;
keep
});
ser
})
.filter(|ser| !ser.samples.is_empty())
.collect();
Ok(PromQLValue::Vector(result))
}
(PromQLValue::Scalar(s), PromQLValue::Vector(series)) => {
let drop_name = op.is_arithmetic() || (op.is_comparison() && bool_mod);
let keep_vector_value = op.is_comparison() && !bool_mod;
let result: Vec<_> = series
.into_iter()
.map(|mut ser| {
if drop_name {
ser.labels.retain(|(k, _)| k != "__name__");
}
ser.samples.retain_mut(|sample| {
let original = sample.value;
let (val, keep) = apply_binary_op(op, s, original, bool_mod);
sample.value = if keep_vector_value { original } else { val };
keep
});
ser
})
.filter(|ser| !ser.samples.is_empty())
.collect();
Ok(PromQLValue::Vector(result))
}
(PromQLValue::Vector(left), PromQLValue::Vector(right)) => {
if op.is_set_operator() {
return Ok(PromQLValue::Vector(apply_set_op(
op, &left, &right, matching,
)));
}
let card = matching.map(|m| m.card);
let is_one_to_many = card == Some(VectorMatchingCardinality::OneToMany);
let mut result = Vec::new();
let mut used_right: HashSet<usize> = HashSet::new();
let drop_name = op.is_arithmetic() || (op.is_comparison() && bool_mod);
if is_one_to_many {
let left_index = build_matching_index(&left, matching);
for rs in &right {
let sig = matching_signature(&rs.labels, matching);
let left_matches = match left_index.get(&sig) {
Some(indices) => indices.as_slice(),
None => continue,
};
let mut matched_left: Option<usize> = None;
for &li in left_matches {
let ls = &left[li];
if let Some(prev) = matched_left
&& prev != li
{
return Err(EvalError(
"many-to-many matching not allowed: matching labels must be unique on one side".into(),
));
}
matched_left = Some(li);
let mut merged = rs.clone();
if drop_name {
merged.labels.retain(|(k, _)| k != "__name__");
}
let lv = ls.samples.first().map_or(f64::NAN, |s| s.value);
merged.samples.retain_mut(|sample| {
let (val, keep) = apply_binary_op(op, lv, sample.value, bool_mod);
sample.value = val;
keep
});
if let Some(m) = matching {
for inc in &m.include {
if let Some((_, v)) = ls.labels.iter().find(|(k, _)| k == inc) {
if let Some(pos) =
merged.labels.iter().position(|(k, _)| k == inc)
{
merged.labels[pos].1 = v.clone();
} else {
merged.labels.push((inc.clone(), v.clone()));
}
}
}
}
if merged.samples.is_empty() {
continue;
}
result.push(merged);
}
}
} else {
let is_many = card == Some(VectorMatchingCardinality::ManyToOne);
let right_index = build_matching_index(&right, matching);
if is_many {
if let Some(sig) = right_index
.iter()
.find(|(_, idx)| idx.len() > 1)
.map(|(sig, _)| sig.clone())
{
return Err(EvalError(format!(
"found duplicate series for the match group {} on the right \
hand-side of the operation: many-to-many matching not allowed, \
matching labels must be unique on one side",
describe_signature(&sig)
)));
}
}
if !is_many {
if let Some(sig) = right_index
.iter()
.find(|(_, idx)| idx.len() > 1)
.map(|(sig, _)| sig.clone())
{
return Err(EvalError(format!(
"found duplicate series on the right hand side of the operation: \
many-to-one matching must be explicit (group_left/group_right); \
match group {}",
describe_signature(&sig)
)));
}
let left_index = build_matching_index(&left, matching);
if let Some(sig) = left_index
.iter()
.find(|(sig, idx)| idx.len() > 1 && right_index.contains_key(*sig))
.map(|(sig, _)| sig.clone())
{
return Err(EvalError(format!(
"found duplicate series on the left hand side of the operation: \
many-to-one matching must be explicit (group_left/group_right); \
match group {}",
describe_signature(&sig)
)));
}
}
for ls in &left {
let sig = matching_signature(&ls.labels, matching);
let right_matches = match right_index.get(&sig) {
Some(indices) => indices.as_slice(),
None => continue,
};
for &ri in right_matches {
if !is_many && used_right.contains(&ri) {
continue;
}
let rs = &right[ri];
let mut merged = ls.clone();
if drop_name {
merged.labels.retain(|(k, _)| k != "__name__");
}
if !is_many {
prune_matching_labels(&mut merged.labels, matching);
}
let rv = rs.samples.first().map_or(f64::NAN, |s| s.value);
merged.samples.retain_mut(|sample| {
let (val, keep) = apply_binary_op(op, sample.value, rv, bool_mod);
sample.value = val;
keep
});
if let Some(m) = matching
&& m.card == VectorMatchingCardinality::ManyToOne
{
for inc in &m.include {
if let Some((_, v)) = rs.labels.iter().find(|(k, _)| k == inc) {
if let Some(pos) =
merged.labels.iter().position(|(k, _)| k == inc)
{
merged.labels[pos].1 = v.clone();
} else {
merged.labels.push((inc.clone(), v.clone()));
}
}
}
}
if merged.samples.is_empty() {
used_right.insert(ri);
if !is_many {
break;
}
continue;
}
result.push(merged);
used_right.insert(ri);
if !is_many {
break; }
}
}
}
Ok(PromQLValue::Vector(result))
}
_ => Err(EvalError("unsupported binary operation types".into())),
}
}
}
pub(crate) fn apply_binary_op(op: BinaryOp, a: f64, b: f64, bool_mod: bool) -> (f64, bool) {
match op {
BinaryOp::Add => (a + b, true),
BinaryOp::Sub => (a - b, true),
BinaryOp::Mul => (a * b, true),
BinaryOp::Div => (a / b, true),
BinaryOp::Mod => (a % b, true),
BinaryOp::Pow => (a.powf(b), true),
BinaryOp::Eql => {
let cmp = a == b;
if bool_mod {
(if cmp { 1.0 } else { 0.0 }, true)
} else {
(a, cmp)
}
}
BinaryOp::Neq => {
let cmp = a != b;
if bool_mod {
(if cmp { 1.0 } else { 0.0 }, true)
} else {
(a, cmp)
}
}
BinaryOp::Lss => {
let cmp = a < b;
if bool_mod {
(if cmp { 1.0 } else { 0.0 }, true)
} else {
(a, cmp)
}
}
BinaryOp::Gtr => {
let cmp = a > b;
if bool_mod {
(if cmp { 1.0 } else { 0.0 }, true)
} else {
(a, cmp)
}
}
BinaryOp::Lte => {
let cmp = a <= b;
if bool_mod {
(if cmp { 1.0 } else { 0.0 }, true)
} else {
(a, cmp)
}
}
BinaryOp::Gte => {
let cmp = a >= b;
if bool_mod {
(if cmp { 1.0 } else { 0.0 }, true)
} else {
(a, cmp)
}
}
BinaryOp::And | BinaryOp::Or | BinaryOp::Unless => {
(a, true)
}
}
}
#[cfg(test)]
pub(crate) fn labels_match(a: &[(String, String)], b: &[(String, String)]) -> bool {
let a_filtered: Vec<_> = a.iter().filter(|(k, _)| k != "__name__").collect();
let b_filtered: Vec<_> = b.iter().filter(|(k, _)| k != "__name__").collect();
a_filtered == b_filtered
}
#[cfg(test)]
pub(crate) fn labels_match_with_matching(
a: &[(String, String)],
b: &[(String, String)],
matching: Option<&VectorMatching>,
) -> bool {
matching_signature(a, matching) == matching_signature(b, matching)
}
fn prune_matching_labels(labels: &mut Vec<(String, String)>, matching: Option<&VectorMatching>) {
let Some(m) = matching else { return };
if m.on {
labels.retain(|(k, _)| m.labels.contains(k));
} else if !m.labels.is_empty() {
labels.retain(|(k, _)| !m.labels.contains(k));
}
}
fn describe_signature(sig: &str) -> String {
let mut out = String::from("{");
for (i, pair) in sig.split('\u{2}').filter(|p| !p.is_empty()).enumerate() {
let (k, v) = pair.split_once('\u{1}').unwrap_or((pair, ""));
if i > 0 {
out.push_str(", ");
}
out.push_str(k);
out.push_str("=\"");
out.push_str(v);
out.push('"');
}
out.push('}');
out
}
fn matching_signature(labels: &[(String, String)], matching: Option<&VectorMatching>) -> String {
let mut sig: Vec<(String, String)> = match matching {
Some(m) if m.on => labels
.iter()
.filter(|(k, _)| m.labels.contains(k))
.cloned()
.collect(),
Some(m) if !m.labels.is_empty() => labels
.iter()
.filter(|(k, _)| k != "__name__" && !m.labels.contains(k))
.cloned()
.collect(),
_ => labels
.iter()
.filter(|(k, _)| k != "__name__")
.cloned()
.collect(),
};
sig.sort();
let mut out = String::new();
for (k, v) in &sig {
out.push_str(k);
out.push('\u{1}');
out.push_str(v);
out.push('\u{2}');
}
out
}
fn build_matching_index(
series: &[Series],
matching: Option<&VectorMatching>,
) -> HashMap<String, Vec<usize>> {
let mut index: HashMap<String, Vec<usize>> = HashMap::new();
for (i, s) in series.iter().enumerate() {
index
.entry(matching_signature(&s.labels, matching))
.or_default()
.push(i);
}
index
}
fn apply_set_op(
op: BinaryOp,
left: &[Series],
right: &[Series],
matching: Option<&VectorMatching>,
) -> Vec<Series> {
let right_map = build_matching_index(right, matching);
match op {
BinaryOp::And => left
.iter()
.filter(|ls| {
let sig = matching_signature(&ls.labels, matching);
right_map.contains_key(&sig)
})
.cloned()
.collect(),
BinaryOp::Or => {
let left_sigs: HashSet<String> = left
.iter()
.map(|ls| matching_signature(&ls.labels, matching))
.collect();
let mut result: Vec<Series> = left.to_vec();
for rs in right {
let sig = matching_signature(&rs.labels, matching);
if !left_sigs.contains(&sig) {
result.push(rs.clone());
}
}
result
}
BinaryOp::Unless => left
.iter()
.filter(|ls| {
let sig = matching_signature(&ls.labels, matching);
!right_map.contains_key(&sig)
})
.cloned()
.collect(),
_ => left.to_vec(), }
}
#[cfg(test)]
mod tests {
use super::*;
use crate::promql::ast::Sample;
use crate::promql::ast::VectorMatchingCardinality;
#[test]
fn test_apply_binary_op() {
assert_eq!(apply_binary_op(BinaryOp::Add, 1.0, 2.0, false), (3.0, true));
assert_eq!(apply_binary_op(BinaryOp::Sub, 5.0, 3.0, false), (2.0, true));
assert_eq!(
apply_binary_op(BinaryOp::Mul, 4.0, 3.0, false),
(12.0, true)
);
assert_eq!(
apply_binary_op(BinaryOp::Div, 10.0, 4.0, false),
(2.5, true)
);
assert_eq!(apply_binary_op(BinaryOp::Pow, 2.0, 3.0, false), (8.0, true));
}
#[test]
fn test_comparison_bool_modifier() {
assert_eq!(apply_binary_op(BinaryOp::Gtr, 5.0, 3.0, true), (1.0, true));
assert_eq!(apply_binary_op(BinaryOp::Gtr, 1.0, 3.0, true), (0.0, true));
assert_eq!(apply_binary_op(BinaryOp::Eql, 5.0, 5.0, true), (1.0, true));
assert_eq!(apply_binary_op(BinaryOp::Eql, 5.0, 3.0, true), (0.0, true));
}
#[test]
fn test_comparison_filter_mode() {
let (val, keep) = apply_binary_op(BinaryOp::Gtr, 5.0, 3.0, false);
assert_eq!(val, 5.0);
assert!(keep);
let (val, keep) = apply_binary_op(BinaryOp::Gtr, 1.0, 3.0, false);
assert_eq!(val, 1.0);
assert!(!keep);
let (val, keep) = apply_binary_op(BinaryOp::Neq, f64::NAN, 5.0, false);
assert!(val.is_nan());
assert!(keep); }
#[test]
fn test_labels_match() {
let a = vec![
("__name__".into(), "a".into()),
("host".into(), "srv1".into()),
];
let b = vec![
("__name__".into(), "b".into()),
("host".into(), "srv1".into()),
];
assert!(labels_match(&a, &b));
let c = vec![
("__name__".into(), "c".into()),
("host".into(), "srv2".into()),
];
assert!(!labels_match(&a, &c));
}
#[test]
fn test_labels_match_with_on_modifier() {
let a = vec![
("__name__".into(), "cpu".into()),
("host".into(), "srv1".into()),
("region".into(), "us".into()),
];
let b = vec![
("__name__".into(), "mem".into()),
("host".into(), "srv1".into()),
("region".into(), "eu".into()),
];
assert!(!labels_match(&a, &b));
let matching = VectorMatching {
card: VectorMatchingCardinality::OneToOne,
labels: vec!["host".into()],
on: true,
include: vec![],
};
assert!(labels_match_with_matching(&a, &b, Some(&matching)));
let matching_ign = VectorMatching {
card: VectorMatchingCardinality::OneToOne,
labels: vec!["region".into()],
on: false,
include: vec![],
};
assert!(labels_match_with_matching(&a, &b, Some(&matching_ign)));
}
#[test]
fn test_labels_match_with_on_no_match() {
let a = vec![
("host".into(), "srv1".into()),
("region".into(), "us".into()),
];
let b = vec![
("host".into(), "srv2".into()),
("region".into(), "us".into()),
];
let matching = VectorMatching {
card: VectorMatchingCardinality::OneToOne,
labels: vec!["host".into()],
on: true,
include: vec![],
};
assert!(!labels_match_with_matching(&a, &b, Some(&matching)));
}
#[test]
fn test_binary_op_drops_name() {
assert!(BinaryOp::Add.is_arithmetic());
assert!(BinaryOp::Sub.is_arithmetic());
assert!(BinaryOp::Mul.is_arithmetic());
assert!(BinaryOp::Div.is_arithmetic());
assert!(BinaryOp::Mod.is_arithmetic());
assert!(BinaryOp::Pow.is_arithmetic());
assert!(!BinaryOp::Eql.is_arithmetic());
assert!(!BinaryOp::And.is_arithmetic());
}
#[test]
fn test_apply_set_op_with_on_matching() {
let left = vec![
Series {
labels: vec![("host".into(), "a".into()), ("region".into(), "us".into())],
samples: vec![Sample {
timestamp: 0,
value: 1.0,
}],
histograms: Vec::new(),
},
Series {
labels: vec![("host".into(), "b".into()), ("region".into(), "eu".into())],
samples: vec![Sample {
timestamp: 0,
value: 2.0,
}],
histograms: Vec::new(),
},
];
let right = vec![Series {
labels: vec![("host".into(), "a".into()), ("region".into(), "eu".into())],
samples: vec![Sample {
timestamp: 0,
value: 3.0,
}],
histograms: Vec::new(),
}];
let result_no_match = apply_set_op(BinaryOp::And, &left, &right, None);
assert!(result_no_match.is_empty());
let matching = VectorMatching {
card: VectorMatchingCardinality::OneToOne,
labels: vec!["host".into()],
on: true,
include: vec![],
};
let result_on = apply_set_op(BinaryOp::And, &left, &right, Some(&matching));
assert_eq!(result_on.len(), 1);
assert_eq!(
result_on[0]
.labels
.iter()
.find(|(k, _)| k == "host")
.unwrap()
.1,
"a"
);
}
}