"""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}")