Source code for sqlspec.core.parameters._processor

"""Parameter processing pipeline orchestrator."""

from collections import OrderedDict
from collections.abc import Callable, Mapping, Sequence
from typing import Any, Final, cast

from mypy_extensions import mypyc_attr

from sqlspec.core.parameters._alignment import looks_like_execute_many
from sqlspec.core.parameters._converter import ParameterConverter
from sqlspec.core.parameters._types import (
    _NAMED_STYLE_VALUES,
    _NAMED_STYLES,
    _POSITIONAL_STYLE_VALUES,
    ConvertedParameters,
    ParameterInfo,
    ParameterPayload,
    ParameterProcessingResult,
    ParameterProfile,
    ParameterStyle,
    ParameterStyleConfig,
    TypedParameter,
    wrap_with_type,
)
from sqlspec.core.parameters._validator import ParameterValidator
from sqlspec.utils.dispatch import TypeDispatcher

__all__ = ("ParameterProcessor", "structural_fingerprint", "value_fingerprint")

TypeCoercionFallback = tuple[type, Callable[[Any], Any]]

_EXECUTE_MANY_SAMPLE_THRESHOLD: Final[int] = 10
_EXECUTE_MANY_SAMPLE_SIZE: Final[int] = 3
_OCCURRENCE_BASED_POSITIONAL_STYLES: Final[frozenset[ParameterStyle]] = frozenset({
    ParameterStyle.QMARK,
    ParameterStyle.POSITIONAL_COLON,
    ParameterStyle.POSITIONAL_PYFORMAT,
})

_TYPE_COERCION_DISPATCHERS: Final[dict[tuple[TypeCoercionFallback, ...], TypeDispatcher[Callable[[Any], Any]]]] = {}


def structural_fingerprint(parameters: "ParameterPayload", is_many: bool = False) -> Any:
    """Return a structural fingerprint for caching parameter payloads.

    Returns a hashable tuple representing the structure (keys, types, count).
    Avoids string formatting for performance.

    Note: Uses Python 3.7+ dict insertion order instead of sorted() for determinism.
    This means fingerprints depend on the order keys were inserted, which is typically
    consistent within a single codebase.
    """
    if parameters is None:
        return None

    # Fast type dispatch: check concrete types first (2-4x faster than ABC isinstance)
    param_type = type(parameters)

    # Handle dict (most common Mapping type) - fast path
    if param_type is dict:
        dict_params = cast("dict[str, Any]", parameters)
        if not dict_params:
            return ("dict",)
        # Use dict insertion order (Python 3.7+ guaranteed) instead of sorted()
        # This is O(n) vs O(n log n) and produces consistent fingerprints for
        # parameters constructed in the same order (typical usage pattern)
        keys = tuple(dict_params.keys())
        type_sig = tuple(type(v) for v in dict_params.values())
        return ("dict", keys, type_sig)

    # Handle list and tuple (most common Sequence types) - fast path
    if param_type is list or param_type is tuple:
        seq_params = cast("Sequence[Any]", parameters)
        if not seq_params:
            return ("seq",)

        # Optimization: Fast path for single-item sequence (extremely common)
        if len(seq_params) == 1:
            return ("seq", (type(seq_params[0]),))

        if is_many:
            return _fingerprint_execute_many(seq_params)

        # Single execution with sequence parameters
        type_sig = tuple(type(v) for v in seq_params)
        return ("seq", type_sig)

    # Fallback to ABC checks for custom types (Mapping, Sequence subclasses)
    if isinstance(parameters, Mapping):
        if not parameters:
            return ("dict",)
        keys = tuple(parameters.keys())
        type_sig = tuple(type(v) for v in parameters.values())
        return ("dict", keys, type_sig)

    if isinstance(parameters, Sequence) and not isinstance(parameters, (str, bytes, bytearray)):
        if not parameters:
            return ("seq",)

        if len(parameters) == 1:
            return ("seq", (type(parameters[0]),))

        if is_many:
            return _fingerprint_execute_many(parameters)

        type_sig = tuple(type(v) for v in parameters)
        return ("seq", type_sig)

    # Scalar parameter
    return ("scalar", param_type)


def value_fingerprint(parameters: "ParameterPayload") -> Any:
    """Return a value-based fingerprint for parameter payloads.

    Unlike structural_fingerprint, this includes actual parameter VALUES in the hash.
    Used for static script compilation where SQL has values embedded directly.

    Args:
        parameters: Original parameter payload supplied by the caller.

    Returns:
        Hashable representation including parameter values.
    """
    if parameters is None:
        return None

    # Use repr for value-based hashing - includes both structure and values
    # Return as tuple to match structural_fingerprint return type (hashable)
    return ("values", repr(parameters))


@mypyc_attr(allow_interpreted_subclasses=False)
class ParameterProcessor:
    """Parameter processing engine coordinating conversion phases."""

    __slots__ = ("_cache", "_cache_hits", "_cache_max_size", "_cache_misses", "_converter", "_validator")

    DEFAULT_CACHE_SIZE = 1000

    def __init__(
        self,
        *,
        converter: "ParameterConverter | None" = None,
        validator: "ParameterValidator | None" = None,
        cache_max_size: int | None = None,
        validator_cache_max_size: int | None = None,
    ) -> None:
        self._cache: OrderedDict[Any, ParameterProcessingResult] = OrderedDict()
        if cache_max_size is None:
            cache_max_size = self.DEFAULT_CACHE_SIZE
        self._cache_max_size = max(cache_max_size, 0)
        self._cache_hits = 0
        self._cache_misses = 0
        if converter is None:
            if validator is None:
                validator_cache = validator_cache_max_size
                if validator_cache is None:
                    validator_cache = self._cache_max_size
                validator = ParameterValidator(cache_max_size=validator_cache)
            self._validator = validator
            self._converter = ParameterConverter(self._validator)
        else:
            self._converter = converter
            if validator is None:
                self._validator = converter.validator
            else:
                self._validator = validator
                self._converter.validator = validator
            if validator_cache_max_size is not None and isinstance(self._validator, ParameterValidator):
                self._validator.set_cache_max_size(validator_cache_max_size)
def clear_cache(self) -> None: """Clear cached processing results and reset stats.""" self._cache.clear() self._cache_hits = 0 self._cache_misses = 0 if isinstance(self._validator, ParameterValidator): self._validator.clear_cache()