"""Small persistence helpers for safer SQL text usage. ClientFlow still uses SQLAlchemy ``text()`` for PostgreSQL-specific JSONB and reconciliation queries. These helpers standardise JSONB patches so new code can prefer ``metadata = metadata || CAST(:metadata_patch AS jsonb)`` over fragile ``jsonb_build_object('x', :param)`` patterns. """ from __future__ import annotations import json from datetime import date, datetime, timezone from decimal import Decimal from typing import Any, Dict, Mapping, Optional from uuid import UUID def json_default(value: Any) -> Any: if isinstance(value, (datetime, date)): return value.isoformat() if isinstance(value, UUID): return str(value) if isinstance(value, Decimal): # Use string to avoid float rounding in money fields. return str(value) if isinstance(value, set): return sorted(str(item) for item in value) return str(value) def jsonb_param(value: Any) -> str: """Return a JSON string suitable for ``CAST(:param AS jsonb)``.""" return json.dumps(value if value is not None else {}, ensure_ascii=False, default=json_default) def metadata_patch(**items: Any) -> Dict[str, Any]: """Create a small metadata patch with an ISO timestamp when requested.""" return {key: value for key, value in items.items() if value is not None} def utc_now_text() -> str: return datetime.now(timezone.utc).isoformat()