Skip to content

Commit 1d43c74

Browse files
committed
Revert "FEAT: Support for Native_UUID Attribute (#282)"
This reverts commit 10a8815.
1 parent 7938b26 commit 1d43c74

File tree

5 files changed

+192
-488
lines changed

5 files changed

+192
-488
lines changed

mssql_python/__init__.py

Lines changed: 109 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,104 @@
33
Licensed under the MIT license.
44
This module initializes the mssql_python package.
55
"""
6-
7-
import sys
8-
import types
9-
from typing import Dict
10-
11-
# Import settings from helpers to avoid circular imports
12-
from .helpers import Settings, get_settings, _settings, _settings_lock
6+
import threading
7+
import locale
138

149
# Exceptions
1510
# https://www.python.org/dev/peps/pep-0249/#exceptions
11+
12+
# GLOBALS
13+
# Read-Only
14+
apilevel = "2.0"
15+
paramstyle = "qmark"
16+
threadsafety = 1
17+
18+
# Initialize the locale setting only once at module import time
19+
# This avoids thread-safety issues with locale
20+
_DEFAULT_DECIMAL_SEPARATOR = "."
21+
try:
22+
# Get the locale setting once during module initialization
23+
_locale_separator = locale.localeconv()['decimal_point']
24+
if _locale_separator and len(_locale_separator) == 1:
25+
_DEFAULT_DECIMAL_SEPARATOR = _locale_separator
26+
except (AttributeError, KeyError, TypeError, ValueError):
27+
pass # Keep the default "." if locale access fails
28+
29+
class Settings:
30+
def __init__(self):
31+
self.lowercase = False
32+
# Use the pre-determined separator - no locale access here
33+
self.decimal_separator = _DEFAULT_DECIMAL_SEPARATOR
34+
35+
# Global settings instance
36+
_settings = Settings()
37+
_settings_lock = threading.Lock()
38+
39+
def get_settings():
40+
"""Return the global settings object"""
41+
with _settings_lock:
42+
_settings.lowercase = lowercase
43+
return _settings
44+
45+
lowercase = _settings.lowercase # Default is False
46+
47+
# Set the initial decimal separator in C++
48+
from .ddbc_bindings import DDBCSetDecimalSeparator
49+
DDBCSetDecimalSeparator(_settings.decimal_separator)
50+
51+
# New functions for decimal separator control
52+
def setDecimalSeparator(separator):
53+
"""
54+
Sets the decimal separator character used when parsing NUMERIC/DECIMAL values
55+
from the database, e.g. the "." in "1,234.56".
56+
57+
The default is to use the current locale's "decimal_point" value when the module
58+
was first imported, or "." if the locale is not available. This function overrides
59+
the default.
60+
61+
Args:
62+
separator (str): The character to use as decimal separator
63+
64+
Raises:
65+
ValueError: If the separator is not a single character string
66+
"""
67+
# Type validation
68+
if not isinstance(separator, str):
69+
raise ValueError("Decimal separator must be a string")
70+
71+
# Length validation
72+
if len(separator) == 0:
73+
raise ValueError("Decimal separator cannot be empty")
74+
75+
if len(separator) > 1:
76+
raise ValueError("Decimal separator must be a single character")
77+
78+
# Character validation
79+
if separator.isspace():
80+
raise ValueError("Whitespace characters are not allowed as decimal separators")
81+
82+
# Check for specific disallowed characters
83+
if separator in ['\t', '\n', '\r', '\v', '\f']:
84+
raise ValueError(f"Control character '{repr(separator)}' is not allowed as a decimal separator")
85+
86+
# Set in Python side settings
87+
_settings.decimal_separator = separator
88+
89+
# Update the C++ side
90+
from .ddbc_bindings import DDBCSetDecimalSeparator
91+
DDBCSetDecimalSeparator(separator)
92+
93+
def getDecimalSeparator():
94+
"""
95+
Returns the decimal separator character used when parsing NUMERIC/DECIMAL values
96+
from the database.
97+
98+
Returns:
99+
str: The current decimal separator character
100+
"""
101+
return _settings.decimal_separator
102+
103+
# Import necessary modules
16104
from .exceptions import (
17105
Warning,
18106
Error,
@@ -174,9 +262,22 @@ def pooling(max_size: int = 100, idle_timeout: int = 600, enabled: bool = True)
174262
else:
175263
PoolingManager.enable(max_size, idle_timeout)
176264

177-
265+
import sys
178266
_original_module_setattr = sys.modules[__name__].__setattr__
179267

268+
def _custom_setattr(name, value):
269+
if name == 'lowercase':
270+
with _settings_lock:
271+
_settings.lowercase = bool(value)
272+
# Update the module's lowercase variable
273+
_original_module_setattr(name, _settings.lowercase)
274+
else:
275+
_original_module_setattr(name, value)
276+
277+
# Replace the module's __setattr__ with our custom version
278+
sys.modules[__name__].__setattr__ = _custom_setattr
279+
280+
180281
# Export SQL constants at module level
181282
SQL_VARCHAR: int = ConstantsDDBC.SQL_VARCHAR.value
182283
SQL_LONGVARCHAR: int = ConstantsDDBC.SQL_LONGVARCHAR.value
@@ -250,52 +351,3 @@ def get_info_constants() -> Dict[str, int]:
250351
dict: Dictionary mapping constant names to their integer values
251352
"""
252353
return {name: member.value for name, member in GetInfoConstants.__members__.items()}
253-
254-
255-
# Create a custom module class that uses properties instead of __setattr__
256-
class _MSSQLModule(types.ModuleType):
257-
@property
258-
def native_uuid(self) -> bool:
259-
"""Get the native UUID setting."""
260-
return _settings.native_uuid
261-
262-
@native_uuid.setter
263-
def native_uuid(self, value: bool) -> None:
264-
"""Set the native UUID setting."""
265-
if not isinstance(value, bool):
266-
raise ValueError("native_uuid must be a boolean value")
267-
with _settings_lock:
268-
_settings.native_uuid = value
269-
270-
@property
271-
def lowercase(self) -> bool:
272-
"""Get the lowercase setting."""
273-
return _settings.lowercase
274-
275-
@lowercase.setter
276-
def lowercase(self, value: bool) -> None:
277-
"""Set the lowercase setting."""
278-
if not isinstance(value, bool):
279-
raise ValueError("lowercase must be a boolean value")
280-
with _settings_lock:
281-
_settings.lowercase = value
282-
283-
284-
# Replace the current module with our custom module class
285-
old_module: types.ModuleType = sys.modules[__name__]
286-
new_module: _MSSQLModule = _MSSQLModule(__name__)
287-
288-
# Copy all existing attributes to the new module
289-
for attr_name in dir(old_module):
290-
if attr_name != "__class__":
291-
try:
292-
setattr(new_module, attr_name, getattr(old_module, attr_name))
293-
except AttributeError:
294-
pass
295-
296-
# Replace the module in sys.modules
297-
sys.modules[__name__] = new_module
298-
299-
# Initialize property values
300-
lowercase: bool = _settings.lowercase
301-
native_uuid: bool = _settings.native_uuid

mssql_python/cursor.py

Lines changed: 15 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1125,40 +1125,29 @@ def execute( # pylint: disable=too-many-locals,too-many-branches,too-many-state
11251125
# After successful execution, initialize description if there are results
11261126
column_metadata = []
11271127
try:
1128-
# ODBC specification guarantees that column metadata is available immediately after
1129-
# a successful SQLExecute/SQLExecDirect for the first result set
11301128
ddbc_bindings.DDBCSQLDescribeCol(self.hstmt, column_metadata)
11311129
self._initialize_description(column_metadata)
11321130
except Exception as e: # pylint: disable=broad-exception-caught
11331131
# If describe fails, it's likely there are no results (e.g., for INSERT)
11341132
self.description = None
1135-
1133+
11361134
# Reset rownumber for new result set (only for SELECT statements)
11371135
if self.description: # If we have column descriptions, it's likely a SELECT
1138-
# Capture settings snapshot for this result set
1139-
settings = get_settings()
1140-
self._settings_snapshot = { # pylint: disable=attribute-defined-outside-init
1141-
"lowercase": settings.lowercase,
1142-
"native_uuid": settings.native_uuid,
1143-
}
1144-
# Identify UUID columns based on Python type in description[1]
1145-
# This relies on _map_data_type correctly mapping SQL_GUID to uuid.UUID
1146-
self._uuid_indices = [] # pylint: disable=attribute-defined-outside-init
1147-
for i, desc in enumerate(self.description):
1148-
if desc and desc[1] == uuid.UUID: # Column type code at index 1
1149-
self._uuid_indices.append(i)
1150-
# Verify we have complete description tuples (7 items per PEP-249)
1151-
elif desc and len(desc) != 7:
1152-
log(
1153-
"warning",
1154-
f"Column description at index {i} has incorrect tuple length: {len(desc)}",
1155-
)
11561136
self.rowcount = -1
11571137
self._reset_rownumber()
11581138
else:
11591139
self.rowcount = ddbc_bindings.DDBCSQLRowCount(self.hstmt)
11601140
self._clear_rownumber()
11611141

1142+
# After successful execution, initialize description if there are results
1143+
column_metadata = []
1144+
try:
1145+
ddbc_bindings.DDBCSQLDescribeCol(self.hstmt, column_metadata)
1146+
self._initialize_description(column_metadata)
1147+
except Exception as e:
1148+
# If describe fails, it's likely there are no results (e.g., for INSERT)
1149+
self.description = None
1150+
11621151
self._reset_inputsizes() # Reset input sizes after execution
11631152
# Return self for method chaining
11641153
return self
@@ -1970,10 +1959,9 @@ def fetchone(self) -> Union[None, Row]:
19701959
self.rowcount = self._next_row_index
19711960

19721961
# Create and return a Row object, passing column name map if available
1973-
column_map = getattr(self, "_column_name_map", None)
1974-
settings_snapshot = getattr(self, "_settings_snapshot", None)
1975-
return Row(self, self.description, row_data, column_map, settings_snapshot)
1976-
except Exception as e: # pylint: disable=broad-exception-caught
1962+
column_map = getattr(self, '_column_name_map', None)
1963+
return Row(self, self.description, row_data, column_map)
1964+
except Exception as e:
19771965
# On error, don't increment rownumber - rethrow the error
19781966
raise e
19791967

@@ -2019,9 +2007,8 @@ def fetchmany(self, size: Optional[int] = None) -> List[Row]:
20192007

20202008
# Convert raw data to Row objects
20212009
column_map = getattr(self, "_column_name_map", None)
2022-
settings_snapshot = getattr(self, "_settings_snapshot", None)
20232010
return [
2024-
Row(self, self.description, row_data, column_map, settings_snapshot)
2011+
Row(self, self.description, row_data, column_map)
20252012
for row_data in rows_data
20262013
]
20272014
except Exception as e: # pylint: disable=broad-exception-caught
@@ -2060,9 +2047,8 @@ def fetchall(self) -> List[Row]:
20602047

20612048
# Convert raw data to Row objects
20622049
column_map = getattr(self, "_column_name_map", None)
2063-
settings_snapshot = getattr(self, "_settings_snapshot", None)
20642050
return [
2065-
Row(self, self.description, row_data, column_map, settings_snapshot)
2051+
Row(self, self.description, row_data, column_map)
20662052
for row_data in rows_data
20672053
]
20682054
except Exception as e: # pylint: disable=broad-exception-caught

0 commit comments

Comments
 (0)