From 5606d5d2e363bbc2f7df66f7e165f5d00ca53944 Mon Sep 17 00:00:00 2001 From: Jean Massucatto Date: Mon, 12 Jan 2026 06:52:19 -0300 Subject: [PATCH] perf(date_parser): bound regex quantifiers for deterministic parsing performance (#36983) Cherry-picked from master commit 459b4cb23d Accepted: - Bounded \s+ to \s{1,5} and \s* to \s{0,5} in time_range_lookup regexes - Added tests for bounded whitespace validation Adapted: - Preserved 6.0-release "first/1st of" patterns between modified sections - Kept existing tests for "first of" expressions Original author: Jean Massucatto --- superset/utils/date_parser.py | 10 ++--- tests/unit_tests/utils/date_parser_tests.py | 47 +++++++++++++++++++++ 2 files changed, 52 insertions(+), 5 deletions(-) diff --git a/superset/utils/date_parser.py b/superset/utils/date_parser.py index af90dc60bcb..ac806476258 100644 --- a/superset/utils/date_parser.py +++ b/superset/utils/date_parser.py @@ -503,9 +503,9 @@ def get_since_until( # pylint: disable=too-many-arguments,too-many-locals,too-m if time_range and separator in time_range: time_range_lookup = [ ( - r"^(start of|beginning of|end of)\s+" - r"(this|last|next|prior)\s+" - r"([0-9]+)?\s*" + r"^(start of|beginning of|end of)\s{1,5}" + r"(this|last|next|prior)\s{1,5}" + r"([0-9]+)?\s{0,5}" r"(day|week|month|quarter|year)s?$", # Matches phrases like "start of next month" # noqa: E501 lambda modifier, scope, delta, unit: handle_modifier_and_unit( modifier, @@ -538,8 +538,8 @@ def get_since_until( # pylint: disable=too-many-arguments,too-many-locals,too-m ), ), ( - r"^(this|last|next|prior)\s+" - r"([0-9]+)?\s*" + r"^(this|last|next|prior)\s{1,5}" + r"([0-9]+)?\s{0,5}" r"(second|minute|day|week|month|quarter|year)s?$", # Matches "next 5 days" or "last 2 weeks" # noqa: E501 lambda scope, delta, unit: handle_scope_and_unit( scope, delta, unit, get_relative_base(unit, relative_start) diff --git a/tests/unit_tests/utils/date_parser_tests.py b/tests/unit_tests/utils/date_parser_tests.py index baf9ed64528..d5cb387f897 100644 --- a/tests/unit_tests/utils/date_parser_tests.py +++ b/tests/unit_tests/utils/date_parser_tests.py @@ -675,3 +675,50 @@ def test_first_subunit_of_with_default_scope() -> None: result = get_since_until("first month of the quarter") assert result == (datetime(2016, 10, 1), datetime(2016, 11, 1)) + + +# Tests for bounded whitespace regex patterns in time_range_lookup +@pytest.mark.parametrize( + "time_range", + [ + "last 7 days : ", + "this week : ", + "start of next month : ", + "prior quarter : ", + "last 7 days : ", + "last 7 days : ", + "last 7 days : ", + "last 7 days : ", + "start of next month : ", # 5 spaces - valid + "last week : ", + "last week : ", + "last week : ", + "next 12 months : ", + "next 12 months : ", + "next 12 months : ", + "last 7days : ", # \s{0,5} allows 0 spaces after number - valid + ], +) +@patch("superset.utils.date_parser.parse_human_datetime", mock_parse_human_datetime) +def test_time_range_bounded_whitespace_regex_valid(time_range: str) -> None: + """Match expressions with 1-5 spaces between tokens.""" + result = get_since_until(time_range) + assert result[0] is not None, f"Expected '{time_range}' to parse successfully" + + +@pytest.mark.parametrize( + "time_range", + [ + "last 7 days : ", + "last7days : ", + "lastweek : ", + "last : ", + "start of : ", + "last 7 days extra : ", + ], +) +@patch("superset.utils.date_parser.parse_human_datetime", mock_parse_human_datetime) +def test_time_range_bounded_whitespace_regex_invalid(time_range: str) -> None: + """Reject expressions with 0 or 6+ spaces (fall back to DATETIME wrapping).""" + result = get_since_until(time_range) + assert result[0] is None, f"Expected '{time_range}' to NOT match bounded regex"