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