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.
Files changed (36) hide show
  1. sqlalchemy_jdbcapi/__init__.py +128 -0
  2. sqlalchemy_jdbcapi/_version.py +34 -0
  3. sqlalchemy_jdbcapi/dialects/__init__.py +30 -0
  4. sqlalchemy_jdbcapi/dialects/base.py +879 -0
  5. sqlalchemy_jdbcapi/dialects/db2.py +134 -0
  6. sqlalchemy_jdbcapi/dialects/mssql.py +117 -0
  7. sqlalchemy_jdbcapi/dialects/mysql.py +152 -0
  8. sqlalchemy_jdbcapi/dialects/oceanbase.py +218 -0
  9. sqlalchemy_jdbcapi/dialects/odbc_base.py +389 -0
  10. sqlalchemy_jdbcapi/dialects/odbc_mssql.py +69 -0
  11. sqlalchemy_jdbcapi/dialects/odbc_mysql.py +101 -0
  12. sqlalchemy_jdbcapi/dialects/odbc_oracle.py +80 -0
  13. sqlalchemy_jdbcapi/dialects/odbc_postgresql.py +63 -0
  14. sqlalchemy_jdbcapi/dialects/oracle.py +180 -0
  15. sqlalchemy_jdbcapi/dialects/postgresql.py +110 -0
  16. sqlalchemy_jdbcapi/dialects/sqlite.py +141 -0
  17. sqlalchemy_jdbcapi/jdbc/__init__.py +98 -0
  18. sqlalchemy_jdbcapi/jdbc/connection.py +244 -0
  19. sqlalchemy_jdbcapi/jdbc/cursor.py +329 -0
  20. sqlalchemy_jdbcapi/jdbc/dataframe.py +198 -0
  21. sqlalchemy_jdbcapi/jdbc/driver_manager.py +353 -0
  22. sqlalchemy_jdbcapi/jdbc/exceptions.py +53 -0
  23. sqlalchemy_jdbcapi/jdbc/jvm.py +176 -0
  24. sqlalchemy_jdbcapi/jdbc/type_converter.py +292 -0
  25. sqlalchemy_jdbcapi/jdbc/types.py +72 -0
  26. sqlalchemy_jdbcapi/odbc/__init__.py +46 -0
  27. sqlalchemy_jdbcapi/odbc/connection.py +136 -0
  28. sqlalchemy_jdbcapi/odbc/exceptions.py +48 -0
  29. sqlalchemy_jdbcapi/py.typed +2 -0
  30. sqlalchemy_jdbcapi-2.0.0.post2.dist-info/METADATA +825 -0
  31. sqlalchemy_jdbcapi-2.0.0.post2.dist-info/RECORD +36 -0
  32. sqlalchemy_jdbcapi-2.0.0.post2.dist-info/WHEEL +5 -0
  33. sqlalchemy_jdbcapi-2.0.0.post2.dist-info/entry_points.txt +20 -0
  34. sqlalchemy_jdbcapi-2.0.0.post2.dist-info/licenses/AUTHORS +7 -0
  35. sqlalchemy_jdbcapi-2.0.0.post2.dist-info/licenses/LICENSE +13 -0
  36. 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