-
Notifications
You must be signed in to change notification settings - Fork 46
Expand file tree
/
Copy path__init__.py
More file actions
378 lines (301 loc) · 12.5 KB
/
__init__.py
File metadata and controls
378 lines (301 loc) · 12.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
"""
Copyright (c) Microsoft Corporation.
Licensed under the MIT license.
This module initializes the mssql_python package.
"""
import atexit
import sys
import threading
import types
import weakref
from typing import Dict
# Import settings from helpers to avoid circular imports
from .helpers import Settings, get_settings, _settings, _settings_lock
# Driver version
__version__ = "1.4.0"
# Exceptions
# https://www.python.org/dev/peps/pep-0249/#exceptions
# Import necessary modules
from .exceptions import (
Warning,
Error,
InterfaceError,
DatabaseError,
DataError,
OperationalError,
IntegrityError,
InternalError,
ProgrammingError,
NotSupportedError,
ConnectionStringParseError,
)
# Type Objects
from .type import (
Date,
Time,
Timestamp,
DateFromTicks,
TimeFromTicks,
TimestampFromTicks,
Binary,
STRING,
BINARY,
NUMBER,
DATETIME,
ROWID,
)
# Connection Objects
from .db_connection import connect, Connection
# Connection String Handling
from .connection_string_parser import _ConnectionStringParser
from .connection_string_builder import _ConnectionStringBuilder
# Cursor Objects
from .cursor import Cursor
# Logging Configuration (Simplified single-level DEBUG system)
from .logging import logger, setup_logging, driver_logger
# Constants
from .constants import ConstantsDDBC, GetInfoConstants
# Pooling
from .pooling import PoolingManager
# Global registry for tracking active connections (using weak references)
_active_connections = weakref.WeakSet()
_connections_lock = threading.Lock()
def _register_connection(conn):
"""Register a connection for cleanup before shutdown."""
with _connections_lock:
_active_connections.add(conn)
def _cleanup_connections():
"""
Cleanup function called by atexit to close all active connections.
This prevents resource leaks during interpreter shutdown by ensuring
all ODBC handles are freed in the correct order before Python finalizes.
"""
# Make a copy of the connections to avoid modification during iteration
with _connections_lock:
connections_to_close = list(_active_connections)
for conn in connections_to_close:
try:
# Check if connection is still valid and not closed
if hasattr(conn, "_closed") and not conn._closed:
# Close will handle both cursors and the connection
conn.close()
except Exception as e:
# Log errors during shutdown cleanup for debugging
# We're prioritizing crash prevention over error propagation
try:
driver_logger.error(
f"Error during connection cleanup at shutdown: {type(e).__name__}: {e}"
)
except Exception:
# If logging fails during shutdown, silently ignore
pass
# Register cleanup function to run before Python exits
atexit.register(_cleanup_connections)
# GLOBALS
# Read-Only
apilevel: str = "2.0"
paramstyle: str = "pyformat"
threadsafety: int = 1
# Set the initial decimal separator in C++
try:
from .ddbc_bindings import DDBCSetDecimalSeparator
DDBCSetDecimalSeparator(_settings.decimal_separator)
except ImportError:
# Handle case where ddbc_bindings is not available
DDBCSetDecimalSeparator = None
# New functions for decimal separator control
def setDecimalSeparator(separator: str) -> None:
"""
Sets the decimal separator character used when parsing NUMERIC/DECIMAL values
from the database, e.g. the "." in "1,234.56".
The default is to use the current locale's "decimal_point" value when the module
was first imported, or "." if the locale is not available. This function overrides
the default.
Args:
separator (str): The character to use as decimal separator
Raises:
ValueError: If the separator is not a single character string
"""
# Type validation
if not isinstance(separator, str):
raise ValueError("Decimal separator must be a string")
# Length validation
if len(separator) == 0:
raise ValueError("Decimal separator cannot be empty")
if len(separator) > 1:
raise ValueError("Decimal separator must be a single character")
# Character validation
if separator.isspace():
raise ValueError("Whitespace characters are not allowed as decimal separators")
# Check for specific disallowed characters
if separator in ["\t", "\n", "\r", "\v", "\f"]:
raise ValueError(
f"Control character '{repr(separator)}' is not allowed as a decimal separator"
)
# Set in Python side settings
_settings.decimal_separator = separator
# Update the C++ side
if DDBCSetDecimalSeparator is not None:
DDBCSetDecimalSeparator(separator)
def getDecimalSeparator() -> str:
"""
Returns the decimal separator character used when parsing NUMERIC/DECIMAL values
from the database.
Returns:
str: The current decimal separator character
"""
return _settings.decimal_separator
# Export specific constants for setencoding()
SQL_CHAR: int = ConstantsDDBC.SQL_CHAR.value
SQL_WCHAR: int = ConstantsDDBC.SQL_WCHAR.value
SQL_WMETADATA: int = -99
# Export connection attribute constants for set_attr()
# Only include driver-level attributes that the SQL Server ODBC driver can handle directly
# Core driver-level attributes
SQL_ATTR_ACCESS_MODE: int = ConstantsDDBC.SQL_ATTR_ACCESS_MODE.value
SQL_ATTR_CONNECTION_TIMEOUT: int = ConstantsDDBC.SQL_ATTR_CONNECTION_TIMEOUT.value
SQL_ATTR_CURRENT_CATALOG: int = ConstantsDDBC.SQL_ATTR_CURRENT_CATALOG.value
SQL_ATTR_LOGIN_TIMEOUT: int = ConstantsDDBC.SQL_ATTR_LOGIN_TIMEOUT.value
SQL_ATTR_PACKET_SIZE: int = ConstantsDDBC.SQL_ATTR_PACKET_SIZE.value
SQL_ATTR_TXN_ISOLATION: int = ConstantsDDBC.SQL_ATTR_TXN_ISOLATION.value
# Transaction Isolation Level Constants
SQL_TXN_READ_UNCOMMITTED: int = ConstantsDDBC.SQL_TXN_READ_UNCOMMITTED.value
SQL_TXN_READ_COMMITTED: int = ConstantsDDBC.SQL_TXN_READ_COMMITTED.value
SQL_TXN_REPEATABLE_READ: int = ConstantsDDBC.SQL_TXN_REPEATABLE_READ.value
SQL_TXN_SERIALIZABLE: int = ConstantsDDBC.SQL_TXN_SERIALIZABLE.value
# Access Mode Constants
SQL_MODE_READ_WRITE: int = ConstantsDDBC.SQL_MODE_READ_WRITE.value
SQL_MODE_READ_ONLY: int = ConstantsDDBC.SQL_MODE_READ_ONLY.value
def pooling(max_size: int = 100, idle_timeout: int = 600, enabled: bool = True) -> None:
"""
Enable connection pooling with the specified parameters.
By default:
- If not explicitly called, pooling will be auto-enabled with default values.
Args:
max_size (int): Maximum number of connections in the pool.
idle_timeout (int): Time in seconds before idle connections are closed.
enabled (bool): Whether to enable or disable pooling.
Returns:
None
"""
if not enabled:
PoolingManager.disable()
else:
PoolingManager.enable(max_size, idle_timeout)
_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
SQL_WVARCHAR: int = ConstantsDDBC.SQL_WVARCHAR.value
SQL_WLONGVARCHAR: int = ConstantsDDBC.SQL_WLONGVARCHAR.value
SQL_DECIMAL: int = ConstantsDDBC.SQL_DECIMAL.value
SQL_NUMERIC: int = ConstantsDDBC.SQL_NUMERIC.value
SQL_BIT: int = ConstantsDDBC.SQL_BIT.value
SQL_TINYINT: int = ConstantsDDBC.SQL_TINYINT.value
SQL_SMALLINT: int = ConstantsDDBC.SQL_SMALLINT.value
SQL_INTEGER: int = ConstantsDDBC.SQL_INTEGER.value
SQL_BIGINT: int = ConstantsDDBC.SQL_BIGINT.value
SQL_REAL: int = ConstantsDDBC.SQL_REAL.value
SQL_FLOAT: int = ConstantsDDBC.SQL_FLOAT.value
SQL_DOUBLE: int = ConstantsDDBC.SQL_DOUBLE.value
SQL_BINARY: int = ConstantsDDBC.SQL_BINARY.value
SQL_VARBINARY: int = ConstantsDDBC.SQL_VARBINARY.value
SQL_LONGVARBINARY: int = ConstantsDDBC.SQL_LONGVARBINARY.value
SQL_DATE: int = ConstantsDDBC.SQL_DATE.value
SQL_TIME: int = ConstantsDDBC.SQL_TIME.value
SQL_TIMESTAMP: int = ConstantsDDBC.SQL_TIMESTAMP.value
# Export GetInfo constants at module level
# Driver and database information
SQL_DRIVER_NAME: int = GetInfoConstants.SQL_DRIVER_NAME.value
SQL_DRIVER_VER: int = GetInfoConstants.SQL_DRIVER_VER.value
SQL_DRIVER_ODBC_VER: int = GetInfoConstants.SQL_DRIVER_ODBC_VER.value
SQL_DATA_SOURCE_NAME: int = GetInfoConstants.SQL_DATA_SOURCE_NAME.value
SQL_DATABASE_NAME: int = GetInfoConstants.SQL_DATABASE_NAME.value
SQL_SERVER_NAME: int = GetInfoConstants.SQL_SERVER_NAME.value
SQL_USER_NAME: int = GetInfoConstants.SQL_USER_NAME.value
# SQL conformance and support
SQL_SQL_CONFORMANCE: int = GetInfoConstants.SQL_SQL_CONFORMANCE.value
SQL_KEYWORDS: int = GetInfoConstants.SQL_KEYWORDS.value
SQL_IDENTIFIER_QUOTE_CHAR: int = GetInfoConstants.SQL_IDENTIFIER_QUOTE_CHAR.value
SQL_SEARCH_PATTERN_ESCAPE: int = GetInfoConstants.SQL_SEARCH_PATTERN_ESCAPE.value
# Catalog and schema support
SQL_CATALOG_TERM: int = GetInfoConstants.SQL_CATALOG_TERM.value
SQL_SCHEMA_TERM: int = GetInfoConstants.SQL_SCHEMA_TERM.value
SQL_TABLE_TERM: int = GetInfoConstants.SQL_TABLE_TERM.value
SQL_PROCEDURE_TERM: int = GetInfoConstants.SQL_PROCEDURE_TERM.value
# Transaction support
SQL_TXN_CAPABLE: int = GetInfoConstants.SQL_TXN_CAPABLE.value
SQL_DEFAULT_TXN_ISOLATION: int = GetInfoConstants.SQL_DEFAULT_TXN_ISOLATION.value
# Data type support
SQL_NUMERIC_FUNCTIONS: int = GetInfoConstants.SQL_NUMERIC_FUNCTIONS.value
SQL_STRING_FUNCTIONS: int = GetInfoConstants.SQL_STRING_FUNCTIONS.value
SQL_DATETIME_FUNCTIONS: int = GetInfoConstants.SQL_DATETIME_FUNCTIONS.value
# Limits
SQL_MAX_COLUMN_NAME_LEN: int = GetInfoConstants.SQL_MAX_COLUMN_NAME_LEN.value
SQL_MAX_TABLE_NAME_LEN: int = GetInfoConstants.SQL_MAX_TABLE_NAME_LEN.value
SQL_MAX_SCHEMA_NAME_LEN: int = GetInfoConstants.SQL_MAX_SCHEMA_NAME_LEN.value
SQL_MAX_CATALOG_NAME_LEN: int = GetInfoConstants.SQL_MAX_CATALOG_NAME_LEN.value
SQL_MAX_IDENTIFIER_LEN: int = GetInfoConstants.SQL_MAX_IDENTIFIER_LEN.value
# Also provide a function to get all constants
def get_info_constants() -> Dict[str, int]:
"""
Returns a dictionary of all available GetInfo constants.
This provides all SQLGetInfo constants that can be used with the Connection.getinfo() method
to retrieve metadata about the database server and driver.
Returns:
dict: Dictionary mapping constant names to their integer values
"""
return {name: member.value for name, member in GetInfoConstants.__members__.items()}
# Create a custom module class that uses properties instead of __setattr__
class _MSSQLModule(types.ModuleType):
@property
def lowercase(self) -> bool:
"""Get the lowercase setting."""
return _settings.lowercase
@lowercase.setter
def lowercase(self, value: bool) -> None:
"""Set the lowercase setting."""
if not isinstance(value, bool):
raise ValueError("lowercase must be a boolean value")
with _settings_lock:
_settings.lowercase = value
@property
def native_uuid(self) -> bool:
"""Get the native_uuid setting.
Controls whether UNIQUEIDENTIFIER columns return uuid.UUID objects (True)
or str (False). Default is True.
Set to False to return str for pyodbc-compatible migration.
"""
return _settings.native_uuid
@native_uuid.setter
def native_uuid(self, value: bool) -> None:
"""Set the native_uuid setting."""
if not isinstance(value, bool):
raise ValueError("native_uuid must be a boolean value")
with _settings_lock:
_settings.native_uuid = value
# Replace the current module with our custom module class
old_module: types.ModuleType = sys.modules[__name__]
new_module: _MSSQLModule = _MSSQLModule(__name__)
# Copy all existing attributes to the new module
for attr_name in dir(old_module):
if attr_name != "__class__":
try:
setattr(new_module, attr_name, getattr(old_module, attr_name))
except AttributeError:
pass
# Replace the module in sys.modules
sys.modules[__name__] = new_module
# Initialize property values
lowercase: bool = _settings.lowercase
native_uuid: bool = _settings.native_uuid