from abc import ABC, abstractmethod import logging from jinja2 import Template, StrictUndefined from pathlib import Path class SQLFormatter(ABC): """Base class for SQL template formatting.""" def load(self, template_path: str | Path, params: dict | None = None, logger: logging.Logger | None = None, verbose: bool = True) -> str: """Load and render a SQL template file. Args: template_path: Path to SQL template file params: Optional dictionary of variables logger: Logger instance (uses module logger if None) verbose: Whether to log pre/post formatting (default True) Returns: Rendered SQL string """ if logger is None: logger = logging.getLogger(__name__) path = Path(template_path) if not path.exists(): raise FileNotFoundError(f"Template file not found: {template_path}") with open(path, 'r') as f: sql_template = f.read() if verbose: logger.info(f"Loading SQL template: {template_path}") logger.debug(f"Template before formatting:\n\n{sql_template}") rendered_sql = self.__class__.render(sql_template, params) if verbose: logger.info(f"Formatted query:\n\n{rendered_sql}") return rendered_sql @classmethod @abstractmethod def render(cls, sql_template: str, params: dict | None = None) -> str: """Render SQL template with parameters. Args: sql_template: SQL string with template syntax params: Optional dictionary of variables Returns: Rendered SQL string """ pass class PythonStringSQLFormatter(SQLFormatter): """Python string format-based SQL formatter.""" @classmethod def render(cls, sql_template: str, params: dict | None = None) -> str: """Render SQL template with Python string formatting. Args: sql_template: SQL string with Python format syntax (e.g., {variable}) params: Optional dictionary of variables Returns: Rendered SQL string """ if params is None: params = {} return sql_template.format(**params) class JinjaSQLFormatter(SQLFormatter): """Jinja2-based SQL formatter.""" @classmethod def render(cls, sql_template: str, params: dict | None = None) -> str: """Render SQL template with Jinja2. Args: sql_template: SQL string with Jinja2 template syntax params: Optional dictionary of Jinja variables Returns: Rendered SQL string """ if params is None: params = {} template = Template(sql_template, undefined=StrictUndefined) return template.render(**params) def format_query(query: str, params: dict) -> str: """Format a query with parameters.""" return query.format(**params) def load_query(query_path: str, params: dict | None = None) -> str: """Load a query from a file path, optionally formatting with parameters. Args: query_path: Path to the SQL query file (can be relative or absolute) params: Optional dictionary of parameters to format the query with Returns: The contents of the SQL file as a string, optionally formatted with params """ path = Path(query_path) if not path.exists(): raise FileNotFoundError(f"Query file not found: {query_path}") with open(path, 'r') as f: query = f.read() if params is not None: return format_query(query, params) else: return query