refactor composer to use sqlalchemy, avoid string concatenations and follow conventions
This commit is contained in:
@@ -1,22 +1,29 @@
|
||||
"""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:
|
||||
Given a `Filter` with `date_range="YYYY"` or `date_range="YYYY-Q[1-4]"`, the
|
||||
target column, and the active encoding map, build a SQL predicate node that
|
||||
evaluates correctly under the column's actual storage. 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.
|
||||
1. `Column.semantic_type` if it names an encoding in the effective map — coerce
|
||||
the raw column to a DATE via the encoding's `as_date` template, then compare.
|
||||
This is the extension point: BIRD's `date_yymmdd` ships as a default
|
||||
(see `api.composer.encodings`); a dataset can add or override encodings in
|
||||
its own `schema_docs.yaml`.
|
||||
2. Otherwise `Column.sql_type` (`DATE`, `TIMESTAMP`, …) — compare the column
|
||||
directly.
|
||||
|
||||
Inclusive on both ends. Bounds quoted with single quotes (Postgres parses
|
||||
date literals from strings).
|
||||
Returns an `exp.Between` node (inclusive on both ends) so it composes into the
|
||||
query tree the composer assembles; the composer is the single place a SQL
|
||||
string is produced.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
|
||||
import sqlglot
|
||||
from sqlglot import exp
|
||||
|
||||
from api.composer.types import PickValidationError
|
||||
from api.recon.types import Column
|
||||
|
||||
@@ -47,27 +54,37 @@ def _bounds(spec: str) -> tuple[str, str]:
|
||||
)
|
||||
|
||||
|
||||
def resolve_date_range(spec: str, column: Column, qualified_col: str) -> str:
|
||||
"""Render the WHERE fragment for `<qualified_col> BETWEEN ...`.
|
||||
def resolve_date_range(
|
||||
spec: str,
|
||||
column: Column,
|
||||
col_expr: exp.Expression,
|
||||
encodings: dict[str, dict[str, str]],
|
||||
) -> exp.Between:
|
||||
"""Build the `<comparable> BETWEEN '<lo>' AND '<hi>'` predicate for `spec`.
|
||||
|
||||
`qualified_col` is the already-quoted reference the composer wants in
|
||||
the SQL (e.g. `"l"."date"`). Returns just the predicate, no `WHERE`.
|
||||
`col_expr` is the (already alias-qualified) column reference node;
|
||||
`encodings` is the effective encoding map (see `api.composer.encodings`).
|
||||
"""
|
||||
lo, hi = _bounds(spec)
|
||||
semantic = (column.semantic_type or "").lower()
|
||||
bounds = (exp.convert(lo), exp.convert(hi))
|
||||
|
||||
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}'"
|
||||
semantic = (column.semantic_type or "").lower()
|
||||
enc = encodings.get(semantic) if semantic else None
|
||||
if enc and enc.get("as_date"):
|
||||
# Coerce the raw column to a DATE via the dataset/default template.
|
||||
template = enc["as_date"]
|
||||
coerced = sqlglot.parse_one(
|
||||
template.format(col=col_expr.sql(dialect="postgres", identify=True)),
|
||||
dialect="postgres",
|
||||
)
|
||||
return exp.Between(this=coerced, low=bounds[0], high=bounds[1])
|
||||
|
||||
sql_type = column.sql_type.upper()
|
||||
if sql_type.startswith("DATE") or sql_type.startswith("TIMESTAMP"):
|
||||
return f"{qualified_col} BETWEEN '{lo}' AND '{hi}'"
|
||||
return exp.Between(this=col_expr, low=bounds[0], high=bounds[1])
|
||||
|
||||
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"
|
||||
f"semantic_type={column.semantic_type!r} is not supported "
|
||||
f"(no native date type and no matching encoding)"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user