Skip to content

Commit 38e2382

Browse files
authored
FEAT: Param as Dict (#385)
### Work Item / Issue Reference <!-- IMPORTANT: Please follow the PR template guidelines below. For mssql-python maintainers: Insert your ADO Work Item ID below (e.g. AB#37452) For external contributors: Insert Github Issue number below (e.g. #149) Only one reference is required - either GitHub issue OR ADO Work Item. --> <!-- mssql-python maintainers: ADO Work Item --> > [AB#40995](https://sqlclientdrivers.visualstudio.com/c6d89619-62de-46a0-8b46-70b92a84d85e/_workitems/edit/40995) <!-- External contributors: GitHub Issue --> > GitHub Issue: #20 ------------------------------------------------------------------- ### Summary This pull request introduces support for both `qmark` (`?`) and `pyformat` (`%(name)s`) parameter styles in SQL queries, improving compatibility and usability for users of the `mssql_python` package. The default `paramstyle` is now set to `pyformat`, and both `execute` and `executemany` methods have been updated to automatically detect and convert parameter styles as needed. A new utility module, `parameter_helper.py`, has been added to handle parameter style parsing and conversion. **Parameter style support and conversion:** * Changed the global `paramstyle` in `mssql_python/__init__.py` from `"qmark"` to `"pyformat"`, making `pyformat` the default parameter style. * Added a new module `mssql_python/parameter_helper.py` containing helper functions to parse pyformat parameters, convert pyformat SQL to qmark style, and auto-detect/convert parameter styles in queries. **Enhancements to parameter handling in cursor methods:** * Updated the `execute` method in `mssql_python/cursor.py` to auto-detect and convert parameter styles, supporting both single values and various parameter formats, and to use the new helper functions for conversion. * Refactored parameter flattening logic in `execute` to rely on the new auto-detection and conversion, removing the old manual flattening. * Enhanced the `executemany` method in `mssql_python/cursor.py` to auto-detect parameter style, convert pyformat to qmark for all rows if needed, and wrap single parameters for backward compatibility. **Tests and validation:** * Updated the `test_paramstyle` test in `tests/test_001_globals.py` to expect the new default `paramstyle` value of `"pyformat"`.
1 parent c666f6c commit 38e2382

5 files changed

Lines changed: 2279 additions & 8 deletions

File tree

mssql_python/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ def _cleanup_connections():
116116
# GLOBALS
117117
# Read-Only
118118
apilevel: str = "2.0"
119-
paramstyle: str = "qmark"
119+
paramstyle: str = "pyformat"
120120
threadsafety: int = 1
121121

122122
# Set the initial decimal separator in C++

mssql_python/cursor.py

Lines changed: 86 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@
2929
)
3030
from mssql_python.row import Row
3131
from mssql_python import get_settings
32+
from mssql_python.parameter_helper import (
33+
detect_and_convert_parameters,
34+
parse_pyformat_params,
35+
convert_pyformat_to_qmark,
36+
)
3237

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

1241+
# Auto-detect and convert parameter style if needed
1242+
# Supports both qmark (?) and pyformat (%(name)s)
1243+
# Note: parameters is always a tuple due to *parameters in method signature
1244+
#
1245+
# Parameter Passing Rules (handling ambiguity):
1246+
#
1247+
# 1. Single value:
1248+
# cursor.execute("SELECT ?", 42)
1249+
# → parameters = (42,)
1250+
# → Wrapped as single parameter
1251+
#
1252+
# 2. Multiple values (two equivalent ways):
1253+
# cursor.execute("SELECT ?, ?", 1, 2) # Varargs
1254+
# cursor.execute("SELECT ?, ?", (1, 2)) # Tuple
1255+
# → Both result in parameters = (1, 2) or ((1, 2),)
1256+
# → If single tuple/list/dict arg, it's unwrapped
1257+
#
1258+
# 3. Dict for named parameters:
1259+
# cursor.execute("SELECT %(id)s", {"id": 42})
1260+
# → parameters = ({"id": 42},)
1261+
# → Unwrapped to {"id": 42}, then converted to qmark style
1262+
#
1263+
# Important: If you pass a tuple/list/dict as the ONLY argument,
1264+
# it will be unwrapped for parameter binding. This means you cannot
1265+
# pass a tuple as a single parameter value (but SQL Server doesn't
1266+
# support tuple types as parameter values anyway).
1267+
if parameters:
1268+
# Check if single parameter is a nested container that should be unwrapped
1269+
# e.g., execute("SELECT ?", (value,)) vs execute("SELECT ?, ?", ((1, 2),))
1270+
if isinstance(parameters, tuple) and len(parameters) == 1:
1271+
# Could be either (value,) for single param or ((tuple),) for nested
1272+
# Check if it's a nested container
1273+
if isinstance(parameters[0], (tuple, list, dict)):
1274+
actual_params = parameters[0]
1275+
else:
1276+
actual_params = parameters
1277+
else:
1278+
actual_params = parameters
1279+
1280+
# Convert parameters based on detected style
1281+
operation, converted_params = detect_and_convert_parameters(operation, actual_params)
1282+
1283+
# Convert back to list format expected by the binding code
1284+
parameters = list(converted_params)
1285+
else:
1286+
parameters = []
1287+
12361288
# Getting encoding setting
12371289
encoding_settings = self._get_encoding_settings()
12381290

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

1244-
# Flatten parameters if a single tuple or list is passed
1245-
if len(parameters) == 1 and isinstance(parameters[0], (tuple, list)):
1246-
parameters = parameters[0]
1247-
1248-
parameters = list(parameters)
1249-
12501296
# Validate that inputsizes matches parameter count if both are present
12511297
if parameters and self._inputsizes:
12521298
if len(self._inputsizes) != len(parameters):
@@ -1933,6 +1979,40 @@ def executemany( # pylint: disable=too-many-locals,too-many-branches,too-many-s
19331979
self.rowcount = 0
19341980
return
19351981

1982+
# Auto-detect and convert parameter style for executemany
1983+
# Check first row to determine if we need to convert from pyformat to qmark
1984+
first_row = (
1985+
seq_of_parameters[0]
1986+
if hasattr(seq_of_parameters, "__getitem__")
1987+
else next(iter(seq_of_parameters))
1988+
)
1989+
1990+
if isinstance(first_row, dict):
1991+
# pyformat style - convert all rows
1992+
# Parse parameter names from SQL (determines order for all rows)
1993+
param_names = parse_pyformat_params(operation)
1994+
1995+
if param_names:
1996+
# Convert SQL to qmark style
1997+
operation, _ = convert_pyformat_to_qmark(operation, first_row)
1998+
1999+
# Convert all parameter dicts to tuples in the same order
2000+
converted_params = []
2001+
for param_dict in seq_of_parameters:
2002+
if not isinstance(param_dict, dict):
2003+
raise TypeError(
2004+
f"Mixed parameter types in executemany: first row is dict, "
2005+
f"but row has {type(param_dict).__name__}"
2006+
)
2007+
# Build tuple in the order determined by param_names
2008+
row_tuple = tuple(param_dict[name] for name in param_names)
2009+
converted_params.append(row_tuple)
2010+
2011+
seq_of_parameters = converted_params
2012+
logger.debug(
2013+
"executemany: Converted %d rows from pyformat to qmark", len(seq_of_parameters)
2014+
)
2015+
19362016
# Apply timeout if set (non-zero)
19372017
if self._timeout > 0:
19382018
try:

0 commit comments

Comments
 (0)