Query Time Range Resolution#
Overview#
Query time range resolution in Superset is the process of converting a raw time range expression (e.g., "Last 7 days", "2024-01-01 : 2024-02-01", or a TEMPORAL_RANGE filter) into concrete (since, until) datetime boundaries used to constrain query execution. This affects chart rendering, time comparison offsets, and dashboard filter propagation.
The pipeline runs at two layers:
- Frontend gate: validates the range string before it is committed (via
/api/v1/time_range/) - Backend resolution: converts the string to
datetimeobjects at query execution time
Backend: Core Parsing (get_since_until)#
The primary backend entry point is get_since_until() in superset/utils/date_parser.py. It accepts up to seven parameters — time_range, since, until, time_shift, relative_start, relative_end, and instant_time_comparison_range — and returns a tuple[datetime | None, datetime | None].
Parsing strategy: When a time_range string is provided, get_since_until() uses a regex-based lookup table to match patterns such as:
- Named shorthands:
"Last day","Last week","Last month","Last quarter","Last year" - Dynamic forms:
"Last N days/weeks/months/years","Next N ..." - Formal expressions:
DATETIME(...),DATEADD(...),DATETRUNC(...),LASTDAY(...),HOLIDAY(...) - Colon-separated explicit ranges:
"2024-01-01T00:00:00.000000 : 2024-02-01T00:00:00.000000"
Matched patterns are converted to "formal time expressions" and evaluated by a datetime_eval() parser built with pyparsing. The final step validates that since ≤ until .
Human datetime parsing: Individual date tokens fall through to parse_human_datetime(), which tries dateutil.parser.parse() first and falls back to parsedatetime.Calendar().parseDT() for natural language. Ambiguous relative strings (e.g., bare "3 days") raise TimeRangeAmbiguousError . Unparseable strings raise TimeRangeParseFailError.
Relative base computation: get_relative_base() determines whether the anchor is "now" (sub-day grains) or "today" (day-or-coarser grains), which affects how expressions like "start of this month" resolve.
Query Object Extraction (get_since_until_from_query_object)#
Before executing a query, get_since_until_from_query_object() in superset/common/query_context_processor.py extracts the time range from a QueryObject via two paths:
query_object.time_range— if set, passed directly toget_since_until_from_time_range()- TEMPORAL_RANGE filter fallback — searches
query_object.filterfor entries withop == FilterOperator.TEMPORAL_RANGE.valueand extracts thevalstring
The TEMPORAL_RANGE fallback exists because some chart types (e.g., aggregate-only or time-comparison-only charts) embed their temporal constraint exclusively in an adhoc filter rather than the time_range property.
Time Comparison Offset Processing#
Time comparison (period-over-period) is handled in processing_time_offsets() in query_context_processor.py. For each configured offset:
- Extract the outer boundaries:
outer_from_dttm, outer_to_dttm = get_since_until_from_query_object(query_object) - Clone the query object and shift its outer bounds with
get_past_or_future():clone.from_dttm = get_past_or_future(offset, outer_from_dttm) - Preserve the original bounds as inner bounds:
clone.inner_from_dttm = outer_from_dttm
The distinction between outer (from_dttm/to_dttm) and inner (inner_from_dttm/inner_to_dttm) bounds is critical for coarse time grains: the outer range defines which data partition is fetched; the inner range constrains what is aggregated, preventing a partial current period from being compared against a full prior bucket.
Known regression (Superset 6.1): The relative-offset branch incorrectly sets
inner_from_dttm = clone.from_dttm(the shifted bound) instead ofouter_from_dttm(the original bound), causing month-grain comparisons to aggregate the full shifted bucket (e.g., all of May 2025) instead of the equivalent partial period (e.g., May 1–28, 2025). See issue #40501 for details.
For non-datetime-series charts, processing_time_offsets() also rewrites TEMPORAL_RANGE filter values in-place: it extracts the original val, shifts it, and writes back f"{new_from} : {new_to}" .
Frontend Validation#
The UI validates range strings via fetchTimeRange() in superset-ui-core, which calls GET /api/v1/time_range/. The API calls get_since_until() and returns an error if the string is unparseable.
DateFilterLabel.tsx maintains a validTimeRange boolean; when false, the Apply button is disabled . The sentinel value NO_TIME_RANGE always passes as valid without hitting the API .
Custom date frame ranges are encoded as colon-separated strings by customTimeRangeEncode() and decoded by customTimeRangeDecode(). All naive timestamps produced are local time (not UTC) — the Z suffix must not be added .
MCP Tool Validation#
MCP tools (query_dataset, get_table, manage_native_filters) now run validate_time_range() in superset/mcp_service/common/time_range_validation.py before applying a TEMPORAL_RANGE filter. Unrecognized strings that would resolve to an unbounded (None, today) range raise ValueError with actionable guidance, preventing silent full-table scans .
Key Files#
| File | Role |
|---|---|
superset/utils/date_parser.py | Core get_since_until(), parse_human_datetime(), get_past_or_future() |
superset/common/query_context_processor.py | get_since_until_from_query_object(), processing_time_offsets() |
superset/mcp_service/common/time_range_validation.py | MCP input gate; rejects unbounded ranges |
DateFilterLabel.tsx | Frontend validTimeRange state and Apply gate |
fetchTimeRange.ts | Calls /api/v1/time_range/; returns { value } or { error } |
dateParser.ts | customTimeRangeEncode, dttmToDayjs, ISO8601_AND_CONSTANT |
customTimeRangeDecode.ts | Decodes custom range strings; sets matchedFlag |