Skip to content

Commit

Permalink
feat: add or operator for metric expression (#84)
Browse files Browse the repository at this point in the history
* feat: add |-operator for metric expression #83

* fix review issues

* Extended Machers structure to support or filters

* add last_filter_action to mark the last append type

* fix review issures

* remove unnecessary variable in tests

* refactor test cases

* add some case insensitive test cases

* fix cargo clippy warnings
  • Loading branch information
groobyming authored May 20, 2024
1 parent 5bc0088 commit b11faca
Show file tree
Hide file tree
Showing 4 changed files with 205 additions and 16 deletions.
88 changes: 72 additions & 16 deletions src/label/matcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,15 +102,19 @@ impl Matcher {
}

pub fn new_matcher(id: TokenId, name: String, value: String) -> Result<Matcher, String> {
let op = Self::find_matcher_op(id, &value)?;
op.map(|op| Matcher::new(op, name.as_str(), value.as_str()))
}

fn find_matcher_op(id: TokenId, value: &str) -> Result<Result<MatchOp, String>, String> {
let op = match id {
T_EQL => Ok(MatchOp::Equal),
T_NEQ => Ok(MatchOp::NotEqual),
T_EQL_REGEX => Ok(MatchOp::Re(Matcher::try_parse_re(&value)?)),
T_NEQ_REGEX => Ok(MatchOp::NotRe(Matcher::try_parse_re(&value)?)),
T_EQL_REGEX => Ok(MatchOp::Re(Matcher::try_parse_re(value)?)),
T_NEQ_REGEX => Ok(MatchOp::NotRe(Matcher::try_parse_re(value)?)),
_ => Err(format!("invalid match op {}", token_display(id))),
};

op.map(|op| Matcher { op, name, value })
Ok(op)
}
}

Expand Down Expand Up @@ -181,24 +185,58 @@ fn try_escape_for_repeat_re(re: &str) -> String {
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Matchers {
pub matchers: Vec<Matcher>,
pub or_matchers: Vec<Vec<Matcher>>,
}

impl Matchers {
pub fn empty() -> Self {
Self { matchers: vec![] }
Self {
matchers: vec![],
or_matchers: vec![],
}
}

pub fn one(matcher: Matcher) -> Self {
let matchers = vec![matcher];
Self { matchers }
Self {
matchers,
or_matchers: vec![],
}
}

pub fn new(matchers: Vec<Matcher>) -> Self {
Self { matchers }
Self {
matchers,
or_matchers: vec![],
}
}

pub fn with_or_matchers(mut self, or_matchers: Vec<Vec<Matcher>>) -> Self {
self.or_matchers = or_matchers;
self
}

pub fn append(mut self, matcher: Matcher) -> Self {
self.matchers.push(matcher);
// Check the latest or_matcher group. If it is not empty,
// we need to add the current matcher to this group.
let last_or_matcher = self.or_matchers.last_mut();
if let Some(last_or_matcher) = last_or_matcher {
last_or_matcher.push(matcher);
} else {
self.matchers.push(matcher);
}
self
}

pub fn append_or(mut self, matcher: Matcher) -> Self {
if !self.matchers.is_empty() {
// Be careful not to move ownership here, because it
// will be used by the subsequent append method.
let last_matchers = std::mem::take(&mut self.matchers);
self.or_matchers.push(last_matchers);
}
let new_or_matchers = vec![matcher];
self.or_matchers.push(new_or_matchers);
self
}

Expand All @@ -208,24 +246,29 @@ impl Matchers {
/// The following expression is illegal:
/// {job=~".*"} # Bad!
pub fn is_empty_matchers(&self) -> bool {
self.matchers.is_empty() || self.matchers.iter().all(|m| m.is_match(""))
(self.matchers.is_empty() && self.or_matchers.is_empty())
|| self
.matchers
.iter()
.chain(self.or_matchers.iter().flatten())
.all(|m| m.is_match(""))
}

/// find the matcher's value whose name equals the specified name. This function
/// is designed to prepare error message of invalid promql expression.
pub(crate) fn find_matcher_value(&self, name: &str) -> Option<String> {
for m in &self.matchers {
if m.name.eq(name) {
return Some(m.value.clone());
}
}
None
self.matchers
.iter()
.chain(self.or_matchers.iter().flatten())
.find(|m| m.name.eq(name))
.map(|m| m.value.clone())
}

/// find matchers whose name equals the specified name
pub fn find_matchers(&self, name: &str) -> Vec<Matcher> {
self.matchers
.iter()
.chain(self.or_matchers.iter().flatten())
.filter(|m| m.name.eq(name))
.cloned()
.collect()
Expand All @@ -234,7 +277,20 @@ impl Matchers {

impl fmt::Display for Matchers {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", join_vector(&self.matchers, ",", true))
let simple_matchers = &self.matchers;
let or_matchers = &self.or_matchers;
if or_matchers.is_empty() {
write!(f, "{}", join_vector(simple_matchers, ",", true))
} else {
let or_matchers_string =
self.or_matchers
.iter()
.fold(String::new(), |or_matchers_str, pair| {
format!("{} or {}", or_matchers_str, join_vector(pair, ", ", false))
});
let or_matchers_string = or_matchers_string.trim_start_matches(" or").trim();
write!(f, "{}", or_matchers_string)
}
}
}

Expand Down
12 changes: 12 additions & 0 deletions src/parser/lex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -564,6 +564,18 @@ impl Lexer {
match self.pop() {
Some('#') => State::LineComment,
Some(',') => State::Lexeme(T_COMMA),
Some('o') | Some('O') => {
if let Some('r') | Some('R') = self.peek() {
self.pop();
if let Some(' ') = self.peek() {
State::Lexeme(T_LOR)
} else {
State::Identifier
}
} else {
State::Identifier
}
}
Some(ch) if ch.is_ascii_whitespace() => State::Space,
Some(ch) if is_alpha(ch) => State::Identifier,
Some(ch) if STRING_SYMBOLS.contains(ch) => State::String(ch),
Expand Down
120 changes: 120 additions & 0 deletions src/parser/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,15 @@ mod tests {
use regex::Regex;

use crate::label::{Labels, MatchOp, Matcher, Matchers, METRIC_NAME};
use crate::parser;
use crate::parser::function::get_function;
use crate::parser::{
token, AtModifier as At, BinModifier, Expr, FunctionArgs, LabelModifier, Offset,
VectorMatchCardinality, VectorSelector, INVALID_QUERY_INFO,
};
use crate::util::duration;
use std::time::Duration;
use std::vec;

struct Case {
input: String,
Expand Down Expand Up @@ -2115,4 +2117,122 @@ mod tests {
];
assert_cases(fail_cases);
}

#[test]
fn test_or_filters() {
let cases = vec![
(r#"foo{label1="1" or label1="2"}"#, {
let matchers = Matchers::new(vec![]).with_or_matchers(vec![
vec![Matcher::new(MatchOp::Equal, "label1", "1")],
vec![Matcher::new(MatchOp::Equal, "label1", "2")],
]);
Expr::new_vector_selector(Some(String::from("foo")), matchers)
}),
(r#"foo{label1="1" OR label1="2"}"#, {
let matchers = Matchers::new(vec![]).with_or_matchers(vec![
vec![Matcher::new(MatchOp::Equal, "label1", "1")],
vec![Matcher::new(MatchOp::Equal, "label1", "2")],
]);
Expr::new_vector_selector(Some(String::from("foo")), matchers)
}),
(r#"foo{label1="1" Or label1="2"}"#, {
let matchers = Matchers::new(vec![]).with_or_matchers(vec![
vec![Matcher::new(MatchOp::Equal, "label1", "1")],
vec![Matcher::new(MatchOp::Equal, "label1", "2")],
]);
Expr::new_vector_selector(Some(String::from("foo")), matchers)
}),
(r#"foo{label1="1" oR label1="2"}"#, {
let matchers = Matchers::new(vec![]).with_or_matchers(vec![
vec![Matcher::new(MatchOp::Equal, "label1", "1")],
vec![Matcher::new(MatchOp::Equal, "label1", "2")],
]);
Expr::new_vector_selector(Some(String::from("foo")), matchers)
}),
(r#"foo{label1="1" or or="or"}"#, {
let matchers = Matchers::new(vec![]).with_or_matchers(vec![
vec![Matcher::new(MatchOp::Equal, "label1", "1")],
vec![Matcher::new(MatchOp::Equal, "or", "or")],
]);
Expr::new_vector_selector(Some(String::from("foo")), matchers)
}),
(
r#"foo{label1="1" or label1="2" or label1="3" or label1="4"}"#,
{
let matchers = Matchers::new(vec![]).with_or_matchers(vec![
vec![Matcher::new(MatchOp::Equal, "label1", "1")],
vec![Matcher::new(MatchOp::Equal, "label1", "2")],
vec![Matcher::new(MatchOp::Equal, "label1", "3")],
vec![Matcher::new(MatchOp::Equal, "label1", "4")],
]);
Expr::new_vector_selector(Some(String::from("foo")), matchers)
},
),
(
r#"foo{label1="1" or label1="2" or label1="3", label2="4"}"#,
{
let matchers = Matchers::new(vec![]).with_or_matchers(vec![
vec![Matcher::new(MatchOp::Equal, "label1", "1")],
vec![Matcher::new(MatchOp::Equal, "label1", "2")],
vec![
Matcher::new(MatchOp::Equal, "label1", "3"),
Matcher::new(MatchOp::Equal, "label2", "4"),
],
]);
Expr::new_vector_selector(Some(String::from("foo")), matchers)
},
),
(
r#"foo{label1="1", label2="2" or label1="3" or label1="4"}"#,
{
let matchers = Matchers::new(vec![]).with_or_matchers(vec![
vec![
Matcher::new(MatchOp::Equal, "label1", "1"),
Matcher::new(MatchOp::Equal, "label2", "2"),
],
vec![Matcher::new(MatchOp::Equal, "label1", "3")],
vec![Matcher::new(MatchOp::Equal, "label1", "4")],
]);
Expr::new_vector_selector(Some(String::from("foo")), matchers)
},
),
];
assert_cases(Case::new_result_cases(cases));

let display_cases = [
r#"a{label1="1"}"#,
r#"a{label1="1" or label2="2"}"#,
r#"a{label1="1" or label2="2" or label3="3" or label4="4"}"#,
r#"a{label1="1", label2="2" or label3="3" or label4="4"}"#,
r#"a{label1="1", label2="2" or label3="3", label4="4"}"#,
];
display_cases
.iter()
.for_each(|expr| assert_eq!(parser::parse(expr).unwrap().to_string(), *expr));

let or_insensitive_cases = [
r#"a{label1="1" or label2="2"}"#,
r#"a{label1="1" OR label2="2"}"#,
r#"a{label1="1" Or label2="2"}"#,
r#"a{label1="1" oR label2="2"}"#,
];

or_insensitive_cases.iter().for_each(|expr| {
assert_eq!(
parser::parse(expr).unwrap().to_string(),
r#"a{label1="1" or label2="2"}"#
)
});

let fail_cases = vec![
(
r#"foo{or}"#,
r#"invalid label matcher, expected label matching operator after 'or'"#,
),
(r#"foo{label1="1" or}"#, INVALID_QUERY_INFO),
(r#"foo{or label1="1"}"#, INVALID_QUERY_INFO),
(r#"foo{label1="1" or or label2="2"}"#, INVALID_QUERY_INFO),
];
assert_cases(Case::new_fail_cases(fail_cases));
}
}
1 change: 1 addition & 0 deletions src/parser/promql.y
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,7 @@ label_matchers -> Result<Matchers, String>:

label_match_list -> Result<Matchers, String>:
label_match_list COMMA label_matcher { Ok($1?.append($3?)) }
| label_match_list LOR label_matcher { Ok($1?.append_or($3?)) }
| label_matcher { Ok(Matchers::empty().append($1?)) }
;

Expand Down

0 comments on commit b11faca

Please sign in to comment.