|
@@ -83,48 +83,50 @@ def translate(pat):
|
|
|
|
|
|
|
|
|
event_search_grammar = Grammar(r"""
|
|
|
-search = search_term*
|
|
|
-search_term = key_val_term / quoted_raw_search / raw_search
|
|
|
-key_val_term = space? (time_filter / rel_time_filter / specific_time_filter
|
|
|
- / numeric_filter / has_filter / is_filter / basic_filter)
|
|
|
- space?
|
|
|
-raw_search = (!key_val_term ~r"\ *([^\ ^\n]+)\ *" )*
|
|
|
-quoted_raw_search = spaces quoted_value spaces
|
|
|
+search = (boolean_term / search_term)*
|
|
|
+boolean_term = search_term (boolean_operator search_term)+
|
|
|
+search_term = key_val_term / quoted_raw_search / raw_search
|
|
|
+key_val_term = space? (time_filter / rel_time_filter / specific_time_filter
|
|
|
+ / numeric_filter / has_filter / is_filter / basic_filter)
|
|
|
+ space?
|
|
|
+raw_search = (!key_val_term ~r"\ *([^\ ^\n]+)\ *" )*
|
|
|
+quoted_raw_search = spaces quoted_value spaces
|
|
|
|
|
|
# standard key:val filter
|
|
|
-basic_filter = negation? search_key sep search_value
|
|
|
+basic_filter = negation? search_key sep search_value
|
|
|
# filter for dates
|
|
|
-time_filter = search_key sep? operator date_format
|
|
|
+time_filter = search_key sep? operator date_format
|
|
|
# filter for relative dates
|
|
|
-rel_time_filter = search_key sep rel_date_format
|
|
|
+rel_time_filter = search_key sep rel_date_format
|
|
|
# exact time filter for dates
|
|
|
specific_time_filter = search_key sep date_format
|
|
|
# Numeric comparison filter
|
|
|
-numeric_filter = search_key sep operator? ~r"[0-9]+(?=\s|$)"
|
|
|
+numeric_filter = search_key sep operator? ~r"[0-9]+(?=\s|$)"
|
|
|
|
|
|
# has filter for not null type checks
|
|
|
-has_filter = negation? "has" sep (search_key / search_value)
|
|
|
-is_filter = negation? "is" sep search_value
|
|
|
-
|
|
|
-search_key = key / quoted_key
|
|
|
-search_value = quoted_value / value
|
|
|
-value = ~r"\S*"
|
|
|
-quoted_value = ~r"\"((?:[^\"]|(?<=\\)[\"])*)?\""s
|
|
|
-key = ~r"[a-zA-Z0-9_\.-]+"
|
|
|
+has_filter = negation? "has" sep (search_key / search_value)
|
|
|
+is_filter = negation? "is" sep search_value
|
|
|
+
|
|
|
+search_key = key / quoted_key
|
|
|
+search_value = quoted_value / value
|
|
|
+value = ~r"\S*"
|
|
|
+quoted_value = ~r"\"((?:[^\"]|(?<=\\)[\"])*)?\""s
|
|
|
+key = ~r"[a-zA-Z0-9_\.-]+"
|
|
|
# only allow colons in quoted keys
|
|
|
-quoted_key = ~r"\"([a-zA-Z0-9_\.:-]+)\""
|
|
|
+quoted_key = ~r"\"([a-zA-Z0-9_\.:-]+)\""
|
|
|
|
|
|
-date_format = ~r"\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}(\.\d{1,6})?)?Z?(?=\s|$)"
|
|
|
-rel_date_format = ~r"[\+\-][0-9]+[wdhm](?=\s|$)"
|
|
|
+date_format = ~r"\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}(\.\d{1,6})?)?Z?(?=\s|$)"
|
|
|
+rel_date_format = ~r"[\+\-][0-9]+[wdhm](?=\s|$)"
|
|
|
|
|
|
# NOTE: the order in which these operators are listed matters
|
|
|
# because for example, if < comes before <= it will match that
|
|
|
# even if the operator is <=
|
|
|
-operator = ">=" / "<=" / ">" / "<" / "=" / "!="
|
|
|
-sep = ":"
|
|
|
-space = " "
|
|
|
-negation = "!"
|
|
|
-spaces = ~r"\ *"
|
|
|
+boolean_operator = "OR" / "AND"
|
|
|
+operator = ">=" / "<=" / ">" / "<" / "=" / "!="
|
|
|
+sep = ":"
|
|
|
+space = " "
|
|
|
+negation = "!"
|
|
|
+spaces = ~r"\ *"
|
|
|
""")
|
|
|
|
|
|
|
|
@@ -144,6 +146,18 @@ class InvalidSearchQuery(Exception):
|
|
|
pass
|
|
|
|
|
|
|
|
|
+def has_boolean_search_terms(search_terms):
|
|
|
+ for term in search_terms:
|
|
|
+ if isinstance(term, SearchBoolean):
|
|
|
+ return True
|
|
|
+ return False
|
|
|
+
|
|
|
+
|
|
|
+class SearchBoolean(namedtuple('SearchBoolean', 'left_term operator right_term')):
|
|
|
+ BOOLEAN_AND = "AND"
|
|
|
+ BOOLEAN_OR = "OR"
|
|
|
+
|
|
|
+
|
|
|
class SearchFilter(namedtuple('SearchFilter', 'key operator value')):
|
|
|
|
|
|
def __str__(self):
|
|
@@ -215,21 +229,28 @@ class SearchVisitor(NodeVisitor):
|
|
|
lookup[source_field] = target_field
|
|
|
return lookup
|
|
|
|
|
|
- def visit_search(self, node, children):
|
|
|
- # there is a list from search_term and one from raw_search, so flatten them.
|
|
|
- # Flatten each group in the list, since nodes can return multiple items
|
|
|
+ def flatten(self, children):
|
|
|
def _flatten(seq):
|
|
|
+ # there is a list from search_term and one from raw_search, so flatten them.
|
|
|
+ # Flatten each group in the list, since nodes can return multiple items
|
|
|
for item in seq:
|
|
|
if isinstance(item, list):
|
|
|
for sub in _flatten(item):
|
|
|
yield sub
|
|
|
else:
|
|
|
yield item
|
|
|
+
|
|
|
+ if not (children and isinstance(children, list) and isinstance(children[0], list)):
|
|
|
+ return children
|
|
|
+
|
|
|
children = [child for group in children for child in _flatten(group)]
|
|
|
children = filter(None, _flatten(children))
|
|
|
|
|
|
return children
|
|
|
|
|
|
+ def visit_search(self, node, children):
|
|
|
+ return self.flatten(children)
|
|
|
+
|
|
|
def visit_key_val_term(self, node, children):
|
|
|
_, key_val_term, _ = children
|
|
|
# key_val_term is a list because of group
|
|
@@ -253,6 +274,34 @@ class SearchVisitor(NodeVisitor):
|
|
|
return None
|
|
|
return SearchFilter(SearchKey('message'), "=", SearchValue(value))
|
|
|
|
|
|
+ def visit_boolean_term(self, node, children):
|
|
|
+ def find_next_operator(children, start, end, operator):
|
|
|
+ for index in range(start, end):
|
|
|
+ if children[index] == operator:
|
|
|
+ return index
|
|
|
+ return None
|
|
|
+
|
|
|
+ def build_boolean_tree_branch(children, start, end, operator):
|
|
|
+ index = find_next_operator(children, start, end, operator)
|
|
|
+ if index is None:
|
|
|
+ return None
|
|
|
+ left = build_boolean_tree(children, start, index)
|
|
|
+ right = build_boolean_tree(children, index + 1, end)
|
|
|
+ return SearchBoolean(left, children[index], right)
|
|
|
+
|
|
|
+ def build_boolean_tree(children, start, end):
|
|
|
+ if end - start == 1:
|
|
|
+ return children[start]
|
|
|
+
|
|
|
+ result = build_boolean_tree_branch(children, start, end, SearchBoolean.BOOLEAN_OR)
|
|
|
+ if result is None:
|
|
|
+ result = build_boolean_tree_branch(children, start, end, SearchBoolean.BOOLEAN_AND)
|
|
|
+
|
|
|
+ return result
|
|
|
+
|
|
|
+ children = self.flatten(children)
|
|
|
+ return [build_boolean_tree(children, 0, len(children))]
|
|
|
+
|
|
|
def visit_numeric_filter(self, node, children):
|
|
|
(search_key, _, operator, search_value) = children
|
|
|
operator = operator[0] if not isinstance(operator, Node) else '='
|
|
@@ -387,6 +436,9 @@ class SearchVisitor(NodeVisitor):
|
|
|
def visit_search_value(self, node, children):
|
|
|
return SearchValue(children[0])
|
|
|
|
|
|
+ def visit_boolean_operator(self, node, children):
|
|
|
+ return node.text
|
|
|
+
|
|
|
def visit_value(self, node, children):
|
|
|
return node.text
|
|
|
|
|
@@ -515,7 +567,16 @@ def get_snuba_query_args(query=None, params=None):
|
|
|
'conditions': [],
|
|
|
'filter_keys': {},
|
|
|
}
|
|
|
+
|
|
|
+ # TODO(lb): remove when boolean terms fully functional
|
|
|
+ if has_boolean_search_terms(parsed_filters):
|
|
|
+ kwargs['has_boolean_terms'] = True
|
|
|
+
|
|
|
for _filter in parsed_filters:
|
|
|
+ # TODO(lb): remove when boolean terms fully functional
|
|
|
+ if isinstance(_filter, SearchBoolean):
|
|
|
+ continue
|
|
|
+
|
|
|
snuba_name = _filter.key.snuba_name
|
|
|
|
|
|
if snuba_name in ('start', 'end'):
|