Browse Source

ref(search) case insensitive grammar (#69146)

Same love for the backend

---------

Co-authored-by: getsantry[bot] <66042841+getsantry[bot]@users.noreply.github.com>
Co-authored-by: Evan Purkhiser <evanpurkhiser@gmail.com>
Jonas 10 months ago
parent
commit
01fdf2fa51
3 changed files with 55 additions and 2 deletions
  1. 8 2
      src/sentry/api/event_search.py
  2. 5 0
      src/sentry/search/utils.py
  3. 42 0
      tests/sentry/search/test_utils.py

+ 8 - 2
src/sentry/api/event_search.py

@@ -150,7 +150,7 @@ quoted_value           = '"' ('\\"' / ~r'[^"]')* '"'
 in_value               = (&in_value_termination in_value_char)+
 text_in_value          = quoted_value / in_value
 search_value           = quoted_value / value
-numeric_value          = "-"? numeric ~r"[kmb]"? &(end_value / comma / closed_bracket)
+numeric_value          = "-"? numeric numeric_unit? &(end_value / comma / closed_bracket)
 boolean_value          = ~r"(true|1|false|0)"i &end_value
 text_in_list           = open_bracket text_in_value (spaces comma spaces !comma text_in_value?)* closed_bracket &end_value
 numeric_in_list        = open_bracket numeric_value (spaces comma spaces !comma numeric_value?)* closed_bracket &end_value
@@ -166,12 +166,18 @@ time_format = ~r"T\d{2}:\d{2}:\d{2}" ("." ms_format)?
 ms_format   = ~r"\d{1,6}"
 tz_format   = ~r"[+-]\d{2}:\d{2}"
 
+
 iso_8601_date_format = date_format time_format? ("Z" / tz_format)? &end_value
 rel_date_format      = ~r"[+-][0-9]+[wdhm]" &end_value
 duration_format      = numeric ("ms"/"s"/"min"/"m"/"hr"/"h"/"day"/"d"/"wk"/"w") &end_value
-size_format          = numeric ("bit"/"nb"/"bytes"/"kb"/"mb"/"gb"/"tb"/"pb"/"eb"/"zb"/"yb"/"kib"/"mib"/"gib"/"tib"/"pib"/"eib"/"zib"/"yib") &end_value
+size_format          = numeric (size_unit) &end_value
 percentage_format    = numeric "%"
 
+numeric_unit        = ~r"[kmb]"i
+size_unit            = bits / bytes
+bits                 = ~r"bit|kib|mib|gib|tib|pib|eib|zib|yib"i
+bytes                = ~r"bytes|nb|kb|mb|gb|tb|pb|eb|zb|yb"i
+
 # 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             = ">=" / "<=" / ">" / "<" / "=" / "!="

+ 5 - 0
src/sentry/search/utils.py

@@ -105,6 +105,9 @@ def parse_size(value: str, size: str) -> float:
     except ValueError:
         raise InvalidQuery(f"{value} is not a valid size value")
 
+    # size units are case insensitive
+    size = size.lower()
+
     if size == "bit":
         byte = size_value / 8
     elif size == "nb":
@@ -169,6 +172,8 @@ def parse_numeric_value(value: str, suffix: str | None = None) -> float:
     if not suffix:
         return parsed_value
 
+    # numeric "nuts" are case insensitive
+    suffix = suffix.lower()
     numeric_multiples = {"k": 10.0**3, "m": 10.0**6, "b": 10.0**9}
     if suffix not in numeric_multiples:
         raise InvalidQuery(f"{suffix} is not a valid number suffix, must be k, m or b")

+ 42 - 0
tests/sentry/search/test_utils.py

@@ -17,8 +17,11 @@ from sentry.search.utils import (
     get_first_last_release_for_group,
     get_latest_release,
     get_numeric_field_value,
+    parse_bool,
     parse_duration,
+    parse_numeric_value,
     parse_query,
+    parse_size,
     tokenize_query,
 )
 from sentry.services.hybrid_cloud.user.model import RpcUser
@@ -51,6 +54,45 @@ def test_get_numeric_field_value():
     }
 
 
+class TestParseNumericValue(TestCase):
+    def test_simple(self):
+        assert parse_numeric_value("10", None) == 10
+
+    def test_k(self):
+        assert parse_numeric_value("1", "k") == 1000.0
+        assert parse_numeric_value("1", "K") == 1000.0
+
+    def test_m(self):
+        assert parse_numeric_value("1", "m") == 1000000.0
+        assert parse_numeric_value("1", "M") == 1000000.0
+
+    def test_b(self):
+        assert parse_numeric_value("1", "b") == 1000000000.0
+        assert parse_numeric_value("1", "B") == 1000000000.0
+
+
+class TestParseSizeValue(TestCase):
+    def test_simple(self):
+        assert parse_size("8", "bit") == 1
+
+    def test_uppercase(self):
+        assert parse_size("1", "KB") == 1000
+        assert parse_size("1", "kb") == 1000
+        assert parse_size("1", "Kb") == 1000
+
+
+class TestParseBooleanValue(TestCase):
+    def test_true(self):
+        assert parse_bool("1") is True
+        assert parse_bool("TRUE") is True
+        assert parse_bool("true") is True
+
+    def test_false(self):
+        assert parse_bool("0") is False
+        assert parse_bool("FALSE") is False
+        assert parse_bool("false") is False
+
+
 class TestParseDuration(TestCase):
     def test_ms(self):
         assert parse_duration("123", "ms") == 123