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,176 @@
|
|
|
1
|
+
"""
|
|
2
|
+
JVM management and initialization.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
import os
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from .driver_manager import get_classpath_with_drivers
|
|
13
|
+
from .exceptions import JVMNotStartedError
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
_jvm_started = False
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_classpath(
|
|
21
|
+
auto_download: bool = True,
|
|
22
|
+
databases: list[str] | None = None,
|
|
23
|
+
) -> list[Path]:
|
|
24
|
+
"""
|
|
25
|
+
Get JDBC driver classpath from environment, auto-download, or both.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
auto_download: Whether to auto-download recommended JDBC drivers.
|
|
29
|
+
databases: List of database names for auto-download (e.g., ['postgresql', 'mysql']).
|
|
30
|
+
If None and auto_download is True, downloads all recommended drivers.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
List of paths to JDBC driver JAR files.
|
|
34
|
+
"""
|
|
35
|
+
# Get manual classpath from environment
|
|
36
|
+
manual_classpath = []
|
|
37
|
+
classpath_env = os.environ.get("CLASSPATH", "")
|
|
38
|
+
if not classpath_env:
|
|
39
|
+
classpath_env = os.environ.get("JDBC_DRIVER_PATH", "")
|
|
40
|
+
|
|
41
|
+
if classpath_env:
|
|
42
|
+
for path_str in classpath_env.split(os.pathsep):
|
|
43
|
+
path = Path(path_str)
|
|
44
|
+
if path.exists():
|
|
45
|
+
manual_classpath.append(path)
|
|
46
|
+
else:
|
|
47
|
+
logger.warning(f"Classpath entry not found: {path}")
|
|
48
|
+
|
|
49
|
+
# Combine manual and auto-downloaded drivers
|
|
50
|
+
return get_classpath_with_drivers(
|
|
51
|
+
databases=databases,
|
|
52
|
+
auto_download=auto_download,
|
|
53
|
+
manual_classpath=manual_classpath or None,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def start_jvm(
|
|
58
|
+
classpath: list[Path] | None = None,
|
|
59
|
+
jvm_path: Path | None = None,
|
|
60
|
+
jvm_args: list[str] | None = None,
|
|
61
|
+
auto_download: bool = True,
|
|
62
|
+
databases: list[str] | None = None,
|
|
63
|
+
) -> None:
|
|
64
|
+
"""
|
|
65
|
+
Start the JVM with specified classpath and arguments.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
classpath: List of paths to add to classpath. If None, uses environment + auto-download.
|
|
69
|
+
jvm_path: Path to JVM library. If None, JPype will auto-detect.
|
|
70
|
+
jvm_args: Additional JVM arguments (e.g., ['-Xmx512m']).
|
|
71
|
+
auto_download: Whether to auto-download JDBC drivers. Default: True.
|
|
72
|
+
databases: List of database names for auto-download. If None, downloads common drivers.
|
|
73
|
+
|
|
74
|
+
Raises:
|
|
75
|
+
JVMNotStartedError: If JVM fails to start.
|
|
76
|
+
"""
|
|
77
|
+
global _jvm_started
|
|
78
|
+
|
|
79
|
+
if _jvm_started:
|
|
80
|
+
logger.debug("JVM already started")
|
|
81
|
+
return
|
|
82
|
+
|
|
83
|
+
try:
|
|
84
|
+
import jpype
|
|
85
|
+
except ImportError as e:
|
|
86
|
+
raise JVMNotStartedError(
|
|
87
|
+
"JPype is not installed. Install with: pip install JPype1"
|
|
88
|
+
) from e
|
|
89
|
+
|
|
90
|
+
if jpype.isJVMStarted():
|
|
91
|
+
_jvm_started = True
|
|
92
|
+
logger.debug("JVM already started by another process")
|
|
93
|
+
return
|
|
94
|
+
|
|
95
|
+
# Build classpath
|
|
96
|
+
if classpath is None:
|
|
97
|
+
classpath = get_classpath(auto_download=auto_download, databases=databases)
|
|
98
|
+
|
|
99
|
+
if not classpath:
|
|
100
|
+
logger.warning(
|
|
101
|
+
"No JDBC drivers found in classpath. "
|
|
102
|
+
"Set CLASSPATH environment variable or enable auto_download."
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
classpath_str = os.pathsep.join(str(p) for p in classpath)
|
|
106
|
+
|
|
107
|
+
# Build JVM arguments
|
|
108
|
+
args = jvm_args or []
|
|
109
|
+
if classpath_str:
|
|
110
|
+
args.append(f"-Djava.class.path={classpath_str}")
|
|
111
|
+
|
|
112
|
+
# Start JVM
|
|
113
|
+
try:
|
|
114
|
+
if jvm_path:
|
|
115
|
+
jpype.startJVM(str(jvm_path), *args, convertStrings=True)
|
|
116
|
+
else:
|
|
117
|
+
jpype.startJVM(*args, convertStrings=True)
|
|
118
|
+
|
|
119
|
+
_jvm_started = True
|
|
120
|
+
logger.info("JVM started successfully")
|
|
121
|
+
logger.debug(f"Classpath: {classpath_str}")
|
|
122
|
+
logger.debug(f"JVM args: {args}")
|
|
123
|
+
if classpath:
|
|
124
|
+
logger.info(f"Loaded {len(classpath)} JDBC driver(s)")
|
|
125
|
+
|
|
126
|
+
except Exception as e:
|
|
127
|
+
raise JVMNotStartedError(f"Failed to start JVM: {e}") from e
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def is_jvm_started() -> bool:
|
|
131
|
+
"""Check if JVM is started."""
|
|
132
|
+
try:
|
|
133
|
+
import jpype
|
|
134
|
+
|
|
135
|
+
return jpype.isJVMStarted()
|
|
136
|
+
except ImportError:
|
|
137
|
+
return False
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def shutdown_jvm() -> None:
|
|
141
|
+
"""Shutdown the JVM (optional, usually not needed)."""
|
|
142
|
+
global _jvm_started
|
|
143
|
+
|
|
144
|
+
try:
|
|
145
|
+
import jpype
|
|
146
|
+
|
|
147
|
+
if jpype.isJVMStarted():
|
|
148
|
+
jpype.shutdownJVM()
|
|
149
|
+
_jvm_started = False
|
|
150
|
+
logger.info("JVM shutdown")
|
|
151
|
+
except ImportError:
|
|
152
|
+
pass
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def get_java_class(class_name: str) -> Any:
|
|
156
|
+
"""
|
|
157
|
+
Get a Java class by name.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
class_name: Fully qualified Java class name.
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
Java class object.
|
|
164
|
+
|
|
165
|
+
Raises:
|
|
166
|
+
JVMNotStartedError: If JVM is not started.
|
|
167
|
+
"""
|
|
168
|
+
if not is_jvm_started():
|
|
169
|
+
raise JVMNotStartedError("JVM is not started. Call start_jvm() first.")
|
|
170
|
+
|
|
171
|
+
try:
|
|
172
|
+
import jpype
|
|
173
|
+
|
|
174
|
+
return jpype.JClass(class_name)
|
|
175
|
+
except Exception as e:
|
|
176
|
+
raise JVMNotStartedError(f"Failed to load Java class {class_name}: {e}") from e
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Type conversion between JDBC and Python types.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import datetime
|
|
8
|
+
import logging
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TypeConverter:
|
|
15
|
+
"""
|
|
16
|
+
Handles conversion between JDBC SQL types and Python types.
|
|
17
|
+
|
|
18
|
+
Java SQL Types (from java.sql.Types):
|
|
19
|
+
-7 BIT
|
|
20
|
+
-6 TINYINT
|
|
21
|
+
-5 BIGINT
|
|
22
|
+
-4 LONGVARBINARY
|
|
23
|
+
-3 VARBINARY
|
|
24
|
+
-2 BINARY
|
|
25
|
+
-1 LONGVARCHAR
|
|
26
|
+
1 CHAR
|
|
27
|
+
2 NUMERIC
|
|
28
|
+
3 DECIMAL
|
|
29
|
+
4 INTEGER
|
|
30
|
+
5 SMALLINT
|
|
31
|
+
6 FLOAT
|
|
32
|
+
7 REAL
|
|
33
|
+
8 DOUBLE
|
|
34
|
+
12 VARCHAR
|
|
35
|
+
91 DATE
|
|
36
|
+
92 TIME
|
|
37
|
+
93 TIMESTAMP
|
|
38
|
+
2004 BLOB
|
|
39
|
+
2005 CLOB
|
|
40
|
+
2011 NCLOB
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
# SQL Type constants (from java.sql.Types)
|
|
44
|
+
JDBC_TYPES = {
|
|
45
|
+
-7: "BIT",
|
|
46
|
+
-6: "TINYINT",
|
|
47
|
+
-5: "BIGINT",
|
|
48
|
+
-4: "LONGVARBINARY",
|
|
49
|
+
-3: "VARBINARY",
|
|
50
|
+
-2: "BINARY",
|
|
51
|
+
-1: "LONGVARCHAR",
|
|
52
|
+
1: "CHAR",
|
|
53
|
+
2: "NUMERIC",
|
|
54
|
+
3: "DECIMAL",
|
|
55
|
+
4: "INTEGER",
|
|
56
|
+
5: "SMALLINT",
|
|
57
|
+
6: "FLOAT",
|
|
58
|
+
7: "REAL",
|
|
59
|
+
8: "DOUBLE",
|
|
60
|
+
12: "VARCHAR",
|
|
61
|
+
91: "DATE",
|
|
62
|
+
92: "TIME",
|
|
63
|
+
93: "TIMESTAMP",
|
|
64
|
+
2004: "BLOB",
|
|
65
|
+
2005: "CLOB",
|
|
66
|
+
2011: "NCLOB",
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
def convert_from_jdbc( # noqa: C901 - Type conversion requires complexity
|
|
70
|
+
self, resultset: Any, column_index: int, sql_type: int
|
|
71
|
+
) -> Any:
|
|
72
|
+
"""
|
|
73
|
+
Convert JDBC result to Python type.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
resultset: JDBC ResultSet object
|
|
77
|
+
column_index: 1-based column index
|
|
78
|
+
sql_type: JDBC SQL type code
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
Python value
|
|
82
|
+
"""
|
|
83
|
+
try:
|
|
84
|
+
# First check if the value is NULL
|
|
85
|
+
# We have to call getObject first to trigger wasNull() properly
|
|
86
|
+
value = resultset.getObject(column_index)
|
|
87
|
+
if value is None or resultset.wasNull():
|
|
88
|
+
return None
|
|
89
|
+
|
|
90
|
+
# String types
|
|
91
|
+
if sql_type in (1, 12, -1): # CHAR, VARCHAR, LONGVARCHAR
|
|
92
|
+
return self._convert_string(resultset, column_index)
|
|
93
|
+
|
|
94
|
+
# Numeric types
|
|
95
|
+
if sql_type in (
|
|
96
|
+
-6,
|
|
97
|
+
5,
|
|
98
|
+
4,
|
|
99
|
+
-5,
|
|
100
|
+
): # TINYINT, SMALLINT, INTEGER, BIGINT
|
|
101
|
+
return self._convert_int(resultset, column_index)
|
|
102
|
+
|
|
103
|
+
if sql_type in (2, 3): # NUMERIC, DECIMAL
|
|
104
|
+
return self._convert_decimal(resultset, column_index)
|
|
105
|
+
|
|
106
|
+
if sql_type in (6, 7, 8): # FLOAT, REAL, DOUBLE
|
|
107
|
+
return self._convert_float(resultset, column_index)
|
|
108
|
+
|
|
109
|
+
# Boolean
|
|
110
|
+
if sql_type == -7: # BIT
|
|
111
|
+
return self._convert_boolean(resultset, column_index)
|
|
112
|
+
|
|
113
|
+
# Date/Time types
|
|
114
|
+
if sql_type == 91: # DATE
|
|
115
|
+
return self._convert_date(resultset, column_index)
|
|
116
|
+
|
|
117
|
+
if sql_type == 92: # TIME
|
|
118
|
+
return self._convert_time(resultset, column_index)
|
|
119
|
+
|
|
120
|
+
if sql_type == 93: # TIMESTAMP
|
|
121
|
+
return self._convert_timestamp(resultset, column_index)
|
|
122
|
+
|
|
123
|
+
# Binary types
|
|
124
|
+
if sql_type in (-4, -3, -2): # LONGVARBINARY, VARBINARY, BINARY
|
|
125
|
+
return self._convert_binary(resultset, column_index)
|
|
126
|
+
|
|
127
|
+
# LOB types
|
|
128
|
+
if sql_type == 2004: # BLOB
|
|
129
|
+
return self._convert_blob(resultset, column_index)
|
|
130
|
+
|
|
131
|
+
if sql_type in (2005, 2011): # CLOB, NCLOB
|
|
132
|
+
return self._convert_clob(resultset, column_index)
|
|
133
|
+
|
|
134
|
+
# Array type (PostgreSQL, Oracle)
|
|
135
|
+
if hasattr(value, "getArray"):
|
|
136
|
+
return self._convert_array(value)
|
|
137
|
+
|
|
138
|
+
# Default: return as-is
|
|
139
|
+
logger.debug(
|
|
140
|
+
f"Unknown SQL type {sql_type} "
|
|
141
|
+
f"({self.JDBC_TYPES.get(sql_type, 'UNKNOWN')}), "
|
|
142
|
+
f"returning as-is"
|
|
143
|
+
)
|
|
144
|
+
return value
|
|
145
|
+
|
|
146
|
+
except Exception as e:
|
|
147
|
+
logger.warning(
|
|
148
|
+
f"Type conversion failed for column {column_index}, "
|
|
149
|
+
f"type {sql_type}: {e}"
|
|
150
|
+
)
|
|
151
|
+
# Fallback to getObject
|
|
152
|
+
return resultset.getObject(column_index)
|
|
153
|
+
|
|
154
|
+
def _convert_string(self, resultset: Any, column_index: int) -> str | None:
|
|
155
|
+
"""Convert to Python string."""
|
|
156
|
+
value = resultset.getString(column_index)
|
|
157
|
+
return value if value is not None else None
|
|
158
|
+
|
|
159
|
+
def _convert_int(self, resultset: Any, column_index: int) -> int | None:
|
|
160
|
+
"""Convert to Python int."""
|
|
161
|
+
value = resultset.getLong(column_index)
|
|
162
|
+
return value if not resultset.wasNull() else None
|
|
163
|
+
|
|
164
|
+
def _convert_decimal(self, resultset: Any, column_index: int) -> float | None:
|
|
165
|
+
"""Convert decimal to Python float or decimal."""
|
|
166
|
+
value = resultset.getBigDecimal(column_index)
|
|
167
|
+
if value is None or resultset.wasNull():
|
|
168
|
+
return None
|
|
169
|
+
# Convert Java BigDecimal to Python float
|
|
170
|
+
# Could use decimal.Decimal for precision, but float is more common
|
|
171
|
+
return float(str(value))
|
|
172
|
+
|
|
173
|
+
def _convert_float(self, resultset: Any, column_index: int) -> float | None:
|
|
174
|
+
"""Convert to Python float."""
|
|
175
|
+
value = resultset.getDouble(column_index)
|
|
176
|
+
return value if not resultset.wasNull() else None
|
|
177
|
+
|
|
178
|
+
def _convert_boolean(self, resultset: Any, column_index: int) -> bool | None:
|
|
179
|
+
"""Convert to Python bool."""
|
|
180
|
+
value = resultset.getBoolean(column_index)
|
|
181
|
+
return value if not resultset.wasNull() else None
|
|
182
|
+
|
|
183
|
+
def _convert_date(self, resultset: Any, column_index: int) -> datetime.date | None:
|
|
184
|
+
"""Convert to Python date."""
|
|
185
|
+
value = resultset.getDate(column_index)
|
|
186
|
+
if value is None or resultset.wasNull():
|
|
187
|
+
return None
|
|
188
|
+
|
|
189
|
+
# Convert java.sql.Date to Python date
|
|
190
|
+
try:
|
|
191
|
+
# Get the timestamp in milliseconds
|
|
192
|
+
timestamp_ms = value.getTime()
|
|
193
|
+
# Convert to seconds
|
|
194
|
+
timestamp_s = timestamp_ms / 1000.0
|
|
195
|
+
# Create Python datetime and extract date
|
|
196
|
+
return datetime.datetime.fromtimestamp(timestamp_s).date()
|
|
197
|
+
except Exception as e:
|
|
198
|
+
logger.warning(f"Date conversion failed: {e}")
|
|
199
|
+
return None
|
|
200
|
+
|
|
201
|
+
def _convert_time(self, resultset: Any, column_index: int) -> datetime.time | None:
|
|
202
|
+
"""Convert to Python time."""
|
|
203
|
+
value = resultset.getTime(column_index)
|
|
204
|
+
if value is None or resultset.wasNull():
|
|
205
|
+
return None
|
|
206
|
+
|
|
207
|
+
try:
|
|
208
|
+
timestamp_ms = value.getTime()
|
|
209
|
+
timestamp_s = timestamp_ms / 1000.0
|
|
210
|
+
return datetime.datetime.fromtimestamp(timestamp_s).time()
|
|
211
|
+
except Exception as e:
|
|
212
|
+
logger.warning(f"Time conversion failed: {e}")
|
|
213
|
+
return None
|
|
214
|
+
|
|
215
|
+
def _convert_timestamp(
|
|
216
|
+
self, resultset: Any, column_index: int
|
|
217
|
+
) -> datetime.datetime | None:
|
|
218
|
+
"""Convert to Python datetime."""
|
|
219
|
+
value = resultset.getTimestamp(column_index)
|
|
220
|
+
if value is None or resultset.wasNull():
|
|
221
|
+
return None
|
|
222
|
+
|
|
223
|
+
try:
|
|
224
|
+
timestamp_ms = value.getTime()
|
|
225
|
+
timestamp_s = timestamp_ms / 1000.0
|
|
226
|
+
# Get nanoseconds for microsecond precision
|
|
227
|
+
nanos = value.getNanos()
|
|
228
|
+
micros = nanos // 1000
|
|
229
|
+
dt = datetime.datetime.fromtimestamp(timestamp_s)
|
|
230
|
+
# Replace microseconds
|
|
231
|
+
return dt.replace(microsecond=micros % 1000000)
|
|
232
|
+
except Exception as e:
|
|
233
|
+
logger.warning(f"Timestamp conversion failed: {e}")
|
|
234
|
+
return None
|
|
235
|
+
|
|
236
|
+
def _convert_binary(self, resultset: Any, column_index: int) -> bytes | None:
|
|
237
|
+
"""Convert to Python bytes."""
|
|
238
|
+
value = resultset.getBytes(column_index)
|
|
239
|
+
if value is None or resultset.wasNull():
|
|
240
|
+
return None
|
|
241
|
+
return bytes(value)
|
|
242
|
+
|
|
243
|
+
def _convert_blob(self, resultset: Any, column_index: int) -> bytes | None:
|
|
244
|
+
"""Convert BLOB to Python bytes."""
|
|
245
|
+
blob = resultset.getBlob(column_index)
|
|
246
|
+
if blob is None or resultset.wasNull():
|
|
247
|
+
return None
|
|
248
|
+
|
|
249
|
+
try:
|
|
250
|
+
length = blob.length()
|
|
251
|
+
return bytes(blob.getBytes(1, length))
|
|
252
|
+
except Exception as e:
|
|
253
|
+
logger.warning(f"BLOB conversion failed: {e}")
|
|
254
|
+
return None
|
|
255
|
+
|
|
256
|
+
def _convert_clob(self, resultset: Any, column_index: int) -> str | None:
|
|
257
|
+
"""Convert CLOB to Python string."""
|
|
258
|
+
clob = resultset.getClob(column_index)
|
|
259
|
+
if clob is None or resultset.wasNull():
|
|
260
|
+
return None
|
|
261
|
+
|
|
262
|
+
try:
|
|
263
|
+
length = clob.length()
|
|
264
|
+
return clob.getSubString(1, length)
|
|
265
|
+
except Exception as e:
|
|
266
|
+
# Fallback: try reading as stream
|
|
267
|
+
try:
|
|
268
|
+
reader = clob.getCharacterStream()
|
|
269
|
+
chars = []
|
|
270
|
+
while True:
|
|
271
|
+
char = reader.read()
|
|
272
|
+
if char == -1:
|
|
273
|
+
break
|
|
274
|
+
chars.append(chr(char))
|
|
275
|
+
return "".join(chars)
|
|
276
|
+
except Exception as e2:
|
|
277
|
+
logger.warning(f"CLOB conversion failed: {e}, {e2}")
|
|
278
|
+
return None
|
|
279
|
+
|
|
280
|
+
def _convert_array(self, array: Any) -> list[Any] | None:
|
|
281
|
+
"""Convert SQL Array to Python list."""
|
|
282
|
+
try:
|
|
283
|
+
# Call getArray() on the java.sql.Array object
|
|
284
|
+
java_array = array.getArray()
|
|
285
|
+
if java_array is None:
|
|
286
|
+
return None
|
|
287
|
+
|
|
288
|
+
# Convert to Python list
|
|
289
|
+
return list(java_array)
|
|
290
|
+
except Exception as e:
|
|
291
|
+
logger.warning(f"Array conversion failed: {e}")
|
|
292
|
+
return None
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""
|
|
2
|
+
DB-API 2.0 type objects and constructors.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import datetime
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# Type Objects for DB-API 2.0 compliance
|
|
12
|
+
class DBAPITypeObject:
|
|
13
|
+
"""Base class for DB-API type objects."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, *values: Any) -> None:
|
|
16
|
+
self.values = frozenset(values)
|
|
17
|
+
|
|
18
|
+
def __eq__(self, other: Any) -> bool:
|
|
19
|
+
if isinstance(other, DBAPITypeObject):
|
|
20
|
+
return self.values == other.values
|
|
21
|
+
return other in self.values
|
|
22
|
+
|
|
23
|
+
def __hash__(self) -> int:
|
|
24
|
+
return hash(self.values)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# Type objects
|
|
28
|
+
STRING = DBAPITypeObject(str)
|
|
29
|
+
BINARY = DBAPITypeObject(bytes, bytearray)
|
|
30
|
+
NUMBER = DBAPITypeObject(int, float)
|
|
31
|
+
DATETIME = DBAPITypeObject(datetime.datetime, datetime.date, datetime.time)
|
|
32
|
+
ROWID = DBAPITypeObject(int)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# Date/Time constructors (DB-API 2.0 requires capitalized names)
|
|
36
|
+
def Date(year: int, month: int, day: int) -> datetime.date: # noqa: N802
|
|
37
|
+
"""Construct a date object."""
|
|
38
|
+
return datetime.date(year, month, day)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def Time(hour: int, minute: int, second: int) -> datetime.time: # noqa: N802
|
|
42
|
+
"""Construct a time object."""
|
|
43
|
+
return datetime.time(hour, minute, second)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def Timestamp( # noqa: N802
|
|
47
|
+
year: int, month: int, day: int, hour: int, minute: int, second: int
|
|
48
|
+
) -> datetime.datetime:
|
|
49
|
+
"""Construct a timestamp object."""
|
|
50
|
+
return datetime.datetime(year, month, day, hour, minute, second) # noqa: DTZ001
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def DateFromTicks(ticks: float) -> datetime.date: # noqa: N802
|
|
54
|
+
"""Construct a date object from ticks since epoch."""
|
|
55
|
+
return datetime.date.fromtimestamp(ticks) # noqa: DTZ012
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def TimeFromTicks(ticks: float) -> datetime.time: # noqa: N802
|
|
59
|
+
"""Construct a time object from ticks since epoch."""
|
|
60
|
+
return datetime.datetime.fromtimestamp(ticks).time()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def TimestampFromTicks(ticks: float) -> datetime.datetime: # noqa: N802
|
|
64
|
+
"""Construct a timestamp object from ticks since epoch."""
|
|
65
|
+
return datetime.datetime.fromtimestamp(ticks)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def Binary(value: bytes | bytearray | str) -> bytes: # noqa: N802
|
|
69
|
+
"""Construct a binary object."""
|
|
70
|
+
if isinstance(value, str):
|
|
71
|
+
return value.encode("utf-8")
|
|
72
|
+
return bytes(value)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ODBC Bridge Layer for SQLAlchemy.
|
|
3
|
+
|
|
4
|
+
This module provides a Python DB-API 2.0 compliant interface to ODBC drivers
|
|
5
|
+
using pyodbc. This complements the JDBC bridge and provides an alternative
|
|
6
|
+
connection method for databases with good ODBC driver support.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from .connection import Connection, connect
|
|
12
|
+
from .exceptions import (
|
|
13
|
+
DatabaseError,
|
|
14
|
+
DataError,
|
|
15
|
+
Error,
|
|
16
|
+
IntegrityError,
|
|
17
|
+
InterfaceError,
|
|
18
|
+
InternalError,
|
|
19
|
+
NotSupportedError,
|
|
20
|
+
OperationalError,
|
|
21
|
+
ProgrammingError,
|
|
22
|
+
Warning,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
# Core functions
|
|
27
|
+
"connect",
|
|
28
|
+
# Classes
|
|
29
|
+
"Connection",
|
|
30
|
+
# Exceptions
|
|
31
|
+
"Error",
|
|
32
|
+
"Warning",
|
|
33
|
+
"InterfaceError",
|
|
34
|
+
"DatabaseError",
|
|
35
|
+
"InternalError",
|
|
36
|
+
"OperationalError",
|
|
37
|
+
"ProgrammingError",
|
|
38
|
+
"IntegrityError",
|
|
39
|
+
"DataError",
|
|
40
|
+
"NotSupportedError",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
# DB-API 2.0 module attributes
|
|
44
|
+
apilevel = "2.0"
|
|
45
|
+
threadsafety = 1 # Threads may share the module, but not connections
|
|
46
|
+
paramstyle = "qmark" # Question mark style, e.g. ...WHERE name=?
|