Files
nvi/api/composer/filters.py

47 lines
1.8 KiB
Python

"""Filter rendering — one typed `Filter` → one SQL predicate node.
Each filter shape maps to a sqlglot expression node (`exp.EQ`, `exp.In`,
`exp.Between`, or the date-range subtree). Literals go through `exp.convert`,
so quoting and escaping are sqlglot's job, not ours. The column is resolved to
its owning table and qualified via the shared `AliasMap` (allocating an alias,
and implying a join later, if the filter introduces a new table).
Adding a new filter shape is a new branch here that returns an `exp` node — no
change to the assembly in `compose.py`. The `Filter` type stays the constraint
boundary: there is no raw-SQL escape hatch.
"""
from __future__ import annotations
from sqlglot import exp
from api.composer.aliases import AliasMap
from api.composer.dates import resolve_date_range
from api.composer.types import Filter, PickValidationError
from api.recon.types import Recon
def render_filter(
f: Filter,
recon: Recon,
aliases: AliasMap,
encodings: dict[str, dict[str, str]],
) -> exp.Expression:
"""Render one `Filter` to a predicate node bound to the column's table."""
table, col = recon.resolve_column(f.column)
col_expr = aliases.col(table, col.name)
kind = f.kind()
if kind == "equals":
return exp.EQ(this=col_expr, expression=exp.convert(f.equals))
if kind == "in_values":
if not f.in_values:
raise PickValidationError(f"filter on {f.column!r} has empty in_values")
return exp.In(this=col_expr, expressions=[exp.convert(v) for v in f.in_values])
if kind == "between":
lo, hi = f.between
return exp.Between(this=col_expr, low=exp.convert(lo), high=exp.convert(hi))
if kind == "date_range":
return resolve_date_range(f.date_range, col, col_expr, encodings)
raise PickValidationError(f"unknown filter kind {kind!r}")