diff --git a/src/label/matcher.rs b/src/label/matcher.rs index fe1b12e..2e009f4 100644 --- a/src/label/matcher.rs +++ b/src/label/matcher.rs @@ -102,15 +102,19 @@ impl Matcher { } pub fn new_matcher(id: TokenId, name: String, value: String) -> Result { + 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, 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) } } @@ -181,24 +185,58 @@ fn try_escape_for_repeat_re(re: &str) -> String { #[derive(Debug, Clone, PartialEq, Eq)] pub struct Matchers { pub matchers: Vec, + pub or_matchers: Vec>, } 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) -> Self { - Self { matchers } + Self { + matchers, + or_matchers: vec![], + } + } + + pub fn with_or_matchers(mut self, or_matchers: Vec>) -> 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 } @@ -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 { - 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 { self.matchers .iter() + .chain(self.or_matchers.iter().flatten()) .filter(|m| m.name.eq(name)) .cloned() .collect() @@ -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) + } } } diff --git a/src/parser/lex.rs b/src/parser/lex.rs index f1bdf9c..2b1ec2e 100644 --- a/src/parser/lex.rs +++ b/src/parser/lex.rs @@ -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), diff --git a/src/parser/parse.rs b/src/parser/parse.rs index 21f2ab0..61a2b36 100644 --- a/src/parser/parse.rs +++ b/src/parser/parse.rs @@ -37,6 +37,7 @@ 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, @@ -44,6 +45,7 @@ mod tests { }; use crate::util::duration; use std::time::Duration; + use std::vec; struct Case { input: String, @@ -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)); + } } diff --git a/src/parser/promql.y b/src/parser/promql.y index 609baca..ee938cf 100644 --- a/src/parser/promql.y +++ b/src/parser/promql.y @@ -407,6 +407,7 @@ label_matchers -> Result: label_match_list -> Result: 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?)) } ;