Files
delphi-database/app/services/query_utils.py
2025-08-14 19:16:28 -05:00

73 lines
2.6 KiB
Python

from typing import Iterable, Optional, Sequence
from sqlalchemy import or_, and_, asc, desc, func
from sqlalchemy.sql.elements import BinaryExpression
from sqlalchemy.sql.schema import Column
def tokenized_ilike_filter(tokens: Sequence[str], columns: Sequence[Column]) -> Optional[BinaryExpression]:
"""Build an AND-of-ORs case-insensitive LIKE filter across columns for each token.
Example: AND(OR(col1 ILIKE %t1%, col2 ILIKE %t1%), OR(col1 ILIKE %t2%, ...))
Returns None when tokens or columns are empty.
"""
if not tokens or not columns:
return None
per_token_clauses = []
for term in tokens:
term = str(term or "").strip()
if not term:
continue
per_token_clauses.append(or_(*[c.ilike(f"%{term}%") for c in columns]))
if not per_token_clauses:
return None
return and_(*per_token_clauses)
def apply_pagination(query, skip: int, limit: int):
"""Apply offset/limit pagination to a SQLAlchemy query in a DRY way."""
return query.offset(skip).limit(limit)
def paginate_with_total(query, skip: int, limit: int, include_total: bool):
"""Return (items, total|None) applying pagination and optionally counting total.
This avoids duplicating count + pagination logic at each endpoint.
"""
total_count = query.count() if include_total else None
items = apply_pagination(query, skip, limit).all()
return items, total_count
def apply_sorting(query, sort_by: Optional[str], sort_dir: Optional[str], allowed: dict[str, list[Column]]):
"""Apply case-insensitive sorting per a whitelist of allowed fields.
allowed: mapping from field name -> list of columns to sort by, in priority order.
For string columns, compares using lower(column) for stable ordering.
Unknown sort_by falls back to the first key in allowed.
sort_dir: "asc" or "desc" (default asc)
"""
if not allowed:
return query
normalized_sort_by = (sort_by or next(iter(allowed.keys()))).lower()
normalized_sort_dir = (sort_dir or "asc").lower()
is_desc = normalized_sort_dir == "desc"
columns = allowed.get(normalized_sort_by)
if not columns:
columns = allowed.get(next(iter(allowed.keys())))
if not columns:
return query
order_exprs = []
for col in columns:
try:
expr = func.lower(col) if getattr(col.type, "python_type", str) is str else col
except Exception:
expr = col
order_exprs.append(desc(expr) if is_desc else asc(expr))
if order_exprs:
query = query.order_by(*order_exprs)
return query