sqlalchemy-jdbcapi 2.0.0.post2__py3-none-any.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.
- sqlalchemy_jdbcapi/__init__.py +128 -0
- sqlalchemy_jdbcapi/_version.py +34 -0
- sqlalchemy_jdbcapi/dialects/__init__.py +30 -0
- sqlalchemy_jdbcapi/dialects/base.py +879 -0
- sqlalchemy_jdbcapi/dialects/db2.py +134 -0
- sqlalchemy_jdbcapi/dialects/mssql.py +117 -0
- sqlalchemy_jdbcapi/dialects/mysql.py +152 -0
- sqlalchemy_jdbcapi/dialects/oceanbase.py +218 -0
- sqlalchemy_jdbcapi/dialects/odbc_base.py +389 -0
- sqlalchemy_jdbcapi/dialects/odbc_mssql.py +69 -0
- sqlalchemy_jdbcapi/dialects/odbc_mysql.py +101 -0
- sqlalchemy_jdbcapi/dialects/odbc_oracle.py +80 -0
- sqlalchemy_jdbcapi/dialects/odbc_postgresql.py +63 -0
- sqlalchemy_jdbcapi/dialects/oracle.py +180 -0
- sqlalchemy_jdbcapi/dialects/postgresql.py +110 -0
- sqlalchemy_jdbcapi/dialects/sqlite.py +141 -0
- sqlalchemy_jdbcapi/jdbc/__init__.py +98 -0
- sqlalchemy_jdbcapi/jdbc/connection.py +244 -0
- sqlalchemy_jdbcapi/jdbc/cursor.py +329 -0
- sqlalchemy_jdbcapi/jdbc/dataframe.py +198 -0
- sqlalchemy_jdbcapi/jdbc/driver_manager.py +353 -0
- sqlalchemy_jdbcapi/jdbc/exceptions.py +53 -0
- sqlalchemy_jdbcapi/jdbc/jvm.py +176 -0
- sqlalchemy_jdbcapi/jdbc/type_converter.py +292 -0
- sqlalchemy_jdbcapi/jdbc/types.py +72 -0
- sqlalchemy_jdbcapi/odbc/__init__.py +46 -0
- sqlalchemy_jdbcapi/odbc/connection.py +136 -0
- sqlalchemy_jdbcapi/odbc/exceptions.py +48 -0
- sqlalchemy_jdbcapi/py.typed +2 -0
- sqlalchemy_jdbcapi-2.0.0.post2.dist-info/METADATA +825 -0
- sqlalchemy_jdbcapi-2.0.0.post2.dist-info/RECORD +36 -0
- sqlalchemy_jdbcapi-2.0.0.post2.dist-info/WHEEL +5 -0
- sqlalchemy_jdbcapi-2.0.0.post2.dist-info/entry_points.txt +20 -0
- sqlalchemy_jdbcapi-2.0.0.post2.dist-info/licenses/AUTHORS +7 -0
- sqlalchemy_jdbcapi-2.0.0.post2.dist-info/licenses/LICENSE +13 -0
- sqlalchemy_jdbcapi-2.0.0.post2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
"""
|
|
2
|
+
JDBC Connection implementation following DB-API 2.0 specification.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from .cursor import Cursor
|
|
11
|
+
from .exceptions import DatabaseError, InterfaceError
|
|
12
|
+
from .jvm import start_jvm
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Connection:
|
|
18
|
+
"""
|
|
19
|
+
DB-API 2.0 compliant connection to a JDBC database.
|
|
20
|
+
|
|
21
|
+
This class wraps a JDBC Connection object from JPype.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
jclassname: str,
|
|
27
|
+
url: str,
|
|
28
|
+
driver_args: dict[str, Any] | list[Any] | None = None,
|
|
29
|
+
jars: list[str] | None = None,
|
|
30
|
+
libs: list[str] | None = None,
|
|
31
|
+
) -> None:
|
|
32
|
+
"""
|
|
33
|
+
Create a JDBC connection.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
jclassname: Fully qualified Java class name of JDBC driver
|
|
37
|
+
url: JDBC connection URL
|
|
38
|
+
driver_args: Connection properties (dict) or [user, password] (list)
|
|
39
|
+
jars: List of JAR file paths (for classpath)
|
|
40
|
+
libs: Additional native library paths
|
|
41
|
+
|
|
42
|
+
Raises:
|
|
43
|
+
InterfaceError: If JVM or driver cannot be loaded
|
|
44
|
+
DatabaseError: If connection fails
|
|
45
|
+
"""
|
|
46
|
+
self._jclassname = jclassname
|
|
47
|
+
self._url = url
|
|
48
|
+
self._jdbc_connection: Any = None
|
|
49
|
+
self._closed = False
|
|
50
|
+
|
|
51
|
+
# Start JVM if not already running
|
|
52
|
+
# Note: JVM can only be started once per Python process
|
|
53
|
+
try:
|
|
54
|
+
start_jvm(classpath=list(jars or []), jvm_args=None)
|
|
55
|
+
except Exception as e:
|
|
56
|
+
raise InterfaceError(f"Failed to start JVM: {e}") from e
|
|
57
|
+
|
|
58
|
+
# Load driver and establish connection
|
|
59
|
+
try:
|
|
60
|
+
import jpype
|
|
61
|
+
|
|
62
|
+
# Load the JDBC driver class - this registers it with DriverManager
|
|
63
|
+
# Even though we don't use the returned class, loading it has side effects
|
|
64
|
+
jpype.JClass(jclassname)
|
|
65
|
+
logger.debug(f"Loaded JDBC driver: {jclassname}")
|
|
66
|
+
|
|
67
|
+
# Get DriverManager - this is the main entry point for JDBC connections
|
|
68
|
+
dm = jpype.JClass("java.sql.DriverManager")
|
|
69
|
+
|
|
70
|
+
# Create connection based on driver_args format
|
|
71
|
+
# We support multiple formats for flexibility
|
|
72
|
+
if driver_args is None:
|
|
73
|
+
# Simple connection without credentials
|
|
74
|
+
self._jdbc_connection = dm.getConnection(url)
|
|
75
|
+
|
|
76
|
+
elif isinstance(driver_args, dict):
|
|
77
|
+
# Dict format - convert to Java Properties
|
|
78
|
+
# This allows passing custom JDBC properties like SSL settings
|
|
79
|
+
props = jpype.JClass("java.util.Properties")()
|
|
80
|
+
for k, v in driver_args.items():
|
|
81
|
+
props.setProperty(str(k), str(v))
|
|
82
|
+
self._jdbc_connection = dm.getConnection(url, props)
|
|
83
|
+
|
|
84
|
+
elif isinstance(driver_args, (list, tuple)) and len(driver_args) == 2:
|
|
85
|
+
# List format for simple user/password auth
|
|
86
|
+
usr, pwd = driver_args
|
|
87
|
+
self._jdbc_connection = dm.getConnection(url, str(usr), str(pwd))
|
|
88
|
+
|
|
89
|
+
else:
|
|
90
|
+
# TODO: Maybe support more argument formats in the future?
|
|
91
|
+
raise ValueError("driver_args must be dict, [user, password], or None")
|
|
92
|
+
|
|
93
|
+
logger.info(f"Connected to database: {url}")
|
|
94
|
+
|
|
95
|
+
except Exception as e:
|
|
96
|
+
logger.exception(f"Connection failed: {e}")
|
|
97
|
+
raise DatabaseError(f"Failed to connect to database: {e}") from e
|
|
98
|
+
|
|
99
|
+
def close(self) -> None:
|
|
100
|
+
"""Close the connection."""
|
|
101
|
+
if self._closed:
|
|
102
|
+
return
|
|
103
|
+
|
|
104
|
+
try:
|
|
105
|
+
if self._jdbc_connection is not None:
|
|
106
|
+
self._jdbc_connection.close()
|
|
107
|
+
logger.debug("Connection closed")
|
|
108
|
+
except Exception as e:
|
|
109
|
+
logger.warning(f"Error closing connection: {e}")
|
|
110
|
+
finally:
|
|
111
|
+
self._jdbc_connection = None
|
|
112
|
+
self._closed = True
|
|
113
|
+
|
|
114
|
+
def commit(self) -> None:
|
|
115
|
+
"""Commit current transaction."""
|
|
116
|
+
if self._closed:
|
|
117
|
+
raise InterfaceError("Connection is closed")
|
|
118
|
+
|
|
119
|
+
try:
|
|
120
|
+
if self._jdbc_connection is not None:
|
|
121
|
+
self._jdbc_connection.commit()
|
|
122
|
+
logger.debug("Transaction committed")
|
|
123
|
+
except Exception as e:
|
|
124
|
+
raise DatabaseError(f"Commit failed: {e}") from e
|
|
125
|
+
|
|
126
|
+
def rollback(self) -> None:
|
|
127
|
+
"""Rollback current transaction."""
|
|
128
|
+
if self._closed:
|
|
129
|
+
raise InterfaceError("Connection is closed")
|
|
130
|
+
|
|
131
|
+
try:
|
|
132
|
+
if self._jdbc_connection is not None:
|
|
133
|
+
self._jdbc_connection.rollback()
|
|
134
|
+
logger.debug("Transaction rolled back")
|
|
135
|
+
except Exception as e:
|
|
136
|
+
raise DatabaseError(f"Rollback failed: {e}") from e
|
|
137
|
+
|
|
138
|
+
def cursor(self) -> Cursor:
|
|
139
|
+
"""
|
|
140
|
+
Create a new cursor.
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
Cursor object
|
|
144
|
+
|
|
145
|
+
Raises:
|
|
146
|
+
InterfaceError: If connection is closed
|
|
147
|
+
"""
|
|
148
|
+
if self._closed:
|
|
149
|
+
raise InterfaceError("Connection is closed")
|
|
150
|
+
|
|
151
|
+
return Cursor(self, self._jdbc_connection)
|
|
152
|
+
|
|
153
|
+
def set_auto_commit(self, auto_commit: bool) -> None:
|
|
154
|
+
"""
|
|
155
|
+
Set auto-commit mode.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
auto_commit: True to enable auto-commit, False to disable
|
|
159
|
+
"""
|
|
160
|
+
if self._closed:
|
|
161
|
+
raise InterfaceError("Connection is closed")
|
|
162
|
+
|
|
163
|
+
try:
|
|
164
|
+
self._jdbc_connection.setAutoCommit(auto_commit)
|
|
165
|
+
logger.debug(f"Auto-commit set to {auto_commit}")
|
|
166
|
+
except Exception as e:
|
|
167
|
+
raise DatabaseError(f"Failed to set auto-commit: {e}") from e
|
|
168
|
+
|
|
169
|
+
def get_auto_commit(self) -> bool:
|
|
170
|
+
"""Get current auto-commit mode."""
|
|
171
|
+
if self._closed:
|
|
172
|
+
raise InterfaceError("Connection is closed")
|
|
173
|
+
|
|
174
|
+
try:
|
|
175
|
+
return self._jdbc_connection.getAutoCommit()
|
|
176
|
+
except Exception as e:
|
|
177
|
+
raise DatabaseError(f"Failed to get auto-commit: {e}") from e
|
|
178
|
+
|
|
179
|
+
@property
|
|
180
|
+
def closed(self) -> bool:
|
|
181
|
+
"""Check if connection is closed."""
|
|
182
|
+
return self._closed
|
|
183
|
+
|
|
184
|
+
def __enter__(self) -> Connection:
|
|
185
|
+
"""Context manager entry."""
|
|
186
|
+
return self
|
|
187
|
+
|
|
188
|
+
def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
|
189
|
+
"""Context manager exit."""
|
|
190
|
+
if exc_type is None:
|
|
191
|
+
self.commit()
|
|
192
|
+
else:
|
|
193
|
+
self.rollback()
|
|
194
|
+
self.close()
|
|
195
|
+
|
|
196
|
+
def __del__(self) -> None:
|
|
197
|
+
"""Destructor to ensure connection is closed."""
|
|
198
|
+
if not self._closed:
|
|
199
|
+
try:
|
|
200
|
+
self.close()
|
|
201
|
+
except Exception:
|
|
202
|
+
pass
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def connect(
|
|
206
|
+
jclassname: str,
|
|
207
|
+
url: str,
|
|
208
|
+
driver_args: dict[str, Any] | list[Any] | None = None,
|
|
209
|
+
jars: list[str] | None = None,
|
|
210
|
+
libs: list[str] | None = None,
|
|
211
|
+
) -> Connection:
|
|
212
|
+
"""
|
|
213
|
+
Create a JDBC database connection.
|
|
214
|
+
|
|
215
|
+
This is the main entry point for creating connections, following DB-API 2.0.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
jclassname: Fully qualified Java class name of JDBC driver
|
|
219
|
+
url: JDBC connection URL
|
|
220
|
+
driver_args: Connection properties (dict) or [user, password] (list)
|
|
221
|
+
jars: List of JAR file paths for classpath
|
|
222
|
+
libs: Additional native library paths
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
Connection object
|
|
226
|
+
|
|
227
|
+
Example:
|
|
228
|
+
>>> conn = connect(
|
|
229
|
+
... 'org.postgresql.Driver',
|
|
230
|
+
... 'jdbc:postgresql://localhost:5432/mydb',
|
|
231
|
+
... {'user': 'myuser', 'password': 'mypass'}
|
|
232
|
+
... )
|
|
233
|
+
>>> cursor = conn.cursor()
|
|
234
|
+
>>> cursor.execute('SELECT * FROM users')
|
|
235
|
+
>>> rows = cursor.fetchall()
|
|
236
|
+
>>> conn.close()
|
|
237
|
+
"""
|
|
238
|
+
return Connection(
|
|
239
|
+
jclassname=jclassname,
|
|
240
|
+
url=url,
|
|
241
|
+
driver_args=driver_args,
|
|
242
|
+
jars=jars,
|
|
243
|
+
libs=libs,
|
|
244
|
+
)
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
"""
|
|
2
|
+
JDBC Cursor implementation following DB-API 2.0 specification.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
from collections.abc import Sequence
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from .exceptions import (
|
|
12
|
+
DataError,
|
|
13
|
+
InterfaceError,
|
|
14
|
+
ProgrammingError,
|
|
15
|
+
)
|
|
16
|
+
from .type_converter import TypeConverter
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Cursor:
|
|
22
|
+
"""
|
|
23
|
+
DB-API 2.0 compliant cursor for JDBC connections.
|
|
24
|
+
|
|
25
|
+
This class wraps a JDBC Statement/PreparedStatement and ResultSet.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(self, connection: Any, jdbc_connection: Any) -> None:
|
|
29
|
+
"""
|
|
30
|
+
Initialize cursor.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
connection: Parent Connection object
|
|
34
|
+
jdbc_connection: JDBC Connection object from JPype
|
|
35
|
+
"""
|
|
36
|
+
self._connection = connection
|
|
37
|
+
self._jdbc_connection = jdbc_connection
|
|
38
|
+
|
|
39
|
+
# These will be set when we execute queries
|
|
40
|
+
self._jdbc_statement: Any = None
|
|
41
|
+
self._jdbc_resultset: Any = None
|
|
42
|
+
|
|
43
|
+
# DB-API 2.0 spec requires these attributes
|
|
44
|
+
self._description: tuple[tuple[str, ...], ...] | None = None
|
|
45
|
+
self._rowcount: int = -1 # -1 means "unknown" per DB-API spec
|
|
46
|
+
self._arraysize: int = 1 # default fetch size
|
|
47
|
+
|
|
48
|
+
# We handle type conversion ourselves since JDBC types don't map 1:1 to Python
|
|
49
|
+
self._type_converter = TypeConverter()
|
|
50
|
+
self._closed = False
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def description(
|
|
54
|
+
self,
|
|
55
|
+
) -> tuple[tuple[str, Any, None, None, None, None, None], ...] | None:
|
|
56
|
+
"""
|
|
57
|
+
Get column descriptions from last query.
|
|
58
|
+
|
|
59
|
+
Returns 7-item tuples: (name, type_code, display_size, internal_size,
|
|
60
|
+
precision, scale, null_ok). We only provide name and type_code.
|
|
61
|
+
"""
|
|
62
|
+
return self._description
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def rowcount(self) -> int:
|
|
66
|
+
"""Get number of rows affected by last operation."""
|
|
67
|
+
return self._rowcount
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def arraysize(self) -> int:
|
|
71
|
+
"""Get default number of rows to fetch."""
|
|
72
|
+
return self._arraysize
|
|
73
|
+
|
|
74
|
+
@arraysize.setter
|
|
75
|
+
def arraysize(self, size: int) -> None:
|
|
76
|
+
"""Set default number of rows to fetch."""
|
|
77
|
+
self._arraysize = size
|
|
78
|
+
|
|
79
|
+
def close(self) -> None:
|
|
80
|
+
"""Close the cursor."""
|
|
81
|
+
if self._closed:
|
|
82
|
+
return
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
if self._jdbc_resultset is not None:
|
|
86
|
+
self._jdbc_resultset.close()
|
|
87
|
+
if self._jdbc_statement is not None:
|
|
88
|
+
self._jdbc_statement.close()
|
|
89
|
+
except Exception as e:
|
|
90
|
+
logger.warning(f"Error closing cursor: {e}")
|
|
91
|
+
finally:
|
|
92
|
+
self._jdbc_resultset = None
|
|
93
|
+
self._jdbc_statement = None
|
|
94
|
+
self._closed = True
|
|
95
|
+
|
|
96
|
+
def execute(
|
|
97
|
+
self, operation: str, parameters: Sequence[Any] | None = None
|
|
98
|
+
) -> Cursor:
|
|
99
|
+
"""
|
|
100
|
+
Execute a database operation.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
operation: SQL query or command
|
|
104
|
+
parameters: Parameters for the query
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
Self for chaining
|
|
108
|
+
|
|
109
|
+
Raises:
|
|
110
|
+
ProgrammingError: If cursor is closed or operation fails
|
|
111
|
+
"""
|
|
112
|
+
if self._closed:
|
|
113
|
+
raise InterfaceError("Cursor is closed")
|
|
114
|
+
|
|
115
|
+
try:
|
|
116
|
+
# Close previous statement and resultset
|
|
117
|
+
if self._jdbc_statement is not None:
|
|
118
|
+
self._jdbc_statement.close()
|
|
119
|
+
if self._jdbc_resultset is not None:
|
|
120
|
+
self._jdbc_resultset.close()
|
|
121
|
+
|
|
122
|
+
self._description = None
|
|
123
|
+
self._rowcount = -1
|
|
124
|
+
|
|
125
|
+
# Prepare statement
|
|
126
|
+
if parameters:
|
|
127
|
+
self._jdbc_statement = self._jdbc_connection.prepareStatement(operation)
|
|
128
|
+
self._bind_parameters(self._jdbc_statement, parameters)
|
|
129
|
+
has_resultset = self._jdbc_statement.execute()
|
|
130
|
+
else:
|
|
131
|
+
self._jdbc_statement = self._jdbc_connection.createStatement()
|
|
132
|
+
has_resultset = self._jdbc_statement.execute(operation)
|
|
133
|
+
|
|
134
|
+
# Process results
|
|
135
|
+
if has_resultset:
|
|
136
|
+
self._jdbc_resultset = self._jdbc_statement.getResultSet()
|
|
137
|
+
self._build_description()
|
|
138
|
+
else:
|
|
139
|
+
self._rowcount = self._jdbc_statement.getUpdateCount()
|
|
140
|
+
|
|
141
|
+
return self
|
|
142
|
+
|
|
143
|
+
except Exception as e:
|
|
144
|
+
logger.exception(f"Execute failed: {e}")
|
|
145
|
+
raise ProgrammingError(f"Failed to execute operation: {e}") from e
|
|
146
|
+
|
|
147
|
+
def executemany(
|
|
148
|
+
self, operation: str, seq_of_parameters: Sequence[Sequence[Any]]
|
|
149
|
+
) -> None:
|
|
150
|
+
"""
|
|
151
|
+
Execute a database operation multiple times.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
operation: SQL query or command
|
|
155
|
+
seq_of_parameters: Sequence of parameter sequences
|
|
156
|
+
|
|
157
|
+
Raises:
|
|
158
|
+
ProgrammingError: If operation fails
|
|
159
|
+
"""
|
|
160
|
+
if self._closed:
|
|
161
|
+
raise InterfaceError("Cursor is closed")
|
|
162
|
+
|
|
163
|
+
try:
|
|
164
|
+
self._jdbc_statement = self._jdbc_connection.prepareStatement(operation)
|
|
165
|
+
|
|
166
|
+
for parameters in seq_of_parameters:
|
|
167
|
+
self._bind_parameters(self._jdbc_statement, parameters)
|
|
168
|
+
self._jdbc_statement.addBatch()
|
|
169
|
+
|
|
170
|
+
results = self._jdbc_statement.executeBatch()
|
|
171
|
+
self._rowcount = sum(r for r in results if r >= 0)
|
|
172
|
+
|
|
173
|
+
except Exception as e:
|
|
174
|
+
logger.exception(f"ExecuteManyJDBC failed: {e}")
|
|
175
|
+
raise ProgrammingError(f"Failed to execute batch: {e}") from e
|
|
176
|
+
|
|
177
|
+
def fetchone(self) -> tuple[Any, ...] | None:
|
|
178
|
+
"""
|
|
179
|
+
Fetch next row from query result.
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
Tuple of column values or None if no more rows
|
|
183
|
+
|
|
184
|
+
Raises:
|
|
185
|
+
InterfaceError: If no query has been executed
|
|
186
|
+
"""
|
|
187
|
+
if self._jdbc_resultset is None:
|
|
188
|
+
raise InterfaceError("No query has been executed")
|
|
189
|
+
|
|
190
|
+
try:
|
|
191
|
+
if self._jdbc_resultset.next():
|
|
192
|
+
return self._fetch_row()
|
|
193
|
+
return None
|
|
194
|
+
except Exception as e:
|
|
195
|
+
raise DataError(f"Failed to fetch row: {e}") from e
|
|
196
|
+
|
|
197
|
+
def fetchmany(self, size: int | None = None) -> list[tuple[Any, ...]]:
|
|
198
|
+
"""
|
|
199
|
+
Fetch multiple rows from query result.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
size: Number of rows to fetch (default: arraysize)
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
List of row tuples
|
|
206
|
+
|
|
207
|
+
Raises:
|
|
208
|
+
InterfaceError: If no query has been executed
|
|
209
|
+
"""
|
|
210
|
+
if self._jdbc_resultset is None:
|
|
211
|
+
raise InterfaceError("No query has been executed")
|
|
212
|
+
|
|
213
|
+
if size is None:
|
|
214
|
+
size = self._arraysize
|
|
215
|
+
|
|
216
|
+
rows = []
|
|
217
|
+
try:
|
|
218
|
+
for _ in range(size):
|
|
219
|
+
if self._jdbc_resultset.next():
|
|
220
|
+
rows.append(self._fetch_row())
|
|
221
|
+
else:
|
|
222
|
+
break
|
|
223
|
+
return rows
|
|
224
|
+
except Exception as e:
|
|
225
|
+
raise DataError(f"Failed to fetch rows: {e}") from e
|
|
226
|
+
|
|
227
|
+
def fetchall(self) -> list[tuple[Any, ...]]:
|
|
228
|
+
"""
|
|
229
|
+
Fetch all remaining rows from query result.
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
List of row tuples
|
|
233
|
+
|
|
234
|
+
Raises:
|
|
235
|
+
InterfaceError: If no query has been executed
|
|
236
|
+
"""
|
|
237
|
+
if self._jdbc_resultset is None:
|
|
238
|
+
raise InterfaceError("No query has been executed")
|
|
239
|
+
|
|
240
|
+
rows = []
|
|
241
|
+
try:
|
|
242
|
+
while self._jdbc_resultset.next():
|
|
243
|
+
rows.append(self._fetch_row())
|
|
244
|
+
return rows
|
|
245
|
+
except Exception as e:
|
|
246
|
+
raise DataError(f"Failed to fetch all rows: {e}") from e
|
|
247
|
+
|
|
248
|
+
def setinputsizes(self, sizes: Sequence[int | None]) -> None:
|
|
249
|
+
"""Does nothing, for DB-API 2.0 compliance."""
|
|
250
|
+
# JDBC handles this automatically, so we just ignore it
|
|
251
|
+
|
|
252
|
+
def setoutputsize(self, size: int, column: int | None = None) -> None:
|
|
253
|
+
"""Does nothing, for DB-API 2.0 compliance."""
|
|
254
|
+
# Same as above - JDBC doesn't need this hint
|
|
255
|
+
|
|
256
|
+
def _bind_parameters(self, statement: Any, parameters: Sequence[Any]) -> None:
|
|
257
|
+
"""Bind parameters to prepared statement."""
|
|
258
|
+
# JDBC uses 1-based indexing for parameters (yeah, I know...)
|
|
259
|
+
for i, param in enumerate(parameters, start=1):
|
|
260
|
+
if param is None:
|
|
261
|
+
# NULL values need special handling in JDBC
|
|
262
|
+
statement.setNull(i, 0) # 0 = java.sql.Types.NULL
|
|
263
|
+
else:
|
|
264
|
+
# setObject auto-converts Python types to Java
|
|
265
|
+
statement.setObject(i, param)
|
|
266
|
+
|
|
267
|
+
def _build_description(self) -> None:
|
|
268
|
+
"""Build column description from ResultSetMetaData."""
|
|
269
|
+
if self._jdbc_resultset is None:
|
|
270
|
+
return
|
|
271
|
+
|
|
272
|
+
try:
|
|
273
|
+
metadata = self._jdbc_resultset.getMetaData()
|
|
274
|
+
column_count = metadata.getColumnCount()
|
|
275
|
+
|
|
276
|
+
description = []
|
|
277
|
+
for i in range(1, column_count + 1):
|
|
278
|
+
name = metadata.getColumnName(i)
|
|
279
|
+
type_code = metadata.getColumnType(i)
|
|
280
|
+
description.append(
|
|
281
|
+
(
|
|
282
|
+
name,
|
|
283
|
+
type_code,
|
|
284
|
+
None, # display_size
|
|
285
|
+
None, # internal_size
|
|
286
|
+
None, # precision
|
|
287
|
+
None, # scale
|
|
288
|
+
None, # null_ok
|
|
289
|
+
)
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
self._description = tuple(description)
|
|
293
|
+
|
|
294
|
+
except Exception as e:
|
|
295
|
+
logger.warning(f"Failed to build description: {e}")
|
|
296
|
+
self._description = None
|
|
297
|
+
|
|
298
|
+
def _fetch_row(self) -> tuple[Any, ...]:
|
|
299
|
+
"""Fetch a single row and convert types."""
|
|
300
|
+
if self._jdbc_resultset is None or self._description is None:
|
|
301
|
+
return ()
|
|
302
|
+
|
|
303
|
+
row = []
|
|
304
|
+
for i, (name, type_code, *_) in enumerate(self._description, start=1):
|
|
305
|
+
value = self._type_converter.convert_from_jdbc(
|
|
306
|
+
self._jdbc_resultset, i, type_code
|
|
307
|
+
)
|
|
308
|
+
row.append(value)
|
|
309
|
+
|
|
310
|
+
return tuple(row)
|
|
311
|
+
|
|
312
|
+
def __enter__(self) -> Cursor:
|
|
313
|
+
"""Context manager entry."""
|
|
314
|
+
return self
|
|
315
|
+
|
|
316
|
+
def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
|
317
|
+
"""Context manager exit."""
|
|
318
|
+
self.close()
|
|
319
|
+
|
|
320
|
+
def __iter__(self) -> Cursor:
|
|
321
|
+
"""Make cursor iterable."""
|
|
322
|
+
return self
|
|
323
|
+
|
|
324
|
+
def __next__(self) -> tuple[Any, ...]:
|
|
325
|
+
"""Fetch next row for iteration."""
|
|
326
|
+
row = self.fetchone()
|
|
327
|
+
if row is None:
|
|
328
|
+
raise StopIteration
|
|
329
|
+
return row
|