mssql-python 1.0.0__cp314-cp314-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.
- mssql_python/__init__.py +307 -0
- mssql_python/auth.py +222 -0
- mssql_python/connection.py +1493 -0
- mssql_python/connection_string_builder.py +113 -0
- mssql_python/connection_string_parser.py +381 -0
- mssql_python/constants.py +509 -0
- mssql_python/cursor.py +2601 -0
- mssql_python/db_connection.py +53 -0
- mssql_python/ddbc_bindings.cp314-amd64.pyd +0 -0
- mssql_python/ddbc_bindings.py +137 -0
- mssql_python/exceptions.py +574 -0
- mssql_python/helpers.py +357 -0
- mssql_python/libs/LICENSING +19 -0
- mssql_python/libs/linux/alpine/arm64/lib/MICROSOFT_ODBC_DRIVER_FOR_SQL_SERVER_LICENSE.txt +76 -0
- mssql_python/libs/linux/alpine/arm64/lib/libmsodbcsql-18.5.so.1.1 +0 -0
- mssql_python/libs/linux/alpine/arm64/lib/libodbcinst.so.2 +0 -0
- mssql_python/libs/linux/alpine/arm64/share/resources/en_US/msodbcsqlr18.rll +0 -0
- mssql_python/libs/linux/alpine/x86_64/lib/MICROSOFT_ODBC_DRIVER_FOR_SQL_SERVER_LICENSE.txt +76 -0
- mssql_python/libs/linux/alpine/x86_64/lib/libmsodbcsql-18.5.so.1.1 +0 -0
- mssql_python/libs/linux/alpine/x86_64/lib/libodbcinst.so.2 +0 -0
- mssql_python/libs/linux/alpine/x86_64/share/resources/en_US/msodbcsqlr18.rll +0 -0
- mssql_python/libs/linux/debian_ubuntu/arm64/lib/MICROSOFT_ODBC_DRIVER_FOR_SQL_SERVER_LICENSE.txt +76 -0
- mssql_python/libs/linux/debian_ubuntu/arm64/lib/libmsodbcsql-18.5.so.1.1 +0 -0
- mssql_python/libs/linux/debian_ubuntu/arm64/lib/libodbcinst.so.2 +0 -0
- mssql_python/libs/linux/debian_ubuntu/arm64/share/resources/en_US/msodbcsqlr18.rll +0 -0
- mssql_python/libs/linux/debian_ubuntu/x86_64/lib/MICROSOFT_ODBC_DRIVER_FOR_SQL_SERVER_LICENSE.txt +76 -0
- mssql_python/libs/linux/debian_ubuntu/x86_64/lib/libmsodbcsql-18.5.so.1.1 +0 -0
- mssql_python/libs/linux/debian_ubuntu/x86_64/lib/libodbcinst.so.2 +0 -0
- mssql_python/libs/linux/debian_ubuntu/x86_64/share/resources/en_US/msodbcsqlr18.rll +0 -0
- mssql_python/libs/linux/rhel/arm64/lib/MICROSOFT_ODBC_DRIVER_FOR_SQL_SERVER_LICENSE.txt +76 -0
- mssql_python/libs/linux/rhel/arm64/lib/libmsodbcsql-18.5.so.1.1 +0 -0
- mssql_python/libs/linux/rhel/arm64/lib/libodbcinst.so.2 +0 -0
- mssql_python/libs/linux/rhel/arm64/share/resources/en_US/msodbcsqlr18.rll +0 -0
- mssql_python/libs/linux/rhel/x86_64/lib/MICROSOFT_ODBC_DRIVER_FOR_SQL_SERVER_LICENSE.txt +76 -0
- mssql_python/libs/linux/rhel/x86_64/lib/libmsodbcsql-18.5.so.1.1 +0 -0
- mssql_python/libs/linux/rhel/x86_64/lib/libodbcinst.so.2 +0 -0
- mssql_python/libs/linux/rhel/x86_64/share/resources/en_US/msodbcsqlr18.rll +0 -0
- mssql_python/libs/linux/suse/x86_64/lib/libmsodbcsql-18.5.so.1.1 +0 -0
- mssql_python/libs/linux/suse/x86_64/lib/libodbcinst.so.2 +0 -0
- mssql_python/libs/linux/suse/x86_64/share/resources/en_US/msodbcsqlr18.rll +0 -0
- mssql_python/libs/macos/arm64/lib/MICROSOFT_ODBC_DRIVER_FOR_SQL_SERVER_LICENSE.txt +76 -0
- mssql_python/libs/macos/arm64/lib/libltdl.7.dylib +0 -0
- mssql_python/libs/macos/arm64/lib/libmsodbcsql.18.dylib +0 -0
- mssql_python/libs/macos/arm64/lib/libodbc.2.dylib +0 -0
- mssql_python/libs/macos/arm64/lib/libodbcinst.2.dylib +0 -0
- mssql_python/libs/macos/arm64/share/doc/msodbcsql18/LICENSE.txt +101 -0
- mssql_python/libs/macos/arm64/share/doc/msodbcsql18/RELEASE_NOTES +465 -0
- mssql_python/libs/macos/arm64/share/msodbcsql18/resources/en_US/msodbcsqlr18.rll +0 -0
- mssql_python/libs/macos/x86_64/lib/MICROSOFT_ODBC_DRIVER_FOR_SQL_SERVER_LICENSE.txt +76 -0
- mssql_python/libs/macos/x86_64/lib/libltdl.7.dylib +0 -0
- mssql_python/libs/macos/x86_64/lib/libmsodbcsql.18.dylib +0 -0
- mssql_python/libs/macos/x86_64/lib/libodbc.2.dylib +0 -0
- mssql_python/libs/macos/x86_64/lib/libodbcinst.2.dylib +0 -0
- mssql_python/libs/macos/x86_64/share/doc/msodbcsql18/LICENSE.txt +101 -0
- mssql_python/libs/macos/x86_64/share/doc/msodbcsql18/RELEASE_NOTES +465 -0
- mssql_python/libs/macos/x86_64/share/msodbcsql18/resources/en_US/msodbcsqlr18.rll +0 -0
- mssql_python/libs/windows/x64/1033/msodbcsqlr18.rll +0 -0
- mssql_python/libs/windows/x64/MICROSOFT_ODBC_DRIVER_FOR_SQL_SERVER_LICENSE.txt +76 -0
- mssql_python/libs/windows/x64/MICROSOFT_VISUAL_STUDIO_LICENSE.txt +54 -0
- mssql_python/libs/windows/x64/msodbcdiag18.dll +0 -0
- mssql_python/libs/windows/x64/msodbcsql18.dll +0 -0
- mssql_python/libs/windows/x64/mssql-auth.dll +0 -0
- mssql_python/logging.py +610 -0
- mssql_python/mssql_python.pyi +375 -0
- mssql_python/msvcp140.dll +0 -0
- mssql_python/pooling.py +125 -0
- mssql_python/row.py +194 -0
- mssql_python/type.py +143 -0
- mssql_python-1.0.0.dist-info/METADATA +76 -0
- mssql_python-1.0.0.dist-info/RECORD +73 -0
- mssql_python-1.0.0.dist-info/WHEEL +5 -0
- mssql_python-1.0.0.dist-info/licenses/LICENSE +47 -0
- mssql_python-1.0.0.dist-info/top_level.txt +1 -0
mssql_python/cursor.py
ADDED
|
@@ -0,0 +1,2601 @@
|
|
|
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
|
+
Resource Management:
|
|
6
|
+
- Cursors are tracked by their parent connection.
|
|
7
|
+
- Closing the connection will automatically close all open cursors.
|
|
8
|
+
- Do not use a cursor after it is closed, or after its parent connection is closed.
|
|
9
|
+
- Use close() to release resources held by the cursor as soon as it is no longer needed.
|
|
10
|
+
"""
|
|
11
|
+
# pylint: disable=too-many-lines # Large file due to comprehensive DB-API 2.0 implementation
|
|
12
|
+
|
|
13
|
+
import decimal
|
|
14
|
+
import uuid
|
|
15
|
+
import datetime
|
|
16
|
+
import warnings
|
|
17
|
+
from typing import List, Union, Any, Optional, Tuple, Sequence, TYPE_CHECKING
|
|
18
|
+
from mssql_python.constants import ConstantsDDBC as ddbc_sql_const, SQLTypes
|
|
19
|
+
from mssql_python.helpers import check_error
|
|
20
|
+
from mssql_python.logging import logger
|
|
21
|
+
from mssql_python import ddbc_bindings
|
|
22
|
+
from mssql_python.exceptions import InterfaceError, NotSupportedError, ProgrammingError
|
|
23
|
+
from mssql_python.row import Row
|
|
24
|
+
from mssql_python import get_settings
|
|
25
|
+
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
from mssql_python.connection import Connection
|
|
28
|
+
|
|
29
|
+
# Constants for string handling
|
|
30
|
+
MAX_INLINE_CHAR: int = (
|
|
31
|
+
4000 # NVARCHAR/VARCHAR inline limit; this triggers NVARCHAR(MAX)/VARCHAR(MAX) + DAE
|
|
32
|
+
)
|
|
33
|
+
SMALLMONEY_MIN: decimal.Decimal = decimal.Decimal("-214748.3648")
|
|
34
|
+
SMALLMONEY_MAX: decimal.Decimal = decimal.Decimal("214748.3647")
|
|
35
|
+
MONEY_MIN: decimal.Decimal = decimal.Decimal("-922337203685477.5808")
|
|
36
|
+
MONEY_MAX: decimal.Decimal = decimal.Decimal("922337203685477.5807")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class Cursor: # pylint: disable=too-many-instance-attributes,too-many-public-methods
|
|
40
|
+
"""
|
|
41
|
+
Represents a database cursor, which is used to manage the context of a fetch operation.
|
|
42
|
+
|
|
43
|
+
Attributes:
|
|
44
|
+
connection: Database connection object.
|
|
45
|
+
description: Sequence of 7-item sequences describing one result column.
|
|
46
|
+
rowcount: Number of rows produced or affected by the last execute operation.
|
|
47
|
+
arraysize: Number of rows to fetch at a time with fetchmany().
|
|
48
|
+
rownumber: Track the current row index in the result set.
|
|
49
|
+
|
|
50
|
+
Methods:
|
|
51
|
+
__init__(connection_str) -> None.
|
|
52
|
+
callproc(procname, parameters=None) ->
|
|
53
|
+
Modified copy of the input sequence with output parameters.
|
|
54
|
+
close() -> None.
|
|
55
|
+
execute(operation, parameters=None) -> Cursor.
|
|
56
|
+
executemany(operation, seq_of_parameters) -> None.
|
|
57
|
+
fetchone() -> Single sequence or None if no more data is available.
|
|
58
|
+
fetchmany(size=None) -> Sequence of sequences (e.g. list of tuples).
|
|
59
|
+
fetchall() -> Sequence of sequences (e.g. list of tuples).
|
|
60
|
+
nextset() -> True if there is another result set, None otherwise.
|
|
61
|
+
next() -> Fetch the next row from the cursor.
|
|
62
|
+
setinputsizes(sizes) -> None.
|
|
63
|
+
setoutputsize(size, column=None) -> None.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
# TODO(jathakkar): Thread safety considerations
|
|
67
|
+
# The cursor class contains methods that are not thread-safe due to:
|
|
68
|
+
# 1. Methods that mutate cursor state (_reset_cursor, self.description, etc.)
|
|
69
|
+
# 2. Methods that call ODBC functions with shared handles (self.hstmt)
|
|
70
|
+
#
|
|
71
|
+
# These methods should be properly synchronized or redesigned when implementing
|
|
72
|
+
# async functionality to prevent race conditions and data corruption.
|
|
73
|
+
# Consider using locks, redesigning for immutability, or ensuring
|
|
74
|
+
# cursor objects are never shared across threads.
|
|
75
|
+
|
|
76
|
+
def __init__(self, connection: "Connection", timeout: int = 0) -> None:
|
|
77
|
+
"""
|
|
78
|
+
Initialize the cursor with a database connection.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
connection: Database connection object.
|
|
82
|
+
timeout: Query timeout in seconds
|
|
83
|
+
"""
|
|
84
|
+
self._connection: "Connection" = connection # Store as private attribute
|
|
85
|
+
self._timeout: int = timeout
|
|
86
|
+
self._inputsizes: Optional[List[Union[int, Tuple[Any, ...]]]] = None
|
|
87
|
+
# self.connection.autocommit = False
|
|
88
|
+
self.hstmt: Optional[Any] = None
|
|
89
|
+
self._initialize_cursor()
|
|
90
|
+
self.description: Optional[
|
|
91
|
+
List[
|
|
92
|
+
Tuple[
|
|
93
|
+
str,
|
|
94
|
+
Any,
|
|
95
|
+
Optional[int],
|
|
96
|
+
Optional[int],
|
|
97
|
+
Optional[int],
|
|
98
|
+
Optional[int],
|
|
99
|
+
Optional[bool],
|
|
100
|
+
]
|
|
101
|
+
]
|
|
102
|
+
] = None
|
|
103
|
+
self.rowcount: int = -1
|
|
104
|
+
self.arraysize: int = (
|
|
105
|
+
1 # Default number of rows to fetch at a time is 1, user can change it
|
|
106
|
+
)
|
|
107
|
+
self.buffer_length: int = 1024 # Default buffer length for string data
|
|
108
|
+
self.closed: bool = False
|
|
109
|
+
self._result_set_empty: bool = False # Add this initialization
|
|
110
|
+
self.last_executed_stmt: str = (
|
|
111
|
+
"" # Stores the last statement executed by this cursor
|
|
112
|
+
)
|
|
113
|
+
self.is_stmt_prepared: List[bool] = [
|
|
114
|
+
False
|
|
115
|
+
] # Indicates if last_executed_stmt was prepared by ddbc shim.
|
|
116
|
+
# Is a list instead of a bool coz bools in Python are immutable.
|
|
117
|
+
|
|
118
|
+
# Initialize attributes that may be defined later to avoid pylint warnings
|
|
119
|
+
# Note: _original_fetch* methods are not initialized here as they need to be
|
|
120
|
+
# conditionally set based on hasattr() checks
|
|
121
|
+
# Hence, we can't pass around bools by reference & modify them.
|
|
122
|
+
# Therefore, it must be a list with exactly one bool element.
|
|
123
|
+
|
|
124
|
+
self._rownumber = -1 # DB-API extension: last returned row index, -1 before first
|
|
125
|
+
|
|
126
|
+
self._cached_column_map = None
|
|
127
|
+
self._cached_converter_map = None
|
|
128
|
+
self._next_row_index = 0 # internal: index of the next row the driver will return (0-based)
|
|
129
|
+
self._has_result_set = False # Track if we have an active result set
|
|
130
|
+
self._skip_increment_for_next_fetch = False # Track if we need to skip incrementing the row index
|
|
131
|
+
self.messages = [] # Store diagnostic messages
|
|
132
|
+
|
|
133
|
+
def _is_unicode_string(self, param: str) -> bool:
|
|
134
|
+
"""
|
|
135
|
+
Check if a string contains non-ASCII characters.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
param: The string to check.
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
True if the string contains non-ASCII characters, False otherwise.
|
|
142
|
+
"""
|
|
143
|
+
try:
|
|
144
|
+
param.encode("ascii")
|
|
145
|
+
return False # Can be encoded to ASCII, so not Unicode
|
|
146
|
+
except UnicodeEncodeError:
|
|
147
|
+
return True # Contains non-ASCII characters, so treat as Unicode
|
|
148
|
+
|
|
149
|
+
def _parse_date(self, param: str) -> Optional[datetime.date]:
|
|
150
|
+
"""
|
|
151
|
+
Attempt to parse a string as a date.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
param: The string to parse.
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
A datetime.date object if parsing is successful, else None.
|
|
158
|
+
"""
|
|
159
|
+
formats = ["%Y-%m-%d"]
|
|
160
|
+
for fmt in formats:
|
|
161
|
+
try:
|
|
162
|
+
return datetime.datetime.strptime(param, fmt).date()
|
|
163
|
+
except ValueError:
|
|
164
|
+
continue
|
|
165
|
+
return None
|
|
166
|
+
|
|
167
|
+
def _parse_datetime(self, param: str) -> Optional[datetime.datetime]:
|
|
168
|
+
"""
|
|
169
|
+
Attempt to parse a string as a datetime, smalldatetime, datetime2, timestamp.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
param: The string to parse.
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
A datetime.datetime object if parsing is successful, else None.
|
|
176
|
+
"""
|
|
177
|
+
formats = [
|
|
178
|
+
"%Y-%m-%dT%H:%M:%S.%f", # ISO 8601 datetime with fractional seconds
|
|
179
|
+
"%Y-%m-%dT%H:%M:%S", # ISO 8601 datetime
|
|
180
|
+
"%Y-%m-%d %H:%M:%S.%f", # Datetime with fractional seconds
|
|
181
|
+
"%Y-%m-%d %H:%M:%S", # Datetime without fractional seconds
|
|
182
|
+
]
|
|
183
|
+
for fmt in formats:
|
|
184
|
+
try:
|
|
185
|
+
return datetime.datetime.strptime(param, fmt) # Valid datetime
|
|
186
|
+
except ValueError:
|
|
187
|
+
continue # Try next format
|
|
188
|
+
|
|
189
|
+
return None # If all formats fail, return None
|
|
190
|
+
|
|
191
|
+
def _parse_time(self, param: str) -> Optional[datetime.time]:
|
|
192
|
+
"""
|
|
193
|
+
Attempt to parse a string as a time.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
param: The string to parse.
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
A datetime.time object if parsing is successful, else None.
|
|
200
|
+
"""
|
|
201
|
+
formats = [
|
|
202
|
+
"%H:%M:%S", # Time only
|
|
203
|
+
"%H:%M:%S.%f", # Time with fractional seconds
|
|
204
|
+
]
|
|
205
|
+
for fmt in formats:
|
|
206
|
+
try:
|
|
207
|
+
return datetime.datetime.strptime(param, fmt).time()
|
|
208
|
+
except ValueError:
|
|
209
|
+
continue
|
|
210
|
+
return None
|
|
211
|
+
|
|
212
|
+
def _get_numeric_data(self, param: decimal.Decimal) -> Any:
|
|
213
|
+
"""
|
|
214
|
+
Get the data for a numeric parameter.
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
param: The numeric parameter.
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
numeric_data: A NumericData struct containing
|
|
221
|
+
the numeric data.
|
|
222
|
+
"""
|
|
223
|
+
decimal_as_tuple = param.as_tuple()
|
|
224
|
+
digits_tuple = decimal_as_tuple.digits
|
|
225
|
+
num_digits = len(digits_tuple)
|
|
226
|
+
exponent = decimal_as_tuple.exponent
|
|
227
|
+
|
|
228
|
+
# Handle special values (NaN, Infinity, etc.)
|
|
229
|
+
if isinstance(exponent, str):
|
|
230
|
+
# For special values like 'n' (NaN), 'N' (sNaN), 'F' (Infinity)
|
|
231
|
+
# Return default precision and scale
|
|
232
|
+
precision = 38 # SQL Server default max precision
|
|
233
|
+
scale = 0
|
|
234
|
+
else:
|
|
235
|
+
# Calculate the SQL precision & scale
|
|
236
|
+
# precision = no. of significant digits
|
|
237
|
+
# scale = no. digits after decimal point
|
|
238
|
+
if exponent >= 0:
|
|
239
|
+
# digits=314, exp=2 ---> '31400' --> precision=5, scale=0
|
|
240
|
+
precision = num_digits + exponent
|
|
241
|
+
scale = 0
|
|
242
|
+
elif (-1 * exponent) <= num_digits:
|
|
243
|
+
# digits=3140, exp=-3 ---> '3.140' --> precision=4, scale=3
|
|
244
|
+
precision = num_digits
|
|
245
|
+
scale = exponent * -1
|
|
246
|
+
else:
|
|
247
|
+
# digits=3140, exp=-5 ---> '0.03140' --> precision=5, scale=5
|
|
248
|
+
# TODO: double check the precision calculation here with SQL documentation
|
|
249
|
+
precision = exponent * -1
|
|
250
|
+
scale = exponent * -1
|
|
251
|
+
|
|
252
|
+
if precision > 38:
|
|
253
|
+
raise ValueError(
|
|
254
|
+
"Precision of the numeric value is too high - "
|
|
255
|
+
+ str(param)
|
|
256
|
+
+ ". Should be less than or equal to 38"
|
|
257
|
+
)
|
|
258
|
+
Numeric_Data = ddbc_bindings.NumericData
|
|
259
|
+
numeric_data = Numeric_Data()
|
|
260
|
+
numeric_data.scale = scale
|
|
261
|
+
numeric_data.precision = precision
|
|
262
|
+
numeric_data.sign = 1 if decimal_as_tuple.sign == 0 else 0
|
|
263
|
+
# strip decimal point from param & convert the significant digits to integer
|
|
264
|
+
# Ex: 12.34 ---> 1234
|
|
265
|
+
int_str = "".join(str(d) for d in digits_tuple)
|
|
266
|
+
if exponent > 0:
|
|
267
|
+
int_str = int_str + ("0" * exponent)
|
|
268
|
+
elif exponent < 0:
|
|
269
|
+
if -exponent > num_digits:
|
|
270
|
+
int_str = ("0" * (-exponent - num_digits)) + int_str
|
|
271
|
+
|
|
272
|
+
if int_str == "":
|
|
273
|
+
int_str = "0"
|
|
274
|
+
|
|
275
|
+
# Convert decimal base-10 string to python int, then to 16 little-endian bytes
|
|
276
|
+
big_int = int(int_str)
|
|
277
|
+
byte_array = bytearray(16) # SQL_MAX_NUMERIC_LEN
|
|
278
|
+
for i in range(16):
|
|
279
|
+
byte_array[i] = big_int & 0xFF
|
|
280
|
+
big_int >>= 8
|
|
281
|
+
if big_int == 0:
|
|
282
|
+
break
|
|
283
|
+
|
|
284
|
+
numeric_data.val = bytes(byte_array)
|
|
285
|
+
return numeric_data
|
|
286
|
+
|
|
287
|
+
def _map_sql_type( # pylint: disable=too-many-arguments,too-many-positional-arguments,too-many-locals,too-many-return-statements,too-many-branches
|
|
288
|
+
self,
|
|
289
|
+
param: Any,
|
|
290
|
+
parameters_list: List[Any],
|
|
291
|
+
i: int,
|
|
292
|
+
min_val: Optional[Any] = None,
|
|
293
|
+
max_val: Optional[Any] = None,
|
|
294
|
+
) -> Tuple[int, int, int, int, bool]:
|
|
295
|
+
"""
|
|
296
|
+
Map a Python data type to the corresponding SQL type,
|
|
297
|
+
C type, Column size, and Decimal digits.
|
|
298
|
+
Takes:
|
|
299
|
+
- param: The parameter to map.
|
|
300
|
+
- parameters_list: The list of parameters to bind.
|
|
301
|
+
- i: The index of the parameter in the list.
|
|
302
|
+
Returns:
|
|
303
|
+
- A tuple containing the SQL type, C type, column size, and decimal digits.
|
|
304
|
+
"""
|
|
305
|
+
logger.debug('_map_sql_type: Mapping param index=%d, type=%s', i, type(param).__name__)
|
|
306
|
+
if param is None:
|
|
307
|
+
logger.debug('_map_sql_type: NULL parameter - index=%d', i)
|
|
308
|
+
return (
|
|
309
|
+
ddbc_sql_const.SQL_VARCHAR.value,
|
|
310
|
+
ddbc_sql_const.SQL_C_DEFAULT.value,
|
|
311
|
+
1,
|
|
312
|
+
0,
|
|
313
|
+
False,
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
if isinstance(param, bool):
|
|
317
|
+
logger.debug('_map_sql_type: BOOL detected - index=%d', i)
|
|
318
|
+
return (
|
|
319
|
+
ddbc_sql_const.SQL_BIT.value,
|
|
320
|
+
ddbc_sql_const.SQL_C_BIT.value,
|
|
321
|
+
1,
|
|
322
|
+
0,
|
|
323
|
+
False,
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
if isinstance(param, int):
|
|
327
|
+
# Use min_val/max_val if available
|
|
328
|
+
value_to_check = max_val if max_val is not None else param
|
|
329
|
+
min_to_check = min_val if min_val is not None else param
|
|
330
|
+
logger.debug('_map_sql_type: INT detected - index=%d, min=%s, max=%s',
|
|
331
|
+
i, str(min_to_check)[:50], str(value_to_check)[:50])
|
|
332
|
+
|
|
333
|
+
if 0 <= min_to_check and value_to_check <= 255:
|
|
334
|
+
logger.debug('_map_sql_type: INT -> TINYINT - index=%d', i)
|
|
335
|
+
return (
|
|
336
|
+
ddbc_sql_const.SQL_TINYINT.value,
|
|
337
|
+
ddbc_sql_const.SQL_C_TINYINT.value,
|
|
338
|
+
3,
|
|
339
|
+
0,
|
|
340
|
+
False,
|
|
341
|
+
)
|
|
342
|
+
if -32768 <= min_to_check and value_to_check <= 32767:
|
|
343
|
+
logger.debug('_map_sql_type: INT -> SMALLINT - index=%d', i)
|
|
344
|
+
return (
|
|
345
|
+
ddbc_sql_const.SQL_SMALLINT.value,
|
|
346
|
+
ddbc_sql_const.SQL_C_SHORT.value,
|
|
347
|
+
5,
|
|
348
|
+
0,
|
|
349
|
+
False,
|
|
350
|
+
)
|
|
351
|
+
if -2147483648 <= min_to_check and value_to_check <= 2147483647:
|
|
352
|
+
logger.debug('_map_sql_type: INT -> INTEGER - index=%d', i)
|
|
353
|
+
return (
|
|
354
|
+
ddbc_sql_const.SQL_INTEGER.value,
|
|
355
|
+
ddbc_sql_const.SQL_C_LONG.value,
|
|
356
|
+
10,
|
|
357
|
+
0,
|
|
358
|
+
False,
|
|
359
|
+
)
|
|
360
|
+
logger.debug('_map_sql_type: INT -> BIGINT - index=%d', i)
|
|
361
|
+
return (
|
|
362
|
+
ddbc_sql_const.SQL_BIGINT.value,
|
|
363
|
+
ddbc_sql_const.SQL_C_SBIGINT.value,
|
|
364
|
+
19,
|
|
365
|
+
0,
|
|
366
|
+
False,
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
if isinstance(param, float):
|
|
370
|
+
logger.debug('_map_sql_type: FLOAT detected - index=%d', i)
|
|
371
|
+
return (
|
|
372
|
+
ddbc_sql_const.SQL_DOUBLE.value,
|
|
373
|
+
ddbc_sql_const.SQL_C_DOUBLE.value,
|
|
374
|
+
15,
|
|
375
|
+
0,
|
|
376
|
+
False,
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
if isinstance(param, decimal.Decimal):
|
|
380
|
+
logger.debug('_map_sql_type: DECIMAL detected - index=%d', i)
|
|
381
|
+
# First check precision limit for all decimal values
|
|
382
|
+
decimal_as_tuple = param.as_tuple()
|
|
383
|
+
digits_tuple = decimal_as_tuple.digits
|
|
384
|
+
num_digits = len(digits_tuple)
|
|
385
|
+
exponent = decimal_as_tuple.exponent
|
|
386
|
+
|
|
387
|
+
# Handle special values (NaN, Infinity, etc.)
|
|
388
|
+
if isinstance(exponent, str):
|
|
389
|
+
logger.debug('_map_sql_type: DECIMAL special value - index=%d, exponent=%s', i, exponent)
|
|
390
|
+
# For special values like 'n' (NaN), 'N' (sNaN), 'F' (Infinity)
|
|
391
|
+
# Return default precision and scale
|
|
392
|
+
precision = 38 # SQL Server default max precision
|
|
393
|
+
else:
|
|
394
|
+
# Calculate the SQL precision (same logic as _get_numeric_data)
|
|
395
|
+
if exponent >= 0:
|
|
396
|
+
precision = num_digits + exponent
|
|
397
|
+
elif (-1 * exponent) <= num_digits:
|
|
398
|
+
precision = num_digits
|
|
399
|
+
else:
|
|
400
|
+
precision = exponent * -1
|
|
401
|
+
logger.debug('_map_sql_type: DECIMAL precision calculated - index=%d, precision=%d', i, precision)
|
|
402
|
+
|
|
403
|
+
if precision > 38:
|
|
404
|
+
logger.debug('_map_sql_type: DECIMAL precision too high - index=%d, precision=%d', i, precision)
|
|
405
|
+
raise ValueError(
|
|
406
|
+
f"Precision of the numeric value is too high. "
|
|
407
|
+
f"The maximum precision supported by SQL Server is 38, but got {precision}."
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
# Detect MONEY / SMALLMONEY range
|
|
411
|
+
if SMALLMONEY_MIN <= param <= SMALLMONEY_MAX:
|
|
412
|
+
logger.debug('_map_sql_type: DECIMAL -> SMALLMONEY - index=%d', i)
|
|
413
|
+
# smallmoney
|
|
414
|
+
parameters_list[i] = format(param, 'f')
|
|
415
|
+
return (
|
|
416
|
+
ddbc_sql_const.SQL_VARCHAR.value,
|
|
417
|
+
ddbc_sql_const.SQL_C_CHAR.value,
|
|
418
|
+
len(parameters_list[i]),
|
|
419
|
+
0,
|
|
420
|
+
False,
|
|
421
|
+
)
|
|
422
|
+
if MONEY_MIN <= param <= MONEY_MAX:
|
|
423
|
+
logger.debug('_map_sql_type: DECIMAL -> MONEY - index=%d', i)
|
|
424
|
+
# money
|
|
425
|
+
parameters_list[i] = format(param, 'f')
|
|
426
|
+
return (
|
|
427
|
+
ddbc_sql_const.SQL_VARCHAR.value,
|
|
428
|
+
ddbc_sql_const.SQL_C_CHAR.value,
|
|
429
|
+
len(parameters_list[i]),
|
|
430
|
+
0,
|
|
431
|
+
False,
|
|
432
|
+
)
|
|
433
|
+
# fallback to generic numeric binding
|
|
434
|
+
logger.debug('_map_sql_type: DECIMAL -> NUMERIC - index=%d', i)
|
|
435
|
+
parameters_list[i] = self._get_numeric_data(param)
|
|
436
|
+
logger.debug('_map_sql_type: NUMERIC created - index=%d, precision=%d, scale=%d',
|
|
437
|
+
i, parameters_list[i].precision, parameters_list[i].scale)
|
|
438
|
+
return (
|
|
439
|
+
ddbc_sql_const.SQL_NUMERIC.value,
|
|
440
|
+
ddbc_sql_const.SQL_C_NUMERIC.value,
|
|
441
|
+
parameters_list[i].precision,
|
|
442
|
+
parameters_list[i].scale,
|
|
443
|
+
False,
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
if isinstance(param, uuid.UUID):
|
|
447
|
+
logger.debug('_map_sql_type: UUID detected - index=%d', i)
|
|
448
|
+
parameters_list[i] = param.bytes_le
|
|
449
|
+
return (
|
|
450
|
+
ddbc_sql_const.SQL_GUID.value,
|
|
451
|
+
ddbc_sql_const.SQL_C_GUID.value,
|
|
452
|
+
16,
|
|
453
|
+
0,
|
|
454
|
+
False,
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
if isinstance(param, str):
|
|
458
|
+
logger.debug('_map_sql_type: STR detected - index=%d, length=%d', i, len(param))
|
|
459
|
+
if (
|
|
460
|
+
param.startswith("POINT")
|
|
461
|
+
or param.startswith("LINESTRING")
|
|
462
|
+
or param.startswith("POLYGON")
|
|
463
|
+
):
|
|
464
|
+
logger.debug('_map_sql_type: STR is geometry type - index=%d', i)
|
|
465
|
+
return (
|
|
466
|
+
ddbc_sql_const.SQL_WVARCHAR.value,
|
|
467
|
+
ddbc_sql_const.SQL_C_WCHAR.value,
|
|
468
|
+
len(param),
|
|
469
|
+
0,
|
|
470
|
+
False,
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
# String mapping logic here
|
|
474
|
+
is_unicode = self._is_unicode_string(param)
|
|
475
|
+
|
|
476
|
+
# Computes UTF-16 code units (handles surrogate pairs)
|
|
477
|
+
utf16_len = sum(2 if ord(c) > 0xFFFF else 1 for c in param)
|
|
478
|
+
logger.debug('_map_sql_type: STR analysis - index=%d, is_unicode=%s, utf16_len=%d',
|
|
479
|
+
i, str(is_unicode), utf16_len)
|
|
480
|
+
if utf16_len > MAX_INLINE_CHAR: # Long strings -> DAE
|
|
481
|
+
logger.debug('_map_sql_type: STR exceeds MAX_INLINE_CHAR, using DAE - index=%d', i)
|
|
482
|
+
if is_unicode:
|
|
483
|
+
return (
|
|
484
|
+
ddbc_sql_const.SQL_WVARCHAR.value,
|
|
485
|
+
ddbc_sql_const.SQL_C_WCHAR.value,
|
|
486
|
+
0,
|
|
487
|
+
0,
|
|
488
|
+
True,
|
|
489
|
+
)
|
|
490
|
+
return (
|
|
491
|
+
ddbc_sql_const.SQL_VARCHAR.value,
|
|
492
|
+
ddbc_sql_const.SQL_C_CHAR.value,
|
|
493
|
+
0,
|
|
494
|
+
0,
|
|
495
|
+
True,
|
|
496
|
+
)
|
|
497
|
+
|
|
498
|
+
# Short strings
|
|
499
|
+
if is_unicode:
|
|
500
|
+
return (
|
|
501
|
+
ddbc_sql_const.SQL_WVARCHAR.value,
|
|
502
|
+
ddbc_sql_const.SQL_C_WCHAR.value,
|
|
503
|
+
utf16_len,
|
|
504
|
+
0,
|
|
505
|
+
False,
|
|
506
|
+
)
|
|
507
|
+
return (
|
|
508
|
+
ddbc_sql_const.SQL_VARCHAR.value,
|
|
509
|
+
ddbc_sql_const.SQL_C_CHAR.value,
|
|
510
|
+
len(param),
|
|
511
|
+
0,
|
|
512
|
+
False,
|
|
513
|
+
)
|
|
514
|
+
|
|
515
|
+
if isinstance(param, (bytes, bytearray)):
|
|
516
|
+
length = len(param)
|
|
517
|
+
if length > 8000: # Use VARBINARY(MAX) for large blobs
|
|
518
|
+
return (
|
|
519
|
+
ddbc_sql_const.SQL_VARBINARY.value,
|
|
520
|
+
ddbc_sql_const.SQL_C_BINARY.value,
|
|
521
|
+
0,
|
|
522
|
+
0,
|
|
523
|
+
True,
|
|
524
|
+
)
|
|
525
|
+
# Small blobs → direct binding
|
|
526
|
+
return (
|
|
527
|
+
ddbc_sql_const.SQL_VARBINARY.value,
|
|
528
|
+
ddbc_sql_const.SQL_C_BINARY.value,
|
|
529
|
+
max(length, 1),
|
|
530
|
+
0,
|
|
531
|
+
False,
|
|
532
|
+
)
|
|
533
|
+
|
|
534
|
+
if isinstance(param, datetime.datetime):
|
|
535
|
+
if param.tzinfo is not None:
|
|
536
|
+
# Timezone-aware datetime -> DATETIMEOFFSET
|
|
537
|
+
return (
|
|
538
|
+
ddbc_sql_const.SQL_DATETIMEOFFSET.value,
|
|
539
|
+
ddbc_sql_const.SQL_C_SS_TIMESTAMPOFFSET.value,
|
|
540
|
+
34,
|
|
541
|
+
7,
|
|
542
|
+
False,
|
|
543
|
+
)
|
|
544
|
+
# Naive datetime -> TIMESTAMP
|
|
545
|
+
return (
|
|
546
|
+
ddbc_sql_const.SQL_TIMESTAMP.value,
|
|
547
|
+
ddbc_sql_const.SQL_C_TYPE_TIMESTAMP.value,
|
|
548
|
+
26,
|
|
549
|
+
6,
|
|
550
|
+
False,
|
|
551
|
+
)
|
|
552
|
+
|
|
553
|
+
if isinstance(param, datetime.date):
|
|
554
|
+
return (
|
|
555
|
+
ddbc_sql_const.SQL_DATE.value,
|
|
556
|
+
ddbc_sql_const.SQL_C_TYPE_DATE.value,
|
|
557
|
+
10,
|
|
558
|
+
0,
|
|
559
|
+
False,
|
|
560
|
+
)
|
|
561
|
+
|
|
562
|
+
if isinstance(param, datetime.time):
|
|
563
|
+
return (
|
|
564
|
+
ddbc_sql_const.SQL_TIME.value,
|
|
565
|
+
ddbc_sql_const.SQL_C_TYPE_TIME.value,
|
|
566
|
+
8,
|
|
567
|
+
0,
|
|
568
|
+
False,
|
|
569
|
+
)
|
|
570
|
+
|
|
571
|
+
# For safety: unknown/unhandled Python types should not silently go to SQL
|
|
572
|
+
raise TypeError(
|
|
573
|
+
"Unsupported parameter type: The driver cannot safely convert it to a SQL type."
|
|
574
|
+
)
|
|
575
|
+
|
|
576
|
+
def _initialize_cursor(self) -> None:
|
|
577
|
+
"""
|
|
578
|
+
Initialize the DDBC statement handle.
|
|
579
|
+
"""
|
|
580
|
+
self._allocate_statement_handle()
|
|
581
|
+
|
|
582
|
+
def _allocate_statement_handle(self) -> None:
|
|
583
|
+
"""
|
|
584
|
+
Allocate the DDBC statement handle.
|
|
585
|
+
"""
|
|
586
|
+
self.hstmt = self._connection._conn.alloc_statement_handle()
|
|
587
|
+
|
|
588
|
+
def _reset_cursor(self) -> None:
|
|
589
|
+
"""
|
|
590
|
+
Reset the DDBC statement handle.
|
|
591
|
+
"""
|
|
592
|
+
if self.hstmt:
|
|
593
|
+
self.hstmt.free()
|
|
594
|
+
self.hstmt = None
|
|
595
|
+
logger.debug( "SQLFreeHandle succeeded")
|
|
596
|
+
|
|
597
|
+
self._clear_rownumber()
|
|
598
|
+
|
|
599
|
+
# Reinitialize the statement handle
|
|
600
|
+
self._initialize_cursor()
|
|
601
|
+
|
|
602
|
+
def close(self) -> None:
|
|
603
|
+
"""
|
|
604
|
+
Close the connection now (rather than whenever .__del__() is called).
|
|
605
|
+
Idempotent: subsequent calls have no effect and will be no-ops.
|
|
606
|
+
|
|
607
|
+
The cursor will be unusable from this point forward; an InterfaceError
|
|
608
|
+
will be raised if any operation (other than close) is attempted with the cursor.
|
|
609
|
+
This is a deviation from pyodbc, which raises an exception if the cursor is already closed.
|
|
610
|
+
"""
|
|
611
|
+
if self.closed:
|
|
612
|
+
# Do nothing - not calling _check_closed() here since we want this to be idempotent
|
|
613
|
+
return
|
|
614
|
+
|
|
615
|
+
# Clear messages per DBAPI
|
|
616
|
+
self.messages = []
|
|
617
|
+
|
|
618
|
+
# Remove this cursor from the connection's tracking
|
|
619
|
+
if (
|
|
620
|
+
hasattr(self, "connection")
|
|
621
|
+
and self.connection
|
|
622
|
+
and hasattr(self.connection, "_cursors")
|
|
623
|
+
):
|
|
624
|
+
try:
|
|
625
|
+
self.connection._cursors.discard(self)
|
|
626
|
+
except Exception as e: # pylint: disable=broad-exception-caught
|
|
627
|
+
logger.warning( "Error removing cursor from connection tracking: %s", e)
|
|
628
|
+
|
|
629
|
+
if self.hstmt:
|
|
630
|
+
self.hstmt.free()
|
|
631
|
+
self.hstmt = None
|
|
632
|
+
logger.debug( "SQLFreeHandle succeeded")
|
|
633
|
+
self._clear_rownumber()
|
|
634
|
+
self.closed = True
|
|
635
|
+
|
|
636
|
+
def _check_closed(self) -> None:
|
|
637
|
+
"""
|
|
638
|
+
Check if the cursor is closed and raise an exception if it is.
|
|
639
|
+
|
|
640
|
+
Raises:
|
|
641
|
+
ProgrammingError: If the cursor is closed.
|
|
642
|
+
"""
|
|
643
|
+
if self.closed:
|
|
644
|
+
raise ProgrammingError(
|
|
645
|
+
driver_error="Operation cannot be performed: The cursor is closed.",
|
|
646
|
+
ddbc_error="",
|
|
647
|
+
)
|
|
648
|
+
|
|
649
|
+
def setinputsizes(self, sizes: List[Union[int, tuple]]) -> None:
|
|
650
|
+
"""
|
|
651
|
+
Sets the type information to be used for parameters in execute and executemany.
|
|
652
|
+
|
|
653
|
+
This method can be used to explicitly declare the types and sizes of query parameters.
|
|
654
|
+
For example:
|
|
655
|
+
|
|
656
|
+
sql = "INSERT INTO product (item, price) VALUES (?, ?)"
|
|
657
|
+
params = [('bicycle', 499.99), ('ham', 17.95)]
|
|
658
|
+
# specify that parameters are for NVARCHAR(50) and DECIMAL(18,4) columns
|
|
659
|
+
cursor.setinputsizes([(SQL_WVARCHAR, 50, 0), (SQL_DECIMAL, 18, 4)])
|
|
660
|
+
cursor.executemany(sql, params)
|
|
661
|
+
|
|
662
|
+
Args:
|
|
663
|
+
sizes: A sequence of tuples, one for each parameter. Each tuple contains
|
|
664
|
+
(sql_type, size, decimal_digits) where size and decimal_digits are optional.
|
|
665
|
+
"""
|
|
666
|
+
|
|
667
|
+
# Get valid SQL types from centralized constants
|
|
668
|
+
valid_sql_types = SQLTypes.get_valid_types()
|
|
669
|
+
|
|
670
|
+
self._inputsizes = []
|
|
671
|
+
|
|
672
|
+
if sizes:
|
|
673
|
+
for size_info in sizes:
|
|
674
|
+
if isinstance(size_info, tuple):
|
|
675
|
+
# Handle tuple format (sql_type, size, decimal_digits)
|
|
676
|
+
if len(size_info) == 1:
|
|
677
|
+
sql_type = size_info[0]
|
|
678
|
+
column_size = 0
|
|
679
|
+
decimal_digits = 0
|
|
680
|
+
elif len(size_info) == 2:
|
|
681
|
+
sql_type, column_size = size_info
|
|
682
|
+
decimal_digits = 0
|
|
683
|
+
elif len(size_info) >= 3:
|
|
684
|
+
sql_type, column_size, decimal_digits = size_info
|
|
685
|
+
|
|
686
|
+
# Validate SQL type
|
|
687
|
+
if not isinstance(sql_type, int) or sql_type not in valid_sql_types:
|
|
688
|
+
raise ValueError(
|
|
689
|
+
f"Invalid SQL type: {sql_type}. Must be a valid SQL type constant."
|
|
690
|
+
)
|
|
691
|
+
|
|
692
|
+
# Validate size and precision
|
|
693
|
+
if not isinstance(column_size, int) or column_size < 0:
|
|
694
|
+
raise ValueError(
|
|
695
|
+
f"Invalid column size: {column_size}. Must be a non-negative integer."
|
|
696
|
+
)
|
|
697
|
+
|
|
698
|
+
if not isinstance(decimal_digits, int) or decimal_digits < 0:
|
|
699
|
+
raise ValueError(
|
|
700
|
+
f"Invalid decimal digits: {decimal_digits}. "
|
|
701
|
+
f"Must be a non-negative integer."
|
|
702
|
+
)
|
|
703
|
+
|
|
704
|
+
self._inputsizes.append((sql_type, column_size, decimal_digits))
|
|
705
|
+
else:
|
|
706
|
+
# Handle single value (just sql_type)
|
|
707
|
+
sql_type = size_info
|
|
708
|
+
|
|
709
|
+
# Validate SQL type
|
|
710
|
+
if not isinstance(sql_type, int) or sql_type not in valid_sql_types:
|
|
711
|
+
raise ValueError(
|
|
712
|
+
f"Invalid SQL type: {sql_type}. Must be a valid SQL type constant."
|
|
713
|
+
)
|
|
714
|
+
|
|
715
|
+
self._inputsizes.append((sql_type, 0, 0))
|
|
716
|
+
|
|
717
|
+
def _reset_inputsizes(self) -> None:
|
|
718
|
+
"""Reset input sizes after execution"""
|
|
719
|
+
self._inputsizes = None
|
|
720
|
+
|
|
721
|
+
def _get_c_type_for_sql_type(self, sql_type: int) -> int:
|
|
722
|
+
"""Map SQL type to appropriate C type for parameter binding"""
|
|
723
|
+
sql_to_c_type = {
|
|
724
|
+
ddbc_sql_const.SQL_CHAR.value: ddbc_sql_const.SQL_C_CHAR.value,
|
|
725
|
+
ddbc_sql_const.SQL_VARCHAR.value: ddbc_sql_const.SQL_C_CHAR.value,
|
|
726
|
+
ddbc_sql_const.SQL_LONGVARCHAR.value: ddbc_sql_const.SQL_C_CHAR.value,
|
|
727
|
+
ddbc_sql_const.SQL_WCHAR.value: ddbc_sql_const.SQL_C_WCHAR.value,
|
|
728
|
+
ddbc_sql_const.SQL_WVARCHAR.value: ddbc_sql_const.SQL_C_WCHAR.value,
|
|
729
|
+
ddbc_sql_const.SQL_WLONGVARCHAR.value: ddbc_sql_const.SQL_C_WCHAR.value,
|
|
730
|
+
ddbc_sql_const.SQL_DECIMAL.value: ddbc_sql_const.SQL_C_NUMERIC.value,
|
|
731
|
+
ddbc_sql_const.SQL_NUMERIC.value: ddbc_sql_const.SQL_C_NUMERIC.value,
|
|
732
|
+
ddbc_sql_const.SQL_BIT.value: ddbc_sql_const.SQL_C_BIT.value,
|
|
733
|
+
ddbc_sql_const.SQL_TINYINT.value: ddbc_sql_const.SQL_C_TINYINT.value,
|
|
734
|
+
ddbc_sql_const.SQL_SMALLINT.value: ddbc_sql_const.SQL_C_SHORT.value,
|
|
735
|
+
ddbc_sql_const.SQL_INTEGER.value: ddbc_sql_const.SQL_C_LONG.value,
|
|
736
|
+
ddbc_sql_const.SQL_BIGINT.value: ddbc_sql_const.SQL_C_SBIGINT.value,
|
|
737
|
+
ddbc_sql_const.SQL_REAL.value: ddbc_sql_const.SQL_C_FLOAT.value,
|
|
738
|
+
ddbc_sql_const.SQL_FLOAT.value: ddbc_sql_const.SQL_C_DOUBLE.value,
|
|
739
|
+
ddbc_sql_const.SQL_DOUBLE.value: ddbc_sql_const.SQL_C_DOUBLE.value,
|
|
740
|
+
ddbc_sql_const.SQL_BINARY.value: ddbc_sql_const.SQL_C_BINARY.value,
|
|
741
|
+
ddbc_sql_const.SQL_VARBINARY.value: ddbc_sql_const.SQL_C_BINARY.value,
|
|
742
|
+
ddbc_sql_const.SQL_LONGVARBINARY.value: ddbc_sql_const.SQL_C_BINARY.value,
|
|
743
|
+
ddbc_sql_const.SQL_DATE.value: ddbc_sql_const.SQL_C_TYPE_DATE.value,
|
|
744
|
+
ddbc_sql_const.SQL_TIME.value: ddbc_sql_const.SQL_C_TYPE_TIME.value,
|
|
745
|
+
ddbc_sql_const.SQL_TIMESTAMP.value: ddbc_sql_const.SQL_C_TYPE_TIMESTAMP.value,
|
|
746
|
+
}
|
|
747
|
+
return sql_to_c_type.get(sql_type, ddbc_sql_const.SQL_C_DEFAULT.value)
|
|
748
|
+
|
|
749
|
+
def _create_parameter_types_list( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
750
|
+
self,
|
|
751
|
+
parameter: Any,
|
|
752
|
+
param_info: Optional[Tuple[Any, ...]],
|
|
753
|
+
parameters_list: List[Any],
|
|
754
|
+
i: int,
|
|
755
|
+
min_val: Optional[Any] = None,
|
|
756
|
+
max_val: Optional[Any] = None,
|
|
757
|
+
) -> Tuple[int, int, int, int, bool]:
|
|
758
|
+
"""
|
|
759
|
+
Maps parameter types for the given parameter.
|
|
760
|
+
Args:
|
|
761
|
+
parameter: parameter to bind.
|
|
762
|
+
Returns:
|
|
763
|
+
paraminfo.
|
|
764
|
+
"""
|
|
765
|
+
paraminfo = param_info()
|
|
766
|
+
|
|
767
|
+
# Check if we have explicit type information from setinputsizes
|
|
768
|
+
if self._inputsizes and i < len(self._inputsizes):
|
|
769
|
+
# Use explicit type information
|
|
770
|
+
sql_type, column_size, decimal_digits = self._inputsizes[i]
|
|
771
|
+
|
|
772
|
+
# Default is_dae to False for explicit types, but set to True for large strings/binary
|
|
773
|
+
is_dae = False
|
|
774
|
+
|
|
775
|
+
if parameter is None:
|
|
776
|
+
# For NULL parameters, always use SQL_C_DEFAULT regardless of SQL type
|
|
777
|
+
c_type = ddbc_sql_const.SQL_C_DEFAULT.value
|
|
778
|
+
else:
|
|
779
|
+
# For non-NULL parameters, determine the appropriate C type based on SQL type
|
|
780
|
+
c_type = self._get_c_type_for_sql_type(sql_type)
|
|
781
|
+
|
|
782
|
+
# Check if this should be a DAE (data at execution) parameter
|
|
783
|
+
# For string types with large column sizes
|
|
784
|
+
if isinstance(parameter, str) and column_size > MAX_INLINE_CHAR:
|
|
785
|
+
is_dae = True
|
|
786
|
+
# For binary types with large column sizes
|
|
787
|
+
elif isinstance(parameter, (bytes, bytearray)) and column_size > 8000:
|
|
788
|
+
is_dae = True
|
|
789
|
+
|
|
790
|
+
# Sanitize precision/scale for numeric types
|
|
791
|
+
if sql_type in (
|
|
792
|
+
ddbc_sql_const.SQL_DECIMAL.value,
|
|
793
|
+
ddbc_sql_const.SQL_NUMERIC.value,
|
|
794
|
+
):
|
|
795
|
+
column_size = max(
|
|
796
|
+
1, min(int(column_size) if column_size > 0 else 18, 38)
|
|
797
|
+
)
|
|
798
|
+
decimal_digits = min(max(0, decimal_digits), column_size)
|
|
799
|
+
|
|
800
|
+
else:
|
|
801
|
+
# Fall back to automatic type inference
|
|
802
|
+
sql_type, c_type, column_size, decimal_digits, is_dae = self._map_sql_type(
|
|
803
|
+
parameter, parameters_list, i, min_val=min_val, max_val=max_val
|
|
804
|
+
)
|
|
805
|
+
|
|
806
|
+
paraminfo.paramCType = c_type
|
|
807
|
+
paraminfo.paramSQLType = sql_type
|
|
808
|
+
paraminfo.inputOutputType = ddbc_sql_const.SQL_PARAM_INPUT.value
|
|
809
|
+
paraminfo.columnSize = column_size
|
|
810
|
+
paraminfo.decimalDigits = decimal_digits
|
|
811
|
+
paraminfo.isDAE = is_dae
|
|
812
|
+
|
|
813
|
+
if is_dae:
|
|
814
|
+
paraminfo.dataPtr = parameter # Will be converted to py::object* in C++
|
|
815
|
+
|
|
816
|
+
return paraminfo
|
|
817
|
+
|
|
818
|
+
def _initialize_description(self, column_metadata: Optional[Any] = None) -> None:
|
|
819
|
+
"""Initialize the description attribute from column metadata."""
|
|
820
|
+
if not column_metadata:
|
|
821
|
+
self.description = None
|
|
822
|
+
return
|
|
823
|
+
|
|
824
|
+
description = []
|
|
825
|
+
for _, col in enumerate(column_metadata):
|
|
826
|
+
# Get column name - lowercase it if the lowercase flag is set
|
|
827
|
+
column_name = col["ColumnName"]
|
|
828
|
+
|
|
829
|
+
# Use the current global setting to ensure tests pass correctly
|
|
830
|
+
if get_settings().lowercase:
|
|
831
|
+
column_name = column_name.lower()
|
|
832
|
+
|
|
833
|
+
# Add to description tuple (7 elements as per PEP-249)
|
|
834
|
+
description.append(
|
|
835
|
+
(
|
|
836
|
+
column_name, # name
|
|
837
|
+
self._map_data_type(col["DataType"]), # type_code
|
|
838
|
+
None, # display_size
|
|
839
|
+
col["ColumnSize"], # internal_size
|
|
840
|
+
col["ColumnSize"], # precision - should match ColumnSize
|
|
841
|
+
col["DecimalDigits"], # scale
|
|
842
|
+
col["Nullable"] == ddbc_sql_const.SQL_NULLABLE.value, # null_ok
|
|
843
|
+
)
|
|
844
|
+
)
|
|
845
|
+
self.description = description
|
|
846
|
+
|
|
847
|
+
def _build_converter_map(self):
|
|
848
|
+
"""
|
|
849
|
+
Build a pre-computed converter map for output converters.
|
|
850
|
+
Returns a list where each element is either a converter function or None.
|
|
851
|
+
This eliminates the need to look up converters for every row.
|
|
852
|
+
"""
|
|
853
|
+
if not self.description or not hasattr(self.connection, '_output_converters') or not self.connection._output_converters:
|
|
854
|
+
return None
|
|
855
|
+
|
|
856
|
+
converter_map = []
|
|
857
|
+
|
|
858
|
+
for desc in self.description:
|
|
859
|
+
if desc is None:
|
|
860
|
+
converter_map.append(None)
|
|
861
|
+
continue
|
|
862
|
+
sql_type = desc[1]
|
|
863
|
+
converter = self.connection.get_output_converter(sql_type)
|
|
864
|
+
# If no converter found for the SQL type, try the WVARCHAR converter as a fallback
|
|
865
|
+
if converter is None:
|
|
866
|
+
from mssql_python.constants import ConstantsDDBC
|
|
867
|
+
converter = self.connection.get_output_converter(ConstantsDDBC.SQL_WVARCHAR.value)
|
|
868
|
+
|
|
869
|
+
converter_map.append(converter)
|
|
870
|
+
|
|
871
|
+
return converter_map
|
|
872
|
+
|
|
873
|
+
def _get_column_and_converter_maps(self):
|
|
874
|
+
"""
|
|
875
|
+
Get column map and converter map for Row construction (thread-safe).
|
|
876
|
+
This centralizes the column map building logic to eliminate duplication
|
|
877
|
+
and ensure thread-safe lazy initialization.
|
|
878
|
+
|
|
879
|
+
Returns:
|
|
880
|
+
tuple: (column_map, converter_map)
|
|
881
|
+
"""
|
|
882
|
+
# Thread-safe lazy initialization of column map
|
|
883
|
+
column_map = self._cached_column_map
|
|
884
|
+
if column_map is None and self.description:
|
|
885
|
+
# Build column map locally first, then assign to cache
|
|
886
|
+
column_map = {col_desc[0]: i for i, col_desc in enumerate(self.description)}
|
|
887
|
+
self._cached_column_map = column_map
|
|
888
|
+
|
|
889
|
+
# Fallback to legacy column name map if no cached map
|
|
890
|
+
column_map = column_map or getattr(self, '_column_name_map', None)
|
|
891
|
+
|
|
892
|
+
# Get cached converter map
|
|
893
|
+
converter_map = getattr(self, '_cached_converter_map', None)
|
|
894
|
+
|
|
895
|
+
return column_map, converter_map
|
|
896
|
+
|
|
897
|
+
def _map_data_type(self, sql_type):
|
|
898
|
+
"""
|
|
899
|
+
Map SQL data type to Python data type.
|
|
900
|
+
|
|
901
|
+
Args:
|
|
902
|
+
sql_type: SQL data type.
|
|
903
|
+
|
|
904
|
+
Returns:
|
|
905
|
+
Corresponding Python data type.
|
|
906
|
+
"""
|
|
907
|
+
sql_to_python_type = {
|
|
908
|
+
ddbc_sql_const.SQL_INTEGER.value: int,
|
|
909
|
+
ddbc_sql_const.SQL_VARCHAR.value: str,
|
|
910
|
+
ddbc_sql_const.SQL_WVARCHAR.value: str,
|
|
911
|
+
ddbc_sql_const.SQL_CHAR.value: str,
|
|
912
|
+
ddbc_sql_const.SQL_WCHAR.value: str,
|
|
913
|
+
ddbc_sql_const.SQL_FLOAT.value: float,
|
|
914
|
+
ddbc_sql_const.SQL_DOUBLE.value: float,
|
|
915
|
+
ddbc_sql_const.SQL_DECIMAL.value: decimal.Decimal,
|
|
916
|
+
ddbc_sql_const.SQL_NUMERIC.value: decimal.Decimal,
|
|
917
|
+
ddbc_sql_const.SQL_DATE.value: datetime.date,
|
|
918
|
+
ddbc_sql_const.SQL_TIMESTAMP.value: datetime.datetime,
|
|
919
|
+
ddbc_sql_const.SQL_TIME.value: datetime.time,
|
|
920
|
+
ddbc_sql_const.SQL_BIT.value: bool,
|
|
921
|
+
ddbc_sql_const.SQL_TINYINT.value: int,
|
|
922
|
+
ddbc_sql_const.SQL_SMALLINT.value: int,
|
|
923
|
+
ddbc_sql_const.SQL_BIGINT.value: int,
|
|
924
|
+
ddbc_sql_const.SQL_BINARY.value: bytes,
|
|
925
|
+
ddbc_sql_const.SQL_VARBINARY.value: bytes,
|
|
926
|
+
ddbc_sql_const.SQL_LONGVARBINARY.value: bytes,
|
|
927
|
+
ddbc_sql_const.SQL_GUID.value: uuid.UUID,
|
|
928
|
+
# Add more mappings as needed
|
|
929
|
+
}
|
|
930
|
+
return sql_to_python_type.get(sql_type, str)
|
|
931
|
+
|
|
932
|
+
@property
|
|
933
|
+
def rownumber(self) -> int:
|
|
934
|
+
"""
|
|
935
|
+
DB-API extension: Current 0-based index of the cursor in the result set.
|
|
936
|
+
|
|
937
|
+
Returns:
|
|
938
|
+
int or None: The current 0-based index of the cursor in the result set,
|
|
939
|
+
or None if no row has been fetched yet or the index cannot be determined.
|
|
940
|
+
|
|
941
|
+
Note:
|
|
942
|
+
- Returns -1 before the first successful fetch
|
|
943
|
+
- Returns 0 after fetching the first row
|
|
944
|
+
- Returns -1 for empty result sets (since no rows can be fetched)
|
|
945
|
+
|
|
946
|
+
Warning:
|
|
947
|
+
This is a DB-API extension and may not be portable across different
|
|
948
|
+
database modules.
|
|
949
|
+
"""
|
|
950
|
+
# Use mssql_python logging system instead of standard warnings
|
|
951
|
+
logger.warning( "DB-API extension cursor.rownumber used")
|
|
952
|
+
|
|
953
|
+
# Return None if cursor is closed or no result set is available
|
|
954
|
+
if self.closed or not self._has_result_set:
|
|
955
|
+
return -1
|
|
956
|
+
|
|
957
|
+
return self._rownumber # Will be None until first fetch, then 0, 1, 2, etc.
|
|
958
|
+
|
|
959
|
+
@property
|
|
960
|
+
def connection(self) -> "Connection":
|
|
961
|
+
"""
|
|
962
|
+
DB-API 2.0 attribute: Connection object that created this cursor.
|
|
963
|
+
|
|
964
|
+
This is a read-only reference to the Connection object that was used to create
|
|
965
|
+
this cursor. This attribute is useful for polymorphic code that needs access
|
|
966
|
+
to connection-level functionality.
|
|
967
|
+
|
|
968
|
+
Returns:
|
|
969
|
+
Connection: The connection object that created this cursor.
|
|
970
|
+
|
|
971
|
+
Note:
|
|
972
|
+
This attribute is read-only as specified by DB-API 2.0. Attempting to
|
|
973
|
+
assign to this attribute will raise an AttributeError.
|
|
974
|
+
"""
|
|
975
|
+
return self._connection
|
|
976
|
+
|
|
977
|
+
def _reset_rownumber(self) -> None:
|
|
978
|
+
"""Reset the rownumber tracking when starting a new result set."""
|
|
979
|
+
self._rownumber = -1
|
|
980
|
+
self._next_row_index = 0
|
|
981
|
+
self._has_result_set = True
|
|
982
|
+
self._skip_increment_for_next_fetch = False
|
|
983
|
+
|
|
984
|
+
def _increment_rownumber(self):
|
|
985
|
+
"""
|
|
986
|
+
Called after a successful fetch from the driver. Keep both counters consistent.
|
|
987
|
+
"""
|
|
988
|
+
if self._has_result_set:
|
|
989
|
+
# driver returned one row, so the next row index increments by 1
|
|
990
|
+
self._next_row_index += 1
|
|
991
|
+
# rownumber is last returned row index
|
|
992
|
+
self._rownumber = self._next_row_index - 1
|
|
993
|
+
else:
|
|
994
|
+
raise InterfaceError(
|
|
995
|
+
"Cannot increment rownumber: no active result set.",
|
|
996
|
+
"No active result set.",
|
|
997
|
+
)
|
|
998
|
+
|
|
999
|
+
# Will be used when we add support for scrollable cursors
|
|
1000
|
+
def _decrement_rownumber(self):
|
|
1001
|
+
"""
|
|
1002
|
+
Decrement the rownumber by 1.
|
|
1003
|
+
|
|
1004
|
+
This could be used for error recovery or cursor positioning operations.
|
|
1005
|
+
"""
|
|
1006
|
+
if self._has_result_set and self._rownumber >= 0:
|
|
1007
|
+
if self._rownumber > 0:
|
|
1008
|
+
self._rownumber -= 1
|
|
1009
|
+
else:
|
|
1010
|
+
self._rownumber = -1
|
|
1011
|
+
else:
|
|
1012
|
+
raise InterfaceError(
|
|
1013
|
+
"Cannot decrement rownumber: no active result set.",
|
|
1014
|
+
"No active result set.",
|
|
1015
|
+
)
|
|
1016
|
+
|
|
1017
|
+
def _clear_rownumber(self):
|
|
1018
|
+
"""
|
|
1019
|
+
Clear the rownumber tracking.
|
|
1020
|
+
|
|
1021
|
+
This should be called when the result set is cleared or when the cursor is reset.
|
|
1022
|
+
"""
|
|
1023
|
+
self._rownumber = -1
|
|
1024
|
+
self._has_result_set = False
|
|
1025
|
+
self._skip_increment_for_next_fetch = False
|
|
1026
|
+
|
|
1027
|
+
def __iter__(self):
|
|
1028
|
+
"""
|
|
1029
|
+
Return the cursor itself as an iterator.
|
|
1030
|
+
|
|
1031
|
+
This allows direct iteration over the cursor after execute():
|
|
1032
|
+
|
|
1033
|
+
for row in cursor.execute("SELECT * FROM table"):
|
|
1034
|
+
print(row)
|
|
1035
|
+
"""
|
|
1036
|
+
self._check_closed()
|
|
1037
|
+
return self
|
|
1038
|
+
|
|
1039
|
+
def __next__(self):
|
|
1040
|
+
"""
|
|
1041
|
+
Fetch the next row when iterating over the cursor.
|
|
1042
|
+
|
|
1043
|
+
Returns:
|
|
1044
|
+
The next Row object.
|
|
1045
|
+
|
|
1046
|
+
Raises:
|
|
1047
|
+
StopIteration: When no more rows are available.
|
|
1048
|
+
"""
|
|
1049
|
+
self._check_closed()
|
|
1050
|
+
row = self.fetchone()
|
|
1051
|
+
if row is None:
|
|
1052
|
+
raise StopIteration
|
|
1053
|
+
return row
|
|
1054
|
+
|
|
1055
|
+
def next(self):
|
|
1056
|
+
"""
|
|
1057
|
+
Fetch the next row from the cursor.
|
|
1058
|
+
|
|
1059
|
+
This is an alias for __next__() to maintain compatibility with older code.
|
|
1060
|
+
|
|
1061
|
+
Returns:
|
|
1062
|
+
The next Row object.
|
|
1063
|
+
|
|
1064
|
+
Raises:
|
|
1065
|
+
StopIteration: When no more rows are available.
|
|
1066
|
+
"""
|
|
1067
|
+
return next(self)
|
|
1068
|
+
|
|
1069
|
+
def execute( # pylint: disable=too-many-locals,too-many-branches,too-many-statements
|
|
1070
|
+
self,
|
|
1071
|
+
operation: str,
|
|
1072
|
+
*parameters,
|
|
1073
|
+
use_prepare: bool = True,
|
|
1074
|
+
reset_cursor: bool = True,
|
|
1075
|
+
) -> "Cursor":
|
|
1076
|
+
"""
|
|
1077
|
+
Prepare and execute a database operation (query or command).
|
|
1078
|
+
|
|
1079
|
+
Args:
|
|
1080
|
+
operation: SQL query or command.
|
|
1081
|
+
parameters: Sequence of parameters to bind.
|
|
1082
|
+
use_prepare: Whether to use SQLPrepareW (default) or SQLExecDirectW.
|
|
1083
|
+
reset_cursor: Whether to reset the cursor before execution.
|
|
1084
|
+
"""
|
|
1085
|
+
logger.debug('execute: Starting - operation_length=%d, param_count=%d, use_prepare=%s',
|
|
1086
|
+
len(operation), len(parameters), str(use_prepare))
|
|
1087
|
+
|
|
1088
|
+
# Log the actual query being executed
|
|
1089
|
+
logger.debug('Executing query: %s', operation)
|
|
1090
|
+
|
|
1091
|
+
# Restore original fetch methods if they exist
|
|
1092
|
+
if hasattr(self, "_original_fetchone"):
|
|
1093
|
+
logger.debug('execute: Restoring original fetch methods')
|
|
1094
|
+
self.fetchone = self._original_fetchone
|
|
1095
|
+
self.fetchmany = self._original_fetchmany
|
|
1096
|
+
self.fetchall = self._original_fetchall
|
|
1097
|
+
del self._original_fetchone
|
|
1098
|
+
del self._original_fetchmany
|
|
1099
|
+
del self._original_fetchall
|
|
1100
|
+
|
|
1101
|
+
self._check_closed() # Check if the cursor is closed
|
|
1102
|
+
if reset_cursor:
|
|
1103
|
+
logger.debug('execute: Resetting cursor state')
|
|
1104
|
+
self._reset_cursor()
|
|
1105
|
+
|
|
1106
|
+
# Clear any previous messages
|
|
1107
|
+
self.messages = []
|
|
1108
|
+
|
|
1109
|
+
# Apply timeout if set (non-zero)
|
|
1110
|
+
if self._timeout > 0:
|
|
1111
|
+
logger.debug('execute: Setting query timeout=%d seconds', self._timeout)
|
|
1112
|
+
try:
|
|
1113
|
+
timeout_value = int(self._timeout)
|
|
1114
|
+
ret = ddbc_bindings.DDBCSQLSetStmtAttr(
|
|
1115
|
+
self.hstmt,
|
|
1116
|
+
ddbc_sql_const.SQL_ATTR_QUERY_TIMEOUT.value,
|
|
1117
|
+
timeout_value,
|
|
1118
|
+
)
|
|
1119
|
+
check_error(ddbc_sql_const.SQL_HANDLE_STMT.value, self.hstmt, ret)
|
|
1120
|
+
logger.debug("Set query timeout to %d seconds", timeout_value)
|
|
1121
|
+
except Exception as e: # pylint: disable=broad-exception-caught
|
|
1122
|
+
logger.warning("Failed to set query timeout: %s", str(e))
|
|
1123
|
+
|
|
1124
|
+
logger.debug('execute: Creating parameter type list')
|
|
1125
|
+
param_info = ddbc_bindings.ParamInfo
|
|
1126
|
+
parameters_type = []
|
|
1127
|
+
|
|
1128
|
+
# Flatten parameters if a single tuple or list is passed
|
|
1129
|
+
if len(parameters) == 1 and isinstance(parameters[0], (tuple, list)):
|
|
1130
|
+
parameters = parameters[0]
|
|
1131
|
+
|
|
1132
|
+
parameters = list(parameters)
|
|
1133
|
+
|
|
1134
|
+
# Validate that inputsizes matches parameter count if both are present
|
|
1135
|
+
if parameters and self._inputsizes:
|
|
1136
|
+
if len(self._inputsizes) != len(parameters):
|
|
1137
|
+
|
|
1138
|
+
warnings.warn(
|
|
1139
|
+
f"Number of input sizes ({len(self._inputsizes)}) does not match "
|
|
1140
|
+
f"number of parameters ({len(parameters)}). "
|
|
1141
|
+
f"This may lead to unexpected behavior.",
|
|
1142
|
+
Warning,
|
|
1143
|
+
)
|
|
1144
|
+
|
|
1145
|
+
if parameters:
|
|
1146
|
+
for i, param in enumerate(parameters):
|
|
1147
|
+
paraminfo = self._create_parameter_types_list(
|
|
1148
|
+
param, param_info, parameters, i
|
|
1149
|
+
)
|
|
1150
|
+
parameters_type.append(paraminfo)
|
|
1151
|
+
|
|
1152
|
+
# TODO: Use a more sophisticated string compare that handles redundant spaces etc.
|
|
1153
|
+
# Also consider storing last query's hash instead of full query string. This will help
|
|
1154
|
+
# in low-memory conditions
|
|
1155
|
+
# (Ex: huge number of parallel queries with huge query string sizes)
|
|
1156
|
+
if operation != self.last_executed_stmt:
|
|
1157
|
+
# Executing a new statement. Reset is_stmt_prepared to false
|
|
1158
|
+
self.is_stmt_prepared = [False]
|
|
1159
|
+
|
|
1160
|
+
for i, param in enumerate(parameters):
|
|
1161
|
+
logger.debug(
|
|
1162
|
+
"""Parameter number: %s, Parameter: %s,
|
|
1163
|
+
Param Python Type: %s, ParamInfo: %s, %s, %s, %s, %s""",
|
|
1164
|
+
i + 1,
|
|
1165
|
+
param,
|
|
1166
|
+
str(type(param)),
|
|
1167
|
+
parameters_type[i].paramSQLType,
|
|
1168
|
+
parameters_type[i].paramCType,
|
|
1169
|
+
parameters_type[i].columnSize,
|
|
1170
|
+
parameters_type[i].decimalDigits,
|
|
1171
|
+
parameters_type[i].inputOutputType,
|
|
1172
|
+
)
|
|
1173
|
+
|
|
1174
|
+
ret = ddbc_bindings.DDBCSQLExecute(
|
|
1175
|
+
self.hstmt,
|
|
1176
|
+
operation,
|
|
1177
|
+
parameters,
|
|
1178
|
+
parameters_type,
|
|
1179
|
+
self.is_stmt_prepared,
|
|
1180
|
+
use_prepare,
|
|
1181
|
+
)
|
|
1182
|
+
# Check return code
|
|
1183
|
+
try:
|
|
1184
|
+
|
|
1185
|
+
# Check for errors but don't raise exceptions for info/warning messages
|
|
1186
|
+
check_error(ddbc_sql_const.SQL_HANDLE_STMT.value, self.hstmt, ret)
|
|
1187
|
+
except Exception as e: # pylint: disable=broad-exception-caught
|
|
1188
|
+
logger.warning( "Execute failed, resetting cursor: %s", e)
|
|
1189
|
+
self._reset_cursor()
|
|
1190
|
+
raise
|
|
1191
|
+
|
|
1192
|
+
# Capture any diagnostic messages (SQL_SUCCESS_WITH_INFO, etc.)
|
|
1193
|
+
if self.hstmt:
|
|
1194
|
+
self.messages.extend(ddbc_bindings.DDBCSQLGetAllDiagRecords(self.hstmt))
|
|
1195
|
+
|
|
1196
|
+
self.last_executed_stmt = operation
|
|
1197
|
+
|
|
1198
|
+
# Update rowcount after execution
|
|
1199
|
+
# TODO: rowcount return code from SQL needs to be handled
|
|
1200
|
+
self.rowcount = ddbc_bindings.DDBCSQLRowCount(self.hstmt)
|
|
1201
|
+
|
|
1202
|
+
# Initialize description after execution
|
|
1203
|
+
# After successful execution, initialize description if there are results
|
|
1204
|
+
column_metadata = []
|
|
1205
|
+
try:
|
|
1206
|
+
ddbc_bindings.DDBCSQLDescribeCol(self.hstmt, column_metadata)
|
|
1207
|
+
self._initialize_description(column_metadata)
|
|
1208
|
+
except Exception as e: # pylint: disable=broad-exception-caught
|
|
1209
|
+
# If describe fails, it's likely there are no results (e.g., for INSERT)
|
|
1210
|
+
self.description = None
|
|
1211
|
+
|
|
1212
|
+
# Reset rownumber for new result set (only for SELECT statements)
|
|
1213
|
+
if self.description: # If we have column descriptions, it's likely a SELECT
|
|
1214
|
+
self.rowcount = -1
|
|
1215
|
+
self._reset_rownumber()
|
|
1216
|
+
# Pre-build column map and converter map
|
|
1217
|
+
self._cached_column_map = {col_desc[0]: i for i, col_desc in enumerate(self.description)}
|
|
1218
|
+
self._cached_converter_map = self._build_converter_map()
|
|
1219
|
+
else:
|
|
1220
|
+
self.rowcount = ddbc_bindings.DDBCSQLRowCount(self.hstmt)
|
|
1221
|
+
self._clear_rownumber()
|
|
1222
|
+
self._cached_column_map = None
|
|
1223
|
+
self._cached_converter_map = None
|
|
1224
|
+
|
|
1225
|
+
# After successful execution, initialize description if there are results
|
|
1226
|
+
column_metadata = []
|
|
1227
|
+
try:
|
|
1228
|
+
ddbc_bindings.DDBCSQLDescribeCol(self.hstmt, column_metadata)
|
|
1229
|
+
self._initialize_description(column_metadata)
|
|
1230
|
+
except Exception as e:
|
|
1231
|
+
# If describe fails, it's likely there are no results (e.g., for INSERT)
|
|
1232
|
+
self.description = None
|
|
1233
|
+
|
|
1234
|
+
self._reset_inputsizes() # Reset input sizes after execution
|
|
1235
|
+
# Return self for method chaining
|
|
1236
|
+
return self
|
|
1237
|
+
|
|
1238
|
+
def _prepare_metadata_result_set( # pylint: disable=too-many-statements
|
|
1239
|
+
self, column_metadata=None, fallback_description=None, specialized_mapping=None
|
|
1240
|
+
):
|
|
1241
|
+
"""
|
|
1242
|
+
Prepares a metadata result set by:
|
|
1243
|
+
1. Retrieving column metadata if not provided
|
|
1244
|
+
2. Initializing the description attribute
|
|
1245
|
+
3. Setting up column name mappings
|
|
1246
|
+
4. Creating wrapper fetch methods with column mapping support
|
|
1247
|
+
|
|
1248
|
+
Args:
|
|
1249
|
+
column_metadata (list, optional): Pre-fetched column metadata.
|
|
1250
|
+
If None, it will be retrieved.
|
|
1251
|
+
fallback_description (list, optional): Fallback description to use if
|
|
1252
|
+
metadata retrieval fails.
|
|
1253
|
+
specialized_mapping (dict, optional): Custom column mapping for special cases.
|
|
1254
|
+
|
|
1255
|
+
Returns:
|
|
1256
|
+
Cursor: Self, for method chaining
|
|
1257
|
+
"""
|
|
1258
|
+
# Retrieve column metadata if not provided
|
|
1259
|
+
if column_metadata is None:
|
|
1260
|
+
column_metadata = []
|
|
1261
|
+
try:
|
|
1262
|
+
ddbc_bindings.DDBCSQLDescribeCol(self.hstmt, column_metadata)
|
|
1263
|
+
except InterfaceError as e:
|
|
1264
|
+
logger.warning( f"Driver interface error during metadata retrieval: {e}")
|
|
1265
|
+
except Exception as e: # pylint: disable=broad-exception-caught
|
|
1266
|
+
# Log the exception with appropriate context
|
|
1267
|
+
logger.warning(
|
|
1268
|
+
f"Failed to retrieve column metadata: {e}. "
|
|
1269
|
+
f"Using standard ODBC column definitions instead.",
|
|
1270
|
+
)
|
|
1271
|
+
|
|
1272
|
+
# Initialize the description attribute with the column metadata
|
|
1273
|
+
self._initialize_description(column_metadata)
|
|
1274
|
+
|
|
1275
|
+
# Use fallback description if provided and current description is empty
|
|
1276
|
+
if not self.description and fallback_description:
|
|
1277
|
+
self.description = fallback_description
|
|
1278
|
+
|
|
1279
|
+
# Define column names in ODBC standard order
|
|
1280
|
+
self._column_map = {} # pylint: disable=attribute-defined-outside-init
|
|
1281
|
+
for i, (name, *_) in enumerate(self.description):
|
|
1282
|
+
# Add standard name
|
|
1283
|
+
self._column_map[name] = i
|
|
1284
|
+
# Add lowercase alias
|
|
1285
|
+
self._column_map[name.lower()] = i
|
|
1286
|
+
|
|
1287
|
+
# If specialized mapping is provided, handle it differently
|
|
1288
|
+
if specialized_mapping:
|
|
1289
|
+
# Define specialized fetch methods that use the custom mapping
|
|
1290
|
+
def fetchone_with_specialized_mapping():
|
|
1291
|
+
row = self._original_fetchone()
|
|
1292
|
+
if row is not None:
|
|
1293
|
+
merged_map = getattr(row, "_column_map", {}).copy()
|
|
1294
|
+
merged_map.update(specialized_mapping)
|
|
1295
|
+
row._column_map = merged_map
|
|
1296
|
+
return row
|
|
1297
|
+
|
|
1298
|
+
def fetchmany_with_specialized_mapping(size=None):
|
|
1299
|
+
rows = self._original_fetchmany(size)
|
|
1300
|
+
for row in rows:
|
|
1301
|
+
merged_map = getattr(row, "_column_map", {}).copy()
|
|
1302
|
+
merged_map.update(specialized_mapping)
|
|
1303
|
+
row._column_map = merged_map
|
|
1304
|
+
return rows
|
|
1305
|
+
|
|
1306
|
+
def fetchall_with_specialized_mapping():
|
|
1307
|
+
rows = self._original_fetchall()
|
|
1308
|
+
for row in rows:
|
|
1309
|
+
merged_map = getattr(row, "_column_map", {}).copy()
|
|
1310
|
+
merged_map.update(specialized_mapping)
|
|
1311
|
+
row._column_map = merged_map
|
|
1312
|
+
return rows
|
|
1313
|
+
|
|
1314
|
+
# Save original fetch methods
|
|
1315
|
+
if not hasattr(self, "_original_fetchone"):
|
|
1316
|
+
self._original_fetchone = self.fetchone # pylint: disable=attribute-defined-outside-init
|
|
1317
|
+
self._original_fetchmany = self.fetchmany # pylint: disable=attribute-defined-outside-init
|
|
1318
|
+
self._original_fetchall = self.fetchall # pylint: disable=attribute-defined-outside-init
|
|
1319
|
+
|
|
1320
|
+
# Use specialized mapping methods
|
|
1321
|
+
self.fetchone = fetchone_with_specialized_mapping
|
|
1322
|
+
self.fetchmany = fetchmany_with_specialized_mapping
|
|
1323
|
+
self.fetchall = fetchall_with_specialized_mapping
|
|
1324
|
+
else:
|
|
1325
|
+
# Standard column mapping
|
|
1326
|
+
# Remember original fetch methods (store only once)
|
|
1327
|
+
if not hasattr(self, "_original_fetchone"):
|
|
1328
|
+
self._original_fetchone = self.fetchone # pylint: disable=attribute-defined-outside-init
|
|
1329
|
+
self._original_fetchmany = self.fetchmany # pylint: disable=attribute-defined-outside-init
|
|
1330
|
+
self._original_fetchall = self.fetchall # pylint: disable=attribute-defined-outside-init
|
|
1331
|
+
|
|
1332
|
+
# Create wrapper fetch methods that add column mappings
|
|
1333
|
+
def fetchone_with_mapping():
|
|
1334
|
+
row = self._original_fetchone()
|
|
1335
|
+
if row is not None:
|
|
1336
|
+
row._column_map = self._column_map
|
|
1337
|
+
return row
|
|
1338
|
+
|
|
1339
|
+
def fetchmany_with_mapping(size=None):
|
|
1340
|
+
rows = self._original_fetchmany(size)
|
|
1341
|
+
for row in rows:
|
|
1342
|
+
row._column_map = self._column_map
|
|
1343
|
+
return rows
|
|
1344
|
+
|
|
1345
|
+
def fetchall_with_mapping():
|
|
1346
|
+
rows = self._original_fetchall()
|
|
1347
|
+
for row in rows:
|
|
1348
|
+
row._column_map = self._column_map
|
|
1349
|
+
return rows
|
|
1350
|
+
|
|
1351
|
+
# Replace fetch methods
|
|
1352
|
+
self.fetchone = fetchone_with_mapping
|
|
1353
|
+
self.fetchmany = fetchmany_with_mapping
|
|
1354
|
+
self.fetchall = fetchall_with_mapping
|
|
1355
|
+
|
|
1356
|
+
# Return the cursor itself for method chaining
|
|
1357
|
+
return self
|
|
1358
|
+
|
|
1359
|
+
def getTypeInfo(self, sqlType=None):
|
|
1360
|
+
"""
|
|
1361
|
+
Executes SQLGetTypeInfo and creates a result set with information about
|
|
1362
|
+
the specified data type or all data types supported by the ODBC driver if not specified.
|
|
1363
|
+
"""
|
|
1364
|
+
self._check_closed()
|
|
1365
|
+
self._reset_cursor()
|
|
1366
|
+
|
|
1367
|
+
sql_all_types = 0 # SQL_ALL_TYPES = 0
|
|
1368
|
+
|
|
1369
|
+
try:
|
|
1370
|
+
# Get information about data types
|
|
1371
|
+
ret = ddbc_bindings.DDBCSQLGetTypeInfo(
|
|
1372
|
+
self.hstmt, sqlType if sqlType is not None else sql_all_types
|
|
1373
|
+
)
|
|
1374
|
+
check_error(ddbc_sql_const.SQL_HANDLE_STMT.value, self.hstmt, ret)
|
|
1375
|
+
|
|
1376
|
+
# Use the helper method to prepare the result set
|
|
1377
|
+
return self._prepare_metadata_result_set()
|
|
1378
|
+
except Exception as e: # pylint: disable=broad-exception-caught
|
|
1379
|
+
self._reset_cursor()
|
|
1380
|
+
raise e
|
|
1381
|
+
|
|
1382
|
+
def procedures(self, procedure=None, catalog=None, schema=None):
|
|
1383
|
+
"""
|
|
1384
|
+
Executes SQLProcedures and creates a result set of information about procedures
|
|
1385
|
+
in the data source.
|
|
1386
|
+
|
|
1387
|
+
Args:
|
|
1388
|
+
procedure (str, optional): Procedure name pattern. Default is None (all procedures).
|
|
1389
|
+
catalog (str, optional): Catalog name pattern. Default is None (current catalog).
|
|
1390
|
+
schema (str, optional): Schema name pattern. Default is None (all schemas).
|
|
1391
|
+
"""
|
|
1392
|
+
self._check_closed()
|
|
1393
|
+
self._reset_cursor()
|
|
1394
|
+
|
|
1395
|
+
# Call the SQLProcedures function
|
|
1396
|
+
retcode = ddbc_bindings.DDBCSQLProcedures(
|
|
1397
|
+
self.hstmt, catalog, schema, procedure
|
|
1398
|
+
)
|
|
1399
|
+
check_error(ddbc_sql_const.SQL_HANDLE_STMT.value, self.hstmt, retcode)
|
|
1400
|
+
|
|
1401
|
+
# Define fallback description for procedures
|
|
1402
|
+
fallback_description = [
|
|
1403
|
+
("procedure_cat", str, None, 128, 128, 0, True),
|
|
1404
|
+
("procedure_schem", str, None, 128, 128, 0, True),
|
|
1405
|
+
("procedure_name", str, None, 128, 128, 0, False),
|
|
1406
|
+
("num_input_params", int, None, 10, 10, 0, True),
|
|
1407
|
+
("num_output_params", int, None, 10, 10, 0, True),
|
|
1408
|
+
("num_result_sets", int, None, 10, 10, 0, True),
|
|
1409
|
+
("remarks", str, None, 254, 254, 0, True),
|
|
1410
|
+
("procedure_type", int, None, 10, 10, 0, False),
|
|
1411
|
+
]
|
|
1412
|
+
|
|
1413
|
+
# Use the helper method to prepare the result set
|
|
1414
|
+
return self._prepare_metadata_result_set(
|
|
1415
|
+
fallback_description=fallback_description
|
|
1416
|
+
)
|
|
1417
|
+
|
|
1418
|
+
def primaryKeys(self, table, catalog=None, schema=None):
|
|
1419
|
+
"""
|
|
1420
|
+
Creates a result set of column names that make up the primary key for a table
|
|
1421
|
+
by executing the SQLPrimaryKeys function.
|
|
1422
|
+
|
|
1423
|
+
Args:
|
|
1424
|
+
table (str): The name of the table
|
|
1425
|
+
catalog (str, optional): The catalog name (database). Defaults to None.
|
|
1426
|
+
schema (str, optional): The schema name. Defaults to None.
|
|
1427
|
+
"""
|
|
1428
|
+
self._check_closed()
|
|
1429
|
+
self._reset_cursor()
|
|
1430
|
+
|
|
1431
|
+
if not table:
|
|
1432
|
+
raise ProgrammingError("Table name must be specified", "HY000")
|
|
1433
|
+
|
|
1434
|
+
# Call the SQLPrimaryKeys function
|
|
1435
|
+
retcode = ddbc_bindings.DDBCSQLPrimaryKeys(self.hstmt, catalog, schema, table)
|
|
1436
|
+
check_error(ddbc_sql_const.SQL_HANDLE_STMT.value, self.hstmt, retcode)
|
|
1437
|
+
|
|
1438
|
+
# Define fallback description for primary keys
|
|
1439
|
+
fallback_description = [
|
|
1440
|
+
("table_cat", str, None, 128, 128, 0, True),
|
|
1441
|
+
("table_schem", str, None, 128, 128, 0, True),
|
|
1442
|
+
("table_name", str, None, 128, 128, 0, False),
|
|
1443
|
+
("column_name", str, None, 128, 128, 0, False),
|
|
1444
|
+
("key_seq", int, None, 10, 10, 0, False),
|
|
1445
|
+
("pk_name", str, None, 128, 128, 0, True),
|
|
1446
|
+
]
|
|
1447
|
+
|
|
1448
|
+
# Use the helper method to prepare the result set
|
|
1449
|
+
return self._prepare_metadata_result_set(
|
|
1450
|
+
fallback_description=fallback_description
|
|
1451
|
+
)
|
|
1452
|
+
|
|
1453
|
+
def foreignKeys( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
1454
|
+
self,
|
|
1455
|
+
table=None,
|
|
1456
|
+
catalog=None,
|
|
1457
|
+
schema=None,
|
|
1458
|
+
foreignTable=None,
|
|
1459
|
+
foreignCatalog=None,
|
|
1460
|
+
foreignSchema=None,
|
|
1461
|
+
):
|
|
1462
|
+
"""
|
|
1463
|
+
Executes the SQLForeignKeys function and creates a result set of column names
|
|
1464
|
+
that are foreign keys.
|
|
1465
|
+
|
|
1466
|
+
This function returns:
|
|
1467
|
+
1. Foreign keys in the specified table that reference primary keys in other tables, OR
|
|
1468
|
+
2. Foreign keys in other tables that reference the primary key in the specified table
|
|
1469
|
+
"""
|
|
1470
|
+
self._check_closed()
|
|
1471
|
+
self._reset_cursor()
|
|
1472
|
+
|
|
1473
|
+
# Check if we have at least one table specified
|
|
1474
|
+
if table is None and foreignTable is None:
|
|
1475
|
+
raise ProgrammingError(
|
|
1476
|
+
"Either table or foreignTable must be specified", "HY000"
|
|
1477
|
+
)
|
|
1478
|
+
|
|
1479
|
+
# Call the SQLForeignKeys function
|
|
1480
|
+
retcode = ddbc_bindings.DDBCSQLForeignKeys(
|
|
1481
|
+
self.hstmt,
|
|
1482
|
+
foreignCatalog,
|
|
1483
|
+
foreignSchema,
|
|
1484
|
+
foreignTable,
|
|
1485
|
+
catalog,
|
|
1486
|
+
schema,
|
|
1487
|
+
table,
|
|
1488
|
+
)
|
|
1489
|
+
check_error(ddbc_sql_const.SQL_HANDLE_STMT.value, self.hstmt, retcode)
|
|
1490
|
+
|
|
1491
|
+
# Define fallback description for foreign keys
|
|
1492
|
+
fallback_description = [
|
|
1493
|
+
("pktable_cat", str, None, 128, 128, 0, True),
|
|
1494
|
+
("pktable_schem", str, None, 128, 128, 0, True),
|
|
1495
|
+
("pktable_name", str, None, 128, 128, 0, False),
|
|
1496
|
+
("pkcolumn_name", str, None, 128, 128, 0, False),
|
|
1497
|
+
("fktable_cat", str, None, 128, 128, 0, True),
|
|
1498
|
+
("fktable_schem", str, None, 128, 128, 0, True),
|
|
1499
|
+
("fktable_name", str, None, 128, 128, 0, False),
|
|
1500
|
+
("fkcolumn_name", str, None, 128, 128, 0, False),
|
|
1501
|
+
("key_seq", int, None, 10, 10, 0, False),
|
|
1502
|
+
("update_rule", int, None, 10, 10, 0, False),
|
|
1503
|
+
("delete_rule", int, None, 10, 10, 0, False),
|
|
1504
|
+
("fk_name", str, None, 128, 128, 0, True),
|
|
1505
|
+
("pk_name", str, None, 128, 128, 0, True),
|
|
1506
|
+
("deferrability", int, None, 10, 10, 0, False),
|
|
1507
|
+
]
|
|
1508
|
+
|
|
1509
|
+
# Use the helper method to prepare the result set
|
|
1510
|
+
return self._prepare_metadata_result_set(
|
|
1511
|
+
fallback_description=fallback_description
|
|
1512
|
+
)
|
|
1513
|
+
|
|
1514
|
+
def rowIdColumns(self, table, catalog=None, schema=None, nullable=True):
|
|
1515
|
+
"""
|
|
1516
|
+
Executes SQLSpecialColumns with SQL_BEST_ROWID which creates a result set of
|
|
1517
|
+
columns that uniquely identify a row.
|
|
1518
|
+
"""
|
|
1519
|
+
self._check_closed()
|
|
1520
|
+
self._reset_cursor()
|
|
1521
|
+
|
|
1522
|
+
if not table:
|
|
1523
|
+
raise ProgrammingError("Table name must be specified", "HY000")
|
|
1524
|
+
|
|
1525
|
+
# Set the identifier type and options
|
|
1526
|
+
identifier_type = ddbc_sql_const.SQL_BEST_ROWID.value
|
|
1527
|
+
scope = ddbc_sql_const.SQL_SCOPE_CURROW.value
|
|
1528
|
+
nullable_flag = (
|
|
1529
|
+
ddbc_sql_const.SQL_NULLABLE.value
|
|
1530
|
+
if nullable
|
|
1531
|
+
else ddbc_sql_const.SQL_NO_NULLS.value
|
|
1532
|
+
)
|
|
1533
|
+
|
|
1534
|
+
# Call the SQLSpecialColumns function
|
|
1535
|
+
retcode = ddbc_bindings.DDBCSQLSpecialColumns(
|
|
1536
|
+
self.hstmt, identifier_type, catalog, schema, table, scope, nullable_flag
|
|
1537
|
+
)
|
|
1538
|
+
check_error(ddbc_sql_const.SQL_HANDLE_STMT.value, self.hstmt, retcode)
|
|
1539
|
+
|
|
1540
|
+
# Define fallback description for special columns
|
|
1541
|
+
fallback_description = [
|
|
1542
|
+
("scope", int, None, 10, 10, 0, False),
|
|
1543
|
+
("column_name", str, None, 128, 128, 0, False),
|
|
1544
|
+
("data_type", int, None, 10, 10, 0, False),
|
|
1545
|
+
("type_name", str, None, 128, 128, 0, False),
|
|
1546
|
+
("column_size", int, None, 10, 10, 0, False),
|
|
1547
|
+
("buffer_length", int, None, 10, 10, 0, False),
|
|
1548
|
+
("decimal_digits", int, None, 10, 10, 0, True),
|
|
1549
|
+
("pseudo_column", int, None, 10, 10, 0, False),
|
|
1550
|
+
]
|
|
1551
|
+
|
|
1552
|
+
# Use the helper method to prepare the result set
|
|
1553
|
+
return self._prepare_metadata_result_set(
|
|
1554
|
+
fallback_description=fallback_description
|
|
1555
|
+
)
|
|
1556
|
+
|
|
1557
|
+
def rowVerColumns(self, table, catalog=None, schema=None, nullable=True):
|
|
1558
|
+
"""
|
|
1559
|
+
Executes SQLSpecialColumns with SQL_ROWVER which creates a result set of
|
|
1560
|
+
columns that are automatically updated when any value in the row is updated.
|
|
1561
|
+
"""
|
|
1562
|
+
self._check_closed()
|
|
1563
|
+
self._reset_cursor()
|
|
1564
|
+
|
|
1565
|
+
if not table:
|
|
1566
|
+
raise ProgrammingError("Table name must be specified", "HY000")
|
|
1567
|
+
|
|
1568
|
+
# Set the identifier type and options
|
|
1569
|
+
identifier_type = ddbc_sql_const.SQL_ROWVER.value
|
|
1570
|
+
scope = ddbc_sql_const.SQL_SCOPE_CURROW.value
|
|
1571
|
+
nullable_flag = (
|
|
1572
|
+
ddbc_sql_const.SQL_NULLABLE.value
|
|
1573
|
+
if nullable
|
|
1574
|
+
else ddbc_sql_const.SQL_NO_NULLS.value
|
|
1575
|
+
)
|
|
1576
|
+
|
|
1577
|
+
# Call the SQLSpecialColumns function
|
|
1578
|
+
retcode = ddbc_bindings.DDBCSQLSpecialColumns(
|
|
1579
|
+
self.hstmt, identifier_type, catalog, schema, table, scope, nullable_flag
|
|
1580
|
+
)
|
|
1581
|
+
check_error(ddbc_sql_const.SQL_HANDLE_STMT.value, self.hstmt, retcode)
|
|
1582
|
+
|
|
1583
|
+
# Same fallback description as rowIdColumns
|
|
1584
|
+
fallback_description = [
|
|
1585
|
+
("scope", int, None, 10, 10, 0, False),
|
|
1586
|
+
("column_name", str, None, 128, 128, 0, False),
|
|
1587
|
+
("data_type", int, None, 10, 10, 0, False),
|
|
1588
|
+
("type_name", str, None, 128, 128, 0, False),
|
|
1589
|
+
("column_size", int, None, 10, 10, 0, False),
|
|
1590
|
+
("buffer_length", int, None, 10, 10, 0, False),
|
|
1591
|
+
("decimal_digits", int, None, 10, 10, 0, True),
|
|
1592
|
+
("pseudo_column", int, None, 10, 10, 0, False),
|
|
1593
|
+
]
|
|
1594
|
+
|
|
1595
|
+
# Use the helper method to prepare the result set
|
|
1596
|
+
return self._prepare_metadata_result_set(
|
|
1597
|
+
fallback_description=fallback_description
|
|
1598
|
+
)
|
|
1599
|
+
|
|
1600
|
+
def statistics( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
1601
|
+
self,
|
|
1602
|
+
table: str,
|
|
1603
|
+
catalog: str = None,
|
|
1604
|
+
schema: str = None,
|
|
1605
|
+
unique: bool = False,
|
|
1606
|
+
quick: bool = True,
|
|
1607
|
+
) -> "Cursor":
|
|
1608
|
+
"""
|
|
1609
|
+
Creates a result set of statistics about a single table and the indexes associated
|
|
1610
|
+
with the table by executing SQLStatistics.
|
|
1611
|
+
"""
|
|
1612
|
+
self._check_closed()
|
|
1613
|
+
self._reset_cursor()
|
|
1614
|
+
|
|
1615
|
+
if not table:
|
|
1616
|
+
raise ProgrammingError("Table name is required", "HY000")
|
|
1617
|
+
|
|
1618
|
+
# Set unique and quick flags
|
|
1619
|
+
unique_option = (
|
|
1620
|
+
ddbc_sql_const.SQL_INDEX_UNIQUE.value
|
|
1621
|
+
if unique
|
|
1622
|
+
else ddbc_sql_const.SQL_INDEX_ALL.value
|
|
1623
|
+
)
|
|
1624
|
+
reserved_option = (
|
|
1625
|
+
ddbc_sql_const.SQL_QUICK.value if quick else ddbc_sql_const.SQL_ENSURE.value
|
|
1626
|
+
)
|
|
1627
|
+
|
|
1628
|
+
# Call the SQLStatistics function
|
|
1629
|
+
retcode = ddbc_bindings.DDBCSQLStatistics(
|
|
1630
|
+
self.hstmt, catalog, schema, table, unique_option, reserved_option
|
|
1631
|
+
)
|
|
1632
|
+
check_error(ddbc_sql_const.SQL_HANDLE_STMT.value, self.hstmt, retcode)
|
|
1633
|
+
|
|
1634
|
+
# Define fallback description for statistics
|
|
1635
|
+
fallback_description = [
|
|
1636
|
+
("table_cat", str, None, 128, 128, 0, True),
|
|
1637
|
+
("table_schem", str, None, 128, 128, 0, True),
|
|
1638
|
+
("table_name", str, None, 128, 128, 0, False),
|
|
1639
|
+
("non_unique", bool, None, 1, 1, 0, False),
|
|
1640
|
+
("index_qualifier", str, None, 128, 128, 0, True),
|
|
1641
|
+
("index_name", str, None, 128, 128, 0, True),
|
|
1642
|
+
("type", int, None, 10, 10, 0, False),
|
|
1643
|
+
("ordinal_position", int, None, 10, 10, 0, False),
|
|
1644
|
+
("column_name", str, None, 128, 128, 0, True),
|
|
1645
|
+
("asc_or_desc", str, None, 1, 1, 0, True),
|
|
1646
|
+
("cardinality", int, None, 20, 20, 0, True),
|
|
1647
|
+
("pages", int, None, 20, 20, 0, True),
|
|
1648
|
+
("filter_condition", str, None, 128, 128, 0, True),
|
|
1649
|
+
]
|
|
1650
|
+
|
|
1651
|
+
# Use the helper method to prepare the result set
|
|
1652
|
+
return self._prepare_metadata_result_set(
|
|
1653
|
+
fallback_description=fallback_description
|
|
1654
|
+
)
|
|
1655
|
+
|
|
1656
|
+
def columns(self, table=None, catalog=None, schema=None, column=None):
|
|
1657
|
+
"""
|
|
1658
|
+
Creates a result set of column information in the specified tables
|
|
1659
|
+
using the SQLColumns function.
|
|
1660
|
+
"""
|
|
1661
|
+
self._check_closed()
|
|
1662
|
+
self._reset_cursor()
|
|
1663
|
+
|
|
1664
|
+
# Call the SQLColumns function
|
|
1665
|
+
retcode = ddbc_bindings.DDBCSQLColumns(
|
|
1666
|
+
self.hstmt, catalog, schema, table, column
|
|
1667
|
+
)
|
|
1668
|
+
check_error(ddbc_sql_const.SQL_HANDLE_STMT.value, self.hstmt, retcode)
|
|
1669
|
+
|
|
1670
|
+
# Define fallback description for columns
|
|
1671
|
+
fallback_description = [
|
|
1672
|
+
("table_cat", str, None, 128, 128, 0, True),
|
|
1673
|
+
("table_schem", str, None, 128, 128, 0, True),
|
|
1674
|
+
("table_name", str, None, 128, 128, 0, False),
|
|
1675
|
+
("column_name", str, None, 128, 128, 0, False),
|
|
1676
|
+
("data_type", int, None, 10, 10, 0, False),
|
|
1677
|
+
("type_name", str, None, 128, 128, 0, False),
|
|
1678
|
+
("column_size", int, None, 10, 10, 0, True),
|
|
1679
|
+
("buffer_length", int, None, 10, 10, 0, True),
|
|
1680
|
+
("decimal_digits", int, None, 10, 10, 0, True),
|
|
1681
|
+
("num_prec_radix", int, None, 10, 10, 0, True),
|
|
1682
|
+
("nullable", int, None, 10, 10, 0, False),
|
|
1683
|
+
("remarks", str, None, 254, 254, 0, True),
|
|
1684
|
+
("column_def", str, None, 254, 254, 0, True),
|
|
1685
|
+
("sql_data_type", int, None, 10, 10, 0, False),
|
|
1686
|
+
("sql_datetime_sub", int, None, 10, 10, 0, True),
|
|
1687
|
+
("char_octet_length", int, None, 10, 10, 0, True),
|
|
1688
|
+
("ordinal_position", int, None, 10, 10, 0, False),
|
|
1689
|
+
("is_nullable", str, None, 254, 254, 0, True),
|
|
1690
|
+
]
|
|
1691
|
+
|
|
1692
|
+
# Use the helper method to prepare the result set
|
|
1693
|
+
return self._prepare_metadata_result_set(
|
|
1694
|
+
fallback_description=fallback_description
|
|
1695
|
+
)
|
|
1696
|
+
|
|
1697
|
+
def _transpose_rowwise_to_columnwise(
|
|
1698
|
+
self, seq_of_parameters: list
|
|
1699
|
+
) -> tuple[list, int]:
|
|
1700
|
+
"""
|
|
1701
|
+
Convert sequence of rows (row-wise) into list of columns (column-wise),
|
|
1702
|
+
for array binding via ODBC. Works with both iterables and generators.
|
|
1703
|
+
|
|
1704
|
+
Args:
|
|
1705
|
+
seq_of_parameters: Sequence of sequences or mappings of parameters.
|
|
1706
|
+
|
|
1707
|
+
Returns:
|
|
1708
|
+
tuple: (columnwise_data, row_count)
|
|
1709
|
+
"""
|
|
1710
|
+
columnwise = []
|
|
1711
|
+
first_row = True
|
|
1712
|
+
row_count = 0
|
|
1713
|
+
|
|
1714
|
+
for row in seq_of_parameters:
|
|
1715
|
+
row_count += 1
|
|
1716
|
+
if first_row:
|
|
1717
|
+
# Initialize columnwise lists based on first row
|
|
1718
|
+
num_params = len(row)
|
|
1719
|
+
columnwise = [[] for _ in range(num_params)]
|
|
1720
|
+
first_row = False
|
|
1721
|
+
else:
|
|
1722
|
+
# Validate row size consistency
|
|
1723
|
+
if len(row) != num_params:
|
|
1724
|
+
raise ValueError("Inconsistent parameter row size in executemany()")
|
|
1725
|
+
|
|
1726
|
+
# Add each value to its column list
|
|
1727
|
+
for i, val in enumerate(row):
|
|
1728
|
+
columnwise[i].append(val)
|
|
1729
|
+
|
|
1730
|
+
return columnwise, row_count
|
|
1731
|
+
|
|
1732
|
+
def _compute_column_type(self, column):
|
|
1733
|
+
"""
|
|
1734
|
+
Determine representative value and integer min/max for a column.
|
|
1735
|
+
|
|
1736
|
+
Returns:
|
|
1737
|
+
sample_value: Representative value for type inference and modified_row.
|
|
1738
|
+
min_val: Minimum for integers (None otherwise).
|
|
1739
|
+
max_val: Maximum for integers (None otherwise).
|
|
1740
|
+
"""
|
|
1741
|
+
non_nulls = [v for v in column if v is not None]
|
|
1742
|
+
if not non_nulls:
|
|
1743
|
+
return None, None, None
|
|
1744
|
+
|
|
1745
|
+
int_values = [v for v in non_nulls if isinstance(v, int)]
|
|
1746
|
+
if int_values:
|
|
1747
|
+
min_val, max_val = min(int_values), max(int_values)
|
|
1748
|
+
sample_value = max(int_values, key=abs)
|
|
1749
|
+
return sample_value, min_val, max_val
|
|
1750
|
+
|
|
1751
|
+
sample_value = None
|
|
1752
|
+
for v in non_nulls:
|
|
1753
|
+
if not sample_value:
|
|
1754
|
+
sample_value = v
|
|
1755
|
+
elif isinstance(v, (str, bytes, bytearray)) and isinstance(sample_value, (str, bytes, bytearray)):
|
|
1756
|
+
# For string/binary objects, prefer the longer one
|
|
1757
|
+
# Use safe length comparison to avoid exceptions from custom __len__ implementations
|
|
1758
|
+
try:
|
|
1759
|
+
if len(v) > len(sample_value):
|
|
1760
|
+
sample_value = v
|
|
1761
|
+
except (TypeError, ValueError, AttributeError):
|
|
1762
|
+
# If length comparison fails, keep the current sample_value
|
|
1763
|
+
pass
|
|
1764
|
+
elif isinstance(v, decimal.Decimal) and isinstance(sample_value, decimal.Decimal):
|
|
1765
|
+
# For Decimal objects, prefer the one that requires higher precision or scale
|
|
1766
|
+
v_tuple = v.as_tuple()
|
|
1767
|
+
sample_tuple = sample_value.as_tuple()
|
|
1768
|
+
|
|
1769
|
+
# Calculate precision (total significant digits) and scale (decimal places)
|
|
1770
|
+
# For a number like 0.000123456789, we need precision = 9, scale = 12
|
|
1771
|
+
# The precision is the number of significant digits (len(digits))
|
|
1772
|
+
# The scale is the number of decimal places needed to represent the number
|
|
1773
|
+
|
|
1774
|
+
v_precision = len(v_tuple.digits)
|
|
1775
|
+
if v_tuple.exponent < 0:
|
|
1776
|
+
v_scale = -v_tuple.exponent
|
|
1777
|
+
else:
|
|
1778
|
+
v_scale = 0
|
|
1779
|
+
|
|
1780
|
+
sample_precision = len(sample_tuple.digits)
|
|
1781
|
+
if sample_tuple.exponent < 0:
|
|
1782
|
+
sample_scale = -sample_tuple.exponent
|
|
1783
|
+
else:
|
|
1784
|
+
sample_scale = 0
|
|
1785
|
+
|
|
1786
|
+
# For SQL DECIMAL(precision, scale), we need:
|
|
1787
|
+
# precision >= number of significant digits
|
|
1788
|
+
# scale >= number of decimal places
|
|
1789
|
+
# For 0.000123456789: precision needs to be at least 12 (to accommodate 12 decimal places)
|
|
1790
|
+
# So we need to adjust precision to be at least as large as scale
|
|
1791
|
+
v_required_precision = max(v_precision, v_scale)
|
|
1792
|
+
sample_required_precision = max(sample_precision, sample_scale)
|
|
1793
|
+
|
|
1794
|
+
# Prefer the decimal that requires higher precision or scale
|
|
1795
|
+
# This ensures we can accommodate all values in the column
|
|
1796
|
+
if (v_required_precision > sample_required_precision or
|
|
1797
|
+
(v_required_precision == sample_required_precision and v_scale > sample_scale)):
|
|
1798
|
+
sample_value = v
|
|
1799
|
+
elif isinstance(v, decimal.Decimal) and not isinstance(sample_value, decimal.Decimal):
|
|
1800
|
+
# If comparing Decimal to non-Decimal, prefer Decimal for better type inference
|
|
1801
|
+
sample_value = v
|
|
1802
|
+
|
|
1803
|
+
return sample_value, None, None
|
|
1804
|
+
|
|
1805
|
+
def executemany( # pylint: disable=too-many-locals,too-many-branches,too-many-statements
|
|
1806
|
+
self, operation: str, seq_of_parameters: List[Sequence[Any]]
|
|
1807
|
+
) -> None:
|
|
1808
|
+
"""
|
|
1809
|
+
Prepare a database operation and execute it against all parameter sequences.
|
|
1810
|
+
This version uses column-wise parameter binding and a single batched SQLExecute().
|
|
1811
|
+
Args:
|
|
1812
|
+
operation: SQL query or command.
|
|
1813
|
+
seq_of_parameters: Sequence of sequences or mappings of parameters.
|
|
1814
|
+
Raises:
|
|
1815
|
+
Error: If the operation fails.
|
|
1816
|
+
"""
|
|
1817
|
+
logger.debug( 'executemany: Starting - operation_length=%d, batch_count=%d',
|
|
1818
|
+
len(operation), len(seq_of_parameters))
|
|
1819
|
+
|
|
1820
|
+
self._check_closed()
|
|
1821
|
+
self._reset_cursor()
|
|
1822
|
+
self.messages = []
|
|
1823
|
+
logger.debug( 'executemany: Cursor reset complete')
|
|
1824
|
+
|
|
1825
|
+
if not seq_of_parameters:
|
|
1826
|
+
self.rowcount = 0
|
|
1827
|
+
return
|
|
1828
|
+
|
|
1829
|
+
# Apply timeout if set (non-zero)
|
|
1830
|
+
if self._timeout > 0:
|
|
1831
|
+
try:
|
|
1832
|
+
timeout_value = int(self._timeout)
|
|
1833
|
+
ret = ddbc_bindings.DDBCSQLSetStmtAttr(
|
|
1834
|
+
self.hstmt,
|
|
1835
|
+
ddbc_sql_const.SQL_ATTR_QUERY_TIMEOUT.value,
|
|
1836
|
+
timeout_value,
|
|
1837
|
+
)
|
|
1838
|
+
check_error(ddbc_sql_const.SQL_HANDLE_STMT.value, self.hstmt, ret)
|
|
1839
|
+
logger.debug( f"Set query timeout to {self._timeout} seconds")
|
|
1840
|
+
except Exception as e: # pylint: disable=broad-exception-caught
|
|
1841
|
+
logger.warning( f"Failed to set query timeout: {e}")
|
|
1842
|
+
|
|
1843
|
+
# Get sample row for parameter type detection and validation
|
|
1844
|
+
sample_row = (
|
|
1845
|
+
seq_of_parameters[0]
|
|
1846
|
+
if hasattr(seq_of_parameters, "__getitem__")
|
|
1847
|
+
else next(iter(seq_of_parameters))
|
|
1848
|
+
)
|
|
1849
|
+
param_count = len(sample_row)
|
|
1850
|
+
param_info = ddbc_bindings.ParamInfo
|
|
1851
|
+
parameters_type = []
|
|
1852
|
+
any_dae = False
|
|
1853
|
+
|
|
1854
|
+
# Check if we have explicit input sizes set
|
|
1855
|
+
if self._inputsizes:
|
|
1856
|
+
# Validate input sizes match parameter count
|
|
1857
|
+
if len(self._inputsizes) != param_count:
|
|
1858
|
+
warnings.warn(
|
|
1859
|
+
f"Number of input sizes ({len(self._inputsizes)}) does not match "
|
|
1860
|
+
f"number of parameters ({param_count}). This may lead to unexpected behavior.",
|
|
1861
|
+
Warning,
|
|
1862
|
+
)
|
|
1863
|
+
|
|
1864
|
+
# Prepare parameter type information
|
|
1865
|
+
for col_index in range(param_count):
|
|
1866
|
+
column = (
|
|
1867
|
+
[row[col_index] for row in seq_of_parameters]
|
|
1868
|
+
if hasattr(seq_of_parameters, "__getitem__")
|
|
1869
|
+
else []
|
|
1870
|
+
)
|
|
1871
|
+
sample_value, min_val, max_val = self._compute_column_type(column)
|
|
1872
|
+
|
|
1873
|
+
if self._inputsizes and col_index < len(self._inputsizes):
|
|
1874
|
+
# Use explicitly set input sizes
|
|
1875
|
+
sql_type, column_size, decimal_digits = self._inputsizes[col_index]
|
|
1876
|
+
|
|
1877
|
+
# Default is_dae to False
|
|
1878
|
+
is_dae = False
|
|
1879
|
+
|
|
1880
|
+
# Determine appropriate C type based on SQL type
|
|
1881
|
+
c_type = self._get_c_type_for_sql_type(sql_type)
|
|
1882
|
+
|
|
1883
|
+
# Check if this should be a DAE (data at execution) parameter based on column size
|
|
1884
|
+
if sample_value is not None:
|
|
1885
|
+
if isinstance(sample_value, str) and column_size > MAX_INLINE_CHAR:
|
|
1886
|
+
is_dae = True
|
|
1887
|
+
elif (
|
|
1888
|
+
isinstance(sample_value, (bytes, bytearray))
|
|
1889
|
+
and column_size > 8000
|
|
1890
|
+
):
|
|
1891
|
+
is_dae = True
|
|
1892
|
+
|
|
1893
|
+
# Sanitize precision/scale for numeric types
|
|
1894
|
+
if sql_type in (
|
|
1895
|
+
ddbc_sql_const.SQL_DECIMAL.value,
|
|
1896
|
+
ddbc_sql_const.SQL_NUMERIC.value,
|
|
1897
|
+
):
|
|
1898
|
+
column_size = max(
|
|
1899
|
+
1, min(int(column_size) if column_size > 0 else 18, 38)
|
|
1900
|
+
)
|
|
1901
|
+
decimal_digits = min(max(0, decimal_digits), column_size)
|
|
1902
|
+
|
|
1903
|
+
# For binary data columns with mixed content, we need to find max size
|
|
1904
|
+
if sql_type in (
|
|
1905
|
+
ddbc_sql_const.SQL_BINARY.value,
|
|
1906
|
+
ddbc_sql_const.SQL_VARBINARY.value,
|
|
1907
|
+
ddbc_sql_const.SQL_LONGVARBINARY.value,
|
|
1908
|
+
):
|
|
1909
|
+
# Find the maximum size needed for any row's binary data
|
|
1910
|
+
max_binary_size = 0
|
|
1911
|
+
for row in seq_of_parameters:
|
|
1912
|
+
value = row[col_index]
|
|
1913
|
+
if value is not None and isinstance(value, (bytes, bytearray)):
|
|
1914
|
+
max_binary_size = max(max_binary_size, len(value))
|
|
1915
|
+
|
|
1916
|
+
# For SQL Server VARBINARY(MAX), we need to use large object binding
|
|
1917
|
+
if column_size > 8000 or max_binary_size > 8000:
|
|
1918
|
+
sql_type = ddbc_sql_const.SQL_LONGVARBINARY.value
|
|
1919
|
+
is_dae = True
|
|
1920
|
+
|
|
1921
|
+
# Update column_size to actual maximum size if it's larger
|
|
1922
|
+
# Always ensure at least a minimum size of 1 for empty strings
|
|
1923
|
+
column_size = max(max_binary_size, 1)
|
|
1924
|
+
|
|
1925
|
+
paraminfo = param_info()
|
|
1926
|
+
paraminfo.paramCType = c_type
|
|
1927
|
+
paraminfo.paramSQLType = sql_type
|
|
1928
|
+
paraminfo.inputOutputType = ddbc_sql_const.SQL_PARAM_INPUT.value
|
|
1929
|
+
paraminfo.columnSize = column_size
|
|
1930
|
+
paraminfo.decimalDigits = decimal_digits
|
|
1931
|
+
paraminfo.isDAE = is_dae
|
|
1932
|
+
|
|
1933
|
+
# Ensure we never have SQL_C_DEFAULT (0) for C-type
|
|
1934
|
+
if paraminfo.paramCType == 0:
|
|
1935
|
+
paraminfo.paramCType = ddbc_sql_const.SQL_C_DEFAULT.value
|
|
1936
|
+
|
|
1937
|
+
parameters_type.append(paraminfo)
|
|
1938
|
+
else:
|
|
1939
|
+
# Use auto-detection for columns without explicit types
|
|
1940
|
+
column = (
|
|
1941
|
+
[row[col_index] for row in seq_of_parameters]
|
|
1942
|
+
if hasattr(seq_of_parameters, "__getitem__")
|
|
1943
|
+
else []
|
|
1944
|
+
)
|
|
1945
|
+
sample_value, min_val, max_val = self._compute_column_type(column)
|
|
1946
|
+
|
|
1947
|
+
dummy_row = list(sample_row)
|
|
1948
|
+
paraminfo = self._create_parameter_types_list(
|
|
1949
|
+
sample_value,
|
|
1950
|
+
param_info,
|
|
1951
|
+
dummy_row,
|
|
1952
|
+
col_index,
|
|
1953
|
+
min_val=min_val,
|
|
1954
|
+
max_val=max_val,
|
|
1955
|
+
)
|
|
1956
|
+
# Special handling for binary data in auto-detected types
|
|
1957
|
+
if paraminfo.paramSQLType in (
|
|
1958
|
+
ddbc_sql_const.SQL_BINARY.value,
|
|
1959
|
+
ddbc_sql_const.SQL_VARBINARY.value,
|
|
1960
|
+
ddbc_sql_const.SQL_LONGVARBINARY.value,
|
|
1961
|
+
):
|
|
1962
|
+
# Find the maximum size needed for any row's binary data
|
|
1963
|
+
max_binary_size = 0
|
|
1964
|
+
for row in seq_of_parameters:
|
|
1965
|
+
value = row[col_index]
|
|
1966
|
+
if value is not None and isinstance(value, (bytes, bytearray)):
|
|
1967
|
+
max_binary_size = max(max_binary_size, len(value))
|
|
1968
|
+
|
|
1969
|
+
# For SQL Server VARBINARY(MAX), we need to use large object binding
|
|
1970
|
+
if max_binary_size > 8000:
|
|
1971
|
+
paraminfo.paramSQLType = ddbc_sql_const.SQL_LONGVARBINARY.value
|
|
1972
|
+
paraminfo.isDAE = True
|
|
1973
|
+
|
|
1974
|
+
# Update column_size to actual maximum size
|
|
1975
|
+
# Always ensure at least a minimum size of 1 for empty strings
|
|
1976
|
+
paraminfo.columnSize = max(max_binary_size, 1)
|
|
1977
|
+
|
|
1978
|
+
parameters_type.append(paraminfo)
|
|
1979
|
+
if paraminfo.isDAE:
|
|
1980
|
+
any_dae = True
|
|
1981
|
+
|
|
1982
|
+
if any_dae:
|
|
1983
|
+
logger.debug(
|
|
1984
|
+
"DAE parameters detected. Falling back to row-by-row execution with streaming.",
|
|
1985
|
+
)
|
|
1986
|
+
for row in seq_of_parameters:
|
|
1987
|
+
self.execute(operation, row)
|
|
1988
|
+
return
|
|
1989
|
+
|
|
1990
|
+
# Process parameters into column-wise format with possible type conversions
|
|
1991
|
+
# First, convert any Decimal types as needed for NUMERIC/DECIMAL columns
|
|
1992
|
+
processed_parameters = []
|
|
1993
|
+
for row in seq_of_parameters:
|
|
1994
|
+
processed_row = list(row)
|
|
1995
|
+
for i, val in enumerate(processed_row):
|
|
1996
|
+
if val is None:
|
|
1997
|
+
continue
|
|
1998
|
+
if (
|
|
1999
|
+
isinstance(val, decimal.Decimal)
|
|
2000
|
+
and parameters_type[i].paramSQLType
|
|
2001
|
+
== ddbc_sql_const.SQL_VARCHAR.value
|
|
2002
|
+
):
|
|
2003
|
+
processed_row[i] = format(val, 'f')
|
|
2004
|
+
# Existing numeric conversion
|
|
2005
|
+
elif parameters_type[i].paramSQLType in (
|
|
2006
|
+
ddbc_sql_const.SQL_DECIMAL.value,
|
|
2007
|
+
ddbc_sql_const.SQL_NUMERIC.value,
|
|
2008
|
+
) and not isinstance(val, decimal.Decimal):
|
|
2009
|
+
try:
|
|
2010
|
+
processed_row[i] = decimal.Decimal(str(val))
|
|
2011
|
+
except Exception as e: # pylint: disable=broad-exception-caught
|
|
2012
|
+
raise ValueError(
|
|
2013
|
+
f"Failed to convert parameter at row {row}, column {i} to Decimal: {e}"
|
|
2014
|
+
) from e
|
|
2015
|
+
processed_parameters.append(processed_row)
|
|
2016
|
+
|
|
2017
|
+
# Now transpose the processed parameters
|
|
2018
|
+
columnwise_params, row_count = self._transpose_rowwise_to_columnwise(
|
|
2019
|
+
processed_parameters
|
|
2020
|
+
)
|
|
2021
|
+
|
|
2022
|
+
# Add debug logging
|
|
2023
|
+
logger.debug(
|
|
2024
|
+
"Executing batch query with %d parameter sets:\n%s",
|
|
2025
|
+
len(seq_of_parameters),
|
|
2026
|
+
"\n".join(
|
|
2027
|
+
f" {i+1}: {tuple(p) if isinstance(p, (list, tuple)) else p}"
|
|
2028
|
+
for i, p in enumerate(seq_of_parameters[:5])
|
|
2029
|
+
), # Limit to first 5 rows for large batches
|
|
2030
|
+
)
|
|
2031
|
+
|
|
2032
|
+
ret = ddbc_bindings.SQLExecuteMany(
|
|
2033
|
+
self.hstmt, operation, columnwise_params, parameters_type, row_count
|
|
2034
|
+
)
|
|
2035
|
+
|
|
2036
|
+
# Capture any diagnostic messages after execution
|
|
2037
|
+
if self.hstmt:
|
|
2038
|
+
self.messages.extend(ddbc_bindings.DDBCSQLGetAllDiagRecords(self.hstmt))
|
|
2039
|
+
|
|
2040
|
+
try:
|
|
2041
|
+
check_error(ddbc_sql_const.SQL_HANDLE_STMT.value, self.hstmt, ret)
|
|
2042
|
+
self.rowcount = ddbc_bindings.DDBCSQLRowCount(self.hstmt)
|
|
2043
|
+
self.last_executed_stmt = operation
|
|
2044
|
+
self._initialize_description()
|
|
2045
|
+
|
|
2046
|
+
if self.description:
|
|
2047
|
+
self.rowcount = -1
|
|
2048
|
+
self._reset_rownumber()
|
|
2049
|
+
else:
|
|
2050
|
+
self.rowcount = ddbc_bindings.DDBCSQLRowCount(self.hstmt)
|
|
2051
|
+
self._clear_rownumber()
|
|
2052
|
+
finally:
|
|
2053
|
+
# Reset input sizes after execution
|
|
2054
|
+
self._reset_inputsizes()
|
|
2055
|
+
|
|
2056
|
+
def fetchone(self) -> Union[None, Row]:
|
|
2057
|
+
"""
|
|
2058
|
+
Fetch the next row of a query result set.
|
|
2059
|
+
|
|
2060
|
+
Returns:
|
|
2061
|
+
Single Row object or None if no more data is available.
|
|
2062
|
+
"""
|
|
2063
|
+
self._check_closed() # Check if the cursor is closed
|
|
2064
|
+
|
|
2065
|
+
# Fetch raw data
|
|
2066
|
+
row_data = []
|
|
2067
|
+
try:
|
|
2068
|
+
ret = ddbc_bindings.DDBCSQLFetchOne(self.hstmt, row_data)
|
|
2069
|
+
|
|
2070
|
+
if self.hstmt:
|
|
2071
|
+
self.messages.extend(ddbc_bindings.DDBCSQLGetAllDiagRecords(self.hstmt))
|
|
2072
|
+
|
|
2073
|
+
if ret == ddbc_sql_const.SQL_NO_DATA.value:
|
|
2074
|
+
# No more data available
|
|
2075
|
+
if self._next_row_index == 0 and self.description is not None:
|
|
2076
|
+
# This is an empty result set, set rowcount to 0
|
|
2077
|
+
self.rowcount = 0
|
|
2078
|
+
return None
|
|
2079
|
+
|
|
2080
|
+
# Update internal position after successful fetch
|
|
2081
|
+
if self._skip_increment_for_next_fetch:
|
|
2082
|
+
self._skip_increment_for_next_fetch = False
|
|
2083
|
+
self._next_row_index += 1
|
|
2084
|
+
else:
|
|
2085
|
+
self._increment_rownumber()
|
|
2086
|
+
|
|
2087
|
+
self.rowcount = self._next_row_index
|
|
2088
|
+
|
|
2089
|
+
# Get column and converter maps
|
|
2090
|
+
column_map, converter_map = self._get_column_and_converter_maps()
|
|
2091
|
+
return Row(row_data, column_map, cursor=self, converter_map=converter_map)
|
|
2092
|
+
except Exception as e:
|
|
2093
|
+
# On error, don't increment rownumber - rethrow the error
|
|
2094
|
+
raise e
|
|
2095
|
+
|
|
2096
|
+
def fetchmany(self, size: Optional[int] = None) -> List[Row]:
|
|
2097
|
+
"""
|
|
2098
|
+
Fetch the next set of rows of a query result.
|
|
2099
|
+
|
|
2100
|
+
Args:
|
|
2101
|
+
size: Number of rows to fetch at a time.
|
|
2102
|
+
|
|
2103
|
+
Returns:
|
|
2104
|
+
List of Row objects.
|
|
2105
|
+
"""
|
|
2106
|
+
self._check_closed() # Check if the cursor is closed
|
|
2107
|
+
if not self._has_result_set and self.description:
|
|
2108
|
+
self._reset_rownumber()
|
|
2109
|
+
|
|
2110
|
+
if size is None:
|
|
2111
|
+
size = self.arraysize
|
|
2112
|
+
|
|
2113
|
+
if size <= 0:
|
|
2114
|
+
return []
|
|
2115
|
+
|
|
2116
|
+
# Fetch raw data
|
|
2117
|
+
rows_data = []
|
|
2118
|
+
try:
|
|
2119
|
+
_ = ddbc_bindings.DDBCSQLFetchMany(self.hstmt, rows_data, size)
|
|
2120
|
+
|
|
2121
|
+
if self.hstmt:
|
|
2122
|
+
self.messages.extend(ddbc_bindings.DDBCSQLGetAllDiagRecords(self.hstmt))
|
|
2123
|
+
|
|
2124
|
+
# Update rownumber for the number of rows actually fetched
|
|
2125
|
+
if rows_data and self._has_result_set:
|
|
2126
|
+
# advance counters by number of rows actually returned
|
|
2127
|
+
self._next_row_index += len(rows_data)
|
|
2128
|
+
self._rownumber = self._next_row_index - 1
|
|
2129
|
+
|
|
2130
|
+
# Centralize rowcount assignment after fetch
|
|
2131
|
+
if len(rows_data) == 0 and self._next_row_index == 0:
|
|
2132
|
+
self.rowcount = 0
|
|
2133
|
+
else:
|
|
2134
|
+
self.rowcount = self._next_row_index
|
|
2135
|
+
|
|
2136
|
+
# Get column and converter maps
|
|
2137
|
+
column_map, converter_map = self._get_column_and_converter_maps()
|
|
2138
|
+
|
|
2139
|
+
# Convert raw data to Row objects
|
|
2140
|
+
return [Row(row_data, column_map, cursor=self, converter_map=converter_map) for row_data in rows_data]
|
|
2141
|
+
except Exception as e:
|
|
2142
|
+
# On error, don't increment rownumber - rethrow the error
|
|
2143
|
+
raise e
|
|
2144
|
+
|
|
2145
|
+
def fetchall(self) -> List[Row]:
|
|
2146
|
+
"""
|
|
2147
|
+
Fetch all (remaining) rows of a query result.
|
|
2148
|
+
|
|
2149
|
+
Returns:
|
|
2150
|
+
List of Row objects.
|
|
2151
|
+
"""
|
|
2152
|
+
self._check_closed() # Check if the cursor is closed
|
|
2153
|
+
if not self._has_result_set and self.description:
|
|
2154
|
+
self._reset_rownumber()
|
|
2155
|
+
|
|
2156
|
+
# Fetch raw data
|
|
2157
|
+
rows_data = []
|
|
2158
|
+
try:
|
|
2159
|
+
_ = ddbc_bindings.DDBCSQLFetchAll(self.hstmt, rows_data)
|
|
2160
|
+
|
|
2161
|
+
if self.hstmt:
|
|
2162
|
+
self.messages.extend(ddbc_bindings.DDBCSQLGetAllDiagRecords(self.hstmt))
|
|
2163
|
+
|
|
2164
|
+
# Update rownumber for the number of rows actually fetched
|
|
2165
|
+
if rows_data and self._has_result_set:
|
|
2166
|
+
self._next_row_index += len(rows_data)
|
|
2167
|
+
self._rownumber = self._next_row_index - 1
|
|
2168
|
+
|
|
2169
|
+
# Centralize rowcount assignment after fetch
|
|
2170
|
+
if len(rows_data) == 0 and self._next_row_index == 0:
|
|
2171
|
+
self.rowcount = 0
|
|
2172
|
+
else:
|
|
2173
|
+
self.rowcount = self._next_row_index
|
|
2174
|
+
|
|
2175
|
+
# Get column and converter maps
|
|
2176
|
+
column_map, converter_map = self._get_column_and_converter_maps()
|
|
2177
|
+
|
|
2178
|
+
# Convert raw data to Row objects
|
|
2179
|
+
return [Row(row_data, column_map, cursor=self, converter_map=converter_map) for row_data in rows_data]
|
|
2180
|
+
except Exception as e:
|
|
2181
|
+
# On error, don't increment rownumber - rethrow the error
|
|
2182
|
+
raise e
|
|
2183
|
+
|
|
2184
|
+
def nextset(self) -> Union[bool, None]:
|
|
2185
|
+
"""
|
|
2186
|
+
Skip to the next available result set.
|
|
2187
|
+
|
|
2188
|
+
Returns:
|
|
2189
|
+
True if there is another result set, None otherwise.
|
|
2190
|
+
|
|
2191
|
+
Raises:
|
|
2192
|
+
Error: If the previous call to execute did not produce any result set.
|
|
2193
|
+
"""
|
|
2194
|
+
logger.debug('nextset: Moving to next result set')
|
|
2195
|
+
self._check_closed() # Check if the cursor is closed
|
|
2196
|
+
|
|
2197
|
+
# Clear messages per DBAPI
|
|
2198
|
+
self.messages = []
|
|
2199
|
+
|
|
2200
|
+
# Clear cached column and converter maps for the new result set
|
|
2201
|
+
self._cached_column_map = None
|
|
2202
|
+
self._cached_converter_map = None
|
|
2203
|
+
|
|
2204
|
+
# Skip to the next result set
|
|
2205
|
+
ret = ddbc_bindings.DDBCSQLMoreResults(self.hstmt)
|
|
2206
|
+
check_error(ddbc_sql_const.SQL_HANDLE_STMT.value, self.hstmt, ret)
|
|
2207
|
+
|
|
2208
|
+
if ret == ddbc_sql_const.SQL_NO_DATA.value:
|
|
2209
|
+
logger.debug('nextset: No more result sets available')
|
|
2210
|
+
self._clear_rownumber()
|
|
2211
|
+
self.description = None
|
|
2212
|
+
return False
|
|
2213
|
+
|
|
2214
|
+
self._reset_rownumber()
|
|
2215
|
+
|
|
2216
|
+
# Initialize description for the new result set
|
|
2217
|
+
column_metadata = []
|
|
2218
|
+
try:
|
|
2219
|
+
ddbc_bindings.DDBCSQLDescribeCol(self.hstmt, column_metadata)
|
|
2220
|
+
self._initialize_description(column_metadata)
|
|
2221
|
+
|
|
2222
|
+
# Pre-build column map and converter map for the new result set
|
|
2223
|
+
if self.description:
|
|
2224
|
+
self._cached_column_map = {col_desc[0]: i for i, col_desc in enumerate(self.description)}
|
|
2225
|
+
self._cached_converter_map = self._build_converter_map()
|
|
2226
|
+
except Exception as e: # pylint: disable=broad-exception-caught
|
|
2227
|
+
# If describe fails, there might be no results in this result set
|
|
2228
|
+
self.description = None
|
|
2229
|
+
|
|
2230
|
+
logger.debug('nextset: Moved to next result set - column_count=%d',
|
|
2231
|
+
len(self.description) if self.description else 0)
|
|
2232
|
+
return True
|
|
2233
|
+
|
|
2234
|
+
def __enter__(self):
|
|
2235
|
+
"""
|
|
2236
|
+
Enter the runtime context for the cursor.
|
|
2237
|
+
|
|
2238
|
+
Returns:
|
|
2239
|
+
The cursor instance itself.
|
|
2240
|
+
"""
|
|
2241
|
+
self._check_closed()
|
|
2242
|
+
return self
|
|
2243
|
+
|
|
2244
|
+
def __exit__(self, *args):
|
|
2245
|
+
"""Closes the cursor when exiting the context, ensuring proper resource cleanup."""
|
|
2246
|
+
if not self.closed:
|
|
2247
|
+
self.close()
|
|
2248
|
+
|
|
2249
|
+
def fetchval(self):
|
|
2250
|
+
"""
|
|
2251
|
+
Fetch the first column of the first row if there are results.
|
|
2252
|
+
|
|
2253
|
+
This is a convenience method for queries that return a single value,
|
|
2254
|
+
such as SELECT COUNT(*) FROM table, SELECT MAX(id) FROM table, etc.
|
|
2255
|
+
|
|
2256
|
+
Returns:
|
|
2257
|
+
The value of the first column of the first row, or None if no rows
|
|
2258
|
+
are available or the first column value is NULL.
|
|
2259
|
+
|
|
2260
|
+
Raises:
|
|
2261
|
+
Exception: If the cursor is closed.
|
|
2262
|
+
|
|
2263
|
+
Example:
|
|
2264
|
+
>>> count = cursor.execute('SELECT COUNT(*) FROM users').fetchval()
|
|
2265
|
+
>>> max_id = cursor.execute('SELECT MAX(id) FROM users').fetchval()
|
|
2266
|
+
>>> name = cursor.execute('SELECT name FROM users WHERE id = ?', user_id).fetchval()
|
|
2267
|
+
|
|
2268
|
+
Note:
|
|
2269
|
+
This is a convenience extension beyond the DB-API 2.0 specification.
|
|
2270
|
+
After calling fetchval(), the cursor position advances by one row,
|
|
2271
|
+
just like fetchone().
|
|
2272
|
+
"""
|
|
2273
|
+
logger.debug('fetchval: Fetching single value from first column')
|
|
2274
|
+
self._check_closed() # Check if the cursor is closed
|
|
2275
|
+
|
|
2276
|
+
# Check if this is a result-producing statement
|
|
2277
|
+
if not self.description:
|
|
2278
|
+
# Non-result-set statement (INSERT, UPDATE, DELETE, etc.)
|
|
2279
|
+
logger.debug('fetchval: No result set available (non-SELECT statement)')
|
|
2280
|
+
return None
|
|
2281
|
+
|
|
2282
|
+
# Fetch the first row
|
|
2283
|
+
row = self.fetchone()
|
|
2284
|
+
|
|
2285
|
+
if row is None:
|
|
2286
|
+
logger.debug('fetchval: No value available (no rows)')
|
|
2287
|
+
return None
|
|
2288
|
+
|
|
2289
|
+
logger.debug('fetchval: Value retrieved successfully')
|
|
2290
|
+
return row[0]
|
|
2291
|
+
|
|
2292
|
+
def commit(self):
|
|
2293
|
+
"""
|
|
2294
|
+
Commit all SQL statements executed on the connection that created this cursor.
|
|
2295
|
+
|
|
2296
|
+
This is a convenience method that calls commit() on the underlying connection.
|
|
2297
|
+
It affects all cursors created by the same connection since the last commit/rollback.
|
|
2298
|
+
|
|
2299
|
+
The benefit is that many uses can now just use the cursor and not have to track
|
|
2300
|
+
the connection object.
|
|
2301
|
+
|
|
2302
|
+
Raises:
|
|
2303
|
+
Exception: If the cursor is closed or if the commit operation fails.
|
|
2304
|
+
|
|
2305
|
+
Example:
|
|
2306
|
+
>>> cursor.execute("INSERT INTO users (name) VALUES (?)", "John")
|
|
2307
|
+
>>> cursor.commit() # Commits the INSERT
|
|
2308
|
+
|
|
2309
|
+
Note:
|
|
2310
|
+
This is equivalent to calling connection.commit() but provides convenience
|
|
2311
|
+
for code that only has access to the cursor object.
|
|
2312
|
+
"""
|
|
2313
|
+
self._check_closed() # Check if the cursor is closed
|
|
2314
|
+
|
|
2315
|
+
# Clear messages per DBAPI
|
|
2316
|
+
self.messages = []
|
|
2317
|
+
|
|
2318
|
+
# Delegate to the connection's commit method
|
|
2319
|
+
self._connection.commit()
|
|
2320
|
+
|
|
2321
|
+
def rollback(self):
|
|
2322
|
+
"""
|
|
2323
|
+
Roll back all SQL statements executed on the connection that created this cursor.
|
|
2324
|
+
|
|
2325
|
+
This is a convenience method that calls rollback() on the underlying connection.
|
|
2326
|
+
It affects all cursors created by the same connection since the last commit/rollback.
|
|
2327
|
+
|
|
2328
|
+
The benefit is that many uses can now just use the cursor and not have to track
|
|
2329
|
+
the connection object.
|
|
2330
|
+
|
|
2331
|
+
Raises:
|
|
2332
|
+
Exception: If the cursor is closed or if the rollback operation fails.
|
|
2333
|
+
|
|
2334
|
+
Example:
|
|
2335
|
+
>>> cursor.execute("INSERT INTO users (name) VALUES (?)", "John")
|
|
2336
|
+
>>> cursor.rollback() # Rolls back the INSERT
|
|
2337
|
+
|
|
2338
|
+
Note:
|
|
2339
|
+
This is equivalent to calling connection.rollback() but provides convenience
|
|
2340
|
+
for code that only has access to the cursor object.
|
|
2341
|
+
"""
|
|
2342
|
+
self._check_closed() # Check if the cursor is closed
|
|
2343
|
+
|
|
2344
|
+
# Clear messages per DBAPI
|
|
2345
|
+
self.messages = []
|
|
2346
|
+
|
|
2347
|
+
# Delegate to the connection's rollback method
|
|
2348
|
+
self._connection.rollback()
|
|
2349
|
+
|
|
2350
|
+
def __del__(self):
|
|
2351
|
+
"""
|
|
2352
|
+
Destructor to ensure the cursor is closed when it is no longer needed.
|
|
2353
|
+
This is a safety net to ensure resources are cleaned up
|
|
2354
|
+
even if close() was not called explicitly.
|
|
2355
|
+
If the cursor is already closed, it will not raise an exception during cleanup.
|
|
2356
|
+
"""
|
|
2357
|
+
if "closed" not in self.__dict__ or not self.closed:
|
|
2358
|
+
try:
|
|
2359
|
+
self.close()
|
|
2360
|
+
except Exception as e: # pylint: disable=broad-exception-caught
|
|
2361
|
+
# Don't raise an exception in __del__, just log it
|
|
2362
|
+
# If interpreter is shutting down, we might not have logging set up
|
|
2363
|
+
import sys
|
|
2364
|
+
|
|
2365
|
+
if sys and sys._is_finalizing():
|
|
2366
|
+
# Suppress logging during interpreter shutdown
|
|
2367
|
+
return
|
|
2368
|
+
logger.debug( "Exception during cursor cleanup in __del__: %s", e)
|
|
2369
|
+
|
|
2370
|
+
def scroll(self, value: int, mode: str = "relative") -> None: # pylint: disable=too-many-branches
|
|
2371
|
+
"""
|
|
2372
|
+
Scroll using SQLFetchScroll only, matching test semantics:
|
|
2373
|
+
- relative(N>0): consume N rows; rownumber = previous + N;
|
|
2374
|
+
next fetch returns the following row.
|
|
2375
|
+
- absolute(-1): before first (rownumber = -1), no data consumed.
|
|
2376
|
+
- absolute(0): position so next fetch returns first row;
|
|
2377
|
+
rownumber stays 0 even after that fetch.
|
|
2378
|
+
- absolute(k>0): next fetch returns row index k (0-based);
|
|
2379
|
+
rownumber == k after scroll.
|
|
2380
|
+
"""
|
|
2381
|
+
logger.debug('scroll: Scrolling cursor - mode=%s, value=%d, current_rownumber=%d',
|
|
2382
|
+
mode, value, self._rownumber)
|
|
2383
|
+
self._check_closed()
|
|
2384
|
+
|
|
2385
|
+
# Clear messages per DBAPI
|
|
2386
|
+
self.messages = []
|
|
2387
|
+
|
|
2388
|
+
if mode not in ("relative", "absolute"):
|
|
2389
|
+
logger.error('scroll: Invalid mode - mode=%s', mode)
|
|
2390
|
+
raise ProgrammingError(
|
|
2391
|
+
"Invalid scroll mode",
|
|
2392
|
+
f"mode must be 'relative' or 'absolute', got '{mode}'",
|
|
2393
|
+
)
|
|
2394
|
+
if not self._has_result_set:
|
|
2395
|
+
logger.error('scroll: No active result set')
|
|
2396
|
+
raise ProgrammingError(
|
|
2397
|
+
"No active result set",
|
|
2398
|
+
"Cannot scroll: no result set available. Execute a query first.",
|
|
2399
|
+
)
|
|
2400
|
+
if not isinstance(value, int):
|
|
2401
|
+
logger.error('scroll: Invalid value type - type=%s', type(value).__name__)
|
|
2402
|
+
raise ProgrammingError(
|
|
2403
|
+
"Invalid scroll value type",
|
|
2404
|
+
f"scroll value must be an integer, got {type(value).__name__}",
|
|
2405
|
+
)
|
|
2406
|
+
|
|
2407
|
+
# Relative backward not supported
|
|
2408
|
+
if mode == "relative" and value < 0:
|
|
2409
|
+
logger.error('scroll: Backward scrolling not supported - value=%d', value)
|
|
2410
|
+
raise NotSupportedError(
|
|
2411
|
+
"Backward scrolling not supported",
|
|
2412
|
+
f"Cannot move backward by {value} rows on a forward-only cursor",
|
|
2413
|
+
)
|
|
2414
|
+
|
|
2415
|
+
row_data: list = []
|
|
2416
|
+
|
|
2417
|
+
# Absolute positioning not supported with forward-only cursors
|
|
2418
|
+
if mode == "absolute":
|
|
2419
|
+
raise NotSupportedError(
|
|
2420
|
+
"Absolute positioning not supported",
|
|
2421
|
+
"Forward-only cursors do not support absolute positioning"
|
|
2422
|
+
)
|
|
2423
|
+
|
|
2424
|
+
try:
|
|
2425
|
+
if mode == "relative":
|
|
2426
|
+
if value == 0:
|
|
2427
|
+
return
|
|
2428
|
+
|
|
2429
|
+
# For forward-only cursors, use multiple SQL_FETCH_NEXT calls
|
|
2430
|
+
# This matches pyodbc's approach for skip operations
|
|
2431
|
+
for i in range(value):
|
|
2432
|
+
ret = ddbc_bindings.DDBCSQLFetchScroll(
|
|
2433
|
+
self.hstmt, ddbc_sql_const.SQL_FETCH_NEXT.value, 0, row_data
|
|
2434
|
+
)
|
|
2435
|
+
if ret == ddbc_sql_const.SQL_NO_DATA.value:
|
|
2436
|
+
raise IndexError(
|
|
2437
|
+
"Cannot scroll to specified position: end of result set reached"
|
|
2438
|
+
)
|
|
2439
|
+
|
|
2440
|
+
# Update position tracking
|
|
2441
|
+
self._rownumber = self._rownumber + value
|
|
2442
|
+
self._next_row_index = self._rownumber + 1
|
|
2443
|
+
logger.debug('scroll: Scroll complete - new_rownumber=%d, next_row_index=%d',
|
|
2444
|
+
self._rownumber, self._next_row_index)
|
|
2445
|
+
return
|
|
2446
|
+
|
|
2447
|
+
except Exception as e: # pylint: disable=broad-exception-caught
|
|
2448
|
+
if isinstance(e, (IndexError, NotSupportedError)):
|
|
2449
|
+
raise
|
|
2450
|
+
raise IndexError(f"Scroll operation failed: {e}") from e
|
|
2451
|
+
|
|
2452
|
+
def skip(self, count: int) -> None:
|
|
2453
|
+
"""
|
|
2454
|
+
Skip the next count records in the query result set.
|
|
2455
|
+
|
|
2456
|
+
Args:
|
|
2457
|
+
count: Number of records to skip.
|
|
2458
|
+
|
|
2459
|
+
Raises:
|
|
2460
|
+
IndexError: If attempting to skip past the end of the result set.
|
|
2461
|
+
ProgrammingError: If count is not an integer.
|
|
2462
|
+
NotSupportedError: If attempting to skip backwards.
|
|
2463
|
+
"""
|
|
2464
|
+
self._check_closed()
|
|
2465
|
+
|
|
2466
|
+
# Clear messages
|
|
2467
|
+
self.messages = []
|
|
2468
|
+
|
|
2469
|
+
# Simply delegate to the scroll method with 'relative' mode
|
|
2470
|
+
self.scroll(count, "relative")
|
|
2471
|
+
|
|
2472
|
+
def _execute_tables( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
2473
|
+
self,
|
|
2474
|
+
stmt_handle,
|
|
2475
|
+
catalog_name=None,
|
|
2476
|
+
schema_name=None,
|
|
2477
|
+
table_name=None,
|
|
2478
|
+
table_type=None,
|
|
2479
|
+
):
|
|
2480
|
+
"""
|
|
2481
|
+
Execute SQLTables ODBC function to retrieve table metadata.
|
|
2482
|
+
|
|
2483
|
+
Args:
|
|
2484
|
+
stmt_handle: ODBC statement handle
|
|
2485
|
+
catalog_name: The catalog name pattern
|
|
2486
|
+
schema_name: The schema name pattern
|
|
2487
|
+
table_name: The table name pattern
|
|
2488
|
+
table_type: The table type filter
|
|
2489
|
+
search_escape: The escape character for pattern matching
|
|
2490
|
+
"""
|
|
2491
|
+
# Convert None values to empty strings for ODBC
|
|
2492
|
+
catalog = "" if catalog_name is None else catalog_name
|
|
2493
|
+
schema = "" if schema_name is None else schema_name
|
|
2494
|
+
table = "" if table_name is None else table_name
|
|
2495
|
+
types = "" if table_type is None else table_type
|
|
2496
|
+
|
|
2497
|
+
# Call the ODBC SQLTables function
|
|
2498
|
+
retcode = ddbc_bindings.DDBCSQLTables(
|
|
2499
|
+
stmt_handle, catalog, schema, table, types
|
|
2500
|
+
)
|
|
2501
|
+
|
|
2502
|
+
# Check return code and handle errors
|
|
2503
|
+
check_error(ddbc_sql_const.SQL_HANDLE_STMT.value, stmt_handle, retcode)
|
|
2504
|
+
|
|
2505
|
+
# Capture any diagnostic messages
|
|
2506
|
+
if stmt_handle:
|
|
2507
|
+
self.messages.extend(ddbc_bindings.DDBCSQLGetAllDiagRecords(stmt_handle))
|
|
2508
|
+
|
|
2509
|
+
def tables(self, table=None, catalog=None, schema=None, tableType=None): # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
2510
|
+
"""
|
|
2511
|
+
Returns information about tables in the database that match the given criteria using
|
|
2512
|
+
the SQLTables ODBC function.
|
|
2513
|
+
|
|
2514
|
+
Args:
|
|
2515
|
+
table (str, optional): The table name pattern. Default is None (all tables).
|
|
2516
|
+
catalog (str, optional): The catalog name. Default is None.
|
|
2517
|
+
schema (str, optional): The schema name pattern. Default is None.
|
|
2518
|
+
tableType (str or list, optional): The table type filter. Default is None.
|
|
2519
|
+
Example: "TABLE" or ["TABLE", "VIEW"]
|
|
2520
|
+
|
|
2521
|
+
Returns:
|
|
2522
|
+
Cursor: The cursor object itself for method chaining with fetch methods.
|
|
2523
|
+
"""
|
|
2524
|
+
self._check_closed()
|
|
2525
|
+
self._reset_cursor()
|
|
2526
|
+
|
|
2527
|
+
# Format table_type parameter - SQLTables expects comma-separated string
|
|
2528
|
+
table_type_str = None
|
|
2529
|
+
if tableType is not None:
|
|
2530
|
+
if isinstance(tableType, (list, tuple)):
|
|
2531
|
+
table_type_str = ",".join(tableType)
|
|
2532
|
+
else:
|
|
2533
|
+
table_type_str = str(tableType)
|
|
2534
|
+
|
|
2535
|
+
try:
|
|
2536
|
+
# Call SQLTables via the helper method
|
|
2537
|
+
self._execute_tables(
|
|
2538
|
+
self.hstmt,
|
|
2539
|
+
catalog_name=catalog,
|
|
2540
|
+
schema_name=schema,
|
|
2541
|
+
table_name=table,
|
|
2542
|
+
table_type=table_type_str,
|
|
2543
|
+
)
|
|
2544
|
+
|
|
2545
|
+
# Define fallback description for tables
|
|
2546
|
+
fallback_description = [
|
|
2547
|
+
("table_cat", str, None, 128, 128, 0, True),
|
|
2548
|
+
("table_schem", str, None, 128, 128, 0, True),
|
|
2549
|
+
("table_name", str, None, 128, 128, 0, False),
|
|
2550
|
+
("table_type", str, None, 128, 128, 0, False),
|
|
2551
|
+
("remarks", str, None, 254, 254, 0, True),
|
|
2552
|
+
]
|
|
2553
|
+
|
|
2554
|
+
# Use the helper method to prepare the result set
|
|
2555
|
+
return self._prepare_metadata_result_set(
|
|
2556
|
+
fallback_description=fallback_description
|
|
2557
|
+
)
|
|
2558
|
+
|
|
2559
|
+
except Exception as e: # pylint: disable=broad-exception-caught
|
|
2560
|
+
# Log the error and re-raise
|
|
2561
|
+
logger.error( f"Error executing tables query: {e}")
|
|
2562
|
+
raise
|
|
2563
|
+
|
|
2564
|
+
def callproc(
|
|
2565
|
+
self, procname: str, parameters: Optional[Sequence[Any]] = None
|
|
2566
|
+
) -> Optional[Sequence[Any]]:
|
|
2567
|
+
"""
|
|
2568
|
+
Call a stored database procedure with the given name.
|
|
2569
|
+
|
|
2570
|
+
Args:
|
|
2571
|
+
procname: Name of the stored procedure to call
|
|
2572
|
+
parameters: Optional sequence of parameters to pass to the procedure
|
|
2573
|
+
|
|
2574
|
+
Returns:
|
|
2575
|
+
A sequence containing the result parameters (input parameters unchanged,
|
|
2576
|
+
output parameters with their new values)
|
|
2577
|
+
|
|
2578
|
+
Raises:
|
|
2579
|
+
NotSupportedError: This method is not yet implemented
|
|
2580
|
+
"""
|
|
2581
|
+
raise NotSupportedError(
|
|
2582
|
+
driver_error="callproc() is not yet implemented",
|
|
2583
|
+
ddbc_error="Stored procedure calls are not currently supported",
|
|
2584
|
+
)
|
|
2585
|
+
|
|
2586
|
+
def setoutputsize(self, size: int, column: Optional[int] = None) -> None:
|
|
2587
|
+
"""
|
|
2588
|
+
Set a column buffer size for fetches of large columns.
|
|
2589
|
+
|
|
2590
|
+
This method is optional and is not implemented in this driver.
|
|
2591
|
+
|
|
2592
|
+
Args:
|
|
2593
|
+
size: Maximum size of the column buffer
|
|
2594
|
+
column: Optional column index (0-based) to set the size for
|
|
2595
|
+
|
|
2596
|
+
Note:
|
|
2597
|
+
This method is a no-op in this implementation as buffer sizes
|
|
2598
|
+
are managed automatically by the underlying driver.
|
|
2599
|
+
"""
|
|
2600
|
+
# This is a no-op - buffer sizes are managed automatically
|
|
2601
|
+
|