mssql-python 0.1.3__cp313-cp313-win_amd64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of mssql-python might be problematic. Click here for more details.
- mssql_python/__init__.py +31 -0
- mssql_python/connection.py +302 -0
- mssql_python/constants.py +108 -0
- mssql_python/cursor.py +688 -0
- mssql_python/db_connection.py +37 -0
- mssql_python/ddbc_bindings.pyd +0 -0
- mssql_python/exceptions.py +254 -0
- mssql_python/helpers.py +99 -0
- mssql_python/libs/win/1033/msodbcsqlr18.rll +0 -0
- mssql_python/libs/win/msodbcdiag18.dll +0 -0
- mssql_python/libs/win/msodbcsql18.dll +0 -0
- mssql_python/logging_config.py +56 -0
- mssql_python/mssql_python.pyi +177 -0
- mssql_python/msvcp140.dll +0 -0
- mssql_python/testing_ddbc_bindings.py +321 -0
- mssql_python/type.py +81 -0
- mssql_python-0.1.3.dist-info/METADATA +24 -0
- mssql_python-0.1.3.dist-info/RECORD +20 -0
- mssql_python-0.1.3.dist-info/WHEEL +5 -0
- mssql_python-0.1.3.dist-info/top_level.txt +1 -0
mssql_python/cursor.py
ADDED
|
@@ -0,0 +1,688 @@
|
|
|
1
|
+
import ctypes
|
|
2
|
+
import decimal, uuid
|
|
3
|
+
from typing import List, Union
|
|
4
|
+
from mssql_python.constants import ConstantsODBC as odbc_sql_const
|
|
5
|
+
from mssql_python.helpers import check_error
|
|
6
|
+
from mssql_python.logging_config import get_logger, ENABLE_LOGGING
|
|
7
|
+
import datetime
|
|
8
|
+
import decimal
|
|
9
|
+
import uuid
|
|
10
|
+
import os
|
|
11
|
+
from mssql_python.exceptions import raise_exception
|
|
12
|
+
from mssql_python import ddbc_bindings
|
|
13
|
+
|
|
14
|
+
logger = get_logger()
|
|
15
|
+
|
|
16
|
+
class Cursor:
|
|
17
|
+
"""
|
|
18
|
+
Represents a database cursor, which is used to manage the context of a fetch operation.
|
|
19
|
+
|
|
20
|
+
Attributes:
|
|
21
|
+
connection: Database connection object.
|
|
22
|
+
description: Sequence of 7-item sequences describing one result column.
|
|
23
|
+
rowcount: Number of rows produced or affected by the last execute operation.
|
|
24
|
+
arraysize: Number of rows to fetch at a time with fetchmany().
|
|
25
|
+
|
|
26
|
+
Methods:
|
|
27
|
+
__init__(connection_str) -> None.
|
|
28
|
+
callproc(procname, parameters=None) -> Modified copy of the input sequence with output parameters.
|
|
29
|
+
close() -> None.
|
|
30
|
+
execute(operation, parameters=None) -> None.
|
|
31
|
+
executemany(operation, seq_of_parameters) -> None.
|
|
32
|
+
fetchone() -> Single sequence or None if no more data is available.
|
|
33
|
+
fetchmany(size=None) -> Sequence of sequences (e.g. list of tuples).
|
|
34
|
+
fetchall() -> Sequence of sequences (e.g. list of tuples).
|
|
35
|
+
nextset() -> True if there is another result set, None otherwise.
|
|
36
|
+
setinputsizes(sizes) -> None.
|
|
37
|
+
setoutputsize(size, column=None) -> None.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(self, connection) -> None:
|
|
41
|
+
"""
|
|
42
|
+
Initialize the cursor with a database connection.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
connection: Database connection object.
|
|
46
|
+
"""
|
|
47
|
+
self.connection = connection
|
|
48
|
+
# self.connection.autocommit = False
|
|
49
|
+
self.hstmt = ctypes.c_void_p()
|
|
50
|
+
self._initialize_cursor()
|
|
51
|
+
self.description = None
|
|
52
|
+
self.rowcount = -1
|
|
53
|
+
self.arraysize = 1 # Default number of rows to fetch at a time is 1, user can change it
|
|
54
|
+
self.buffer_length = 1024 # Default buffer length for string data
|
|
55
|
+
self.closed = False # Flag to indicate if the cursor is closed
|
|
56
|
+
self.last_executed_stmt = "" # Stores the last statement executed by this cursor
|
|
57
|
+
self.is_stmt_prepared = [False] # Indicates if last_executed_stmt was prepared by ddbc shim.
|
|
58
|
+
# Is a list instead of a bool coz bools in Python are immutable.
|
|
59
|
+
# Hence, we can't pass around bools by reference & modify them.
|
|
60
|
+
# Therefore, it must be a list with exactly one bool element.
|
|
61
|
+
|
|
62
|
+
def _is_unicode_string(self, param):
|
|
63
|
+
"""
|
|
64
|
+
Check if a string contains non-ASCII characters.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
param: The string to check.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
True if the string contains non-ASCII characters, False otherwise.
|
|
71
|
+
"""
|
|
72
|
+
try:
|
|
73
|
+
param.encode('ascii')
|
|
74
|
+
return False # Can be encoded to ASCII, so not Unicode
|
|
75
|
+
except UnicodeEncodeError:
|
|
76
|
+
return True # Contains non-ASCII characters, so treat as Unicode
|
|
77
|
+
|
|
78
|
+
def _parse_date(self, param):
|
|
79
|
+
"""
|
|
80
|
+
Attempt to parse a string as a date.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
param: The string to parse.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
A datetime.date object if parsing is successful, else None.
|
|
87
|
+
"""
|
|
88
|
+
formats = ["%Y-%m-%d"]
|
|
89
|
+
for fmt in formats:
|
|
90
|
+
try:
|
|
91
|
+
return datetime.datetime.strptime(param, fmt).date()
|
|
92
|
+
except ValueError:
|
|
93
|
+
continue
|
|
94
|
+
return None
|
|
95
|
+
|
|
96
|
+
def _parse_datetime(self, param):
|
|
97
|
+
"""
|
|
98
|
+
Attempt to parse a string as a datetime.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
param: The string to parse.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
A datetime.datetime object if parsing is successful, else None.
|
|
105
|
+
"""
|
|
106
|
+
formats = [
|
|
107
|
+
"%Y-%m-%dT%H:%M:%S", # ISO 8601 datetime
|
|
108
|
+
"%Y-%m-%d %H:%M:%S.%f", # Datetime with fractional seconds (up to 3 digits)
|
|
109
|
+
]
|
|
110
|
+
for fmt in formats:
|
|
111
|
+
try:
|
|
112
|
+
dt = datetime.datetime.strptime(param, fmt)
|
|
113
|
+
if fmt == "%Y-%m-%d %H:%M:%S.%f" and len(param.split('.')[-1]) <= 3 or fmt == "%Y-%m-%dT%H:%M:%S":
|
|
114
|
+
return dt
|
|
115
|
+
except ValueError:
|
|
116
|
+
continue
|
|
117
|
+
return None
|
|
118
|
+
|
|
119
|
+
def _parse_time(self, param):
|
|
120
|
+
"""
|
|
121
|
+
Attempt to parse a string as a time.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
param: The string to parse.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
A datetime.time object if parsing is successful, else None.
|
|
128
|
+
"""
|
|
129
|
+
formats = [
|
|
130
|
+
"%H:%M:%S", # Time only
|
|
131
|
+
"%H:%M:%S.%f", # Time with fractional seconds
|
|
132
|
+
]
|
|
133
|
+
for fmt in formats:
|
|
134
|
+
try:
|
|
135
|
+
return datetime.datetime.strptime(param, fmt).time()
|
|
136
|
+
except ValueError:
|
|
137
|
+
continue
|
|
138
|
+
return None
|
|
139
|
+
|
|
140
|
+
def _parse_timestamptz(self, param):
|
|
141
|
+
"""
|
|
142
|
+
Attempt to parse a string as a timestamp with time zone (timestamptz).
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
param: The string to parse.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
A datetime.datetime object if parsing is successful, else None.
|
|
149
|
+
"""
|
|
150
|
+
formats = [
|
|
151
|
+
"%Y-%m-%dT%H:%M:%S%z", # ISO 8601 datetime with timezone offset
|
|
152
|
+
"%Y-%m-%d %H:%M:%S.%f%z", # Datetime with fractional seconds and timezone offset
|
|
153
|
+
]
|
|
154
|
+
for fmt in formats:
|
|
155
|
+
try:
|
|
156
|
+
return datetime.datetime.strptime(param, fmt)
|
|
157
|
+
except ValueError:
|
|
158
|
+
continue
|
|
159
|
+
return None
|
|
160
|
+
|
|
161
|
+
def _parse_smalldatetime(self, param):
|
|
162
|
+
"""
|
|
163
|
+
Attempt to parse a string as a smalldatetime.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
param: The string to parse.
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
A datetime.datetime object if parsing is successful, else None.
|
|
170
|
+
"""
|
|
171
|
+
formats = [
|
|
172
|
+
"%Y-%m-%d %H:%M:%S", # Standard datetime
|
|
173
|
+
]
|
|
174
|
+
for fmt in formats:
|
|
175
|
+
try:
|
|
176
|
+
return datetime.datetime.strptime(param, fmt)
|
|
177
|
+
except ValueError:
|
|
178
|
+
continue
|
|
179
|
+
return None
|
|
180
|
+
|
|
181
|
+
def _parse_datetime2(self, param):
|
|
182
|
+
"""
|
|
183
|
+
Attempt to parse a string as a datetime2.
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
param: The string to parse.
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
A datetime.datetime object if parsing is successful, else None.
|
|
190
|
+
"""
|
|
191
|
+
formats = [
|
|
192
|
+
"%Y-%m-%d %H:%M:%S.%f", # Datetime with fractional seconds (up to 6 digits)
|
|
193
|
+
]
|
|
194
|
+
for fmt in formats:
|
|
195
|
+
try:
|
|
196
|
+
dt = datetime.datetime.strptime(param, fmt)
|
|
197
|
+
if fmt == "%Y-%m-%d %H:%M:%S.%f" and len(param.split('.')[-1]) > 3:
|
|
198
|
+
return dt
|
|
199
|
+
except ValueError:
|
|
200
|
+
continue
|
|
201
|
+
return None
|
|
202
|
+
|
|
203
|
+
def _get_numeric_data(self, param):
|
|
204
|
+
"""
|
|
205
|
+
Get the data for a numeric parameter.
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
param: The numeric parameter.
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
A NumericData struct containing the numeric data.
|
|
212
|
+
"""
|
|
213
|
+
decimal_as_tuple = param.as_tuple()
|
|
214
|
+
num_digits = len(decimal_as_tuple.digits)
|
|
215
|
+
exponent = decimal_as_tuple.exponent
|
|
216
|
+
|
|
217
|
+
# Calculate the SQL precision & scale
|
|
218
|
+
# precision = no. of significant digits
|
|
219
|
+
# scale = no. digits after decimal point
|
|
220
|
+
if exponent >= 0:
|
|
221
|
+
# digits=314, exp=2 ---> '31400' --> precision=5, scale=0
|
|
222
|
+
precision = num_digits + exponent
|
|
223
|
+
scale = 0
|
|
224
|
+
elif (-1 * exponent) <= num_digits:
|
|
225
|
+
# digits=3140, exp=-3 ---> '3.140' --> precision=4, scale=3
|
|
226
|
+
precision = num_digits
|
|
227
|
+
scale = exponent * -1
|
|
228
|
+
else:
|
|
229
|
+
# digits=3140, exp=-5 ---> '0.03140' --> precision=5, scale=5
|
|
230
|
+
# TODO: double check the precision calculation here with SQL documentation
|
|
231
|
+
precision = exponent * -1
|
|
232
|
+
scale = exponent * -1
|
|
233
|
+
|
|
234
|
+
# TODO: Revisit this check, do we want this restriction?
|
|
235
|
+
if precision > 15:
|
|
236
|
+
raise ValueError("Precision of the numeric value is too high - " + str(param) +
|
|
237
|
+
". Should be less than or equal to 15")
|
|
238
|
+
NumericData = ddbc_bindings.NumericData
|
|
239
|
+
numeric_data = NumericData()
|
|
240
|
+
numeric_data.scale = scale
|
|
241
|
+
numeric_data.precision = precision
|
|
242
|
+
numeric_data.sign = 1 if decimal_as_tuple.sign == 0 else 0
|
|
243
|
+
# strip decimal point from param & convert the significant digits to integer
|
|
244
|
+
# Ex: 12.34 ---> 1234
|
|
245
|
+
val = str(param)
|
|
246
|
+
if '.' in val or '-' in val:
|
|
247
|
+
val = val.replace('.', '')
|
|
248
|
+
val = val.replace('-', '')
|
|
249
|
+
val = int(val)
|
|
250
|
+
numeric_data.val = val
|
|
251
|
+
return numeric_data
|
|
252
|
+
|
|
253
|
+
def _map_sql_type(self, param, parameters_list, i):
|
|
254
|
+
"""
|
|
255
|
+
Map a Python data type to the corresponding SQL type,C type,Columnsize and Decimal digits.
|
|
256
|
+
Takes:
|
|
257
|
+
- param: The parameter to map.
|
|
258
|
+
- parameters_list: The list of parameters to bind.
|
|
259
|
+
- i: The index of the parameter in the list.
|
|
260
|
+
Returns:
|
|
261
|
+
- A tuple containing the SQL type, C type, column size, and decimal digits.
|
|
262
|
+
"""
|
|
263
|
+
if param is None:
|
|
264
|
+
return odbc_sql_const.SQL_NULL_DATA.value, odbc_sql_const.SQL_C_DEFAULT.value, 1, 0
|
|
265
|
+
|
|
266
|
+
elif isinstance(param, bool):
|
|
267
|
+
return odbc_sql_const.SQL_BIT.value, odbc_sql_const.SQL_C_BIT.value, 1, 0
|
|
268
|
+
|
|
269
|
+
elif isinstance(param, int):
|
|
270
|
+
if 0 <= param <= 255:
|
|
271
|
+
return odbc_sql_const.SQL_TINYINT.value, odbc_sql_const.SQL_C_TINYINT.value, 3, 0
|
|
272
|
+
elif -32768 <= param <= 32767:
|
|
273
|
+
return odbc_sql_const.SQL_SMALLINT.value, odbc_sql_const.SQL_C_SHORT.value, 5, 0
|
|
274
|
+
elif -2147483648 <= param <= 2147483647:
|
|
275
|
+
return odbc_sql_const.SQL_INTEGER.value, odbc_sql_const.SQL_C_LONG.value, 10, 0
|
|
276
|
+
else:
|
|
277
|
+
return odbc_sql_const.SQL_BIGINT.value, odbc_sql_const.SQL_C_SBIGINT.value, 19, 0
|
|
278
|
+
|
|
279
|
+
elif isinstance(param, float):
|
|
280
|
+
return odbc_sql_const.SQL_DOUBLE.value, odbc_sql_const.SQL_C_DOUBLE.value, 15, 0
|
|
281
|
+
|
|
282
|
+
elif isinstance(param, decimal.Decimal):
|
|
283
|
+
# TODO: Support for other numeric types (smallmoney, money etc.)
|
|
284
|
+
# if param.as_tuple().exponent == -4: # Scale is 4
|
|
285
|
+
# if -214748.3648 <= param <= 214748.3647:
|
|
286
|
+
# return odbc_sql_const.SQL_SMALLMONEY.value, odbc_sql_const.SQL_C_NUMERIC.value, 10, 4
|
|
287
|
+
# elif -922337203685477.5808 <= param <= 922337203685477.5807:
|
|
288
|
+
# return odbc_sql_const.SQL_MONEY.value, odbc_sql_const.SQL_C_NUMERIC.value, 19, 4
|
|
289
|
+
parameters_list[i] = self._get_numeric_data(param) # Replace the parameter with the dictionary
|
|
290
|
+
return odbc_sql_const.SQL_NUMERIC.value, odbc_sql_const.SQL_C_NUMERIC.value, parameters_list[i].precision, parameters_list[i].scale
|
|
291
|
+
|
|
292
|
+
elif isinstance(param, str):
|
|
293
|
+
# Check for Well-Known Text (WKT) format for geography/geometry
|
|
294
|
+
if param.startswith("POINT") or param.startswith("LINESTRING") or param.startswith("POLYGON"):
|
|
295
|
+
return odbc_sql_const.SQL_WVARCHAR.value, odbc_sql_const.SQL_C_WCHAR.value, len(param), 0
|
|
296
|
+
|
|
297
|
+
# Attempt to parse as date, datetime or time
|
|
298
|
+
if self._parse_date(param):
|
|
299
|
+
parameters_list[i] = self._parse_date(param) # Replace the parameter with the date object
|
|
300
|
+
return odbc_sql_const.SQL_DATE.value, odbc_sql_const.SQL_C_TYPE_DATE.value, 10, 0
|
|
301
|
+
elif self._parse_datetime(param):
|
|
302
|
+
parameters_list[i] = self._parse_datetime(param)
|
|
303
|
+
return odbc_sql_const.SQL_TIMESTAMP.value, odbc_sql_const.SQL_C_TYPE_TIMESTAMP.value, 23, 3
|
|
304
|
+
elif self._parse_time(param):
|
|
305
|
+
parameters_list[i] = self._parse_time(param)
|
|
306
|
+
return odbc_sql_const.SQL_TIME.value, odbc_sql_const.SQL_C_TYPE_TIME.value, 8, 0
|
|
307
|
+
# TODO: Support for other types (Timestampoffset etc.)
|
|
308
|
+
# elif self._parse_timestamptz(param):
|
|
309
|
+
# return odbc_sql_const.SQL_TIMESTAMPOFFSET.value, odbc_sql_const.SQL_C_TYPE_TIMESTAMP.value, 34, 7
|
|
310
|
+
# elif self._parse_smalldatetime(param):
|
|
311
|
+
# return odbc_sql_const.SQL_SMALLDATETIME.value, odbc_sql_const.SQL_C_TYPE_TIMESTAMP.value, 16, 0
|
|
312
|
+
# elif self._parse_datetime2(param):
|
|
313
|
+
# return odbc_sql_const.SQL_DATETIME2.value, odbc_sql_const.SQL_C_TYPE_TIMESTAMP.value, 27, 7
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
# String mapping logic here
|
|
317
|
+
is_unicode = self._is_unicode_string(param)
|
|
318
|
+
if len(param) > 4000: # Long strings
|
|
319
|
+
if is_unicode:
|
|
320
|
+
return odbc_sql_const.SQL_WLONGVARCHAR.value, odbc_sql_const.SQL_C_WCHAR.value, len(param), 0
|
|
321
|
+
else:
|
|
322
|
+
return odbc_sql_const.SQL_LONGVARCHAR.value, odbc_sql_const.SQL_C_CHAR.value, len(param), 0
|
|
323
|
+
elif is_unicode: # Short Unicode strings
|
|
324
|
+
return odbc_sql_const.SQL_WVARCHAR.value, odbc_sql_const.SQL_C_WCHAR.value, len(param), 0
|
|
325
|
+
else: # Short non-Unicode strings
|
|
326
|
+
return odbc_sql_const.SQL_VARCHAR.value, odbc_sql_const.SQL_C_CHAR.value, len(param), 0
|
|
327
|
+
|
|
328
|
+
elif isinstance(param, bytes):
|
|
329
|
+
if len(param) > 8000: # Assuming VARBINARY(MAX) for long byte arrays
|
|
330
|
+
return odbc_sql_const.SQL_VARBINARY.value, odbc_sql_const.SQL_C_BINARY.value, len(param), 0
|
|
331
|
+
else:
|
|
332
|
+
return odbc_sql_const.SQL_BINARY.value, odbc_sql_const.SQL_C_BINARY.value, len(param), 0
|
|
333
|
+
|
|
334
|
+
elif isinstance(param, bytearray):
|
|
335
|
+
if len(param) > 8000: # Assuming VARBINARY(MAX) for long byte arrays
|
|
336
|
+
return odbc_sql_const.SQL_VARBINARY.value, odbc_sql_const.SQL_C_BINARY.value, len(param), 0
|
|
337
|
+
else:
|
|
338
|
+
return odbc_sql_const.SQL_BINARY.value, odbc_sql_const.SQL_C_BINARY.value, len(param), 0
|
|
339
|
+
|
|
340
|
+
elif isinstance(param, uuid.UUID): # Handle uniqueidentifier
|
|
341
|
+
return odbc_sql_const.SQL_GUID.value, odbc_sql_const.SQL_C_GUID.value, 36, 0
|
|
342
|
+
|
|
343
|
+
elif isinstance(param, datetime.datetime):
|
|
344
|
+
# Always keep datetime.datetime check before datetime.date check since datetime.datetime is a subclass of datetime (isinstance(datetime.datetime, datetime.date) returns True)
|
|
345
|
+
return odbc_sql_const.SQL_TIMESTAMP.value, odbc_sql_const.SQL_C_TYPE_TIMESTAMP.value, 23, 3
|
|
346
|
+
|
|
347
|
+
elif isinstance(param, datetime.date):
|
|
348
|
+
return odbc_sql_const.SQL_DATE.value, odbc_sql_const.SQL_C_TYPE_DATE.value, 10, 0
|
|
349
|
+
|
|
350
|
+
elif isinstance(param, datetime.time):
|
|
351
|
+
return odbc_sql_const.SQL_TIME.value, odbc_sql_const.SQL_C_TYPE_TIME.value, 8, 0
|
|
352
|
+
|
|
353
|
+
else:
|
|
354
|
+
# Fallback to VARCHAR for unsupported types
|
|
355
|
+
return odbc_sql_const.SQL_VARCHAR.value, odbc_sql_const.SQL_C_CHAR.value, len(str(param)), 0
|
|
356
|
+
|
|
357
|
+
def _initialize_cursor(self) -> None:
|
|
358
|
+
"""
|
|
359
|
+
Initialize the ODBC statement handle.
|
|
360
|
+
"""
|
|
361
|
+
self._allocate_statement_handle()
|
|
362
|
+
|
|
363
|
+
def _allocate_statement_handle(self):
|
|
364
|
+
"""
|
|
365
|
+
Allocate the ODBC statement handle.
|
|
366
|
+
"""
|
|
367
|
+
ret = ddbc_bindings.DDBCSQLAllocHandle(
|
|
368
|
+
odbc_sql_const.SQL_HANDLE_STMT.value,
|
|
369
|
+
self.connection.hdbc.value,
|
|
370
|
+
ctypes.cast(ctypes.pointer(self.hstmt), ctypes.c_void_p).value
|
|
371
|
+
)
|
|
372
|
+
check_error(odbc_sql_const.SQL_HANDLE_STMT.value, self.hstmt.value, ret)
|
|
373
|
+
|
|
374
|
+
def _reset_cursor(self) -> None:
|
|
375
|
+
"""
|
|
376
|
+
Reset the ODBC statement handle.
|
|
377
|
+
"""
|
|
378
|
+
# Free the existing statement handle
|
|
379
|
+
if self.hstmt.value:
|
|
380
|
+
ddbc_bindings.DDBCSQLFreeHandle(odbc_sql_const.SQL_HANDLE_STMT.value, self.hstmt.value)
|
|
381
|
+
# Reinitialize the statement handle
|
|
382
|
+
self._initialize_cursor()
|
|
383
|
+
|
|
384
|
+
def close(self) -> None:
|
|
385
|
+
"""
|
|
386
|
+
Close the cursor now (rather than whenever __del__ is called).
|
|
387
|
+
|
|
388
|
+
Raises:
|
|
389
|
+
Error: If any operation is attempted with the cursor after it is closed.
|
|
390
|
+
"""
|
|
391
|
+
if self.closed:
|
|
392
|
+
raise Exception("Cursor is already closed.")
|
|
393
|
+
|
|
394
|
+
# Free the statement handle
|
|
395
|
+
if self.hstmt.value:
|
|
396
|
+
ret = ddbc_bindings.DDBCSQLFreeHandle(odbc_sql_const.SQL_HANDLE_STMT.value, self.hstmt.value)
|
|
397
|
+
check_error(odbc_sql_const.SQL_HANDLE_STMT.value, self.hstmt.value, ret)
|
|
398
|
+
self.hstmt.value = None
|
|
399
|
+
|
|
400
|
+
self.closed = True
|
|
401
|
+
|
|
402
|
+
def _check_closed(self):
|
|
403
|
+
"""
|
|
404
|
+
Check if the cursor is closed and raise an exception if it is.
|
|
405
|
+
|
|
406
|
+
Raises:
|
|
407
|
+
Error: If the cursor is closed.
|
|
408
|
+
"""
|
|
409
|
+
if self.closed:
|
|
410
|
+
raise Exception("Operation cannot be performed: the cursor is closed.")
|
|
411
|
+
|
|
412
|
+
def _create_parameter_types_list(self, parameter, ParamInfo, parameters_list, i):
|
|
413
|
+
"""
|
|
414
|
+
Maps parameter types for the given parameter.
|
|
415
|
+
|
|
416
|
+
Args:
|
|
417
|
+
parameter: parameter to bind.
|
|
418
|
+
|
|
419
|
+
Returns:
|
|
420
|
+
paraminfo.
|
|
421
|
+
"""
|
|
422
|
+
paraminfo = ParamInfo()
|
|
423
|
+
sql_type, c_type, column_size, decimal_digits = self._map_sql_type(parameter, parameters_list, i)
|
|
424
|
+
paraminfo.paramCType = c_type
|
|
425
|
+
paraminfo.paramSQLType = sql_type
|
|
426
|
+
paraminfo.inputOutputType = odbc_sql_const.SQL_PARAM_INPUT.value
|
|
427
|
+
paraminfo.columnSize = column_size
|
|
428
|
+
paraminfo.decimalDigits = decimal_digits
|
|
429
|
+
return paraminfo
|
|
430
|
+
|
|
431
|
+
def _initialize_description(self):
|
|
432
|
+
"""
|
|
433
|
+
Initialize the description attribute using SQLDescribeCol.
|
|
434
|
+
"""
|
|
435
|
+
col_metadata = []
|
|
436
|
+
ret = ddbc_bindings.DDBCSQLDescribeCol(self.hstmt.value, col_metadata)
|
|
437
|
+
check_error(odbc_sql_const.SQL_HANDLE_STMT.value, self.hstmt.value, ret)
|
|
438
|
+
|
|
439
|
+
self.description = [
|
|
440
|
+
(
|
|
441
|
+
col["ColumnName"],
|
|
442
|
+
self._map_data_type(col["DataType"]),
|
|
443
|
+
None,
|
|
444
|
+
col["ColumnSize"],
|
|
445
|
+
col["ColumnSize"],
|
|
446
|
+
col["DecimalDigits"],
|
|
447
|
+
col["Nullable"] == odbc_sql_const.SQL_NULLABLE.value,
|
|
448
|
+
)
|
|
449
|
+
for col in col_metadata
|
|
450
|
+
]
|
|
451
|
+
|
|
452
|
+
def _map_data_type(self, sql_type):
|
|
453
|
+
"""
|
|
454
|
+
Map SQL data type to Python data type.
|
|
455
|
+
|
|
456
|
+
Args:
|
|
457
|
+
sql_type: SQL data type.
|
|
458
|
+
|
|
459
|
+
Returns:
|
|
460
|
+
Corresponding Python data type.
|
|
461
|
+
"""
|
|
462
|
+
sql_to_python_type = {
|
|
463
|
+
odbc_sql_const.SQL_INTEGER.value: int,
|
|
464
|
+
odbc_sql_const.SQL_VARCHAR.value: str,
|
|
465
|
+
odbc_sql_const.SQL_WVARCHAR.value: str,
|
|
466
|
+
odbc_sql_const.SQL_CHAR.value: str,
|
|
467
|
+
odbc_sql_const.SQL_WCHAR.value: str,
|
|
468
|
+
odbc_sql_const.SQL_FLOAT.value: float,
|
|
469
|
+
odbc_sql_const.SQL_DOUBLE.value: float,
|
|
470
|
+
odbc_sql_const.SQL_DECIMAL.value: decimal.Decimal,
|
|
471
|
+
odbc_sql_const.SQL_NUMERIC.value: decimal.Decimal,
|
|
472
|
+
odbc_sql_const.SQL_DATE.value: datetime.date,
|
|
473
|
+
odbc_sql_const.SQL_TIMESTAMP.value: datetime.datetime,
|
|
474
|
+
odbc_sql_const.SQL_TIME.value: datetime.time,
|
|
475
|
+
odbc_sql_const.SQL_BIT.value: bool,
|
|
476
|
+
odbc_sql_const.SQL_TINYINT.value: int,
|
|
477
|
+
odbc_sql_const.SQL_SMALLINT.value: int,
|
|
478
|
+
odbc_sql_const.SQL_BIGINT.value: int,
|
|
479
|
+
odbc_sql_const.SQL_BINARY.value: bytes,
|
|
480
|
+
odbc_sql_const.SQL_VARBINARY.value: bytes,
|
|
481
|
+
odbc_sql_const.SQL_LONGVARBINARY.value: bytes,
|
|
482
|
+
odbc_sql_const.SQL_GUID.value: uuid.UUID,
|
|
483
|
+
# Add more mappings as needed
|
|
484
|
+
}
|
|
485
|
+
return sql_to_python_type.get(sql_type, str)
|
|
486
|
+
|
|
487
|
+
def execute(self, operation: str, *parameters, use_prepare: bool = True, reset_cursor: bool = True):
|
|
488
|
+
"""
|
|
489
|
+
Prepare and execute a database operation (query or command).
|
|
490
|
+
|
|
491
|
+
Args:
|
|
492
|
+
operation: SQL query or command.
|
|
493
|
+
parameters: Sequence of parameters to bind.
|
|
494
|
+
use_prepare: Whether to use SQLPrepareW (default) or SQLExecDirectW.
|
|
495
|
+
reset_cursor: Whether to reset the cursor before execution.
|
|
496
|
+
"""
|
|
497
|
+
self._check_closed() # Check if the cursor is closed
|
|
498
|
+
|
|
499
|
+
if reset_cursor:
|
|
500
|
+
self._reset_cursor()
|
|
501
|
+
|
|
502
|
+
ParamInfo = ddbc_bindings.ParamInfo
|
|
503
|
+
parameters_type = []
|
|
504
|
+
|
|
505
|
+
# Flatten parameters if a single tuple or list is passed
|
|
506
|
+
if len(parameters) == 1 and isinstance(parameters[0], (tuple, list)):
|
|
507
|
+
parameters = parameters[0]
|
|
508
|
+
|
|
509
|
+
parameters = list(parameters)
|
|
510
|
+
|
|
511
|
+
if len(parameters):
|
|
512
|
+
for i, param in enumerate(parameters):
|
|
513
|
+
paraminfo = self._create_parameter_types_list(param, ParamInfo, parameters, i)
|
|
514
|
+
parameters_type.append(paraminfo)
|
|
515
|
+
|
|
516
|
+
# TODO: Use a more sophisticated string compare that handles redundant spaces etc.
|
|
517
|
+
# Also consider storing last query's hash instead of full query string. This will help
|
|
518
|
+
# in low-memory conditions (Ex: huge number of parallel queries with huge query string sizes)
|
|
519
|
+
if operation != self.last_executed_stmt:
|
|
520
|
+
# Executing a new statement. Reset is_stmt_prepared to false
|
|
521
|
+
self.is_stmt_prepared = [False]
|
|
522
|
+
'''
|
|
523
|
+
Execute SQL Statement - (SQLExecute)
|
|
524
|
+
'''
|
|
525
|
+
# TODO - Need to evaluate encrypted logs for query parameters
|
|
526
|
+
if ENABLE_LOGGING:
|
|
527
|
+
logger.debug("Executing query: %s", operation)
|
|
528
|
+
for i, param in enumerate(parameters):
|
|
529
|
+
logger.debug(
|
|
530
|
+
"Parameter number: %s, Parameter: %s, Param Python Type: %s, ParamInfo: %s, %s, %s, %s, %s",
|
|
531
|
+
i+1,
|
|
532
|
+
param,
|
|
533
|
+
str(type(param)),
|
|
534
|
+
parameters_type[i].paramSQLType,
|
|
535
|
+
parameters_type[i].paramCType,
|
|
536
|
+
parameters_type[i].columnSize,
|
|
537
|
+
parameters_type[i].decimalDigits,
|
|
538
|
+
parameters_type[i].inputOutputType
|
|
539
|
+
)
|
|
540
|
+
|
|
541
|
+
ret = ddbc_bindings.DDBCSQLExecute(self.hstmt.value, operation, parameters, parameters_type,
|
|
542
|
+
self.is_stmt_prepared, use_prepare)
|
|
543
|
+
check_error(odbc_sql_const.SQL_HANDLE_STMT.value, self.hstmt.value, ret)
|
|
544
|
+
self.last_executed_stmt = operation
|
|
545
|
+
|
|
546
|
+
# Update rowcount after execution
|
|
547
|
+
self.rowcount = ddbc_bindings.DDBCSQLRowCount(self.hstmt.value)
|
|
548
|
+
|
|
549
|
+
# Initialize description after execution
|
|
550
|
+
self._initialize_description()
|
|
551
|
+
|
|
552
|
+
def executemany(self, operation: str, seq_of_parameters: list) -> None:
|
|
553
|
+
"""
|
|
554
|
+
Prepare a database operation and execute it against all parameter sequences.
|
|
555
|
+
|
|
556
|
+
Args:
|
|
557
|
+
operation: SQL query or command.
|
|
558
|
+
seq_of_parameters: Sequence of sequences or mappings of parameters.
|
|
559
|
+
|
|
560
|
+
Raises:
|
|
561
|
+
Error: If the operation fails.
|
|
562
|
+
"""
|
|
563
|
+
self._check_closed() # Check if the cursor is closed
|
|
564
|
+
|
|
565
|
+
# Reset the cursor once before the loop
|
|
566
|
+
self._reset_cursor()
|
|
567
|
+
|
|
568
|
+
try:
|
|
569
|
+
first_execution = True
|
|
570
|
+
total_rowcount = 0
|
|
571
|
+
for parameters in seq_of_parameters:
|
|
572
|
+
# Execute the operation with the current set of parameters without
|
|
573
|
+
# Converting the parameters to a list
|
|
574
|
+
parameters = list(parameters)
|
|
575
|
+
if ENABLE_LOGGING:
|
|
576
|
+
logger.info("Executing query with parameters: %s", parameters)
|
|
577
|
+
# Prepare the statement only during first execution. From second time
|
|
578
|
+
# onwards, skip preparing and directly execute. This helps avoid
|
|
579
|
+
# unnecessary 'prepare' network calls.
|
|
580
|
+
if first_execution:
|
|
581
|
+
prepare_stmt = True
|
|
582
|
+
first_execution = False
|
|
583
|
+
else:
|
|
584
|
+
prepare_stmt = False
|
|
585
|
+
# Execute statement with one parameter set
|
|
586
|
+
self.execute(operation, parameters, use_prepare=prepare_stmt, reset_cursor=False)
|
|
587
|
+
if self.rowcount != -1:
|
|
588
|
+
# Rowcount would get updated inside execute method, add it to the current rowcount
|
|
589
|
+
total_rowcount += self.rowcount
|
|
590
|
+
else:
|
|
591
|
+
total_rowcount = -1
|
|
592
|
+
# Update the rowcount after all executions
|
|
593
|
+
self.rowcount = total_rowcount
|
|
594
|
+
except Exception as e:
|
|
595
|
+
if ENABLE_LOGGING:
|
|
596
|
+
logger.info("Executing query with parameters: %s", parameters)
|
|
597
|
+
# Prepare the statement only during first execution. From second time
|
|
598
|
+
# onwards, skip preparing and directly execute. This helps avoid
|
|
599
|
+
# unnecessary 'prepare' network calls.
|
|
600
|
+
if first_execution:
|
|
601
|
+
prepare_stmt = True
|
|
602
|
+
first_execution = False
|
|
603
|
+
else:
|
|
604
|
+
prepare_stmt = False
|
|
605
|
+
# Execute statement with one parameter set
|
|
606
|
+
self.execute(operation, parameters, use_prepare=prepare_stmt, reset_cursor=False)
|
|
607
|
+
|
|
608
|
+
def fetchone(self) -> Union[None, tuple]:
|
|
609
|
+
"""
|
|
610
|
+
Fetch the next row of a query result set.
|
|
611
|
+
|
|
612
|
+
Returns:
|
|
613
|
+
Single sequence or None if no more data is available.
|
|
614
|
+
|
|
615
|
+
Raises:
|
|
616
|
+
Error: If the previous call to execute did not produce any result set.
|
|
617
|
+
"""
|
|
618
|
+
self._check_closed() # Check if the cursor is closed
|
|
619
|
+
|
|
620
|
+
# Fetch the next row
|
|
621
|
+
row = []
|
|
622
|
+
ret = ddbc_bindings.DDBCSQLFetchOne(self.hstmt.value, row)
|
|
623
|
+
check_error(odbc_sql_const.SQL_HANDLE_STMT.value, self.hstmt.value, ret)
|
|
624
|
+
if ret == odbc_sql_const.SQL_NO_DATA.value:
|
|
625
|
+
return None
|
|
626
|
+
return list(row)
|
|
627
|
+
|
|
628
|
+
def fetchmany(self, size: int = None) -> List[tuple]:
|
|
629
|
+
"""
|
|
630
|
+
Fetch the next set of rows of a query result.
|
|
631
|
+
|
|
632
|
+
Args:
|
|
633
|
+
size: Number of rows to fetch at a time.
|
|
634
|
+
|
|
635
|
+
Returns:
|
|
636
|
+
Sequence of sequences (e.g. list of tuples).
|
|
637
|
+
|
|
638
|
+
Raises:
|
|
639
|
+
Error: If the previous call to execute did not produce any result set.
|
|
640
|
+
"""
|
|
641
|
+
self._check_closed() # Check if the cursor is closed
|
|
642
|
+
|
|
643
|
+
if size is None:
|
|
644
|
+
size = self.arraysize
|
|
645
|
+
|
|
646
|
+
# Fetch the next set of rows
|
|
647
|
+
rows = []
|
|
648
|
+
ret = ddbc_bindings.DDBCSQLFetchMany(self.hstmt.value, rows, size)
|
|
649
|
+
if ret == odbc_sql_const.SQL_NO_DATA.value:
|
|
650
|
+
return []
|
|
651
|
+
return rows
|
|
652
|
+
|
|
653
|
+
def fetchall(self) -> List[tuple]:
|
|
654
|
+
"""
|
|
655
|
+
Fetch all (remaining) rows of a query result.
|
|
656
|
+
|
|
657
|
+
Returns:
|
|
658
|
+
Sequence of sequences (e.g. list of tuples).
|
|
659
|
+
|
|
660
|
+
Raises:
|
|
661
|
+
Error: If the previous call to execute did not produce any result set.
|
|
662
|
+
"""
|
|
663
|
+
self._check_closed() # Check if the cursor is closed
|
|
664
|
+
|
|
665
|
+
# Fetch all remaining rows
|
|
666
|
+
rows = []
|
|
667
|
+
ret = ddbc_bindings.DDBCSQLFetchAll(self.hstmt.value, rows)
|
|
668
|
+
if ret != odbc_sql_const.SQL_NO_DATA.value:
|
|
669
|
+
return []
|
|
670
|
+
return list(rows)
|
|
671
|
+
|
|
672
|
+
def nextset(self) -> Union[bool, None]:
|
|
673
|
+
"""
|
|
674
|
+
Skip to the next available result set.
|
|
675
|
+
|
|
676
|
+
Returns:
|
|
677
|
+
True if there is another result set, None otherwise.
|
|
678
|
+
|
|
679
|
+
Raises:
|
|
680
|
+
Error: If the previous call to execute did not produce any result set.
|
|
681
|
+
"""
|
|
682
|
+
self._check_closed() # Check if the cursor is closed
|
|
683
|
+
|
|
684
|
+
# Skip to the next result set
|
|
685
|
+
ret = ddbc_bindings.DDBCSQLMoreResults(self.hstmt.value)
|
|
686
|
+
if ret == odbc_sql_const.SQL_NO_DATA.value:
|
|
687
|
+
return False
|
|
688
|
+
return True
|