Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
e3e3083
test main.py
gargsaumya Oct 14, 2025
7a05ed6
better-8
gargsaumya Oct 14, 2025
1a68058
latest
gargsaumya Oct 14, 2025
0811744
latest 2
gargsaumya Oct 14, 2025
d9e2056
working
gargsaumya Oct 15, 2025
2eb1a51
cleanup
gargsaumya Oct 28, 2025
cbbe9d6
cleaning up
gargsaumya Oct 28, 2025
cb5ae54
cleanup
gargsaumya Oct 28, 2025
766ea85
cleanup
gargsaumya Oct 28, 2025
fa7d718
fixing test failure
gargsaumya Oct 30, 2025
909a653
fixed test crash
gargsaumya Oct 31, 2025
c2d5a28
FIX: Retain original timezone in Python datetime objects (#281)
gargsaumya Oct 13, 2025
b9fe50c
FIX: Access Token Issue (EntraID Auth) (#285)
bewithgaurav Oct 14, 2025
d9ec83d
RELEASE: 0.13.1 (#294)
bewithgaurav Oct 15, 2025
47ffca1
FEAT: Complex Datatype support-XML (#293)
gargsaumya Oct 16, 2025
bb5337c
FIX: Fix precision loss when binding large Decimal values to SQL_NUME…
gargsaumya Oct 16, 2025
20c2d84
FEAT: Adding set_attr in connection class (#177)
jahnvi480 Oct 17, 2025
59daf47
STYLE: Linting python files and making it fully typed (#298)
jahnvi480 Oct 27, 2025
08497cb
STYLE: Cpp linting (#300)
jahnvi480 Oct 28, 2025
47e1f8f
improved perf
gargsaumya Nov 3, 2025
0db3e27
fixed pipeline tests
gargsaumya Nov 3, 2025
e1a45c1
Merge branch 'main' into saumya/pref-setup
gargsaumya Nov 3, 2025
377db41
Remove redundant error assertions in connection tests
gargsaumya Nov 3, 2025
d956d85
Merge branch 'main' into saumya/pref-setup
gargsaumya Nov 4, 2025
5830dc2
Merge branch 'saumya/pref-setup' of https://github.com/microsoft/mssq…
gargsaumya Nov 4, 2025
a1b0603
fix
gargsaumya Nov 4, 2025
7c36cb3
FEAT: Refine Perf Benchmarking and add it to CI
bewithgaurav Nov 5, 2025
661395d
Re-add VLDB
bewithgaurav Nov 5, 2025
3a7eb75
load mssql-python from parent folder
bewithgaurav Nov 5, 2025
fbbf9d7
Merge branch 'bewithgaurav/perf-check-CI' into saumya/pref-setup
bewithgaurav Nov 5, 2025
29c8b9d
keep odbc driver from pyodbc
bewithgaurav Nov 5, 2025
f4cca91
Merge branch 'bewithgaurav/perf-check-CI' into saumya/pref-setup
bewithgaurav Nov 5, 2025
f1f22bd
installation uninstallation nuances
bewithgaurav Nov 5, 2025
6becc44
Merge branch 'bewithgaurav/perf-check-CI' into saumya/pref-setup
bewithgaurav Nov 5, 2025
baa6f40
fix odbc driver
bewithgaurav Nov 5, 2025
1992cb3
odbc installation in windows
bewithgaurav Nov 5, 2025
5308b1f
odbc installation in windows
bewithgaurav Nov 5, 2025
e000808
Merge branch 'bewithgaurav/perf-check-CI' into saumya/pref-setup
bewithgaurav Nov 5, 2025
dea7946
addressed review comments
gargsaumya Nov 5, 2025
01855f8
Merge branch 'saumya/pref-setup' of https://github.com/microsoft/mssq…
gargsaumya Nov 5, 2025
1b38ba4
Merge branch 'main' into saumya/pref-setup
gargsaumya Nov 5, 2025
0a995d4
addressed review comments
gargsaumya Nov 5, 2025
0e290a1
Merge branch 'saumya/pref-setup' of https://github.com/microsoft/mssq…
gargsaumya Nov 5, 2025
0e24554
debug hang in arm
gargsaumya Nov 5, 2025
723ab4b
debug hang in arm
gargsaumya Nov 5, 2025
e297104
Merge branch 'saumya/pref-setup' of https://github.com/microsoft/mssq…
gargsaumya Nov 5, 2025
4ae3861
skipping stress tests in pipeline as locally all stress tests pass
gargsaumya Nov 5, 2025
8fc8546
skipping stress tests in pipeline as locally all stress tests pass
gargsaumya Nov 5, 2025
4acc3d7
Merge branch 'saumya/pref-setup' of https://github.com/microsoft/mssq…
gargsaumya Nov 5, 2025
618c511
Merge branch 'main' into saumya/pref-setup
gargsaumya Nov 6, 2025
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
1 change: 1 addition & 0 deletions benchmarks/perf-benchmarking.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
if not CONN_STR:
print("Error: The environment variable DB_CONNECTION_STRING is not set. Please set it to a valid SQL Server connection string and try again.")
sys.exit(1)

# Ensure pyodbc connection string has ODBC driver specified
if CONN_STR and 'Driver=' not in CONN_STR:
CONN_STR = f"Driver={{ODBC Driver 18 for SQL Server}};{CONN_STR}"
Expand Down
17 changes: 16 additions & 1 deletion mssql_python/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@

# Exceptions
# https://www.python.org/dev/peps/pep-0249/#exceptions

# Import necessary modules
from .exceptions import (
Warning,
Error,
Expand Down Expand Up @@ -175,6 +177,19 @@ def pooling(max_size: int = 100, idle_timeout: int = 600, enabled: bool = True)

_original_module_setattr = sys.modules[__name__].__setattr__

def _custom_setattr(name, value):
if name == 'lowercase':
with _settings_lock:
_settings.lowercase = bool(value)
# Update the module's lowercase variable
_original_module_setattr(name, _settings.lowercase)
else:
_original_module_setattr(name, value)

# Replace the module's __setattr__ with our custom version
sys.modules[__name__].__setattr__ = _custom_setattr


# Export SQL constants at module level
SQL_VARCHAR: int = ConstantsDDBC.SQL_VARCHAR.value
SQL_LONGVARCHAR: int = ConstantsDDBC.SQL_LONGVARCHAR.value
Expand Down Expand Up @@ -281,4 +296,4 @@ def lowercase(self, value: bool) -> None:
sys.modules[__name__] = new_module

# Initialize property values
lowercase: bool = _settings.lowercase
lowercase: bool = _settings.lowercase
193 changes: 119 additions & 74 deletions mssql_python/cursor.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,18 +121,14 @@ def __init__(self, connection: "Connection", timeout: int = 0) -> None:
# Therefore, it must be a list with exactly one bool element.

# rownumber attribute
self._rownumber: int = (
-1
) # DB-API extension: last returned row index, -1 before first
self._next_row_index: int = (
0 # internal: index of the next row the driver will return (0-based)
)
self._has_result_set: bool = False # Track if we have an active result set
self._skip_increment_for_next_fetch: bool = (
False # Track if we need to skip incrementing the row index
)

self.messages: List[str] = [] # Store diagnostic messages
self._rownumber = -1 # DB-API extension: last returned row index, -1 before first

self._cached_column_map = None
self._cached_converter_map = None
self._next_row_index = 0 # internal: index of the next row the driver will return (0-based)
self._has_result_set = False # Track if we have an active result set
self._skip_increment_for_next_fetch = False # Track if we need to skip incrementing the row index
self.messages = [] # Store diagnostic messages

def _is_unicode_string(self, param: str) -> bool:
"""
Expand Down Expand Up @@ -823,7 +819,57 @@ def _initialize_description(self, column_metadata: Optional[Any] = None) -> None
)
self.description = description

def _map_data_type(self, sql_type: int) -> type:
def _build_converter_map(self):
"""
Build a pre-computed converter map for output converters.
Returns a list where each element is either a converter function or None.
This eliminates the need to look up converters for every row.
"""
if not self.description or not hasattr(self.connection, '_output_converters') or not self.connection._output_converters:
return None

converter_map = []

for desc in self.description:
if desc is None:
converter_map.append(None)
continue
sql_type = desc[1]
converter = self.connection.get_output_converter(sql_type)
# If no converter found for the SQL type, try the WVARCHAR converter as a fallback
if converter is None:
from mssql_python.constants import ConstantsDDBC
converter = self.connection.get_output_converter(ConstantsDDBC.SQL_WVARCHAR.value)

converter_map.append(converter)

return converter_map

def _get_column_and_converter_maps(self):
"""
Get column map and converter map for Row construction (thread-safe).
This centralizes the column map building logic to eliminate duplication
and ensure thread-safe lazy initialization.

Returns:
tuple: (column_map, converter_map)
"""
# Thread-safe lazy initialization of column map
column_map = self._cached_column_map
if column_map is None and self.description:
# Build column map locally first, then assign to cache
column_map = {col_desc[0]: i for i, col_desc in enumerate(self.description)}
self._cached_column_map = column_map

# Fallback to legacy column name map if no cached map
column_map = column_map or getattr(self, '_column_name_map', None)

# Get cached converter map
converter_map = getattr(self, '_cached_converter_map', None)

return column_map, converter_map

def _map_data_type(self, sql_type):
"""
Map SQL data type to Python data type.

Expand Down Expand Up @@ -1135,9 +1181,14 @@ def execute( # pylint: disable=too-many-locals,too-many-branches,too-many-state
if self.description: # If we have column descriptions, it's likely a SELECT
self.rowcount = -1
self._reset_rownumber()
# Pre-build column map and converter map
self._cached_column_map = {col_desc[0]: i for i, col_desc in enumerate(self.description)}
self._cached_converter_map = self._build_converter_map()
else:
self.rowcount = ddbc_bindings.DDBCSQLRowCount(self.hstmt)
self._clear_rownumber()
self._cached_column_map = None
self._cached_converter_map = None

# After successful execution, initialize description if there are results
column_metadata = []
Expand Down Expand Up @@ -1957,11 +2008,11 @@ def fetchone(self) -> Union[None, Row]:
self._increment_rownumber()

self.rowcount = self._next_row_index

# Create and return a Row object, passing column name map if available
column_map = getattr(self, "_column_name_map", None)
return Row(self, self.description, row_data, column_map)
except Exception as e: # pylint: disable=broad-exception-caught
# Get column and converter maps
column_map, converter_map = self._get_column_and_converter_maps()
return Row(row_data, column_map, cursor=self, converter_map=converter_map)
except Exception as e:
# On error, don't increment rownumber - rethrow the error
raise e

Expand Down Expand Up @@ -2004,14 +2055,13 @@ def fetchmany(self, size: Optional[int] = None) -> List[Row]:
self.rowcount = 0
else:
self.rowcount = self._next_row_index


# Get column and converter maps
column_map, converter_map = self._get_column_and_converter_maps()

# Convert raw data to Row objects
column_map = getattr(self, "_column_name_map", None)
return [
Row(self, self.description, row_data, column_map)
for row_data in rows_data
]
except Exception as e: # pylint: disable=broad-exception-caught
return [Row(row_data, column_map, cursor=self, converter_map=converter_map) for row_data in rows_data]
except Exception as e:
# On error, don't increment rownumber - rethrow the error
raise e

Expand Down Expand Up @@ -2044,14 +2094,13 @@ def fetchall(self) -> List[Row]:
self.rowcount = 0
else:
self.rowcount = self._next_row_index


# Get column and converter maps
column_map, converter_map = self._get_column_and_converter_maps()

# Convert raw data to Row objects
column_map = getattr(self, "_column_name_map", None)
return [
Row(self, self.description, row_data, column_map)
for row_data in rows_data
]
except Exception as e: # pylint: disable=broad-exception-caught
return [Row(row_data, column_map, cursor=self, converter_map=converter_map) for row_data in rows_data]
except Exception as e:
# On error, don't increment rownumber - rethrow the error
raise e

Expand All @@ -2070,16 +2119,35 @@ def nextset(self) -> Union[bool, None]:
# Clear messages per DBAPI
self.messages = []

# Clear cached column and converter maps for the new result set
self._cached_column_map = None
self._cached_converter_map = None

# Skip to the next result set
ret = ddbc_bindings.DDBCSQLMoreResults(self.hstmt)
check_error(ddbc_sql_const.SQL_HANDLE_STMT.value, self.hstmt, ret)

if ret == ddbc_sql_const.SQL_NO_DATA.value:
self._clear_rownumber()
self.description = None
return False

self._reset_rownumber()

# Initialize description for the new result set
column_metadata = []
try:
ddbc_bindings.DDBCSQLDescribeCol(self.hstmt, column_metadata)
self._initialize_description(column_metadata)

# Pre-build column map and converter map for the new result set
if self.description:
self._cached_column_map = {col_desc[0]: i for i, col_desc in enumerate(self.description)}
self._cached_converter_map = self._build_converter_map()
except Exception as e: # pylint: disable=broad-exception-caught
# If describe fails, there might be no results in this result set
self.description = None

return True

def __enter__(self):
Expand Down Expand Up @@ -2252,58 +2320,34 @@ def scroll(self, value: int, mode: str = "relative") -> None: # pylint: disable

row_data: list = []

# Absolute special cases
# Absolute positioning not supported with forward-only cursors
if mode == "absolute":
if value == -1:
# Before first
ddbc_bindings.DDBCSQLFetchScroll(
self.hstmt, ddbc_sql_const.SQL_FETCH_ABSOLUTE.value, 0, row_data
)
self._rownumber = -1
self._next_row_index = 0
return
if value == 0:
# Before first, but tests want rownumber==0 pre and post the next fetch
ddbc_bindings.DDBCSQLFetchScroll(
self.hstmt, ddbc_sql_const.SQL_FETCH_ABSOLUTE.value, 0, row_data
)
self._rownumber = 0
self._next_row_index = 0
self._skip_increment_for_next_fetch = True
return
raise NotSupportedError(
"Absolute positioning not supported",
"Forward-only cursors do not support absolute positioning"
)

try:
if mode == "relative":
if value == 0:
return
ret = ddbc_bindings.DDBCSQLFetchScroll(
self.hstmt, ddbc_sql_const.SQL_FETCH_RELATIVE.value, value, row_data
)
if ret == ddbc_sql_const.SQL_NO_DATA.value:
raise IndexError(
"Cannot scroll to specified position: end of result set reached"

# For forward-only cursors, use multiple SQL_FETCH_NEXT calls
# This matches pyodbc's approach for skip operations
for i in range(value):
ret = ddbc_bindings.DDBCSQLFetchScroll(
self.hstmt, ddbc_sql_const.SQL_FETCH_NEXT.value, 0, row_data
)
# Consume N rows; last-returned index advances by N
if ret == ddbc_sql_const.SQL_NO_DATA.value:
raise IndexError(
"Cannot scroll to specified position: end of result set reached"
)

# Update position tracking
self._rownumber = self._rownumber + value
self._next_row_index = self._rownumber + 1
return

# absolute(k>0): map Python k (0-based next row) to ODBC ABSOLUTE k (1-based),
# intentionally passing k so ODBC fetches row #k (1-based), i.e., 0-based (k-1),
# leaving the NEXT fetch to return 0-based index k.
ret = ddbc_bindings.DDBCSQLFetchScroll(
self.hstmt, ddbc_sql_const.SQL_FETCH_ABSOLUTE.value, value, row_data
)
if ret == ddbc_sql_const.SQL_NO_DATA.value:
raise IndexError(
f"Cannot scroll to position {value}: end of result set reached"
)

# Tests expect rownumber == value after absolute(value)
# Next fetch should return row index 'value'
self._rownumber = value
self._next_row_index = value

except Exception as e: # pylint: disable=broad-exception-caught
if isinstance(e, (IndexError, NotSupportedError)):
raise
Expand Down Expand Up @@ -2457,4 +2501,5 @@ def setoutputsize(self, size: int, column: Optional[int] = None) -> None:
This method is a no-op in this implementation as buffer sizes
are managed automatically by the underlying driver.
"""
# This is a no-op - buffer sizes are managed automatically
# This is a no-op - buffer sizes are managed automatically

2 changes: 1 addition & 1 deletion mssql_python/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -343,4 +343,4 @@ def __init__(self) -> None:
def get_settings() -> Settings:
"""Return the global settings object"""
with _settings_lock:
return _settings
return _settings
Loading
Loading