Skip to content

Commit 749a958

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

File tree

5 files changed

+192
-487
lines changed

5 files changed

+192
-487
lines changed

mssql_python/__init__.py

Lines changed: 109 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,105 @@
33
Licensed under the MIT license.
44
This module initializes the mssql_python package.
55
"""
6-
7-
import sys
8-
import types
6+
import threading
7+
import locale
98
from typing import Dict
109

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

177-
266+
import sys
178267
_original_module_setattr = sys.modules[__name__].__setattr__
179268

269+
def _custom_setattr(name, value):
270+
if name == 'lowercase':
271+
with _settings_lock:
272+
_settings.lowercase = bool(value)
273+
# Update the module's lowercase variable
274+
_original_module_setattr(name, _settings.lowercase)
275+
else:
276+
_original_module_setattr(name, value)
277+
278+
# Replace the module's __setattr__ with our custom version
279+
sys.modules[__name__].__setattr__ = _custom_setattr
280+
281+
180282
# Export SQL constants at module level
181283
SQL_VARCHAR: int = ConstantsDDBC.SQL_VARCHAR.value
182284
SQL_LONGVARCHAR: int = ConstantsDDBC.SQL_LONGVARCHAR.value
@@ -250,52 +352,3 @@ def get_info_constants() -> Dict[str, int]:
250352
dict: Dictionary mapping constant names to their integer values
251353
"""
252354
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)