Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion mssql_python/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ def _cleanup_connections():
# GLOBALS
# Read-Only
apilevel: str = "2.0"
paramstyle: str = "qmark"
paramstyle: str = "pyformat"
threadsafety: int = 1

# Set the initial decimal separator in C++
Expand Down
92 changes: 86 additions & 6 deletions mssql_python/cursor.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@
)
from mssql_python.row import Row
from mssql_python import get_settings
from mssql_python.parameter_helper import (
detect_and_convert_parameters,
parse_pyformat_params,
convert_pyformat_to_qmark,
)

if TYPE_CHECKING:
from mssql_python.connection import Connection
Expand Down Expand Up @@ -1233,6 +1238,53 @@ def execute( # pylint: disable=too-many-locals,too-many-branches,too-many-state
# Clear any previous messages
self.messages = []

# Auto-detect and convert parameter style if needed
# Supports both qmark (?) and pyformat (%(name)s)
# Note: parameters is always a tuple due to *parameters in method signature
#
# Parameter Passing Rules (handling ambiguity):
#
# 1. Single value:
# cursor.execute("SELECT ?", 42)
# → parameters = (42,)
# → Wrapped as single parameter
#
# 2. Multiple values (two equivalent ways):
# cursor.execute("SELECT ?, ?", 1, 2) # Varargs
# cursor.execute("SELECT ?, ?", (1, 2)) # Tuple
# → Both result in parameters = (1, 2) or ((1, 2),)
# → If single tuple/list/dict arg, it's unwrapped
#
# 3. Dict for named parameters:
# cursor.execute("SELECT %(id)s", {"id": 42})
# → parameters = ({"id": 42},)
# → Unwrapped to {"id": 42}, then converted to qmark style
#
# Important: If you pass a tuple/list/dict as the ONLY argument,
# it will be unwrapped for parameter binding. This means you cannot
# pass a tuple as a single parameter value (but SQL Server doesn't
# support tuple types as parameter values anyway).
if parameters:
# Check if single parameter is a nested container that should be unwrapped
# e.g., execute("SELECT ?", (value,)) vs execute("SELECT ?, ?", ((1, 2),))
if isinstance(parameters, tuple) and len(parameters) == 1:
# Could be either (value,) for single param or ((tuple),) for nested
# Check if it's a nested container
if isinstance(parameters[0], (tuple, list, dict)):
actual_params = parameters[0]
else:
actual_params = parameters
else:
actual_params = parameters

# Convert parameters based on detected style
operation, converted_params = detect_and_convert_parameters(operation, actual_params)

# Convert back to list format expected by the binding code
parameters = list(converted_params)
else:
parameters = []

# Getting encoding setting
encoding_settings = self._get_encoding_settings()

Expand All @@ -1241,12 +1293,6 @@ def execute( # pylint: disable=too-many-locals,too-many-branches,too-many-state
param_info = ddbc_bindings.ParamInfo
parameters_type = []

# Flatten parameters if a single tuple or list is passed
if len(parameters) == 1 and isinstance(parameters[0], (tuple, list)):
parameters = parameters[0]

parameters = list(parameters)

# Validate that inputsizes matches parameter count if both are present
if parameters and self._inputsizes:
if len(self._inputsizes) != len(parameters):
Expand Down Expand Up @@ -1933,6 +1979,40 @@ def executemany( # pylint: disable=too-many-locals,too-many-branches,too-many-s
self.rowcount = 0
return

# Auto-detect and convert parameter style for executemany
# Check first row to determine if we need to convert from pyformat to qmark
first_row = (
seq_of_parameters[0]
if hasattr(seq_of_parameters, "__getitem__")
else next(iter(seq_of_parameters))
)

if isinstance(first_row, dict):
# pyformat style - convert all rows
# Parse parameter names from SQL (determines order for all rows)
param_names = parse_pyformat_params(operation)

if param_names:
# Convert SQL to qmark style
operation, _ = convert_pyformat_to_qmark(operation, first_row)

# Convert all parameter dicts to tuples in the same order
converted_params = []
for param_dict in seq_of_parameters:
if not isinstance(param_dict, dict):
raise TypeError(
f"Mixed parameter types in executemany: first row is dict, "
f"but row has {type(param_dict).__name__}"
)
# Build tuple in the order determined by param_names
row_tuple = tuple(param_dict[name] for name in param_names)
converted_params.append(row_tuple)

seq_of_parameters = converted_params
logger.debug(
"executemany: Converted %d rows from pyformat to qmark", len(seq_of_parameters)
)

# Apply timeout if set (non-zero)
if self._timeout > 0:
try:
Expand Down
Loading
Loading