74 lines
2.4 KiB
Python
74 lines
2.4 KiB
Python
"""Date-range resolver for typed Filters.
|
|
|
|
Given a `Filter` with `date_range="YYYY"` or `date_range="YYYY-Q[1-4]"` and
|
|
the target column, render a SQL fragment that evaluates correctly under the
|
|
column's actual storage type. Dispatched in two layers:
|
|
|
|
1. `Column.semantic_type` if set (extension point — e.g. `"date_yymmdd"` for
|
|
datasets that ship YYMMDD-encoded integers).
|
|
2. Otherwise `Column.sql_type` (`DATE`, `TIMESTAMP`, …) — the default path
|
|
for warehouses that store dates as real date columns.
|
|
|
|
Inclusive on both ends. Bounds quoted with single quotes (Postgres parses
|
|
date literals from strings).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
|
|
from api.composer.types import PickValidationError
|
|
from api.recon.types import Column
|
|
|
|
_YEAR_ONLY = re.compile(r"^\d{4}$")
|
|
_YEAR_QUARTER = re.compile(r"^(\d{4})-Q([1-4])$")
|
|
|
|
|
|
_QUARTER_BOUNDS = {
|
|
1: ("01-01", "03-31"),
|
|
2: ("04-01", "06-30"),
|
|
3: ("07-01", "09-30"),
|
|
4: ("10-01", "12-31"),
|
|
}
|
|
|
|
|
|
def _bounds(spec: str) -> tuple[str, str]:
|
|
"""Parse a date_range spec to (lo, hi) calendar dates as ISO strings."""
|
|
if _YEAR_ONLY.match(spec):
|
|
year = spec
|
|
return f"{year}-01-01", f"{year}-12-31"
|
|
m = _YEAR_QUARTER.match(spec)
|
|
if m:
|
|
year, q = m.group(1), int(m.group(2))
|
|
lo_mmdd, hi_mmdd = _QUARTER_BOUNDS[q]
|
|
return f"{year}-{lo_mmdd}", f"{year}-{hi_mmdd}"
|
|
raise PickValidationError(
|
|
f"unsupported date_range {spec!r}; expected 'YYYY' or 'YYYY-Q1..Q4'"
|
|
)
|
|
|
|
|
|
def resolve_date_range(spec: str, column: Column, qualified_col: str) -> str:
|
|
"""Render the WHERE fragment for `<qualified_col> BETWEEN ...`.
|
|
|
|
`qualified_col` is the already-quoted reference the composer wants in
|
|
the SQL (e.g. `"l"."date"`). Returns just the predicate, no `WHERE`.
|
|
"""
|
|
lo, hi = _bounds(spec)
|
|
semantic = (column.semantic_type or "").lower()
|
|
|
|
if semantic == "date_yymmdd":
|
|
# Column stores YYMMDD as integer (some BIRD datasets ship this way).
|
|
return (
|
|
f"TO_DATE(LPAD({qualified_col}::text, 6, '0'), 'YYMMDD') "
|
|
f"BETWEEN '{lo}' AND '{hi}'"
|
|
)
|
|
|
|
sql_type = column.sql_type.upper()
|
|
if sql_type.startswith("DATE") or sql_type.startswith("TIMESTAMP"):
|
|
return f"{qualified_col} BETWEEN '{lo}' AND '{hi}'"
|
|
|
|
raise PickValidationError(
|
|
f"date_range on column with sql_type={column.sql_type!r} and "
|
|
f"semantic_type={column.semantic_type!r} is not supported"
|
|
)
|