mssql-python 1.0.0a0__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/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