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,825 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sqlalchemy-jdbcapi
|
|
3
|
+
Version: 2.0.0.post2
|
|
4
|
+
Summary: Modern SQLAlchemy dialect for JDBC connections with native implementation
|
|
5
|
+
Author: Pavel Henrykhsen
|
|
6
|
+
Author-email: Danesh Patel <danesh_patel@outlook.com>
|
|
7
|
+
Maintainer-email: Danesh Patel <danesh_patel@outlook.com>
|
|
8
|
+
License: Apache-2.0
|
|
9
|
+
Project-URL: Homepage, https://github.com/daneshpatel/sqlalchemy-jdbcapi
|
|
10
|
+
Project-URL: Documentation, https://sqlalchemy-jdbcapi.readthedocs.io
|
|
11
|
+
Project-URL: Repository, https://github.com/daneshpatel/sqlalchemy-jdbcapi
|
|
12
|
+
Project-URL: Issues, https://github.com/daneshpatel/sqlalchemy-jdbcapi/issues
|
|
13
|
+
Project-URL: Changelog, https://github.com/daneshpatel/sqlalchemy-jdbcapi/blob/main/CHANGELOG.md
|
|
14
|
+
Keywords: sqlalchemy,jdbc,database,postgresql,oracle,mysql,sql-server,db2,oceanbase
|
|
15
|
+
Classifier: Development Status :: 4 - Beta
|
|
16
|
+
Classifier: Intended Audience :: Developers
|
|
17
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
18
|
+
Classifier: Operating System :: OS Independent
|
|
19
|
+
Classifier: Programming Language :: Python :: 3
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
24
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
25
|
+
Classifier: Topic :: Database
|
|
26
|
+
Classifier: Topic :: Database :: Front-Ends
|
|
27
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
28
|
+
Classifier: Typing :: Typed
|
|
29
|
+
Requires-Python: >=3.10
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
License-File: LICENSE
|
|
32
|
+
License-File: AUTHORS
|
|
33
|
+
Requires-Dist: sqlalchemy>=2.0.0
|
|
34
|
+
Requires-Dist: JPype1>=1.5.0
|
|
35
|
+
Provides-Extra: odbc
|
|
36
|
+
Requires-Dist: pyodbc>=5.0.0; extra == "odbc"
|
|
37
|
+
Provides-Extra: dataframe
|
|
38
|
+
Requires-Dist: pandas>=2.0.0; extra == "dataframe"
|
|
39
|
+
Requires-Dist: polars>=0.20.0; extra == "dataframe"
|
|
40
|
+
Requires-Dist: pyarrow>=14.0.0; extra == "dataframe"
|
|
41
|
+
Provides-Extra: dev
|
|
42
|
+
Requires-Dist: pytest>=8.0.0; extra == "dev"
|
|
43
|
+
Requires-Dist: pytest-cov>=4.1.0; extra == "dev"
|
|
44
|
+
Requires-Dist: pytest-mock>=3.12.0; extra == "dev"
|
|
45
|
+
Requires-Dist: pytest-asyncio>=0.23.0; extra == "dev"
|
|
46
|
+
Requires-Dist: pytest-xdist>=3.5.0; extra == "dev"
|
|
47
|
+
Requires-Dist: mypy>=1.8.0; extra == "dev"
|
|
48
|
+
Requires-Dist: ruff>=0.2.0; extra == "dev"
|
|
49
|
+
Requires-Dist: pre-commit>=3.6.0; extra == "dev"
|
|
50
|
+
Requires-Dist: tox>=4.11.0; extra == "dev"
|
|
51
|
+
Requires-Dist: build>=1.0.0; extra == "dev"
|
|
52
|
+
Requires-Dist: twine>=5.0.0; extra == "dev"
|
|
53
|
+
Provides-Extra: docs
|
|
54
|
+
Requires-Dist: sphinx>=7.2.0; extra == "docs"
|
|
55
|
+
Requires-Dist: sphinx-rtd-theme>=2.0.0; extra == "docs"
|
|
56
|
+
Requires-Dist: sphinx-autodoc-typehints>=1.25.0; extra == "docs"
|
|
57
|
+
Requires-Dist: myst-parser>=2.0.0; extra == "docs"
|
|
58
|
+
Provides-Extra: types
|
|
59
|
+
Requires-Dist: types-python-dateutil; extra == "types"
|
|
60
|
+
Provides-Extra: all
|
|
61
|
+
Requires-Dist: sqlalchemy-jdbcapi[dataframe,dev,docs,odbc,types]; extra == "all"
|
|
62
|
+
Dynamic: license-file
|
|
63
|
+
|
|
64
|
+
# SQLAlchemy JDBC/ODBC API 2.0
|
|
65
|
+
|
|
66
|
+
[](https://github.com/daneshpatel/sqlalchemy-jdbcapi/actions)
|
|
67
|
+
[](https://pypi.org/project/sqlalchemy-jdbcapi/)
|
|
68
|
+
[](https://pypi.org/project/sqlalchemy-jdbcapi/)
|
|
69
|
+
[](https://github.com/daneshpatel/sqlalchemy-jdbcapi/blob/main/LICENSE)
|
|
70
|
+
[](https://pypi.org/project/sqlalchemy-jdbcapi/)
|
|
71
|
+
[](htmlcov/index.html)
|
|
72
|
+
|
|
73
|
+
Modern, type-safe SQLAlchemy dialect for JDBC and ODBC connections with native Python implementation.
|
|
74
|
+
|
|
75
|
+
## ๐ Documentation
|
|
76
|
+
|
|
77
|
+
- **[Quick Start Guide](QUICKSTART.md)** - Get started in 5 minutes
|
|
78
|
+
- **[Usage Guide](USAGE.md)** - Comprehensive usage examples
|
|
79
|
+
- **[Drivers Guide](DRIVERS.md)** - Detailed driver documentation
|
|
80
|
+
- **[Contributing](CONTRIBUTING.md)** - Contribution guidelines
|
|
81
|
+
|
|
82
|
+
## ๐ Version 2.0 - Major Modernization
|
|
83
|
+
|
|
84
|
+
Version 2.0 is a complete modernization of the library with:
|
|
85
|
+
- โจ **Automatic JDBC driver download** from Maven Central (zero configuration!)
|
|
86
|
+
- ๐ **ODBC support** for native database connectivity
|
|
87
|
+
- ๐ฏ **Full SQLAlchemy native dialect integration** (ORM, reflection, Alembic, Inspector API)
|
|
88
|
+
- ๐ **DataFrame integration** (pandas, polars, pyarrow)
|
|
89
|
+
- ๐๏ธ **12 database dialects** (8 JDBC + 4 ODBC)
|
|
90
|
+
- ๐ **Modern Python 3.10+** with full type hints
|
|
91
|
+
- โก **SQLAlchemy 2.0+** compatible
|
|
92
|
+
- ๐๏ธ **SOLID architecture** with clean code principles
|
|
93
|
+
|
|
94
|
+
## โจ Features
|
|
95
|
+
|
|
96
|
+
### JDBC Support
|
|
97
|
+
- **Automatic Driver Download**: JDBC drivers auto-download from Maven Central (zero configuration!)
|
|
98
|
+
- **Native JDBC Bridge**: Our own DB-API 2.0 implementation using JPype (no JayDeBeApi)
|
|
99
|
+
- **Manual Driver Support**: Optional manual driver management via CLASSPATH
|
|
100
|
+
- **8 Major Databases**: PostgreSQL, MySQL, MariaDB, SQL Server, Oracle, DB2, SQLite, OceanBase
|
|
101
|
+
|
|
102
|
+
### ODBC Support (NEW!)
|
|
103
|
+
- **Native ODBC Connectivity**: Alternative connection method using pyodbc
|
|
104
|
+
- **No JVM Required**: Direct database access without Java runtime
|
|
105
|
+
- **4 Major Databases**: PostgreSQL, MySQL, SQL Server, Oracle
|
|
106
|
+
- **System Integration**: Uses OS-installed ODBC drivers
|
|
107
|
+
|
|
108
|
+
### SQLAlchemy Integration
|
|
109
|
+
- **Full SQLAlchemy Integration**: Complete native dialect with ORM, reflection, Inspector API, and Alembic support
|
|
110
|
+
- **Database Reflection**: Auto-load tables, columns, constraints, indexes, foreign keys from existing databases
|
|
111
|
+
- **ORM & Automapping**: Full SQLAlchemy ORM support with automatic model generation from existing schemas
|
|
112
|
+
- **DataFrame Integration**: Direct conversion to pandas/polars/arrow for data science workflows
|
|
113
|
+
|
|
114
|
+
### Code Quality
|
|
115
|
+
- **Type Safe**: Comprehensive type hints throughout
|
|
116
|
+
- **Modern Python**: Python 3.10+ with latest syntax
|
|
117
|
+
- **Best Practices**: Ruff formatting, mypy type checking, comprehensive linting
|
|
118
|
+
- **Clean Architecture**: SOLID principles and design patterns
|
|
119
|
+
|
|
120
|
+
> **๐ฏ What's Different?** Unlike other JDBC bridges, we provide **true SQLAlchemy native dialect integration**. This means you can use all SQLAlchemy features including table autoload, Inspector API, Alembic migrations, and ORM automapping - not just basic SQL execution!
|
|
121
|
+
|
|
122
|
+
## ๐ฆ Installation
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
# Basic installation
|
|
126
|
+
pip install sqlalchemy-jdbcapi
|
|
127
|
+
|
|
128
|
+
# With DataFrame support (pandas, polars, pyarrow)
|
|
129
|
+
pip install sqlalchemy-jdbcapi[dataframe]
|
|
130
|
+
|
|
131
|
+
# For development
|
|
132
|
+
pip install sqlalchemy-jdbcapi[dev]
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## ๐๏ธ Supported Databases
|
|
136
|
+
|
|
137
|
+
### JDBC Dialects (Auto-Download Supported)
|
|
138
|
+
|
|
139
|
+
| Database | Connection URL | Auto-Download |
|
|
140
|
+
|----------|----------------|---------------|
|
|
141
|
+
| PostgreSQL | `jdbcapi+postgresql://user:pass@host:5432/db` | โ
|
|
|
142
|
+
| Oracle | `jdbcapi+oracle://user:pass@host:1521/SID` | โ
|
|
|
143
|
+
| MySQL | `jdbcapi+mysql://user:pass@host:3306/db` | โ
|
|
|
144
|
+
| MariaDB | `jdbcapi+mariadb://user:pass@host:3306/db` | โ
|
|
|
145
|
+
| SQL Server | `jdbcapi+mssql://user:pass@host:1433/db` | โ
|
|
|
146
|
+
| DB2 | `jdbcapi+db2://user:pass@host:50000/db` | โ
|
|
|
147
|
+
| OceanBase | `jdbcapi+oceanbase://user:pass@host:2881/db` | โ
|
|
|
148
|
+
| SQLite | `jdbcapi+sqlite:///path/to/db.db` | โ
|
|
|
149
|
+
|
|
150
|
+
### ODBC Dialects (OS-Installed Drivers Required)
|
|
151
|
+
|
|
152
|
+
| Database | Connection URL | Install Guide |
|
|
153
|
+
|----------|----------------|---------------|
|
|
154
|
+
| PostgreSQL | `odbcapi+postgresql://user:pass@host:5432/db` | [See DRIVERS.md](DRIVERS.md#postgresql-odbc) |
|
|
155
|
+
| MySQL | `odbcapi+mysql://user:pass@host:3306/db` | [See DRIVERS.md](DRIVERS.md#mysql-odbc) |
|
|
156
|
+
| SQL Server | `odbcapi+mssql://user:pass@host:1433/db` | [See DRIVERS.md](DRIVERS.md#microsoft-sql-server-odbc) |
|
|
157
|
+
| Oracle | `odbcapi+oracle://user:pass@host:1521/service` | [See DRIVERS.md](DRIVERS.md#oracle-odbc) |
|
|
158
|
+
|
|
159
|
+
For detailed driver documentation, see **[DRIVERS.md](DRIVERS.md)**.
|
|
160
|
+
|
|
161
|
+
## ๐ Quick Start
|
|
162
|
+
|
|
163
|
+
### JDBC with Auto-Download (Recommended!)
|
|
164
|
+
|
|
165
|
+
No setup required - drivers auto-download on first use!
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
from sqlalchemy import create_engine, text
|
|
169
|
+
|
|
170
|
+
# PostgreSQL - driver auto-downloads from Maven Central
|
|
171
|
+
engine = create_engine('jdbcapi+postgresql://user:password@localhost:5432/mydb')
|
|
172
|
+
|
|
173
|
+
# Execute queries
|
|
174
|
+
with engine.connect() as conn:
|
|
175
|
+
result = conn.execute(text("SELECT version()"))
|
|
176
|
+
print(result.scalar())
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Drivers are cached in `~/.sqlalchemy-jdbcapi/drivers/` for future use.
|
|
180
|
+
|
|
181
|
+
### ODBC (Alternative)
|
|
182
|
+
|
|
183
|
+
Requires ODBC driver installation (see [DRIVERS.md](DRIVERS.md)):
|
|
184
|
+
|
|
185
|
+
```python
|
|
186
|
+
from sqlalchemy import create_engine
|
|
187
|
+
|
|
188
|
+
# PostgreSQL via ODBC (no JVM needed!)
|
|
189
|
+
engine = create_engine('odbcapi+postgresql://user:password@localhost:5432/mydb')
|
|
190
|
+
|
|
191
|
+
with engine.connect() as conn:
|
|
192
|
+
result = conn.execute(text("SELECT version()"))
|
|
193
|
+
print(result.scalar())
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### Manual JDBC Driver Setup (Optional)
|
|
197
|
+
|
|
198
|
+
If you prefer to manage drivers manually:
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
# Set CLASSPATH environment variable
|
|
202
|
+
export CLASSPATH="/path/to/postgresql-42.7.1.jar"
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
```python
|
|
206
|
+
from sqlalchemy import create_engine
|
|
207
|
+
|
|
208
|
+
# Will use driver from CLASSPATH
|
|
209
|
+
engine = create_engine('jdbcapi+postgresql://user:password@localhost/mydb')
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
For detailed usage, see **[QUICKSTART.md](QUICKSTART.md)** and **[USAGE.md](USAGE.md)**.
|
|
213
|
+
|
|
214
|
+
## ๐ก Examples
|
|
215
|
+
|
|
216
|
+
**๐ Full Examples Available:**
|
|
217
|
+
- **[examples/basic_usage.py](examples/basic_usage.py)** - 8 fundamental usage patterns (JDBC, ODBC, ORM, pooling, transactions)
|
|
218
|
+
- **[examples/data_analysis.py](examples/data_analysis.py)** - 8 data science examples (pandas, polars, ETL, analytics, streaming)
|
|
219
|
+
- **[examples/README.md](examples/README.md)** - Complete guide with configuration and troubleshooting
|
|
220
|
+
|
|
221
|
+
### PostgreSQL
|
|
222
|
+
|
|
223
|
+
```python
|
|
224
|
+
from sqlalchemy import create_engine, Table, Column, Integer, String, MetaData
|
|
225
|
+
|
|
226
|
+
# Create engine
|
|
227
|
+
engine = create_engine('jdbcapi+postgresql://user:pass@localhost:5432/mydb')
|
|
228
|
+
|
|
229
|
+
# Define schema
|
|
230
|
+
metadata = MetaData()
|
|
231
|
+
users = Table('users', metadata,
|
|
232
|
+
Column('id', Integer, primary_key=True),
|
|
233
|
+
Column('name', String(50)),
|
|
234
|
+
Column('email', String(100))
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
# Create tables
|
|
238
|
+
metadata.create_all(engine)
|
|
239
|
+
|
|
240
|
+
# Insert data
|
|
241
|
+
with engine.connect() as conn:
|
|
242
|
+
conn.execute(users.insert().values(name='Alice', email='alice@example.com'))
|
|
243
|
+
conn.commit()
|
|
244
|
+
|
|
245
|
+
# Query data
|
|
246
|
+
with engine.connect() as conn:
|
|
247
|
+
result = conn.execute(users.select())
|
|
248
|
+
for row in result:
|
|
249
|
+
print(f"User: {row.name}, Email: {row.email}")
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### Oracle
|
|
253
|
+
|
|
254
|
+
```python
|
|
255
|
+
from sqlalchemy import create_engine, text
|
|
256
|
+
|
|
257
|
+
# Standard connection
|
|
258
|
+
engine = create_engine('jdbcapi+oracle://user:password@localhost:1521/ORCL')
|
|
259
|
+
|
|
260
|
+
# TNS name connection
|
|
261
|
+
engine = create_engine('jdbcapi+oracle://user:password@TNSNAME')
|
|
262
|
+
|
|
263
|
+
# With query parameters
|
|
264
|
+
from sqlalchemy.engine.url import URL
|
|
265
|
+
|
|
266
|
+
url = URL.create(
|
|
267
|
+
'jdbcapi+oracle',
|
|
268
|
+
username='user',
|
|
269
|
+
password='password',
|
|
270
|
+
host='localhost',
|
|
271
|
+
port=1521,
|
|
272
|
+
database='ORCL',
|
|
273
|
+
query={'ssl': 'true'}
|
|
274
|
+
)
|
|
275
|
+
engine = create_engine(url)
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
### MySQL / MariaDB
|
|
279
|
+
|
|
280
|
+
```python
|
|
281
|
+
from sqlalchemy import create_engine
|
|
282
|
+
|
|
283
|
+
# MySQL
|
|
284
|
+
engine = create_engine('jdbcapi+mysql://root:password@localhost:3306/mydb')
|
|
285
|
+
|
|
286
|
+
# MariaDB
|
|
287
|
+
engine = create_engine('jdbcapi+mariadb://root:password@localhost:3306/mydb')
|
|
288
|
+
|
|
289
|
+
# With SSL
|
|
290
|
+
engine = create_engine(
|
|
291
|
+
'jdbcapi+mysql://user:pass@localhost:3306/mydb?useSSL=true&requireSSL=true'
|
|
292
|
+
)
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
### SQL Server
|
|
296
|
+
|
|
297
|
+
```python
|
|
298
|
+
from sqlalchemy import create_engine
|
|
299
|
+
|
|
300
|
+
# Standard connection
|
|
301
|
+
engine = create_engine('jdbcapi+mssql://user:password@localhost:1433/mydb')
|
|
302
|
+
|
|
303
|
+
# Windows Authentication (if supported by JDBC driver)
|
|
304
|
+
engine = create_engine(
|
|
305
|
+
'jdbcapi+mssql://localhost:1433/mydb?integratedSecurity=true'
|
|
306
|
+
)
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
### DB2
|
|
310
|
+
|
|
311
|
+
```python
|
|
312
|
+
from sqlalchemy import create_engine
|
|
313
|
+
|
|
314
|
+
# DB2 LUW
|
|
315
|
+
engine = create_engine('jdbcapi+db2://user:password@localhost:50000/mydb')
|
|
316
|
+
|
|
317
|
+
# DB2 z/OS (mainframe)
|
|
318
|
+
engine = create_engine('jdbcapi+db2://user:password@mainframe:446/DBCG')
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
### OceanBase
|
|
322
|
+
|
|
323
|
+
```python
|
|
324
|
+
from sqlalchemy import create_engine
|
|
325
|
+
from urllib.parse import quote
|
|
326
|
+
|
|
327
|
+
# OceanBase with tenant and cluster
|
|
328
|
+
user = quote('username@tenant#cluster')
|
|
329
|
+
engine = create_engine(f'jdbcapi+oceanbase://{user}:password@localhost:2881/mydb')
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
## ๐ฏ Full SQLAlchemy Integration
|
|
333
|
+
|
|
334
|
+
Version 2.0 provides **complete SQLAlchemy native dialect integration** with full ORM, reflection, and Inspector API support. See [SQLALCHEMY_INTEGRATION.md](SQLALCHEMY_INTEGRATION.md) for comprehensive documentation.
|
|
335
|
+
|
|
336
|
+
### ORM Support
|
|
337
|
+
|
|
338
|
+
Use declarative models with full relationship support:
|
|
339
|
+
|
|
340
|
+
```python
|
|
341
|
+
from sqlalchemy import create_engine, Column, Integer, String, ForeignKey
|
|
342
|
+
from sqlalchemy.orm import declarative_base, relationship, Session
|
|
343
|
+
|
|
344
|
+
Base = declarative_base()
|
|
345
|
+
|
|
346
|
+
class User(Base):
|
|
347
|
+
__tablename__ = 'users'
|
|
348
|
+
|
|
349
|
+
id = Column(Integer, primary_key=True)
|
|
350
|
+
name = Column(String(50))
|
|
351
|
+
email = Column(String(100))
|
|
352
|
+
posts = relationship("Post", back_populates="author")
|
|
353
|
+
|
|
354
|
+
class Post(Base):
|
|
355
|
+
__tablename__ = 'posts'
|
|
356
|
+
|
|
357
|
+
id = Column(Integer, primary_key=True)
|
|
358
|
+
title = Column(String(200))
|
|
359
|
+
user_id = Column(Integer, ForeignKey('users.id'))
|
|
360
|
+
author = relationship("User", back_populates="posts")
|
|
361
|
+
|
|
362
|
+
# Create engine
|
|
363
|
+
engine = create_engine('jdbcapi+postgresql://user:pass@localhost:5432/mydb')
|
|
364
|
+
|
|
365
|
+
# Create tables
|
|
366
|
+
Base.metadata.create_all(engine)
|
|
367
|
+
|
|
368
|
+
# Use ORM
|
|
369
|
+
session = Session(engine)
|
|
370
|
+
user = User(name='Alice', email='alice@example.com')
|
|
371
|
+
user.posts.append(Post(title='My First Post'))
|
|
372
|
+
session.add(user)
|
|
373
|
+
session.commit()
|
|
374
|
+
|
|
375
|
+
# Query with relationships
|
|
376
|
+
users = session.query(User).join(User.posts).filter(Post.title.like('%First%')).all()
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
### Table Reflection & Auto-load
|
|
380
|
+
|
|
381
|
+
Automatically load existing table structures from your database:
|
|
382
|
+
|
|
383
|
+
```python
|
|
384
|
+
from sqlalchemy import Table, MetaData, select
|
|
385
|
+
|
|
386
|
+
metadata = MetaData()
|
|
387
|
+
|
|
388
|
+
# Auto-load table structure from database
|
|
389
|
+
users = Table('users', metadata, autoload_with=engine)
|
|
390
|
+
|
|
391
|
+
# Now you can use it!
|
|
392
|
+
print(users.columns.keys()) # ['id', 'name', 'email']
|
|
393
|
+
print(users.primary_key) # PrimaryKeyConstraint('id')
|
|
394
|
+
|
|
395
|
+
# Query the reflected table
|
|
396
|
+
with engine.connect() as conn:
|
|
397
|
+
stmt = select(users).where(users.c.name == 'Alice')
|
|
398
|
+
result = conn.execute(stmt)
|
|
399
|
+
for row in result:
|
|
400
|
+
print(row)
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
### Database Inspector
|
|
404
|
+
|
|
405
|
+
Explore your database schema programmatically:
|
|
406
|
+
|
|
407
|
+
```python
|
|
408
|
+
from sqlalchemy import inspect
|
|
409
|
+
|
|
410
|
+
inspector = inspect(engine)
|
|
411
|
+
|
|
412
|
+
# List all schemas
|
|
413
|
+
schemas = inspector.get_schema_names()
|
|
414
|
+
print(f"Schemas: {schemas}")
|
|
415
|
+
|
|
416
|
+
# List all tables
|
|
417
|
+
tables = inspector.get_table_names(schema='public')
|
|
418
|
+
print(f"Tables: {tables}")
|
|
419
|
+
|
|
420
|
+
# Get column information
|
|
421
|
+
columns = inspector.get_columns('users', schema='public')
|
|
422
|
+
for col in columns:
|
|
423
|
+
print(f"{col['name']}: {col['type']} (nullable={col['nullable']})")
|
|
424
|
+
|
|
425
|
+
# Get primary keys
|
|
426
|
+
pk = inspector.get_pk_constraint('users', schema='public')
|
|
427
|
+
print(f"Primary key: {pk['constrained_columns']}")
|
|
428
|
+
|
|
429
|
+
# Get foreign keys
|
|
430
|
+
fks = inspector.get_foreign_keys('posts', schema='public')
|
|
431
|
+
for fk in fks:
|
|
432
|
+
print(f"{fk['constrained_columns']} -> {fk['referred_table']}.{fk['referred_columns']}")
|
|
433
|
+
|
|
434
|
+
# Get indexes
|
|
435
|
+
indexes = inspector.get_indexes('users', schema='public')
|
|
436
|
+
for idx in indexes:
|
|
437
|
+
print(f"Index {idx['name']}: {idx['column_names']} (unique={idx['unique']})")
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
### ORM Automapping
|
|
441
|
+
|
|
442
|
+
Automatically generate ORM models from existing databases:
|
|
443
|
+
|
|
444
|
+
```python
|
|
445
|
+
from sqlalchemy.ext.automap import automap_base
|
|
446
|
+
from sqlalchemy.orm import Session
|
|
447
|
+
|
|
448
|
+
# Reflect entire database schema
|
|
449
|
+
Base = automap_base()
|
|
450
|
+
Base.prepare(engine, reflect=True)
|
|
451
|
+
|
|
452
|
+
# Classes are generated automatically!
|
|
453
|
+
User = Base.classes.users
|
|
454
|
+
Post = Base.classes.posts
|
|
455
|
+
|
|
456
|
+
# Use them like regular ORM models
|
|
457
|
+
session = Session(engine)
|
|
458
|
+
users = session.query(User).all()
|
|
459
|
+
for user in users:
|
|
460
|
+
print(f"{user.name}: {len(user.posts)} posts")
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
### Alembic Migrations
|
|
464
|
+
|
|
465
|
+
Full support for Alembic database migrations:
|
|
466
|
+
|
|
467
|
+
```bash
|
|
468
|
+
# Initialize Alembic
|
|
469
|
+
alembic init migrations
|
|
470
|
+
|
|
471
|
+
# Configure alembic.ini
|
|
472
|
+
sqlalchemy.url = jdbcapi+postgresql://user:pass@localhost:5432/mydb
|
|
473
|
+
|
|
474
|
+
# Auto-generate migration from model changes
|
|
475
|
+
alembic revision --autogenerate -m "Add user and post tables"
|
|
476
|
+
|
|
477
|
+
# Apply migrations
|
|
478
|
+
alembic upgrade head
|
|
479
|
+
|
|
480
|
+
# Rollback
|
|
481
|
+
alembic downgrade -1
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
### Reflect and Migrate Between Databases
|
|
485
|
+
|
|
486
|
+
Copy schemas between different database systems:
|
|
487
|
+
|
|
488
|
+
```python
|
|
489
|
+
from sqlalchemy import MetaData, create_engine
|
|
490
|
+
|
|
491
|
+
# Reflect schema from PostgreSQL
|
|
492
|
+
pg_engine = create_engine('jdbcapi+postgresql://user:pass@localhost/source_db')
|
|
493
|
+
metadata = MetaData()
|
|
494
|
+
metadata.reflect(bind=pg_engine)
|
|
495
|
+
|
|
496
|
+
# Migrate to Oracle
|
|
497
|
+
oracle_engine = create_engine('jdbcapi+oracle://user:pass@localhost:1521/target_db')
|
|
498
|
+
metadata.create_all(oracle_engine)
|
|
499
|
+
|
|
500
|
+
# Copy data
|
|
501
|
+
for table in metadata.sorted_tables:
|
|
502
|
+
with pg_engine.connect() as source:
|
|
503
|
+
data = source.execute(table.select()).fetchall()
|
|
504
|
+
if data:
|
|
505
|
+
with oracle_engine.connect() as target:
|
|
506
|
+
target.execute(table.insert(), [dict(row._mapping) for row in data])
|
|
507
|
+
target.commit()
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
## ๐ DataFrame Integration
|
|
511
|
+
|
|
512
|
+
Version 2.0 adds powerful DataFrame integration for data science workflows:
|
|
513
|
+
|
|
514
|
+
```python
|
|
515
|
+
from sqlalchemy import create_engine
|
|
516
|
+
|
|
517
|
+
engine = create_engine('jdbcapi+postgresql://user:pass@localhost/mydb')
|
|
518
|
+
|
|
519
|
+
# Method 1: Using helper functions
|
|
520
|
+
from sqlalchemy_jdbcapi.jdbc.dataframe import cursor_to_pandas
|
|
521
|
+
|
|
522
|
+
with engine.connect() as conn:
|
|
523
|
+
cursor = conn.connection.cursor()
|
|
524
|
+
cursor.execute("SELECT * FROM large_table WHERE date > '2024-01-01'")
|
|
525
|
+
|
|
526
|
+
# Convert to pandas
|
|
527
|
+
df = cursor_to_pandas(cursor)
|
|
528
|
+
print(df.describe())
|
|
529
|
+
|
|
530
|
+
# Or polars
|
|
531
|
+
from sqlalchemy_jdbcapi.jdbc.dataframe import cursor_to_polars
|
|
532
|
+
df = cursor_to_polars(cursor)
|
|
533
|
+
print(df.head())
|
|
534
|
+
|
|
535
|
+
# Or Apache Arrow
|
|
536
|
+
from sqlalchemy_jdbcapi.jdbc.dataframe import cursor_to_arrow
|
|
537
|
+
table = cursor_to_arrow(cursor)
|
|
538
|
+
print(table.schema)
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
```python
|
|
542
|
+
# Method 2: Using cursor convenience methods
|
|
543
|
+
with engine.connect() as conn:
|
|
544
|
+
cursor = conn.connection.cursor()
|
|
545
|
+
cursor.execute("SELECT * FROM users")
|
|
546
|
+
|
|
547
|
+
# Direct conversion
|
|
548
|
+
df = cursor.to_pandas() # pandas DataFrame
|
|
549
|
+
df = cursor.to_polars() # polars DataFrame
|
|
550
|
+
table = cursor.to_arrow() # Arrow Table
|
|
551
|
+
dicts = cursor.to_dict() # List of dictionaries
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
## ๐ฏ Advanced Usage
|
|
555
|
+
|
|
556
|
+
### Connection Pooling
|
|
557
|
+
|
|
558
|
+
```python
|
|
559
|
+
from sqlalchemy import create_engine
|
|
560
|
+
from sqlalchemy.pool import QueuePool
|
|
561
|
+
|
|
562
|
+
engine = create_engine(
|
|
563
|
+
'jdbcapi+postgresql://user:pass@localhost/mydb',
|
|
564
|
+
poolclass=QueuePool,
|
|
565
|
+
pool_size=5,
|
|
566
|
+
max_overflow=10,
|
|
567
|
+
pool_timeout=30,
|
|
568
|
+
pool_recycle=3600
|
|
569
|
+
)
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
### Context Managers
|
|
573
|
+
|
|
574
|
+
```python
|
|
575
|
+
from sqlalchemy import create_engine
|
|
576
|
+
|
|
577
|
+
engine = create_engine('jdbcapi+postgresql://user:pass@localhost/mydb')
|
|
578
|
+
|
|
579
|
+
# Connection context
|
|
580
|
+
with engine.connect() as conn:
|
|
581
|
+
result = conn.execute(text("SELECT * FROM users"))
|
|
582
|
+
# Connection automatically closed
|
|
583
|
+
|
|
584
|
+
# Transaction context
|
|
585
|
+
with engine.begin() as conn:
|
|
586
|
+
conn.execute(text("INSERT INTO users (name) VALUES (:name)"), {"name": "Bob"})
|
|
587
|
+
# Automatically committed (or rolled back on exception)
|
|
588
|
+
```
|
|
589
|
+
|
|
590
|
+
### Batch Operations
|
|
591
|
+
|
|
592
|
+
```python
|
|
593
|
+
from sqlalchemy import create_engine, text
|
|
594
|
+
|
|
595
|
+
engine = create_engine('jdbcapi+postgresql://user:pass@localhost/mydb')
|
|
596
|
+
|
|
597
|
+
# Batch insert
|
|
598
|
+
data = [
|
|
599
|
+
{"name": "Alice", "age": 30},
|
|
600
|
+
{"name": "Bob", "age": 25},
|
|
601
|
+
{"name": "Charlie", "age": 35}
|
|
602
|
+
]
|
|
603
|
+
|
|
604
|
+
with engine.begin() as conn:
|
|
605
|
+
conn.execute(
|
|
606
|
+
text("INSERT INTO users (name, age) VALUES (:name, :age)"),
|
|
607
|
+
data
|
|
608
|
+
)
|
|
609
|
+
```
|
|
610
|
+
|
|
611
|
+
### Type Hints Support
|
|
612
|
+
|
|
613
|
+
```python
|
|
614
|
+
from sqlalchemy import create_engine, text, Engine, Connection, Result
|
|
615
|
+
from sqlalchemy.engine import Row
|
|
616
|
+
|
|
617
|
+
engine: Engine = create_engine('jdbcapi+postgresql://user:pass@localhost/mydb')
|
|
618
|
+
|
|
619
|
+
def get_users(conn: Connection) -> list[Row]:
|
|
620
|
+
result: Result = conn.execute(text("SELECT * FROM users"))
|
|
621
|
+
return list(result)
|
|
622
|
+
|
|
623
|
+
with engine.connect() as conn:
|
|
624
|
+
users = get_users(conn)
|
|
625
|
+
```
|
|
626
|
+
|
|
627
|
+
## ๐๏ธ Architecture
|
|
628
|
+
|
|
629
|
+
### Project Structure
|
|
630
|
+
|
|
631
|
+
```
|
|
632
|
+
src/sqlalchemy_jdbcapi/
|
|
633
|
+
โโโ __init__.py # Main package
|
|
634
|
+
โโโ jdbc/ # JDBC bridge layer (DB-API 2.0)
|
|
635
|
+
โ โโโ connection.py # Connection class
|
|
636
|
+
โ โโโ cursor.py # Cursor class
|
|
637
|
+
โ โโโ exceptions.py # Exception hierarchy
|
|
638
|
+
โ โโโ types.py # DB-API type objects
|
|
639
|
+
โ โโโ type_converter.py # JDBC โ Python type conversion
|
|
640
|
+
โ โโโ jvm.py # JVM management
|
|
641
|
+
โ โโโ dataframe.py # DataFrame integration
|
|
642
|
+
โโโ dialects/ # SQLAlchemy dialects
|
|
643
|
+
โโโ base.py # Base dialect (Template Method)
|
|
644
|
+
โโโ postgresql.py # PostgreSQL dialect
|
|
645
|
+
โโโ oracle.py # Oracle dialect
|
|
646
|
+
โโโ mysql.py # MySQL/MariaDB dialects
|
|
647
|
+
โโโ mssql.py # SQL Server dialect
|
|
648
|
+
โโโ db2.py # DB2 dialect
|
|
649
|
+
โโโ oceanbase.py # OceanBase dialect
|
|
650
|
+
โโโ sqlite.py # SQLite dialect
|
|
651
|
+
```
|
|
652
|
+
|
|
653
|
+
### Design Patterns Used
|
|
654
|
+
|
|
655
|
+
- **Template Method**: `BaseJDBCDialect` provides common functionality
|
|
656
|
+
- **Strategy**: Type conversion strategies for different SQL types
|
|
657
|
+
- **Factory**: Dialect creation and registration
|
|
658
|
+
- **Adapter**: SQLAlchemy URL to JDBC connection arguments
|
|
659
|
+
- **Dependency Injection**: Driver configuration
|
|
660
|
+
|
|
661
|
+
### SQLAlchemy Reflection Implementation
|
|
662
|
+
|
|
663
|
+
All reflection methods are implemented in `BaseJDBCDialect` using JDBC's `DatabaseMetaData` API:
|
|
664
|
+
|
|
665
|
+
- **`get_table_names()`** - Uses `DatabaseMetaData.getTables()`
|
|
666
|
+
- **`get_columns()`** - Uses `DatabaseMetaData.getColumns()`
|
|
667
|
+
- **`get_pk_constraint()`** - Uses `DatabaseMetaData.getPrimaryKeys()`
|
|
668
|
+
- **`get_foreign_keys()`** - Uses `DatabaseMetaData.getImportedKeys()`
|
|
669
|
+
- **`get_indexes()`** - Uses `DatabaseMetaData.getIndexInfo()`
|
|
670
|
+
- **`has_table()`** - Uses `DatabaseMetaData.getTables()`
|
|
671
|
+
- **`get_schema_names()`** - Uses `DatabaseMetaData.getSchemas()`
|
|
672
|
+
- **`get_view_names()`** - Uses `DatabaseMetaData.getTables()` with VIEW type
|
|
673
|
+
- **`get_unique_constraints()`** - Extracted from index information
|
|
674
|
+
- **`get_check_constraints()`** - Database-specific implementations
|
|
675
|
+
|
|
676
|
+
These methods enable full SQLAlchemy features:
|
|
677
|
+
- โ
Table autoload (`Table(..., autoload_with=engine)`)
|
|
678
|
+
- โ
Inspector API (`inspect(engine).get_table_names()`)
|
|
679
|
+
- โ
Alembic migrations (`alembic revision --autogenerate`)
|
|
680
|
+
- โ
ORM automapping (`Base.prepare(engine, reflect=True)`)
|
|
681
|
+
- โ
Cross-database schema migration
|
|
682
|
+
|
|
683
|
+
## ๐ง Development
|
|
684
|
+
|
|
685
|
+
### Setup Development Environment
|
|
686
|
+
|
|
687
|
+
```bash
|
|
688
|
+
git clone https://github.com/daneshpatel/sqlalchemy-jdbcapi.git
|
|
689
|
+
cd sqlalchemy-jdbcapi
|
|
690
|
+
|
|
691
|
+
# Create virtual environment
|
|
692
|
+
python -m venv .venv
|
|
693
|
+
source .venv/bin/activate # On Windows: .venv\Scripts\activate
|
|
694
|
+
|
|
695
|
+
# Install in development mode
|
|
696
|
+
pip install -e ".[dev]"
|
|
697
|
+
|
|
698
|
+
# Install pre-commit hooks
|
|
699
|
+
pre-commit install
|
|
700
|
+
```
|
|
701
|
+
|
|
702
|
+
### Running Tests
|
|
703
|
+
|
|
704
|
+
**Test Suite Statistics:**
|
|
705
|
+
- ๐ **168 total tests** (149 unit/integration, 19 functional)
|
|
706
|
+
- โ
**100% passing** (2 skipped - optional pyodbc)
|
|
707
|
+
- ๐ **46.77% coverage** (90%+ on JDBC/ODBC core components)
|
|
708
|
+
- ๐งช **3 test categories**: Unit, Integration, Functional
|
|
709
|
+
|
|
710
|
+
```bash
|
|
711
|
+
# Run all tests (unit + integration)
|
|
712
|
+
pytest
|
|
713
|
+
|
|
714
|
+
# Run with coverage report
|
|
715
|
+
pytest --cov=sqlalchemy_jdbcapi --cov-report=html
|
|
716
|
+
|
|
717
|
+
# Run functional tests (requires real databases)
|
|
718
|
+
pytest tests/functional/ -v -m functional
|
|
719
|
+
|
|
720
|
+
# Run network tests (requires internet for Maven Central)
|
|
721
|
+
pytest tests/functional/ -v -m network
|
|
722
|
+
|
|
723
|
+
# Run specific test file
|
|
724
|
+
pytest tests/unit/test_dialects.py
|
|
725
|
+
|
|
726
|
+
# Run with markers
|
|
727
|
+
pytest -m "not slow" # Skip slow tests
|
|
728
|
+
pytest -m "not functional" # Skip functional tests (default)
|
|
729
|
+
|
|
730
|
+
# View coverage report
|
|
731
|
+
open htmlcov/index.html # macOS
|
|
732
|
+
xdg-open htmlcov/index.html # Linux
|
|
733
|
+
```
|
|
734
|
+
|
|
735
|
+
### Code Quality
|
|
736
|
+
|
|
737
|
+
```bash
|
|
738
|
+
# Format code
|
|
739
|
+
ruff format src tests
|
|
740
|
+
|
|
741
|
+
# Lint code
|
|
742
|
+
ruff check src tests
|
|
743
|
+
|
|
744
|
+
# Type check
|
|
745
|
+
mypy src
|
|
746
|
+
|
|
747
|
+
# Run all pre-commit hooks
|
|
748
|
+
pre-commit run --all-files
|
|
749
|
+
```
|
|
750
|
+
|
|
751
|
+
## ๐ Migration from 1.x to 2.0
|
|
752
|
+
|
|
753
|
+
### Key Changes
|
|
754
|
+
|
|
755
|
+
1. **Python Version**: Minimum is now Python 3.10
|
|
756
|
+
2. **Dependencies**: JayDeBeApi โ JPype1 (automatic)
|
|
757
|
+
3. **SQLAlchemy**: Now requires SQLAlchemy 2.0+
|
|
758
|
+
|
|
759
|
+
### Migration Steps
|
|
760
|
+
|
|
761
|
+
```python
|
|
762
|
+
# 1.x code (still works in 2.0!)
|
|
763
|
+
from sqlalchemy import create_engine
|
|
764
|
+
|
|
765
|
+
engine = create_engine('jdbcapi+pgjdbc://user:pass@localhost/db')
|
|
766
|
+
# โ
Still works with backward compatibility
|
|
767
|
+
|
|
768
|
+
# 2.0 recommended style
|
|
769
|
+
engine = create_engine('jdbcapi+postgresql://user:pass@localhost/db')
|
|
770
|
+
# โจ New preferred naming
|
|
771
|
+
|
|
772
|
+
# New features in 2.0
|
|
773
|
+
with engine.connect() as conn:
|
|
774
|
+
cursor = conn.connection.cursor()
|
|
775
|
+
cursor.execute("SELECT * FROM large_dataset")
|
|
776
|
+
|
|
777
|
+
# DataFrame integration (new in 2.0)
|
|
778
|
+
df = cursor.to_pandas()
|
|
779
|
+
print(df.head())
|
|
780
|
+
```
|
|
781
|
+
|
|
782
|
+
See [CHANGELOG.md](CHANGELOG.md) for complete migration guide.
|
|
783
|
+
|
|
784
|
+
## ๐ค Contributing
|
|
785
|
+
|
|
786
|
+
We welcome contributions! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
|
|
787
|
+
|
|
788
|
+
### Quick Contribution Guide
|
|
789
|
+
|
|
790
|
+
1. Fork the repository
|
|
791
|
+
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
|
|
792
|
+
3. Make your changes
|
|
793
|
+
4. Run tests and linting (`pytest && ruff check`)
|
|
794
|
+
5. Commit your changes (`git commit -m 'Add amazing feature'`)
|
|
795
|
+
6. Push to the branch (`git push origin feature/amazing-feature`)
|
|
796
|
+
7. Open a Pull Request
|
|
797
|
+
|
|
798
|
+
## ๐ License
|
|
799
|
+
|
|
800
|
+
This project is licensed under the Apache License 2.0 - see [LICENSE](LICENSE) file for details.
|
|
801
|
+
|
|
802
|
+
## ๐ Acknowledgments
|
|
803
|
+
|
|
804
|
+
- Original library by Danesh Patel and Pavel Henrykhsen
|
|
805
|
+
- SQLAlchemy team for the excellent ORM framework
|
|
806
|
+
- JPype contributors for the Java-Python bridge
|
|
807
|
+
- All contributors who have helped improve this library
|
|
808
|
+
|
|
809
|
+
## ๐ Links
|
|
810
|
+
|
|
811
|
+
- **Documentation**: https://sqlalchemy-jdbcapi.readthedocs.io (coming soon)
|
|
812
|
+
- **SQLAlchemy Integration Guide**: [SQLALCHEMY_INTEGRATION.md](SQLALCHEMY_INTEGRATION.md)
|
|
813
|
+
- **PyPI**: https://pypi.org/project/sqlalchemy-jdbcapi/
|
|
814
|
+
- **GitHub**: https://github.com/daneshpatel/sqlalchemy-jdbcapi
|
|
815
|
+
- **Issues**: https://github.com/daneshpatel/sqlalchemy-jdbcapi/issues
|
|
816
|
+
- **Changelog**: [CHANGELOG.md](CHANGELOG.md)
|
|
817
|
+
|
|
818
|
+
## ๐ฌ Support
|
|
819
|
+
|
|
820
|
+
- **Issues**: Report bugs or request features on [GitHub Issues](https://github.com/daneshpatel/sqlalchemy-jdbcapi/issues)
|
|
821
|
+
- **Discussions**: Ask questions on [GitHub Discussions](https://github.com/daneshpatel/sqlalchemy-jdbcapi/discussions)
|
|
822
|
+
|
|
823
|
+
---
|
|
824
|
+
|
|
825
|
+
Made with โค๏ธ by the SQLAlchemy JDBC API team
|