Source code for sqlspec.core.sqlcommenter

"""Google SQLCommenter support — structured SQL comments for query attribution.

Implements the `SQLCommenter spec <https://google.github.io/sqlcommenter/spec/>`_
using sqlglot AST-level comment manipulation. Comments are added to the parsed
expression tree and coexist with existing comments and optimizer hints.
"""

from collections.abc import Callable, Generator, Mapping
from contextlib import contextmanager
from contextvars import ContextVar
from typing import Any, ClassVar, Final, TypedDict, final
from urllib.parse import quote, unquote

from sqlglot import exp

from sqlspec.observability import get_trace_context
from sqlspec.utils.correlation import CorrelationContext

__all__ = (
    "SQLCommenterAttributes",
    "SQLCommenterContext",
    "append_comment",
    "create_sqlcommenter_statement_transformer",
    "generate_comment",
    "parse_comment",
)


class SQLCommenterAttributes(TypedDict, total=False):
    """Structured attributes appended as SQL comments for query attribution."""

    db_driver: str
    framework: str
    route: str
    controller: str
    action: str
    correlation_id: str
    traceparent: str
    tracestate: str


_sqlcommenter_ctx: Final[ContextVar[dict[str, str] | None]] = ContextVar("_sqlcommenter_ctx", default=None)


[docs] class SQLCommenterContext: """Request-scoped storage for sqlcommenter attributes via contextvars. Framework middlewares set attributes per-request, and the sqlcommenter statement transformer reads them at compile time. """ _var: ClassVar[ContextVar[dict[str, str] | None]] = _sqlcommenter_ctx
[docs] @classmethod def get(cls) -> dict[str, str] | None: """Get the current request-scoped attributes.""" return cls._var.get()
[docs] @classmethod def set(cls, attrs: dict[str, str] | None) -> None: """Set request-scoped attributes.""" cls._var.set(attrs)
[docs] @classmethod @contextmanager def scope(cls, attrs: dict[str, str]) -> Generator[None, None, None]: """Context manager that sets attributes for the duration of a block.""" previous = cls._var.get() cls._var.set(attrs) try: yield finally: cls._var.set(previous)
[docs] def generate_comment(attrs: Mapping[str, str | None]) -> str: """Serialize attributes into a sqlcommenter comment body. Args: attrs: Key-value pairs to serialize. ``None`` values are skipped. Returns: Comma-separated ``key='value'`` pairs sorted lexicographically, or empty string if no attributes. """ pairs: list[str] = [] for key in sorted(attrs): value = attrs[key] if value is None: continue pairs.append(f"{_encode_key(key)}={_encode_value(value)}") return ",".join(pairs)
[docs] def append_comment(expression: exp.Expr, attrs: Mapping[str, str | None]) -> exp.Expr: """Add sqlcommenter attributes as a comment on a parsed expression. Uses sqlglot's ``add_comments()`` API so the comment coexists with existing comments and optimizer hints. Args: expression: Parsed sqlglot expression tree. attrs: Attributes to serialize into the comment. Returns: The expression with the sqlcommenter comment added (mutated in place). """ comment_body = generate_comment(attrs) if not comment_body: return expression expression.add_comments([comment_body]) return expression
[docs] def parse_comment(expression: exp.Expr) -> tuple[exp.Expr, dict[str, str]]: """Extract sqlcommenter attributes from a parsed expression's comments. Identifies sqlcommenter comments by their ``key='value'`` structure, extracts the attributes, and removes the sqlcommenter comment from the expression while preserving other comments. Args: expression: Parsed sqlglot expression tree. Returns: Tuple of (expression_without_sqlcommenter_comment, parsed_attributes). If no sqlcommenter comment is found, returns the expression unchanged and an empty dict. """ if not expression.comments: return expression, {} attrs: dict[str, str] = {} remaining_comments: list[str] = [] for comment in expression.comments: stripped = comment.strip() if _is_sqlcommenter_comment(stripped): # Parse key='value' pairs for pair in stripped.split(","): eq_idx = pair.find("='") if eq_idx == -1: continue raw_key = pair[:eq_idx] raw_value = pair[eq_idx + 2 :] raw_value = raw_value.removesuffix("'") attrs[_decode(raw_key)] = _decode(raw_value) else: remaining_comments.append(comment) if remaining_comments: expression.comments = remaining_comments else: expression.comments = None return expression, attrs
[docs] def create_sqlcommenter_statement_transformer( *, attributes: dict[str, str | None] | None = None, enable_traceparent: bool = False, enable_context: bool = False ) -> Callable[[exp.Expr, Any], tuple[exp.Expr, Any]]: """Create a ``statement_transformer`` that adds sqlcommenter comments to the AST. Static attributes are pre-serialized at creation time. When ``enable_traceparent`` or ``enable_context`` is True, dynamic attributes are resolved per invocation. Args: attributes: Static key-value pairs to include in every comment. enable_traceparent: If True, auto-populate ``traceparent`` from the current OpenTelemetry span context on each invocation. enable_context: If True, read request-scoped attributes from :class:`SQLCommenterContext` and merge them with static attributes. Returns: A callable suitable for ``StatementConfig(statement_transformers=[...])``. """ static_attrs: dict[str, str | None] = dict(attributes) if attributes else {} is_dynamic = enable_traceparent or enable_context if not is_dynamic and not static_attrs: return _NOOP_SQLCOMMENTER_TRANSFORMER if not is_dynamic: return _StaticSQLCommenterTransformer(static_attrs) return _DynamicSQLCommenterTransformer( static_attrs, enable_traceparent=enable_traceparent, enable_context=enable_context )
def _encode_key(key: str) -> str: """URL-encode a key (single quotes become ``%27``).""" return quote(key, safe="") def _encode_value(value: str) -> str: """URL-encode a value and wrap in single quotes.""" return f"'{quote(value, safe='')}'" def _decode(raw: str) -> str: """Reverse URL-encoding.""" return unquote(raw) def _is_sqlcommenter_comment(comment: str) -> bool: """Check whether a comment string looks like a sqlcommenter payload.""" stripped = comment.strip() # sqlcommenter comments have key='value' pairs return "='" in stripped and stripped.endswith("'") def _append_comment(sql: str, attrs: Mapping[str, str | None]) -> str: """Append a sqlcommenter block to rendered SQL text.""" comment_body = generate_comment(attrs) if not comment_body: return sql stripped_sql = sql.rstrip() if not stripped_sql: return sql trailing_whitespace = sql[len(stripped_sql) :] comment = f"/* {comment_body} */" if stripped_sql.endswith(";"): before_semicolon = stripped_sql[:-1] statement = before_semicolon.rstrip() semicolon_padding = before_semicolon[len(statement) :] return f"{statement} {comment}{semicolon_padding};{trailing_whitespace}" return f"{stripped_sql} {comment}{trailing_whitespace}" def _comment_attributes( static_attrs: Mapping[str, str | None], *, enable_traceparent: bool, enable_context: bool ) -> dict[str, str | None]: """Resolve static and dynamic sqlcommenter attributes for the current call.""" merged: dict[str, str | None] = {} if enable_context: ctx_attrs = SQLCommenterContext.get() if ctx_attrs: merged.update(ctx_attrs) correlation_id = CorrelationContext.get() if correlation_id and "correlation_id" not in merged: merged["correlation_id"] = correlation_id merged.update(static_attrs) if enable_traceparent: trace_id, span_id = get_trace_context() if trace_id and span_id: merged["traceparent"] = _traceparent(trace_id, span_id) return merged def _traceparent(trace_id: str, span_id: str) -> str: """Build a W3C traceparent header value from trace and span IDs.""" return f"00-{trace_id}-{span_id}-01" @final class _NoOpSQLCommenterTransformer: __slots__ = () def __call__(self, expression: exp.Expr, params: Any) -> tuple[exp.Expr, Any]: return expression, params @final class _StaticSQLCommenterTransformer: __slots__ = ("_comment_body",) def __init__(self, attrs: Mapping[str, str | None]) -> None: self._comment_body = generate_comment(attrs) def __call__(self, expression: exp.Expr, params: Any) -> tuple[exp.Expr, Any]: if self._comment_body: expression.add_comments([self._comment_body]) return expression, params @final class _DynamicSQLCommenterTransformer: __slots__ = ("_enable_context", "_enable_traceparent", "_static_attrs") def __init__( self, static_attrs: Mapping[str, str | None], *, enable_traceparent: bool, enable_context: bool ) -> None: self._static_attrs = dict(static_attrs) self._enable_traceparent = enable_traceparent self._enable_context = enable_context def __call__(self, expression: exp.Expr, params: Any) -> tuple[exp.Expr, Any]: merged = _comment_attributes( self._static_attrs, enable_traceparent=self._enable_traceparent, enable_context=self._enable_context ) return append_comment(expression, merged), params _NOOP_SQLCOMMENTER_TRANSFORMER: Final = _NoOpSQLCommenterTransformer()