"""Parameter style conversion utilities."""
from collections.abc import Callable, Mapping, Sequence
from decimal import Decimal
from math import isfinite
from typing import Any, Final
from mypy_extensions import mypyc_attr
from sqlglot import exp
from sqlspec.core.parameters._types import (
_EXPANDING_POSITIONAL_STYLES,
_NAMED_STYLES,
_POSITIONAL_STYLES,
ConvertedParameters,
NamedParameterOutput,
ParameterInfo,
ParameterMapping,
ParameterPayload,
ParameterSequence,
ParameterStyle,
PositionalParameterOutput,
)
from sqlspec.core.parameters._validator import ParameterValidator
from sqlspec.exceptions import SQLSpecError
__all__ = ("ParameterConverter",)
_BINARY_LITERAL_TEMPLATES: Final[dict[str | None, str]] = {
None: "X'{}'",
"mysql": "X'{}'",
"sqlite": "X'{}'",
"postgres": "decode('{}', 'hex')",
"pgvector": "decode('{}', 'hex')",
"paradedb": "decode('{}', 'hex')",
"pg_textsearch": "decode('{}', 'hex')",
"oracle": "HEXTORAW('{}')",
"tsql": "0x{}",
"bigquery": "FROM_HEX('{}')",
"spanner": "FROM_HEX('{}')",
"duckdb": "UNHEX('{}')",
}
_ORDERED_PARAM_INFO_MIN_SIZE = 2
_OCCURRENCE_KEYED_STYLES: Final[frozenset[ParameterStyle]] = frozenset({
ParameterStyle.QMARK,
ParameterStyle.POSITIONAL_PYFORMAT,
})
_INDEX_STYLES: Final[frozenset[ParameterStyle]] = frozenset({ParameterStyle.NUMERIC, ParameterStyle.POSITIONAL_COLON})
[docs]
@mypyc_attr(allow_interpreted_subclasses=False)
class ParameterConverter:
"""Parameter style conversion helper."""
__slots__ = ("_overrides_public_conversion", "_placeholder_generators", "validator")
[docs]
def __init__(self, validator: "ParameterValidator | None" = None) -> None:
self.validator = validator or ParameterValidator()
self._placeholder_generators: dict[ParameterStyle, Callable[[Any], str]] = {
ParameterStyle.QMARK: _placeholder_qmark,
ParameterStyle.NUMERIC: _placeholder_numeric,
ParameterStyle.NAMED_COLON: _placeholder_named_colon,
ParameterStyle.POSITIONAL_COLON: _placeholder_positional_colon,
ParameterStyle.NAMED_AT: _placeholder_named_at,
ParameterStyle.NAMED_DOLLAR: _placeholder_named_dollar,
ParameterStyle.NAMED_PYFORMAT: _placeholder_named_pyformat,
ParameterStyle.POSITIONAL_PYFORMAT: _placeholder_positional_pyformat,
}
converter_type = type(self)
self._overrides_public_conversion = (
converter_type.convert_placeholder_style is not ParameterConverter.convert_placeholder_style
or converter_type.convert_parameter_info_style is not ParameterConverter.convert_parameter_info_style
)
def convert_placeholder_style(
self,
sql: str,
parameters: "ParameterPayload",
target_style: "ParameterStyle",
is_many: bool = False,
*,
dialect: "str | None" = None,
strict_named_parameters: bool = True,
param_info: "list[ParameterInfo] | None" = None,
precomputed_plan: "tuple[list[ParameterInfo], dict[str, int]] | None" = None,
) -> "tuple[str, ConvertedParameters]":
extracted_param_info = param_info if param_info is not None else self.validator.extract_parameters(sql)
if target_style == ParameterStyle.STATIC:
return self._embed_static_parameters(sql, parameters, extracted_param_info, dialect), None
converted_sql, converted_parameters, _ = self._convert_builtin(
sql,
parameters,
target_style,
is_many,
strict_named_parameters=strict_named_parameters,
param_info=extracted_param_info,
precomputed_plan=precomputed_plan,
)
return converted_sql, converted_parameters
def _build_conversion_plan(
self, param_info: "list[ParameterInfo]", target_style: "ParameterStyle"
) -> "tuple[list[ParameterInfo], dict[str, int]]":
ordered_params = _ordered_parameter_info(param_info)
unique_params: dict[str, int] = {}
for param in ordered_params:
param_key = _parameter_lookup_key(param)
if param_key not in unique_params:
unique_params[param_key] = len(unique_params)
return ordered_params, unique_params
def _render_conversion(
self,
sql: str,
param_info: "list[ParameterInfo]",
target_style: "ParameterStyle",
precomputed_plan: "tuple[list[ParameterInfo], dict[str, int]] | None" = None,
*,
collect_metadata: bool = False,
) -> "tuple[str, list[ParameterInfo]]":
"""Rewrite placeholders to the target style and describe them in one traversal.
Returns the rendered SQL and the converted parameter metadata (empty unless
``collect_metadata`` is true).
"""
generator = self._placeholder_generators.get(target_style)
if generator is None:
msg = f"Unsupported target parameter style: {target_style}"
raise ValueError(msg)
if precomputed_plan is not None:
ordered_params, unique_params = precomputed_plan
else:
ordered_params, unique_params = self._build_conversion_plan(param_info, target_style)
is_positional_style = _is_positional_style(target_style)
explicit_indexes = _uses_explicit_indexes(param_info)
target_is_indexed = target_style in _INDEX_STYLES
segments: list[str] = []
last_end = 0
converted_param_info: list[ParameterInfo] = []
delta = 0
names_by_key = {} if is_positional_style else _named_parameter_names(param_info)
for param in ordered_params:
if is_positional_style:
explicit_index = _explicit_index(param) if explicit_indexes and target_is_indexed else -1
converted_index = explicit_index if explicit_index >= 0 else unique_params[_parameter_lookup_key(param)]
new_placeholder = generator(converted_index)
name = str(converted_index + 1) if target_is_indexed else None
else:
name = names_by_key[_parameter_lookup_key(param)]
new_placeholder = generator(name)
segments.extend((sql[last_end : param.position], new_placeholder))
last_end = param.position + len(param.placeholder_text)
if collect_metadata:
converted_param_info.append(
ParameterInfo(
name=name,
style=target_style,
position=param.position + delta,
ordinal=param.ordinal,
placeholder_text=new_placeholder,
)
)
delta += len(new_placeholder) - len(param.placeholder_text)
segments.append(sql[last_end:])
return "".join(segments), converted_param_info
def convert_parameter_info_style(
self,
param_info: "list[ParameterInfo]",
target_style: "ParameterStyle",
precomputed_plan: "tuple[list[ParameterInfo], dict[str, int]] | None" = None,
) -> "list[ParameterInfo]":
_, converted_param_info = self._render_conversion(
"", param_info, target_style, precomputed_plan, collect_metadata=True
)
return converted_param_info
def _convert_builtin(
self,
sql: str,
parameters: "ParameterPayload",
target_style: "ParameterStyle",
is_many: bool,
*,
strict_named_parameters: bool,
param_info: "list[ParameterInfo]",
precomputed_plan: "tuple[list[ParameterInfo], dict[str, int]] | None",
preserve_original_batch: bool = False,
collect_metadata: bool = False,
) -> "tuple[str, ConvertedParameters, list[ParameterInfo]]":
"""Convert placeholders and parameters without dispatching through overridable methods."""
current_style = _single_parameter_style(param_info)
if current_style is not None and target_style == current_style:
converted_sql = sql
converted_param_info = param_info if collect_metadata else []
else:
converted_sql, converted_param_info = self._render_conversion(
sql, param_info, target_style, precomputed_plan, collect_metadata=collect_metadata
)
converted_parameters = self._convert_parameter_format(
parameters,
param_info,
target_style,
parameters,
preserve_parameter_format=True,
is_many=is_many,
strict_named_parameters=strict_named_parameters,
preserve_original_batch=preserve_original_batch,
)
return converted_sql, converted_parameters, converted_param_info
def _convert_with_metadata(
self,
sql: str,
parameters: "ParameterPayload",
target_style: "ParameterStyle",
is_many: bool = False,
*,
strict_named_parameters: bool = True,
param_info: "list[ParameterInfo] | None" = None,
precomputed_plan: "tuple[list[ParameterInfo], dict[str, int]] | None" = None,
preserve_original_batch: bool = False,
) -> "tuple[str, ConvertedParameters, list[ParameterInfo]]":
"""Convert placeholder style and return SQL, parameters, and metadata together."""
extracted_param_info = param_info if param_info is not None else self.validator.extract_parameters(sql)
if self._overrides_public_conversion:
converted_sql, converted_parameters = self.convert_placeholder_style(
sql,
parameters,
target_style,
is_many,
strict_named_parameters=strict_named_parameters,
param_info=extracted_param_info,
precomputed_plan=precomputed_plan,
)
converted_param_info = self.convert_parameter_info_style(
extracted_param_info, target_style, precomputed_plan=precomputed_plan
)
if preserve_original_batch and is_many and isinstance(parameters, (list, tuple)):
return converted_sql, parameters, converted_param_info
return converted_sql, converted_parameters, converted_param_info
return self._convert_builtin(
sql,
parameters,
target_style,
is_many,
strict_named_parameters=strict_named_parameters,
param_info=extracted_param_info,
precomputed_plan=precomputed_plan,
preserve_original_batch=preserve_original_batch,
collect_metadata=True,
)
def _convert_sequence_to_dict(
self, parameters: "ParameterSequence", param_info: "list[ParameterInfo]"
) -> "NamedParameterOutput":
param_dict: dict[str, Any] = {}
explicit_indexes = _uses_explicit_indexes(param_info)
if explicit_indexes:
_validate_explicit_index_count(param_info, len(parameters))
expected = len({_parameter_lookup_key(param) for param in param_info})
if len(parameters) != expected:
msg = f"Parameter count mismatch: {len(parameters)} parameters provided but {expected} placeholders referenced."
raise SQLSpecError(msg)
slots: dict[str, int] = {}
names_by_key = _named_parameter_names(param_info)
for param in param_info:
explicit_index = _explicit_index(param) if explicit_indexes else -1
if explicit_index >= 0:
source_index = explicit_index
else:
lookup_key = _parameter_lookup_key(param)
if lookup_key not in slots:
slots[lookup_key] = len(slots)
source_index = slots[lookup_key]
if source_index < len(parameters):
name = names_by_key[_parameter_lookup_key(param)]
if name not in param_dict:
param_dict[name] = parameters[source_index]
return param_dict
def _align_mapping_for_named_style(
self, parameters: "Mapping[str, Any]", param_info: "list[ParameterInfo]", strict_named_parameters: bool = True
) -> "NamedParameterOutput":
"""Align a mapping with the placeholder names of a named-style target."""
names_by_key = _named_parameter_names(param_info)
expected_names = set(names_by_key.values())
if expected_names.issubset(parameters.keys()):
return dict(parameters)
reserved_keys = _named_placeholder_keys(param_info)
if not reserved_keys or all(param.style in _NAMED_STYLES for param in param_info):
return self._convert_sequence_to_dict(list(parameters.values()), param_info)
if strict_named_parameters:
missing_names = self._missing_named_parameters(param_info, parameters)
if missing_names:
msg = f"Missing named parameter(s): {', '.join(missing_names)}"
raise SQLSpecError(msg)
positional_keys = [key for key in parameters if key not in reserved_keys]
positional_ranks: dict[str, int] = {}
aligned: dict[str, Any] = {}
for param in param_info:
target_name = names_by_key[_parameter_lookup_key(param)]
if target_name in aligned:
continue
if param.style in _NAMED_STYLES:
if param.name is not None and param.name in parameters:
aligned[target_name] = parameters[param.name]
elif param.placeholder_text in parameters:
aligned[target_name] = parameters[param.placeholder_text]
continue
if target_name not in positional_ranks:
positional_ranks[target_name] = len(positional_ranks)
value, found = self._lookup_positional_value(
param, parameters, positional_ranks[target_name], positional_keys
)
if found:
aligned[target_name] = value
elif strict_named_parameters:
raise SQLSpecError(_missing_positional_message(param))
return aligned
def _lookup_parameter_value(
self,
param: "ParameterInfo",
parameters: "ParameterMapping",
param_keys: "list[str]",
fallback_keys: "list[str] | None" = None,
) -> "tuple[object | None, bool, list[str] | None]":
if param.name and param.name in parameters:
return parameters[param.name], True, fallback_keys
if param.placeholder_text in parameters:
return parameters[param.placeholder_text], True, fallback_keys
if f"param_{param.ordinal}" in parameters:
return parameters[f"param_{param.ordinal}"], True, fallback_keys
ordinal_key = str(param.ordinal + 1)
if ordinal_key in parameters:
return parameters[ordinal_key], True, fallback_keys
if fallback_keys is None:
try:
fallback_keys = list(parameters.keys())
except AttributeError:
fallback_keys = []
if fallback_keys and param.ordinal < len(fallback_keys):
key = fallback_keys[param.ordinal]
if key in parameters:
return parameters[key], True, fallback_keys
return None, False, fallback_keys
def _lookup_positional_value(
self,
param: "ParameterInfo",
parameters: "ParameterMapping",
positional_index: int,
positional_keys: "list[str]",
) -> "tuple[object | None, bool]":
"""Resolve a nameless or index-style placeholder from a mapping.
``positional_keys`` holds the mapping keys that are not claimed by a named placeholder
of the same statement, so a named value is never bound to a positional placeholder.
"""
if param.name and param.name in parameters:
return parameters[param.name], True
if param.placeholder_text in parameters:
return parameters[param.placeholder_text], True
if f"param_{param.ordinal}" in positional_keys:
return parameters[f"param_{param.ordinal}"], True
ordinal_key = str(param.ordinal + 1)
if ordinal_key in positional_keys:
return parameters[ordinal_key], True
if 0 <= positional_index < len(positional_keys):
return parameters[positional_keys[positional_index]], True
return None, False
def _missing_named_parameters(
self, param_info: "list[ParameterInfo]", parameters: "ParameterMapping"
) -> "list[str]":
missing: list[str] = []
for param in param_info:
if param.style not in _NAMED_STYLES or not param.name:
continue
if param.name in parameters or param.placeholder_text in parameters:
continue
missing.append(param.name)
return sorted(set(missing))
def _validate_batch_named_parameters(self, param_info: "list[ParameterInfo]", parameters: "Sequence[Any]") -> None:
"""Validate named parameters across batch rows in original order."""
for param_set in parameters:
if isinstance(param_set, Mapping):
missing_names = self._missing_named_parameters(param_info, param_set)
if missing_names:
msg = f"Missing named parameter(s): {', '.join(missing_names)}"
raise SQLSpecError(msg)
def _preserve_original_format(
self, param_values: "list[Any]", original_parameters: object
) -> "PositionalParameterOutput":
if isinstance(original_parameters, list):
return param_values
return tuple(param_values)
def _convert_parameter_format(
self,
parameters: "ParameterPayload",
param_info: "list[ParameterInfo]",
target_style: "ParameterStyle",
original_parameters: object | None = None,
preserve_parameter_format: bool = False,
is_many: bool = False,
*,
strict_named_parameters: bool = True,
preserve_original_batch: bool = False,
) -> "ConvertedParameters":
if not parameters or not param_info:
# When parameters is falsy, it's either None or empty - return None
if parameters is None:
return None
# For empty containers, convert to concrete type
if isinstance(parameters, Mapping):
return dict(parameters)
if isinstance(parameters, (list, tuple)):
return list(parameters) if isinstance(parameters, list) else tuple(parameters)
return None
if (
is_many
and isinstance(parameters, Sequence)
and not isinstance(parameters, (str, bytes, bytearray))
and parameters
):
if preserve_original_batch and isinstance(parameters, (list, tuple)):
if strict_named_parameters and target_style not in _NAMED_STYLES:
self._validate_batch_named_parameters(param_info, parameters)
return parameters
normalized_sets: list[Any] = [
self._convert_parameter_format(
param_set,
param_info,
target_style,
param_set,
preserve_parameter_format,
is_many=False,
strict_named_parameters=strict_named_parameters,
)
for param_set in parameters
]
if preserve_parameter_format and isinstance(parameters, tuple):
return tuple(normalized_sets)
return normalized_sets
is_named_style = target_style in _NAMED_STYLES
if is_named_style:
if isinstance(parameters, Mapping):
return self._align_mapping_for_named_style(parameters, param_info, strict_named_parameters)
if isinstance(parameters, Sequence) and not isinstance(parameters, (str, bytes)):
return self._convert_sequence_to_dict(parameters, param_info)
elif isinstance(parameters, Sequence) and not isinstance(parameters, (str, bytes)):
return _sequence_to_positional(parameters, param_info, target_style)
elif isinstance(parameters, Mapping):
if strict_named_parameters:
missing_names = self._missing_named_parameters(param_info, parameters)
if missing_names:
msg = f"Missing named parameter(s): {', '.join(missing_names)}"
raise SQLSpecError(msg)
param_values: list[Any] = []
parameter_styles = {p.style for p in param_info}
has_mixed_styles = len(parameter_styles) > 1
unique_params: dict[str, Any] = {}
param_order: list[str] = []
fallback_keys: list[str] | None = None
param_keys = list(parameters.keys()) if has_mixed_styles else []
reserved_keys = _named_placeholder_keys(param_info)
positional_keys = [key for key in parameters if key not in reserved_keys]
explicit_indexes = _uses_explicit_indexes(param_info)
positional_ranks: dict[str, int] = {}
for param in param_info:
param_key = param.placeholder_text if param.name else f"{param.placeholder_text}_{param.ordinal}"
if param_key not in unique_params:
if param.style in _NAMED_STYLES:
value, found, fallback_keys = self._lookup_parameter_value(
param, parameters, param_keys, fallback_keys
)
else:
positional_ranks.setdefault(
param_key, _explicit_index(param) if explicit_indexes else len(positional_ranks)
)
value, found = self._lookup_positional_value(
param, parameters, positional_ranks[param_key], positional_keys
)
if not found and strict_named_parameters:
raise SQLSpecError(_missing_positional_message(param))
if found:
unique_params[param_key] = value
param_order.append(param_key)
param_values = _mapping_values_for_style(
unique_params, param_order, param_info, target_style, explicit_indexes
)
if preserve_parameter_format and original_parameters is not None:
return self._preserve_original_format(param_values, original_parameters)
return param_values
# Fallback for non-standard parameters - return None
return None
@staticmethod
def _format_literal(param_value: object | None, dialect: "str | None" = None) -> str:
"""Render a literal for the server's default SQL mode.
MySQL NO_BACKSLASH_ESCAPES stores doubled backslashes as two characters.
"""
if param_value is None:
return "NULL"
if isinstance(param_value, bool):
return "TRUE" if param_value else "FALSE"
if isinstance(param_value, (float, Decimal)):
finite = param_value.is_finite() if isinstance(param_value, Decimal) else isfinite(param_value)
if not finite:
msg = "Cannot embed a non-finite number in statically compiled SQL"
raise SQLSpecError(msg)
return str(param_value)
if isinstance(param_value, int):
return str(param_value)
if isinstance(param_value, (bytes, bytearray, memoryview)):
template = _BINARY_LITERAL_TEMPLATES.get(dialect.lower() if dialect else None)
if template is None:
msg = f"Cannot embed a bytes value in statically compiled SQL for dialect '{dialect}'"
raise SQLSpecError(msg)
return template.format(bytes(param_value).hex())
return exp.Literal.string(str(param_value)).sql(dialect=dialect)
def _embed_static_parameters(
self, sql: str, parameters: "ParameterPayload", param_info: "list[ParameterInfo]", dialect: "str | None" = None
) -> str:
if not param_info:
return sql
if (
isinstance(parameters, Mapping)
and any(param.style in _NAMED_STYLES for param in param_info)
and any(param.style not in _NAMED_STYLES for param in param_info)
):
parameters = self._convert_parameter_format(parameters, param_info, ParameterStyle.NUMERIC)
unique_params: dict[str, int] = {}
for param in param_info:
if param.style in _OCCURRENCE_KEYED_STYLES or not param.name:
param_key = f"{param.placeholder_text}_{param.ordinal}"
else:
param_key = param.placeholder_text
if param_key not in unique_params:
unique_params[param_key] = len(unique_params)
explicit_indexes = _uses_explicit_indexes(param_info)
segments: list[str] = []
last_start = len(sql)
for param in reversed(param_info):
param_value = self._parameter_value(parameters, param, unique_params, explicit_indexes)
literal = self._format_literal(param_value, dialect)
segments.extend((sql[param.position + len(param.placeholder_text) : last_start], literal))
last_start = param.position
segments.append(sql[:last_start])
segments.reverse()
return "".join(segments)
def _parameter_value(
self,
parameters: "ParameterPayload",
param: "ParameterInfo",
unique_params: "dict[str, int]",
explicit_indexes: bool = False,
) -> object | None:
if param.style in _OCCURRENCE_KEYED_STYLES or not param.name:
param_key = f"{param.placeholder_text}_{param.ordinal}"
else:
param_key = param.placeholder_text
unique_ordinal = unique_params.get(param_key)
if unique_ordinal is None:
msg = f"Missing value for placeholder '{param.placeholder_text}' in statically compiled SQL"
raise SQLSpecError(msg)
if explicit_indexes:
unique_ordinal = _explicit_index(param)
if isinstance(parameters, Mapping):
if param.name and param.name in parameters:
return parameters[param.name]
if f"param_{unique_ordinal}" in parameters:
return parameters[f"param_{unique_ordinal}"]
if str(unique_ordinal + 1) in parameters:
return parameters[str(unique_ordinal + 1)]
elif isinstance(parameters, Sequence) and not isinstance(parameters, (str, bytes)):
if unique_ordinal < len(parameters):
return parameters[unique_ordinal]
msg = f"Missing value for placeholder '{param.placeholder_text}' in statically compiled SQL"
raise SQLSpecError(msg)
def _placeholder_qmark(_: Any) -> str:
return "?"
def _placeholder_numeric(index: Any) -> str:
return f"${int(index) + 1}"
def _placeholder_named_colon(name: Any) -> str:
return f":{name}"
def _placeholder_positional_colon(index: Any) -> str:
return f":{int(index) + 1}"
def _placeholder_named_at(name: Any) -> str:
return f"@{name}"
def _placeholder_named_dollar(name: Any) -> str:
return f"${name}"
def _placeholder_named_pyformat(name: Any) -> str:
return f"%({name})s"
def _placeholder_positional_pyformat(_: Any) -> str:
return "%s"
def _ordered_parameter_info(param_info: "list[ParameterInfo]") -> "list[ParameterInfo]":
if len(param_info) < _ORDERED_PARAM_INFO_MIN_SIZE:
return param_info
previous_position = param_info[0].position
for param in param_info[1:]:
if param.position < previous_position:
return sorted(param_info, key=lambda item: item.position)
previous_position = param.position
return param_info
def _single_parameter_style(param_info: "list[ParameterInfo]") -> "ParameterStyle | None":
if not param_info:
return None
style = param_info[0].style
for param in param_info[1:]:
if param.style != style:
return None
return style
def _is_positional_style(style: "ParameterStyle") -> bool:
return style in _POSITIONAL_STYLES
def _parameter_lookup_key(param: "ParameterInfo") -> str:
if param.style in _OCCURRENCE_KEYED_STYLES:
return f"{param.placeholder_text}_{param.ordinal}"
return param.placeholder_text
def _named_parameter_name(param: "ParameterInfo", explicit_indexes: bool = False) -> str:
param_name = param.name or f"param_{param.ordinal}"
if param_name.isdigit():
explicit_index = _explicit_index(param) if explicit_indexes else -1
if explicit_index >= 0:
return f"param_{explicit_index}"
return f"param_{param.ordinal}"
return param_name
def _explicit_index(param: "ParameterInfo") -> int:
"""Return the zero-based index written in a ``$n`` / ``:n`` placeholder, or -1."""
if param.style not in _INDEX_STYLES:
return -1
name = param.name
if name is None or not name.isdigit():
return -1
return int(name) - 1
def _uses_explicit_indexes(param_info: "list[ParameterInfo]") -> bool:
"""Return True when every placeholder is a written ``$n`` / ``:n`` index."""
if not param_info:
return False
return all(_explicit_index(param) >= 0 for param in param_info)
def _validate_explicit_index_count(param_info: "list[ParameterInfo]", provided: int) -> None:
indexes = {_explicit_index(param) for param in param_info}
if provided != len(indexes) or max(indexes) >= provided:
msg = (
f"Parameter count mismatch: {provided} parameters provided but "
f"{len(indexes)} distinct positional indexes referenced."
)
raise SQLSpecError(msg)
def _missing_positional_message(param: "ParameterInfo") -> str:
return (
f"Missing value for positional placeholder {param.placeholder_text!r} "
f"(placeholder {param.ordinal + 1}); values supplied for named "
"parameters are not bound to positional placeholders."
)
def _named_placeholder_keys(param_info: "list[ParameterInfo]") -> "set[str]":
keys: set[str] = set()
for param in param_info:
if param.style in _NAMED_STYLES and param.name:
keys.add(param.name)
keys.add(param.placeholder_text)
return keys
def ambiguous_index_placeholder(param_info: "list[ParameterInfo]") -> "ParameterInfo | None":
"""Return the first ``$n`` / ``:n`` placeholder whose written index is not its slot.
Only statements that mix index placeholders with another placeholder style are
inspected. A slot is the rank of a placeholder among the distinct placeholders of
the statement, which is the position a sequence value is bound to.
"""
has_index = False
has_other = False
for param in param_info:
if _explicit_index(param) >= 0:
has_index = True
else:
has_other = True
if not (has_index and has_other):
return None
slots: dict[str, int] = {}
for param in _ordered_parameter_info(param_info):
key = _parameter_lookup_key(param)
if key not in slots:
slots[key] = len(slots)
written = _explicit_index(param)
if written >= 0 and written != slots[key]:
return param
return None
def ambiguous_index_message(param: "ParameterInfo") -> str:
return (
f"Ambiguous positional binding: index placeholder {param.placeholder_text!r} is mixed with other "
"placeholder styles and its index does not match its position among the statement's placeholders. "
"Use a single placeholder style, number index placeholders by position, or pass a mapping."
)
def _sequence_to_positional(
parameters: "Sequence[Any]", param_info: "list[ParameterInfo]", target_style: ParameterStyle
) -> "list[Any] | tuple[Any, ...]":
if target_style not in _OCCURRENCE_KEYED_STYLES:
return list(parameters) if isinstance(parameters, list) else tuple(parameters)
if _uses_explicit_indexes(param_info):
_validate_explicit_index_count(param_info, len(parameters))
order = [_explicit_index(param) for param in param_info]
else:
slots: dict[str, int] = {}
order = []
for param in param_info:
key = _parameter_lookup_key(param)
if key not in slots:
slots[key] = len(slots)
order.append(slots[key])
if len(parameters) != len(slots):
msg = f"Parameter count mismatch: {len(parameters)} parameters provided but {len(slots)} placeholders referenced."
raise SQLSpecError(msg)
expanded = [parameters[index] for index in order]
return expanded if isinstance(parameters, list) else tuple(expanded)
def _named_parameter_names(param_info: "list[ParameterInfo]") -> dict[str, str]:
reserved = {param.name for param in param_info if param.style in _NAMED_STYLES and param.name}
explicit_indexes = _uses_explicit_indexes(param_info)
names: dict[str, str] = {}
for param in param_info:
key = _parameter_lookup_key(param)
if key in names:
continue
name = _named_parameter_name(param, explicit_indexes)
if param.style not in _NAMED_STYLES:
while name in reserved:
name += "_p"
reserved.add(name)
names[key] = name
return names
def _written_index_key(placeholder: str) -> int:
return int(placeholder[1:])
def _mapping_values_for_style(
values: dict[str, Any],
order: list[str],
param_info: "list[ParameterInfo]",
target: ParameterStyle,
explicit_indexes: bool,
) -> list[Any]:
if target in _EXPANDING_POSITIONAL_STYLES:
expanded = []
for param in param_info:
key = param.placeholder_text if param.name else f"{param.placeholder_text}_{param.ordinal}"
if key in values:
expanded.append(values[key])
return expanded
if explicit_indexes and target in _INDEX_STYLES:
order.sort(key=_written_index_key)
return [values[key] for key in order]