Source code for sqlspec.core.parameters._types

"""Core parameter data structures and utilities."""

from collections.abc import Callable, Collection, Generator, Iterable, Mapping, Sequence
from datetime import date, datetime, time
from decimal import Decimal
from enum import Enum
from functools import singledispatch
from types import MappingProxyType
from typing import Any, Literal, TypeAlias

from mypy_extensions import mypyc_attr

__all__ = (
    "ConvertedParameters",
    "DriverParameterProfile",
    "NamedParameterOutput",
    "ParameterInfo",
    "ParameterMapping",
    "ParameterPayload",
    "ParameterProcessingResult",
    "ParameterProfile",
    "ParameterSequence",
    "ParameterStyle",
    "ParameterStyleConfig",
    "PositionalParameterOutput",
    "TypedParameter",
    "is_iterable_parameters",
    "wrap_with_type",
)


ParameterMapping: TypeAlias = "Mapping[str, object]"
"""Type alias for mapping-based parameter payloads."""


ParameterSequence: TypeAlias = "Sequence[object]"
"""Type alias for sequence-based parameter payloads."""


ParameterPayload: TypeAlias = "ParameterMapping | ParameterSequence | object | None"
"""Type alias for parameter payloads accepted by the processing pipeline."""


ConvertedParameters: TypeAlias = "dict[str, Any] | list[Any] | tuple[Any, ...] | None"
"""Type alias for parameters after conversion to driver-consumable format.

This type represents the concrete output of parameter conversion functions.
Unlike :data:`ParameterPayload` (which represents inputs and can include abstract
Mapping/Sequence types), :data:`ConvertedParameters` only includes concrete types
that database drivers can directly consume.

The union includes:

- ``dict[str, Any]``: Named parameters (e.g., ``{"name": "Alice", "age": 30}``)
- ``list[Any]``: Positional parameters as list (e.g., ``["Alice", 30]``)
- ``tuple[Any, ...]``: Positional parameters as tuple (e.g., ``("Alice", 30)``)
- ``None``: When parameters are statically embedded in SQL string
"""


PositionalParameterOutput: TypeAlias = "list[Any] | tuple[Any, ...]"
"""Type alias for positional-only parameter outputs.

Used when a function is known to return only positional (not named) parameters.
This is narrower than :data:`ConvertedParameters` and excludes ``dict`` and ``None``.
"""


NamedParameterOutput: TypeAlias = "dict[str, Any]"
"""Type alias for named-only parameter outputs.

Used when a function is known to return only named (not positional) parameters.
This is narrower than :data:`ConvertedParameters` and excludes ``list``, ``tuple``, and ``None``.
"""


[docs] @mypyc_attr(allow_interpreted_subclasses=False) class ParameterStyle(str, Enum): """Enumeration of supported SQL parameter placeholder styles.""" NONE = "none" STATIC = "static" QMARK = "qmark" NUMERIC = "numeric" NAMED_COLON = "named_colon" POSITIONAL_COLON = "positional_colon" NAMED_AT = "named_at" NAMED_DOLLAR = "named_dollar" NAMED_PYFORMAT = "pyformat_named" POSITIONAL_PYFORMAT = "pyformat_positional"
[docs] @mypyc_attr(allow_interpreted_subclasses=False) class TypedParameter: """Wrapper that preserves original parameter type information.""" __slots__ = ("_hash", "original_type", "semantic_name", "value")
[docs] def __init__(self, value: Any, original_type: "type | None" = None, semantic_name: "str | None" = None) -> None: self.value = value self.original_type = original_type or type(value) self.semantic_name = semantic_name self._hash: int | None = None
def __hash__(self) -> int: if self._hash is None: value_id = id(self.value) self._hash = hash((value_id, self.original_type, self.semantic_name)) return self._hash def __eq__(self, other: object) -> bool: if not isinstance(other, TypedParameter): return False return ( self.value == other.value and self.original_type == other.original_type and self.semantic_name == other.semantic_name ) def __repr__(self) -> str: name_part = f", semantic_name='{self.semantic_name}'" if self.semantic_name else "" return f"TypedParameter({self.value!r}, original_type={self.original_type.__name__}{name_part})"
class _TupleAdapter: __slots__ = ("_as_list", "_serializer") def __init__(self, serializer: "Callable[[Any], str]", as_list: bool) -> None: self._serializer = serializer self._as_list = as_list def __call__(self, value: Any) -> "Any": if self._as_list: return self._serializer(list(value)) return self._serializer(value) @singledispatch def _wrap_parameter_by_type(value: Any, semantic_name: "str | None" = None) -> Any: return value @_wrap_parameter_by_type.register def _(value: bool, semantic_name: "str | None" = None) -> "TypedParameter": return TypedParameter(value, bool, semantic_name) @_wrap_parameter_by_type.register def _(value: Decimal, semantic_name: "str | None" = None) -> "TypedParameter": return TypedParameter(value, Decimal, semantic_name) @_wrap_parameter_by_type.register def _(value: datetime, semantic_name: "str | None" = None) -> "TypedParameter": return TypedParameter(value, datetime, semantic_name) @_wrap_parameter_by_type.register def _(value: date, semantic_name: "str | None" = None) -> "TypedParameter": return TypedParameter(value, date, semantic_name) @_wrap_parameter_by_type.register def _(value: time, semantic_name: "str | None" = None) -> "TypedParameter": return TypedParameter(value, time, semantic_name) @_wrap_parameter_by_type.register def _(value: bytes, semantic_name: "str | None" = None) -> "TypedParameter": return TypedParameter(value, bytes, semantic_name)
[docs] @mypyc_attr(allow_interpreted_subclasses=False) class ParameterInfo: """Metadata describing a single detected SQL parameter.""" __slots__ = ("name", "ordinal", "placeholder_text", "position", "style")
[docs] def __init__( self, name: "str | None", style: "ParameterStyle", position: int, ordinal: int, placeholder_text: str ) -> None: self.name = name self.style = style self.position = position self.ordinal = ordinal self.placeholder_text = placeholder_text
def __repr__(self) -> str: return ( "ParameterInfo(" f"name={self.name!r}, style={self.style!r}, position={self.position}, " f"ordinal={self.ordinal}, placeholder_text={self.placeholder_text!r}" ")" )
[docs] @mypyc_attr(allow_interpreted_subclasses=False) class ParameterStyleConfig: """Configuration describing parameter behaviour for a statement.""" __slots__ = ( "_hash_cache", "allow_mixed_parameter_styles", "ast_transformer", "default_execution_parameter_style", "default_parameter_style", "has_native_list_expansion", "json_deserializer", "json_serializer", "needs_static_script_compilation", "output_transformer", "preserve_original_params_for_many", "preserve_parameter_format", "strict_named_parameters", "supported_execution_parameter_styles", "supported_parameter_styles", "type_coercion_map", )
[docs] def __init__( self, default_parameter_style: "ParameterStyle", supported_parameter_styles: "Collection[ParameterStyle] | None" = None, supported_execution_parameter_styles: "Collection[ParameterStyle] | None" = None, default_execution_parameter_style: "ParameterStyle | None" = None, type_coercion_map: "Mapping[type, Callable[[Any], Any]] | None" = None, has_native_list_expansion: bool = False, needs_static_script_compilation: bool = False, allow_mixed_parameter_styles: bool = False, preserve_parameter_format: bool = True, preserve_original_params_for_many: bool = False, output_transformer: "Callable[[str, Any], tuple[str, Any]] | None" = None, ast_transformer: "Callable[[Any, Any, ParameterProfile], tuple[Any, Any]] | None" = None, json_serializer: "Callable[[Any], str] | None" = None, json_deserializer: "Callable[[str], Any] | None" = None, strict_named_parameters: bool = True, ) -> None: self.default_parameter_style = default_parameter_style self.supported_parameter_styles = frozenset(supported_parameter_styles or (default_parameter_style,)) self.supported_execution_parameter_styles = ( frozenset(supported_execution_parameter_styles) if supported_execution_parameter_styles else None ) self.default_execution_parameter_style = default_execution_parameter_style or default_parameter_style self.type_coercion_map = dict(type_coercion_map or {}) self.has_native_list_expansion = has_native_list_expansion self.output_transformer = output_transformer self.ast_transformer = ast_transformer self.needs_static_script_compilation = needs_static_script_compilation self.allow_mixed_parameter_styles = allow_mixed_parameter_styles self.preserve_parameter_format = preserve_parameter_format self.preserve_original_params_for_many = preserve_original_params_for_many self.strict_named_parameters = strict_named_parameters self.json_serializer = json_serializer self.json_deserializer = json_deserializer self._hash_cache: int | None = None
def __hash__(self) -> int: if self._hash_cache is None: hash_components = ( self.default_parameter_style.value, frozenset(style.value for style in self.supported_parameter_styles), ( frozenset(style.value for style in self.supported_execution_parameter_styles) if self.supported_execution_parameter_styles is not None else None ), self.default_execution_parameter_style.value, tuple(sorted(self.type_coercion_map.keys(), key=str)) if self.type_coercion_map else None, self.has_native_list_expansion, self.preserve_original_params_for_many, bool(self.output_transformer), self.needs_static_script_compilation, self.allow_mixed_parameter_styles, self.preserve_parameter_format, self.strict_named_parameters, bool(self.ast_transformer), self.json_serializer, self.json_deserializer, ) self._hash_cache = hash(hash_components) return self._hash_cache
[docs] def hash(self) -> int: """Return the hash value for caching compatibility. Returns: Hash value matching :func:`hash` output for this config. """ return hash(self)
def replace(self, **overrides: Any) -> "ParameterStyleConfig": data: dict[str, Any] = { "default_parameter_style": self.default_parameter_style, "supported_parameter_styles": set(self.supported_parameter_styles), "supported_execution_parameter_styles": ( set(self.supported_execution_parameter_styles) if self.supported_execution_parameter_styles is not None else None ), "default_execution_parameter_style": self.default_execution_parameter_style, "type_coercion_map": dict(self.type_coercion_map), "has_native_list_expansion": self.has_native_list_expansion, "needs_static_script_compilation": self.needs_static_script_compilation, "allow_mixed_parameter_styles": self.allow_mixed_parameter_styles, "preserve_parameter_format": self.preserve_parameter_format, "preserve_original_params_for_many": self.preserve_original_params_for_many, "strict_named_parameters": self.strict_named_parameters, "output_transformer": self.output_transformer, "ast_transformer": self.ast_transformer, "json_serializer": self.json_serializer, "json_deserializer": self.json_deserializer, } data.update(overrides) return ParameterStyleConfig(**data)
[docs] def with_json_serializers( self, serializer: "Callable[[Any], str]", *, tuple_strategy: "Literal['list', 'tuple']" = "list", deserializer: "Callable[[str], Any] | None" = None, ) -> "ParameterStyleConfig": """Return a copy configured with JSON serializers for complex parameters.""" if tuple_strategy == "list": tuple_adapter = _TupleAdapter(serializer, True) elif tuple_strategy == "tuple": tuple_adapter = _TupleAdapter(serializer, False) else: msg = f"Unsupported tuple_strategy: {tuple_strategy}" raise ValueError(msg) updated_type_map = dict(self.type_coercion_map) updated_type_map[dict] = serializer updated_type_map[list] = serializer updated_type_map[tuple] = tuple_adapter return self.replace( type_coercion_map=updated_type_map, json_serializer=serializer, json_deserializer=deserializer or self.json_deserializer, )
[docs] @mypyc_attr(allow_interpreted_subclasses=False) class DriverParameterProfile: """Immutable adapter profile describing parameter defaults.""" __slots__ = ( "allow_mixed_parameter_styles", "custom_type_coercions", "default_ast_transformer", "default_dialect", "default_execution_style", "default_output_transformer", "default_style", "extras", "has_native_list_expansion", "json_serializer_strategy", "name", "needs_static_script_compilation", "preserve_original_params_for_many", "preserve_parameter_format", "statement_kwargs", "strict_named_parameters", "supported_execution_styles", "supported_styles", )
[docs] def __init__( self, name: str, default_style: "ParameterStyle", supported_styles: "Collection[ParameterStyle]", default_execution_style: "ParameterStyle", supported_execution_styles: "Collection[ParameterStyle] | None", has_native_list_expansion: bool, preserve_parameter_format: bool, needs_static_script_compilation: bool, allow_mixed_parameter_styles: bool, preserve_original_params_for_many: bool, json_serializer_strategy: "Literal['driver', 'helper', 'none']", custom_type_coercions: "Mapping[type, Callable[[Any], Any]] | None" = None, default_output_transformer: "Callable[[str, Any], tuple[str, Any]] | None" = None, default_ast_transformer: "Callable[[Any, Any, ParameterProfile], tuple[Any, Any]] | None" = None, extras: "Mapping[str, object] | None" = None, default_dialect: "str | None" = None, statement_kwargs: "Mapping[str, object] | None" = None, strict_named_parameters: bool = True, ) -> None: self.name = name self.default_style = default_style self.supported_styles = frozenset(supported_styles) self.default_execution_style = default_execution_style self.supported_execution_styles = ( frozenset(supported_execution_styles) if supported_execution_styles is not None else None ) self.has_native_list_expansion = has_native_list_expansion self.preserve_parameter_format = preserve_parameter_format self.needs_static_script_compilation = needs_static_script_compilation self.allow_mixed_parameter_styles = allow_mixed_parameter_styles self.preserve_original_params_for_many = preserve_original_params_for_many self.strict_named_parameters = strict_named_parameters self.json_serializer_strategy = json_serializer_strategy self.custom_type_coercions = ( MappingProxyType(dict(custom_type_coercions)) if custom_type_coercions else MappingProxyType({}) ) self.default_output_transformer = default_output_transformer self.default_ast_transformer = default_ast_transformer self.extras = MappingProxyType(dict(extras)) if extras else MappingProxyType({}) self.default_dialect = default_dialect self.statement_kwargs = MappingProxyType(dict(statement_kwargs)) if statement_kwargs else MappingProxyType({})
[docs] @mypyc_attr(allow_interpreted_subclasses=False) class ParameterProfile: """Aggregate metadata describing detected parameters.""" __slots__ = ("_parameters", "_placeholder_counts", "named_parameters", "reused_ordinals", "styles") named_parameters: tuple[str, ...] reused_ordinals: tuple[int, ...] styles: tuple[str, ...]
[docs] def __init__(self, parameters: "Sequence[ParameterInfo] | None" = None) -> None: param_tuple: tuple[ParameterInfo, ...] = tuple(parameters) if parameters else () self._parameters = param_tuple # Optimize styles computation: skip sorted() for single-style case (common) if param_tuple: unique_styles = {param.style.value for param in param_tuple} # Skip sort for single style (common case) - O(1) vs O(n log n) if len(unique_styles) == 1: self.styles = (next(iter(unique_styles)),) else: self.styles = tuple(sorted(unique_styles)) else: self.styles = () placeholder_counts: dict[str, int] = {} reused_ordinals: list[int] = [] named_parameters: list[str] = [] for param in param_tuple: placeholder = param.placeholder_text current_count = placeholder_counts.get(placeholder, 0) placeholder_counts[placeholder] = current_count + 1 if current_count: reused_ordinals.append(param.ordinal) if param.name is not None: named_parameters.append(param.name) self._placeholder_counts = placeholder_counts self.reused_ordinals = tuple(reused_ordinals) self.named_parameters = tuple(named_parameters)
@classmethod def empty(cls) -> "ParameterProfile": return cls(()) @property def parameters(self) -> "tuple[ParameterInfo, ...]": return self._parameters @property def total_count(self) -> int: return len(self._parameters) def placeholder_count(self, placeholder: str) -> int: return self._placeholder_counts.get(placeholder, 0) def is_empty(self) -> bool: return not self._parameters
[docs] @mypyc_attr(allow_interpreted_subclasses=False) class ParameterProcessingResult: """Return container for parameter processing output.""" __slots__ = ( "applied_wrap_types", "input_named_parameters", "parameter_profile", "parameters", "parsed_expression", "sql", "sqlglot_sql", )
[docs] def __init__( self, sql: str, parameters: Any, parameter_profile: "ParameterProfile", sqlglot_sql: str | None = None, parsed_expression: Any = None, input_named_parameters: "tuple[str, ...] | None" = None, applied_wrap_types: bool = False, ) -> None: self.sql = sql self.parameters = parameters self.parameter_profile = parameter_profile self.sqlglot_sql = sqlglot_sql or sql self.parsed_expression = parsed_expression self.input_named_parameters = input_named_parameters or () self.applied_wrap_types = applied_wrap_types
def __iter__(self) -> "Generator[str | Any, Any, None]": yield self.sql yield self.parameters def __len__(self) -> int: return 2 def __getitem__(self, index: int) -> Any: if index == 0: return self.sql if index == 1: return self.parameters msg = "ParameterProcessingResult exposes exactly two positional items" raise IndexError(msg)
def is_iterable_parameters(obj: Any) -> bool: """Return True when the object behaves like an iterable parameter payload.""" return isinstance(obj, (list, tuple, set)) or ( isinstance(obj, Iterable) and not isinstance(obj, (str, bytes, Mapping)) ) def wrap_with_type(value: Any, semantic_name: "str | None" = None) -> Any: """Wrap value with :class:`TypedParameter` if it benefits downstream processing.""" if value is None: return None return _wrap_parameter_by_type(value, semantic_name)