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/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