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,198 @@
|
|
|
1
|
+
"""
|
|
2
|
+
DataFrame integration for pandas, polars, and Apache Arrow.
|
|
3
|
+
|
|
4
|
+
This module provides utilities to convert JDBC query results directly
|
|
5
|
+
into DataFrames for data science and ML workflows.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
from typing import TYPE_CHECKING, Any
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from .cursor import Cursor
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def cursor_to_pandas(cursor: Cursor) -> Any:
|
|
20
|
+
"""
|
|
21
|
+
Convert cursor results to pandas DataFrame.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
cursor: Cursor with executed query
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
pandas.DataFrame
|
|
28
|
+
|
|
29
|
+
Raises:
|
|
30
|
+
ImportError: If pandas is not installed
|
|
31
|
+
ValueError: If cursor has no results
|
|
32
|
+
|
|
33
|
+
Example:
|
|
34
|
+
>>> cursor.execute("SELECT * FROM users")
|
|
35
|
+
>>> df = cursor_to_pandas(cursor)
|
|
36
|
+
>>> print(df.head())
|
|
37
|
+
"""
|
|
38
|
+
try:
|
|
39
|
+
import pandas as pd
|
|
40
|
+
except ImportError as e:
|
|
41
|
+
raise ImportError(
|
|
42
|
+
"pandas is not installed. Install with: pip install pandas"
|
|
43
|
+
) from e
|
|
44
|
+
|
|
45
|
+
if cursor.description is None:
|
|
46
|
+
raise ValueError("Cursor has no result set")
|
|
47
|
+
|
|
48
|
+
# Get column names
|
|
49
|
+
columns = [desc[0] for desc in cursor.description]
|
|
50
|
+
|
|
51
|
+
# Fetch all rows
|
|
52
|
+
rows = cursor.fetchall()
|
|
53
|
+
|
|
54
|
+
# Create DataFrame
|
|
55
|
+
df = pd.DataFrame(rows, columns=columns)
|
|
56
|
+
logger.debug(f"Created pandas DataFrame with shape {df.shape}")
|
|
57
|
+
|
|
58
|
+
return df
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def cursor_to_polars(cursor: Cursor) -> Any:
|
|
62
|
+
"""
|
|
63
|
+
Convert cursor results to polars DataFrame.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
cursor: Cursor with executed query
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
polars.DataFrame
|
|
70
|
+
|
|
71
|
+
Raises:
|
|
72
|
+
ImportError: If polars is not installed
|
|
73
|
+
ValueError: If cursor has no results
|
|
74
|
+
|
|
75
|
+
Example:
|
|
76
|
+
>>> cursor.execute("SELECT * FROM users")
|
|
77
|
+
>>> df = cursor_to_polars(cursor)
|
|
78
|
+
>>> print(df.head())
|
|
79
|
+
"""
|
|
80
|
+
try:
|
|
81
|
+
import polars as pl
|
|
82
|
+
except ImportError as e:
|
|
83
|
+
raise ImportError(
|
|
84
|
+
"polars is not installed. Install with: pip install polars"
|
|
85
|
+
) from e
|
|
86
|
+
|
|
87
|
+
if cursor.description is None:
|
|
88
|
+
raise ValueError("Cursor has no result set")
|
|
89
|
+
|
|
90
|
+
# Get column names
|
|
91
|
+
columns = [desc[0] for desc in cursor.description]
|
|
92
|
+
|
|
93
|
+
# Fetch all rows
|
|
94
|
+
rows = cursor.fetchall()
|
|
95
|
+
|
|
96
|
+
# Create DataFrame from dict of lists
|
|
97
|
+
data = {col: [row[i] for row in rows] for i, col in enumerate(columns)}
|
|
98
|
+
df = pl.DataFrame(data)
|
|
99
|
+
logger.debug(f"Created polars DataFrame with shape {df.shape}")
|
|
100
|
+
|
|
101
|
+
return df
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def cursor_to_arrow(cursor: Cursor) -> Any:
|
|
105
|
+
"""
|
|
106
|
+
Convert cursor results to Apache Arrow Table.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
cursor: Cursor with executed query
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
pyarrow.Table
|
|
113
|
+
|
|
114
|
+
Raises:
|
|
115
|
+
ImportError: If pyarrow is not installed
|
|
116
|
+
ValueError: If cursor has no results
|
|
117
|
+
|
|
118
|
+
Example:
|
|
119
|
+
>>> cursor.execute("SELECT * FROM users")
|
|
120
|
+
>>> table = cursor_to_arrow(cursor)
|
|
121
|
+
>>> print(table.schema)
|
|
122
|
+
"""
|
|
123
|
+
try:
|
|
124
|
+
import pyarrow as pa
|
|
125
|
+
except ImportError as e:
|
|
126
|
+
raise ImportError(
|
|
127
|
+
"pyarrow is not installed. Install with: pip install pyarrow"
|
|
128
|
+
) from e
|
|
129
|
+
|
|
130
|
+
if cursor.description is None:
|
|
131
|
+
raise ValueError("Cursor has no result set")
|
|
132
|
+
|
|
133
|
+
# Get column names
|
|
134
|
+
columns = [desc[0] for desc in cursor.description]
|
|
135
|
+
|
|
136
|
+
# Fetch all rows
|
|
137
|
+
rows = cursor.fetchall()
|
|
138
|
+
|
|
139
|
+
# Convert to Arrow Table
|
|
140
|
+
# Build column arrays
|
|
141
|
+
if not rows:
|
|
142
|
+
# Empty result
|
|
143
|
+
arrays = [pa.array([]) for _ in columns]
|
|
144
|
+
else:
|
|
145
|
+
# Transpose rows to columns
|
|
146
|
+
col_data = [[row[i] for row in rows] for i in range(len(columns))]
|
|
147
|
+
arrays = [pa.array(col) for col in col_data]
|
|
148
|
+
|
|
149
|
+
table = pa.Table.from_arrays(arrays, names=columns)
|
|
150
|
+
logger.debug(f"Created Arrow Table with {table.num_rows} rows")
|
|
151
|
+
|
|
152
|
+
return table
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def cursor_to_dict(cursor: Cursor) -> list[dict[str, Any]]:
|
|
156
|
+
"""
|
|
157
|
+
Convert cursor results to list of dictionaries.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
cursor: Cursor with executed query
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
List of row dictionaries
|
|
164
|
+
|
|
165
|
+
Example:
|
|
166
|
+
>>> cursor.execute("SELECT * FROM users")
|
|
167
|
+
>>> rows = cursor_to_dict(cursor)
|
|
168
|
+
>>> print(rows[0])
|
|
169
|
+
{'id': 1, 'name': 'Alice', 'email': 'alice@example.com'}
|
|
170
|
+
"""
|
|
171
|
+
if cursor.description is None:
|
|
172
|
+
raise ValueError("Cursor has no result set")
|
|
173
|
+
|
|
174
|
+
columns = [desc[0] for desc in cursor.description]
|
|
175
|
+
rows = cursor.fetchall()
|
|
176
|
+
|
|
177
|
+
return [dict(zip(columns, row)) for row in rows]
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
# Add convenience methods to Cursor class
|
|
181
|
+
def _add_dataframe_methods() -> None:
|
|
182
|
+
"""Add DataFrame methods to Cursor class."""
|
|
183
|
+
from .cursor import Cursor
|
|
184
|
+
|
|
185
|
+
# Add methods
|
|
186
|
+
Cursor.to_pandas = lambda self: cursor_to_pandas(self) # type: ignore
|
|
187
|
+
Cursor.to_polars = lambda self: cursor_to_polars(self) # type: ignore
|
|
188
|
+
Cursor.to_arrow = lambda self: cursor_to_arrow(self) # type: ignore
|
|
189
|
+
Cursor.to_dict = lambda self: cursor_to_dict(self) # type: ignore
|
|
190
|
+
|
|
191
|
+
logger.debug("Added DataFrame methods to Cursor class")
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
# Auto-register methods on import
|
|
195
|
+
try:
|
|
196
|
+
_add_dataframe_methods()
|
|
197
|
+
except Exception as e:
|
|
198
|
+
logger.debug(f"Could not add DataFrame methods: {e}")
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
"""
|
|
2
|
+
JDBC driver auto-download and management.
|
|
3
|
+
|
|
4
|
+
This module handles automatic downloading of JDBC drivers from Maven Central
|
|
5
|
+
and provides fallback to manual driver configuration via CLASSPATH.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
import os
|
|
12
|
+
import shutil
|
|
13
|
+
import urllib.request
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import NamedTuple
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
# Default driver cache directory
|
|
20
|
+
DEFAULT_DRIVER_CACHE = Path.home() / ".sqlalchemy-jdbcapi" / "drivers"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class JDBCDriver(NamedTuple):
|
|
24
|
+
"""JDBC driver metadata for automatic download."""
|
|
25
|
+
|
|
26
|
+
group_id: str
|
|
27
|
+
artifact_id: str
|
|
28
|
+
version: str
|
|
29
|
+
classifier: str | None = None
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def filename(self) -> str:
|
|
33
|
+
"""Get the JAR filename."""
|
|
34
|
+
if self.classifier:
|
|
35
|
+
return f"{self.artifact_id}-{self.version}-{self.classifier}.jar"
|
|
36
|
+
return f"{self.artifact_id}-{self.version}.jar"
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def maven_url(self) -> str:
|
|
40
|
+
"""Get the Maven Central download URL."""
|
|
41
|
+
base_url = "https://repo1.maven.org/maven2"
|
|
42
|
+
group_path = self.group_id.replace(".", "/")
|
|
43
|
+
return (
|
|
44
|
+
f"{base_url}/{group_path}/{self.artifact_id}/{self.version}/{self.filename}"
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# Recommended JDBC drivers for auto-download from Maven Central
|
|
49
|
+
# These versions are tested and known to work well
|
|
50
|
+
# TODO: Consider checking for newer versions periodically
|
|
51
|
+
RECOMMENDED_JDBC_DRIVERS = {
|
|
52
|
+
"postgresql": JDBCDriver(
|
|
53
|
+
group_id="org.postgresql",
|
|
54
|
+
artifact_id="postgresql",
|
|
55
|
+
version="42.7.1", # Latest stable as of 2024
|
|
56
|
+
),
|
|
57
|
+
"mysql": JDBCDriver(
|
|
58
|
+
group_id="com.mysql",
|
|
59
|
+
artifact_id="mysql-connector-j",
|
|
60
|
+
version="8.3.0", # Note: Oracle renamed this from mysql-connector-java
|
|
61
|
+
),
|
|
62
|
+
"mariadb": JDBCDriver(
|
|
63
|
+
group_id="org.mariadb.jdbc",
|
|
64
|
+
artifact_id="mariadb-java-client",
|
|
65
|
+
version="3.3.2",
|
|
66
|
+
),
|
|
67
|
+
"mssql": JDBCDriver(
|
|
68
|
+
group_id="com.microsoft.sqlserver",
|
|
69
|
+
artifact_id="mssql-jdbc",
|
|
70
|
+
version="12.6.0.jre11", # JRE11 version for Java 11+ compatibility
|
|
71
|
+
),
|
|
72
|
+
"oracle": JDBCDriver(
|
|
73
|
+
group_id="com.oracle.database.jdbc",
|
|
74
|
+
artifact_id="ojdbc11",
|
|
75
|
+
version="23.3.0.23.09",
|
|
76
|
+
),
|
|
77
|
+
"db2": JDBCDriver(
|
|
78
|
+
group_id="com.ibm.db2",
|
|
79
|
+
artifact_id="jcc",
|
|
80
|
+
version="11.5.9.0",
|
|
81
|
+
),
|
|
82
|
+
"sqlite": JDBCDriver(
|
|
83
|
+
group_id="org.xerial",
|
|
84
|
+
artifact_id="sqlite-jdbc",
|
|
85
|
+
version="3.45.0.0",
|
|
86
|
+
),
|
|
87
|
+
"oceanbase": JDBCDriver(
|
|
88
|
+
group_id="com.oceanbase",
|
|
89
|
+
artifact_id="oceanbase-client",
|
|
90
|
+
version="2.4.9",
|
|
91
|
+
),
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def get_driver_cache_dir() -> Path:
|
|
96
|
+
"""
|
|
97
|
+
Get the driver cache directory.
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
Path to the driver cache directory.
|
|
101
|
+
"""
|
|
102
|
+
cache_dir = os.environ.get("SQLALCHEMY_JDBCAPI_DRIVER_CACHE")
|
|
103
|
+
if cache_dir:
|
|
104
|
+
return Path(cache_dir)
|
|
105
|
+
return DEFAULT_DRIVER_CACHE
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def download_driver(
|
|
109
|
+
driver: JDBCDriver,
|
|
110
|
+
cache_dir: Path | None = None,
|
|
111
|
+
force: bool = False,
|
|
112
|
+
) -> Path:
|
|
113
|
+
"""
|
|
114
|
+
Download a JDBC driver from Maven Central.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
driver: JDBC driver metadata.
|
|
118
|
+
cache_dir: Directory to cache downloaded drivers. If None, uses default.
|
|
119
|
+
force: Force re-download even if driver exists.
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
Path to the downloaded driver JAR file.
|
|
123
|
+
|
|
124
|
+
Raises:
|
|
125
|
+
RuntimeError: If download fails.
|
|
126
|
+
"""
|
|
127
|
+
if cache_dir is None:
|
|
128
|
+
cache_dir = get_driver_cache_dir()
|
|
129
|
+
|
|
130
|
+
# Create cache directory if it doesn't exist
|
|
131
|
+
cache_dir.mkdir(parents=True, exist_ok=True)
|
|
132
|
+
|
|
133
|
+
# Target file path
|
|
134
|
+
target_path = cache_dir / driver.filename
|
|
135
|
+
|
|
136
|
+
# Check if driver already exists
|
|
137
|
+
if target_path.exists() and not force:
|
|
138
|
+
logger.debug(f"Driver already cached: {target_path}")
|
|
139
|
+
return target_path
|
|
140
|
+
|
|
141
|
+
# Download driver
|
|
142
|
+
logger.info(f"Downloading JDBC driver: {driver.filename}")
|
|
143
|
+
logger.debug(f"URL: {driver.maven_url}")
|
|
144
|
+
|
|
145
|
+
try:
|
|
146
|
+
with urllib.request.urlopen(driver.maven_url) as response:
|
|
147
|
+
# Download to temporary file first
|
|
148
|
+
temp_path = target_path.with_suffix(".tmp")
|
|
149
|
+
with open(temp_path, "wb") as f:
|
|
150
|
+
shutil.copyfileobj(response, f)
|
|
151
|
+
|
|
152
|
+
# Move to final location
|
|
153
|
+
temp_path.replace(target_path)
|
|
154
|
+
|
|
155
|
+
logger.info(f"Driver downloaded successfully: {target_path}")
|
|
156
|
+
return target_path
|
|
157
|
+
|
|
158
|
+
except Exception as e:
|
|
159
|
+
error_msg = f"Failed to download driver from {driver.maven_url}: {e}"
|
|
160
|
+
logger.error(error_msg)
|
|
161
|
+
raise RuntimeError(error_msg) from e
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def get_driver_path(
|
|
165
|
+
database: str,
|
|
166
|
+
driver: JDBCDriver | None = None,
|
|
167
|
+
auto_download: bool = True,
|
|
168
|
+
cache_dir: Path | None = None,
|
|
169
|
+
) -> Path:
|
|
170
|
+
"""
|
|
171
|
+
Get the path to a JDBC driver, downloading if necessary.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
database: Database name (e.g., 'postgresql', 'mysql').
|
|
175
|
+
driver: Custom driver metadata. If None, uses recommended driver.
|
|
176
|
+
auto_download: Whether to auto-download driver if not found.
|
|
177
|
+
cache_dir: Directory to cache downloaded drivers.
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
Path to the JDBC driver JAR file.
|
|
181
|
+
|
|
182
|
+
Raises:
|
|
183
|
+
RuntimeError: If driver not found and auto-download disabled.
|
|
184
|
+
"""
|
|
185
|
+
if driver is None:
|
|
186
|
+
driver = RECOMMENDED_JDBC_DRIVERS.get(database.lower())
|
|
187
|
+
if driver is None:
|
|
188
|
+
raise ValueError(f"No recommended driver for database: {database}")
|
|
189
|
+
|
|
190
|
+
if cache_dir is None:
|
|
191
|
+
cache_dir = get_driver_cache_dir()
|
|
192
|
+
|
|
193
|
+
target_path = cache_dir / driver.filename
|
|
194
|
+
|
|
195
|
+
# Check if driver exists in cache
|
|
196
|
+
if target_path.exists():
|
|
197
|
+
return target_path
|
|
198
|
+
|
|
199
|
+
# Try to auto-download
|
|
200
|
+
if auto_download:
|
|
201
|
+
return download_driver(driver, cache_dir)
|
|
202
|
+
|
|
203
|
+
raise RuntimeError(
|
|
204
|
+
f"JDBC driver not found: {target_path}. "
|
|
205
|
+
f"Enable auto_download or set CLASSPATH environment variable."
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def get_all_driver_paths(
|
|
210
|
+
databases: list[str] | None = None,
|
|
211
|
+
auto_download: bool = True,
|
|
212
|
+
cache_dir: Path | None = None,
|
|
213
|
+
) -> list[Path]:
|
|
214
|
+
"""
|
|
215
|
+
Get paths to multiple JDBC drivers.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
databases: List of database names. If None, downloads all recommended drivers.
|
|
219
|
+
auto_download: Whether to auto-download drivers if not found.
|
|
220
|
+
cache_dir: Directory to cache downloaded drivers.
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
List of paths to JDBC driver JAR files.
|
|
224
|
+
"""
|
|
225
|
+
if databases is None:
|
|
226
|
+
databases = list(RECOMMENDED_JDBC_DRIVERS.keys())
|
|
227
|
+
|
|
228
|
+
paths = []
|
|
229
|
+
for database in databases:
|
|
230
|
+
try:
|
|
231
|
+
path = get_driver_path(
|
|
232
|
+
database, auto_download=auto_download, cache_dir=cache_dir
|
|
233
|
+
)
|
|
234
|
+
paths.append(path)
|
|
235
|
+
except Exception as e:
|
|
236
|
+
logger.warning(f"Failed to get driver for {database}: {e}")
|
|
237
|
+
|
|
238
|
+
return paths
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def get_classpath_with_drivers(
|
|
242
|
+
databases: list[str] | None = None,
|
|
243
|
+
auto_download: bool = True,
|
|
244
|
+
manual_classpath: list[Path] | None = None,
|
|
245
|
+
) -> list[Path]:
|
|
246
|
+
"""
|
|
247
|
+
Get comprehensive classpath including auto-downloaded and manual drivers.
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
databases: List of database names for auto-download. If None, downloads all recommended.
|
|
251
|
+
auto_download: Whether to auto-download drivers.
|
|
252
|
+
manual_classpath: Additional manual classpath entries.
|
|
253
|
+
|
|
254
|
+
Returns:
|
|
255
|
+
List of all classpath entries.
|
|
256
|
+
"""
|
|
257
|
+
classpath = []
|
|
258
|
+
|
|
259
|
+
# Add manual classpath entries first (higher priority)
|
|
260
|
+
if manual_classpath:
|
|
261
|
+
classpath.extend(manual_classpath)
|
|
262
|
+
|
|
263
|
+
# Add auto-downloaded drivers
|
|
264
|
+
if auto_download:
|
|
265
|
+
try:
|
|
266
|
+
auto_paths = get_all_driver_paths(databases, auto_download=True)
|
|
267
|
+
classpath.extend(auto_paths)
|
|
268
|
+
except Exception as e:
|
|
269
|
+
logger.warning(f"Failed to auto-download some drivers: {e}")
|
|
270
|
+
|
|
271
|
+
# Remove duplicates while preserving order
|
|
272
|
+
seen = set()
|
|
273
|
+
unique_classpath = []
|
|
274
|
+
for path in classpath:
|
|
275
|
+
if path not in seen:
|
|
276
|
+
seen.add(path)
|
|
277
|
+
unique_classpath.append(path)
|
|
278
|
+
|
|
279
|
+
return unique_classpath
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def verify_driver(driver_path: Path) -> bool:
|
|
283
|
+
"""
|
|
284
|
+
Verify that a JDBC driver JAR file is valid.
|
|
285
|
+
|
|
286
|
+
Args:
|
|
287
|
+
driver_path: Path to the driver JAR file.
|
|
288
|
+
|
|
289
|
+
Returns:
|
|
290
|
+
True if driver appears valid, False otherwise.
|
|
291
|
+
"""
|
|
292
|
+
if not driver_path.exists():
|
|
293
|
+
return False
|
|
294
|
+
|
|
295
|
+
if not driver_path.is_file():
|
|
296
|
+
return False
|
|
297
|
+
|
|
298
|
+
if not driver_path.suffix == ".jar":
|
|
299
|
+
return False
|
|
300
|
+
|
|
301
|
+
# Check if file is not empty
|
|
302
|
+
if driver_path.stat().st_size == 0:
|
|
303
|
+
return False
|
|
304
|
+
|
|
305
|
+
# Could add more validation (e.g., ZIP file structure)
|
|
306
|
+
return True
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def list_cached_drivers(cache_dir: Path | None = None) -> list[Path]:
|
|
310
|
+
"""
|
|
311
|
+
List all cached JDBC drivers.
|
|
312
|
+
|
|
313
|
+
Args:
|
|
314
|
+
cache_dir: Directory to check. If None, uses default.
|
|
315
|
+
|
|
316
|
+
Returns:
|
|
317
|
+
List of paths to cached driver JAR files.
|
|
318
|
+
"""
|
|
319
|
+
if cache_dir is None:
|
|
320
|
+
cache_dir = get_driver_cache_dir()
|
|
321
|
+
|
|
322
|
+
if not cache_dir.exists():
|
|
323
|
+
return []
|
|
324
|
+
|
|
325
|
+
return [path for path in cache_dir.glob("*.jar") if verify_driver(path)]
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def clear_driver_cache(cache_dir: Path | None = None) -> int:
|
|
329
|
+
"""
|
|
330
|
+
Clear the driver cache directory.
|
|
331
|
+
|
|
332
|
+
Args:
|
|
333
|
+
cache_dir: Directory to clear. If None, uses default.
|
|
334
|
+
|
|
335
|
+
Returns:
|
|
336
|
+
Number of files deleted.
|
|
337
|
+
"""
|
|
338
|
+
if cache_dir is None:
|
|
339
|
+
cache_dir = get_driver_cache_dir()
|
|
340
|
+
|
|
341
|
+
if not cache_dir.exists():
|
|
342
|
+
return 0
|
|
343
|
+
|
|
344
|
+
count = 0
|
|
345
|
+
for path in cache_dir.glob("*.jar"):
|
|
346
|
+
try:
|
|
347
|
+
path.unlink()
|
|
348
|
+
count += 1
|
|
349
|
+
logger.debug(f"Deleted cached driver: {path}")
|
|
350
|
+
except Exception as e:
|
|
351
|
+
logger.warning(f"Failed to delete {path}: {e}")
|
|
352
|
+
|
|
353
|
+
return count
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""
|
|
2
|
+
JDBC Exception hierarchy following DB-API 2.0 specification.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Error(Exception):
|
|
9
|
+
"""Base class for all JDBC-related errors."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Warning(Exception): # noqa: A001 - DB-API 2.0 requires Warning exception class
|
|
13
|
+
"""Exception raised for important warnings."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class InterfaceError(Error):
|
|
17
|
+
"""Exception raised for errors related to the database interface."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class DatabaseError(Error):
|
|
21
|
+
"""Exception raised for errors related to the database."""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class InternalError(DatabaseError):
|
|
25
|
+
"""Exception raised when the database encounters an internal error."""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class OperationalError(DatabaseError):
|
|
29
|
+
"""Exception raised for operational database errors."""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ProgrammingError(DatabaseError):
|
|
33
|
+
"""Exception raised for programming errors."""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class IntegrityError(DatabaseError):
|
|
37
|
+
"""Exception raised when database integrity is violated."""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class DataError(DatabaseError):
|
|
41
|
+
"""Exception raised for errors related to processed data."""
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class NotSupportedError(DatabaseError):
|
|
45
|
+
"""Exception raised when a method or database API is not supported."""
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class JDBCDriverNotFoundError(InterfaceError):
|
|
49
|
+
"""Exception raised when JDBC driver cannot be found."""
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class JVMNotStartedError(InterfaceError):
|
|
53
|
+
"""Exception raised when JVM is not started."""
|