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,389 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base ODBC dialect for SQLAlchemy.
|
|
3
|
+
|
|
4
|
+
Provides the foundation for ODBC-based database dialects using pyodbc.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
from typing import TYPE_CHECKING, Any
|
|
11
|
+
|
|
12
|
+
from sqlalchemy import pool
|
|
13
|
+
from sqlalchemy.engine import URL, default
|
|
14
|
+
from sqlalchemy.engine.interfaces import ReflectedColumn
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from sqlalchemy.engine import Connection as SAConnection
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ODBCDialect(default.DefaultDialect):
|
|
23
|
+
"""
|
|
24
|
+
Base ODBC dialect for SQLAlchemy.
|
|
25
|
+
|
|
26
|
+
This provides common functionality for all ODBC-based dialects.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
driver = "pyodbc"
|
|
30
|
+
supports_statement_cache = True
|
|
31
|
+
supports_native_decimal = True
|
|
32
|
+
supports_unicode_binds = True
|
|
33
|
+
supports_unicode_statements = True
|
|
34
|
+
supports_multivalues_insert = True
|
|
35
|
+
supports_native_boolean = False
|
|
36
|
+
|
|
37
|
+
default_paramstyle = "qmark"
|
|
38
|
+
poolclass = pool.QueuePool
|
|
39
|
+
|
|
40
|
+
# ODBC-specific attributes
|
|
41
|
+
pyodbc_driver_name: str | None = None
|
|
42
|
+
default_schema_name: str | None = None
|
|
43
|
+
|
|
44
|
+
@classmethod
|
|
45
|
+
def import_dbapi(cls) -> Any:
|
|
46
|
+
"""Import the pyodbc module."""
|
|
47
|
+
try:
|
|
48
|
+
import pyodbc
|
|
49
|
+
|
|
50
|
+
return pyodbc
|
|
51
|
+
except ImportError as e:
|
|
52
|
+
raise ImportError(
|
|
53
|
+
"pyodbc is required for ODBC dialects. "
|
|
54
|
+
"Install with: pip install 'sqlalchemy-jdbcapi[odbc]' or pip install pyodbc"
|
|
55
|
+
) from e
|
|
56
|
+
|
|
57
|
+
def create_connect_args(self, url: URL) -> tuple[list[Any], dict[str, Any]]:
|
|
58
|
+
"""
|
|
59
|
+
Build connection arguments from SQLAlchemy URL.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
url: SQLAlchemy connection URL.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
Tuple of (args, kwargs) for connect function.
|
|
66
|
+
"""
|
|
67
|
+
opts = url.translate_connect_args(
|
|
68
|
+
username="uid", password="pwd", database="database"
|
|
69
|
+
)
|
|
70
|
+
opts.update(url.query)
|
|
71
|
+
|
|
72
|
+
# Build ODBC connection string
|
|
73
|
+
keys = opts.keys()
|
|
74
|
+
conn_str_parts = []
|
|
75
|
+
|
|
76
|
+
# Add driver
|
|
77
|
+
if "driver" not in keys and self.pyodbc_driver_name:
|
|
78
|
+
conn_str_parts.append(f"DRIVER={{{self.pyodbc_driver_name}}}")
|
|
79
|
+
|
|
80
|
+
# Add server
|
|
81
|
+
if "host" in keys and opts["host"]:
|
|
82
|
+
host = opts.pop("host")
|
|
83
|
+
port = opts.pop("port", None)
|
|
84
|
+
if port:
|
|
85
|
+
conn_str_parts.append(f"SERVER={host},{port}")
|
|
86
|
+
else:
|
|
87
|
+
conn_str_parts.append(f"SERVER={host}")
|
|
88
|
+
|
|
89
|
+
# Add database
|
|
90
|
+
if "database" in keys and opts.get("database"):
|
|
91
|
+
conn_str_parts.append(f"DATABASE={opts.pop('database')}")
|
|
92
|
+
|
|
93
|
+
# Add UID/PWD
|
|
94
|
+
if "uid" in keys and opts.get("uid"):
|
|
95
|
+
conn_str_parts.append(f"UID={opts.pop('uid')}")
|
|
96
|
+
if "pwd" in keys and opts.get("pwd"):
|
|
97
|
+
conn_str_parts.append(f"PWD={opts.pop('pwd')}")
|
|
98
|
+
|
|
99
|
+
# Add remaining options
|
|
100
|
+
for key, value in opts.items():
|
|
101
|
+
if value is not None:
|
|
102
|
+
conn_str_parts.append(f"{key}={value}")
|
|
103
|
+
|
|
104
|
+
connection_string = ";".join(conn_str_parts)
|
|
105
|
+
|
|
106
|
+
return ([connection_string], {})
|
|
107
|
+
|
|
108
|
+
def get_schema_names(self, connection: SAConnection, **kwargs: Any) -> list[str]:
|
|
109
|
+
"""
|
|
110
|
+
Get list of schema names.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
connection: SQLAlchemy connection.
|
|
114
|
+
**kwargs: Additional arguments.
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
List of schema names.
|
|
118
|
+
"""
|
|
119
|
+
cursor = connection.connection.cursor()
|
|
120
|
+
try:
|
|
121
|
+
schemas = []
|
|
122
|
+
for row in cursor.tables():
|
|
123
|
+
if row.table_schem and row.table_schem not in schemas:
|
|
124
|
+
schemas.append(row.table_schem)
|
|
125
|
+
return sorted(schemas)
|
|
126
|
+
finally:
|
|
127
|
+
cursor.close()
|
|
128
|
+
|
|
129
|
+
def get_table_names(
|
|
130
|
+
self, connection: SAConnection, schema: str | None = None, **kwargs: Any
|
|
131
|
+
) -> list[str]:
|
|
132
|
+
"""
|
|
133
|
+
Get list of table names in a schema.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
connection: SQLAlchemy connection.
|
|
137
|
+
schema: Schema name (optional).
|
|
138
|
+
**kwargs: Additional arguments.
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
List of table names.
|
|
142
|
+
"""
|
|
143
|
+
cursor = connection.connection.cursor()
|
|
144
|
+
try:
|
|
145
|
+
tables = []
|
|
146
|
+
for row in cursor.tables(schema=schema, tableType="TABLE"):
|
|
147
|
+
tables.append(row.table_name)
|
|
148
|
+
return sorted(tables)
|
|
149
|
+
finally:
|
|
150
|
+
cursor.close()
|
|
151
|
+
|
|
152
|
+
def get_view_names(
|
|
153
|
+
self, connection: SAConnection, schema: str | None = None, **kwargs: Any
|
|
154
|
+
) -> list[str]:
|
|
155
|
+
"""
|
|
156
|
+
Get list of view names in a schema.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
connection: SQLAlchemy connection.
|
|
160
|
+
schema: Schema name (optional).
|
|
161
|
+
**kwargs: Additional arguments.
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
List of view names.
|
|
165
|
+
"""
|
|
166
|
+
cursor = connection.connection.cursor()
|
|
167
|
+
try:
|
|
168
|
+
views = []
|
|
169
|
+
for row in cursor.tables(schema=schema, tableType="VIEW"):
|
|
170
|
+
views.append(row.table_name)
|
|
171
|
+
return sorted(views)
|
|
172
|
+
finally:
|
|
173
|
+
cursor.close()
|
|
174
|
+
|
|
175
|
+
def has_table(
|
|
176
|
+
self,
|
|
177
|
+
connection: SAConnection,
|
|
178
|
+
table_name: str,
|
|
179
|
+
schema: str | None = None,
|
|
180
|
+
**kwargs: Any,
|
|
181
|
+
) -> bool:
|
|
182
|
+
"""
|
|
183
|
+
Check if a table exists.
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
connection: SQLAlchemy connection.
|
|
187
|
+
table_name: Table name.
|
|
188
|
+
schema: Schema name (optional).
|
|
189
|
+
**kwargs: Additional arguments.
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
True if table exists, False otherwise.
|
|
193
|
+
"""
|
|
194
|
+
cursor = connection.connection.cursor()
|
|
195
|
+
try:
|
|
196
|
+
for row in cursor.tables(table=table_name, schema=schema):
|
|
197
|
+
if row.table_name == table_name:
|
|
198
|
+
return True
|
|
199
|
+
return False
|
|
200
|
+
finally:
|
|
201
|
+
cursor.close()
|
|
202
|
+
|
|
203
|
+
def get_columns(
|
|
204
|
+
self,
|
|
205
|
+
connection: SAConnection,
|
|
206
|
+
table_name: str,
|
|
207
|
+
schema: str | None = None,
|
|
208
|
+
**kwargs: Any,
|
|
209
|
+
) -> list[ReflectedColumn]:
|
|
210
|
+
"""
|
|
211
|
+
Get column information for a table.
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
connection: SQLAlchemy connection.
|
|
215
|
+
table_name: Table name.
|
|
216
|
+
schema: Schema name (optional).
|
|
217
|
+
**kwargs: Additional arguments.
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
List of column dictionaries.
|
|
221
|
+
"""
|
|
222
|
+
cursor = connection.connection.cursor()
|
|
223
|
+
try:
|
|
224
|
+
columns = []
|
|
225
|
+
for row in cursor.columns(table=table_name, schema=schema):
|
|
226
|
+
column_info = {
|
|
227
|
+
"name": row.column_name,
|
|
228
|
+
"type": self._get_column_type(
|
|
229
|
+
row.type_name, row.column_size, row.decimal_digits
|
|
230
|
+
),
|
|
231
|
+
"nullable": row.nullable == 1,
|
|
232
|
+
"default": row.column_def,
|
|
233
|
+
}
|
|
234
|
+
columns.append(column_info)
|
|
235
|
+
return columns
|
|
236
|
+
finally:
|
|
237
|
+
cursor.close()
|
|
238
|
+
|
|
239
|
+
def _get_column_type( # noqa: C901 - Type mapping requires complexity
|
|
240
|
+
self, type_name: str, size: int | None, precision: int | None
|
|
241
|
+
) -> Any:
|
|
242
|
+
"""
|
|
243
|
+
Map ODBC type name to SQLAlchemy type.
|
|
244
|
+
|
|
245
|
+
Args:
|
|
246
|
+
type_name: ODBC type name.
|
|
247
|
+
size: Column size.
|
|
248
|
+
precision: Decimal precision.
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
SQLAlchemy type object.
|
|
252
|
+
"""
|
|
253
|
+
from sqlalchemy import types
|
|
254
|
+
|
|
255
|
+
# Basic type mapping
|
|
256
|
+
type_name_upper = type_name.upper()
|
|
257
|
+
|
|
258
|
+
if "INT" in type_name_upper:
|
|
259
|
+
return types.INTEGER()
|
|
260
|
+
if "VARCHAR" in type_name_upper or "CHAR" in type_name_upper:
|
|
261
|
+
return types.VARCHAR(length=size) if size else types.VARCHAR()
|
|
262
|
+
if "TEXT" in type_name_upper:
|
|
263
|
+
return types.TEXT()
|
|
264
|
+
if "FLOAT" in type_name_upper or "REAL" in type_name_upper:
|
|
265
|
+
return types.FLOAT()
|
|
266
|
+
if "DECIMAL" in type_name_upper or "NUMERIC" in type_name_upper:
|
|
267
|
+
return types.NUMERIC(precision=size, scale=precision)
|
|
268
|
+
if "DATE" in type_name_upper and "TIME" not in type_name_upper:
|
|
269
|
+
return types.DATE()
|
|
270
|
+
if "TIME" in type_name_upper and "STAMP" not in type_name_upper:
|
|
271
|
+
return types.TIME()
|
|
272
|
+
if "TIMESTAMP" in type_name_upper or "DATETIME" in type_name_upper:
|
|
273
|
+
return types.TIMESTAMP()
|
|
274
|
+
if "BOOL" in type_name_upper:
|
|
275
|
+
return types.BOOLEAN()
|
|
276
|
+
if "BLOB" in type_name_upper or "BINARY" in type_name_upper:
|
|
277
|
+
return types.BLOB()
|
|
278
|
+
# Default to VARCHAR for unknown types
|
|
279
|
+
return types.VARCHAR()
|
|
280
|
+
|
|
281
|
+
def get_pk_constraint(
|
|
282
|
+
self,
|
|
283
|
+
connection: SAConnection,
|
|
284
|
+
table_name: str,
|
|
285
|
+
schema: str | None = None,
|
|
286
|
+
**kwargs: Any,
|
|
287
|
+
) -> dict[str, Any]:
|
|
288
|
+
"""
|
|
289
|
+
Get primary key constraint information.
|
|
290
|
+
|
|
291
|
+
Args:
|
|
292
|
+
connection: SQLAlchemy connection.
|
|
293
|
+
table_name: Table name.
|
|
294
|
+
schema: Schema name (optional).
|
|
295
|
+
**kwargs: Additional arguments.
|
|
296
|
+
|
|
297
|
+
Returns:
|
|
298
|
+
Dictionary with 'constrained_columns' and 'name'.
|
|
299
|
+
"""
|
|
300
|
+
cursor = connection.connection.cursor()
|
|
301
|
+
try:
|
|
302
|
+
pk_columns = []
|
|
303
|
+
pk_name = None
|
|
304
|
+
for row in cursor.primaryKeys(table=table_name, schema=schema):
|
|
305
|
+
pk_columns.append(row.column_name)
|
|
306
|
+
if pk_name is None:
|
|
307
|
+
pk_name = row.pk_name
|
|
308
|
+
|
|
309
|
+
return {
|
|
310
|
+
"constrained_columns": pk_columns,
|
|
311
|
+
"name": pk_name,
|
|
312
|
+
}
|
|
313
|
+
finally:
|
|
314
|
+
cursor.close()
|
|
315
|
+
|
|
316
|
+
def get_foreign_keys(
|
|
317
|
+
self,
|
|
318
|
+
connection: SAConnection,
|
|
319
|
+
table_name: str,
|
|
320
|
+
schema: str | None = None,
|
|
321
|
+
**kwargs: Any,
|
|
322
|
+
) -> list[dict[str, Any]]:
|
|
323
|
+
"""
|
|
324
|
+
Get foreign key constraints.
|
|
325
|
+
|
|
326
|
+
Args:
|
|
327
|
+
connection: SQLAlchemy connection.
|
|
328
|
+
table_name: Table name.
|
|
329
|
+
schema: Schema name (optional).
|
|
330
|
+
**kwargs: Additional arguments.
|
|
331
|
+
|
|
332
|
+
Returns:
|
|
333
|
+
List of foreign key dictionaries.
|
|
334
|
+
"""
|
|
335
|
+
cursor = connection.connection.cursor()
|
|
336
|
+
try:
|
|
337
|
+
fks: dict[str, dict[str, Any]] = {}
|
|
338
|
+
for row in cursor.foreignKeys(table=table_name, schema=schema):
|
|
339
|
+
fk_name = row.fk_name or f"fk_{len(fks)}"
|
|
340
|
+
if fk_name not in fks:
|
|
341
|
+
fks[fk_name] = {
|
|
342
|
+
"name": fk_name,
|
|
343
|
+
"constrained_columns": [],
|
|
344
|
+
"referred_schema": row.pktable_schem,
|
|
345
|
+
"referred_table": row.pktable_name,
|
|
346
|
+
"referred_columns": [],
|
|
347
|
+
}
|
|
348
|
+
fks[fk_name]["constrained_columns"].append(row.fkcolumn_name)
|
|
349
|
+
fks[fk_name]["referred_columns"].append(row.pkcolumn_name)
|
|
350
|
+
|
|
351
|
+
return list(fks.values())
|
|
352
|
+
finally:
|
|
353
|
+
cursor.close()
|
|
354
|
+
|
|
355
|
+
def get_indexes(
|
|
356
|
+
self,
|
|
357
|
+
connection: SAConnection,
|
|
358
|
+
table_name: str,
|
|
359
|
+
schema: str | None = None,
|
|
360
|
+
**kwargs: Any,
|
|
361
|
+
) -> list[dict[str, Any]]:
|
|
362
|
+
"""
|
|
363
|
+
Get index information.
|
|
364
|
+
|
|
365
|
+
Args:
|
|
366
|
+
connection: SQLAlchemy connection.
|
|
367
|
+
table_name: Table name.
|
|
368
|
+
schema: Schema name (optional).
|
|
369
|
+
**kwargs: Additional arguments.
|
|
370
|
+
|
|
371
|
+
Returns:
|
|
372
|
+
List of index dictionaries.
|
|
373
|
+
"""
|
|
374
|
+
cursor = connection.connection.cursor()
|
|
375
|
+
try:
|
|
376
|
+
indexes: dict[str, dict[str, Any]] = {}
|
|
377
|
+
for row in cursor.statistics(table=table_name, schema=schema):
|
|
378
|
+
if row.index_name:
|
|
379
|
+
if row.index_name not in indexes:
|
|
380
|
+
indexes[row.index_name] = {
|
|
381
|
+
"name": row.index_name,
|
|
382
|
+
"column_names": [],
|
|
383
|
+
"unique": row.non_unique == 0,
|
|
384
|
+
}
|
|
385
|
+
indexes[row.index_name]["column_names"].append(row.column_name)
|
|
386
|
+
|
|
387
|
+
return list(indexes.values())
|
|
388
|
+
finally:
|
|
389
|
+
cursor.close()
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Microsoft SQL Server ODBC dialect for SQLAlchemy.
|
|
3
|
+
|
|
4
|
+
Provides SQL Server database support via ODBC.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from sqlalchemy.dialects.mssql import base as mssql_base
|
|
13
|
+
|
|
14
|
+
from .odbc_base import ODBCDialect
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class MSSQLODBCDialect(ODBCDialect, mssql_base.MSDialect):
|
|
20
|
+
"""
|
|
21
|
+
Microsoft SQL Server ODBC dialect.
|
|
22
|
+
|
|
23
|
+
Supports SQL Server via ODBC using the Microsoft ODBC Driver for SQL Server.
|
|
24
|
+
|
|
25
|
+
Recommended ODBC Driver:
|
|
26
|
+
- ODBC Driver 18 for SQL Server (latest)
|
|
27
|
+
- ODBC Driver 17 for SQL Server
|
|
28
|
+
- Download: https://learn.microsoft.com/en-us/sql/connect/odbc/download-odbc-driver-for-sql-server
|
|
29
|
+
|
|
30
|
+
Connection URL:
|
|
31
|
+
odbcapi+mssql://user:password@host:1433/database
|
|
32
|
+
odbcapi+sqlserver://user:password@host:1433/database (alias)
|
|
33
|
+
|
|
34
|
+
Features:
|
|
35
|
+
- Full T-SQL support
|
|
36
|
+
- Window functions
|
|
37
|
+
- CTEs (Common Table Expressions)
|
|
38
|
+
- JSON support (SQL Server 2016+)
|
|
39
|
+
- Sequence support (SQL Server 2012+)
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
name = "mssql"
|
|
43
|
+
driver = "odbcapi+mssql"
|
|
44
|
+
|
|
45
|
+
# ODBC driver name for SQL Server
|
|
46
|
+
pyodbc_driver_name = "ODBC Driver 18 for SQL Server"
|
|
47
|
+
|
|
48
|
+
default_schema_name = "dbo"
|
|
49
|
+
supports_sequences = True # SQL Server 2012+
|
|
50
|
+
supports_native_boolean = False # Use BIT instead
|
|
51
|
+
|
|
52
|
+
@classmethod
|
|
53
|
+
def import_dbapi(cls) -> Any:
|
|
54
|
+
"""Import pyodbc module."""
|
|
55
|
+
import pyodbc
|
|
56
|
+
|
|
57
|
+
return pyodbc
|
|
58
|
+
|
|
59
|
+
def _get_server_version_info(self, connection: Any) -> tuple[int, ...]:
|
|
60
|
+
"""Get SQL Server version."""
|
|
61
|
+
cursor = connection.connection.cursor()
|
|
62
|
+
try:
|
|
63
|
+
cursor.execute("SELECT SERVERPROPERTY('ProductVersion')")
|
|
64
|
+
version_string = cursor.fetchone()[0]
|
|
65
|
+
# Parse version like "15.0.2000.5"
|
|
66
|
+
version_parts = version_string.split(".")
|
|
67
|
+
return tuple(int(p) for p in version_parts[:2])
|
|
68
|
+
finally:
|
|
69
|
+
cursor.close()
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MySQL/MariaDB ODBC dialect for SQLAlchemy.
|
|
3
|
+
|
|
4
|
+
Provides MySQL and MariaDB database support via ODBC.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from sqlalchemy.dialects.mysql import base as mysql_base
|
|
13
|
+
|
|
14
|
+
from .odbc_base import ODBCDialect
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class MySQLODBCDialect(ODBCDialect, mysql_base.MySQLDialect):
|
|
20
|
+
"""
|
|
21
|
+
MySQL ODBC dialect.
|
|
22
|
+
|
|
23
|
+
Supports MySQL via ODBC using the MySQL Connector/ODBC driver.
|
|
24
|
+
|
|
25
|
+
Recommended ODBC Driver:
|
|
26
|
+
- MySQL Connector/ODBC 8.0+
|
|
27
|
+
- Download: https://dev.mysql.com/downloads/connector/odbc/
|
|
28
|
+
|
|
29
|
+
Connection URL:
|
|
30
|
+
odbcapi+mysql://user:password@host:3306/database
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
name = "mysql"
|
|
34
|
+
driver = "odbcapi+mysql"
|
|
35
|
+
|
|
36
|
+
# ODBC driver name for MySQL
|
|
37
|
+
pyodbc_driver_name = "MySQL ODBC 8.0 Driver"
|
|
38
|
+
|
|
39
|
+
supports_native_decimal = True
|
|
40
|
+
|
|
41
|
+
@classmethod
|
|
42
|
+
def import_dbapi(cls) -> Any:
|
|
43
|
+
"""Import pyodbc module."""
|
|
44
|
+
import pyodbc
|
|
45
|
+
|
|
46
|
+
return pyodbc
|
|
47
|
+
|
|
48
|
+
def _get_server_version_info(self, connection: Any) -> tuple[int, ...]:
|
|
49
|
+
"""Get MySQL server version."""
|
|
50
|
+
cursor = connection.connection.cursor()
|
|
51
|
+
try:
|
|
52
|
+
cursor.execute("SELECT VERSION()")
|
|
53
|
+
version_string = cursor.fetchone()[0]
|
|
54
|
+
# Parse version like "8.0.34" or "10.11.3-MariaDB"
|
|
55
|
+
version_parts = version_string.split("-")[0].split(".")
|
|
56
|
+
return tuple(int(p) for p in version_parts[:3])
|
|
57
|
+
finally:
|
|
58
|
+
cursor.close()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class MariaDBODBCDialect(ODBCDialect, mysql_base.MySQLDialect):
|
|
62
|
+
"""
|
|
63
|
+
MariaDB ODBC dialect.
|
|
64
|
+
|
|
65
|
+
Supports MariaDB via ODBC using the MariaDB Connector/ODBC driver.
|
|
66
|
+
|
|
67
|
+
Recommended ODBC Driver:
|
|
68
|
+
- MariaDB Connector/ODBC 3.1+
|
|
69
|
+
- Download: https://mariadb.com/downloads/connectors/odbc/
|
|
70
|
+
|
|
71
|
+
Connection URL:
|
|
72
|
+
odbcapi+mariadb://user:password@host:3306/database
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
name = "mariadb"
|
|
76
|
+
driver = "odbcapi+mariadb"
|
|
77
|
+
|
|
78
|
+
# ODBC driver name for MariaDB
|
|
79
|
+
pyodbc_driver_name = "MariaDB ODBC 3.1 Driver"
|
|
80
|
+
|
|
81
|
+
supports_native_decimal = True
|
|
82
|
+
supports_sequences = True # MariaDB 10.3+
|
|
83
|
+
|
|
84
|
+
@classmethod
|
|
85
|
+
def import_dbapi(cls) -> Any:
|
|
86
|
+
"""Import pyodbc module."""
|
|
87
|
+
import pyodbc
|
|
88
|
+
|
|
89
|
+
return pyodbc
|
|
90
|
+
|
|
91
|
+
def _get_server_version_info(self, connection: Any) -> tuple[int, ...]:
|
|
92
|
+
"""Get MariaDB server version."""
|
|
93
|
+
cursor = connection.connection.cursor()
|
|
94
|
+
try:
|
|
95
|
+
cursor.execute("SELECT VERSION()")
|
|
96
|
+
version_string = cursor.fetchone()[0]
|
|
97
|
+
# Parse version like "10.11.3-MariaDB"
|
|
98
|
+
version_parts = version_string.split("-")[0].split(".")
|
|
99
|
+
return tuple(int(p) for p in version_parts[:3])
|
|
100
|
+
finally:
|
|
101
|
+
cursor.close()
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Oracle ODBC dialect for SQLAlchemy.
|
|
3
|
+
|
|
4
|
+
Provides Oracle database support via ODBC.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from sqlalchemy.dialects.oracle import base as oracle_base
|
|
13
|
+
|
|
14
|
+
from .odbc_base import ODBCDialect
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class OracleODBCDialect(ODBCDialect, oracle_base.OracleDialect):
|
|
20
|
+
"""
|
|
21
|
+
Oracle ODBC dialect.
|
|
22
|
+
|
|
23
|
+
Supports Oracle Database via ODBC using the Oracle ODBC driver.
|
|
24
|
+
|
|
25
|
+
Recommended ODBC Driver:
|
|
26
|
+
- Oracle Instant Client ODBC (latest)
|
|
27
|
+
- Download: https://www.oracle.com/database/technologies/instant-client/downloads.html
|
|
28
|
+
|
|
29
|
+
Connection URL:
|
|
30
|
+
odbcapi+oracle://user:password@host:1521/service_name
|
|
31
|
+
|
|
32
|
+
Features:
|
|
33
|
+
- Full Oracle SQL support
|
|
34
|
+
- Sequences
|
|
35
|
+
- Synonyms
|
|
36
|
+
- Database links
|
|
37
|
+
- PL/SQL support
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
name = "oracle"
|
|
41
|
+
driver = "odbcapi+oracle"
|
|
42
|
+
|
|
43
|
+
# ODBC driver name for Oracle
|
|
44
|
+
pyodbc_driver_name = "Oracle in instantclient_21_13"
|
|
45
|
+
|
|
46
|
+
supports_sequences = True
|
|
47
|
+
supports_native_boolean = False # Oracle doesn't have native BOOLEAN type
|
|
48
|
+
|
|
49
|
+
@classmethod
|
|
50
|
+
def import_dbapi(cls) -> Any:
|
|
51
|
+
"""Import pyodbc module."""
|
|
52
|
+
import pyodbc
|
|
53
|
+
|
|
54
|
+
return pyodbc
|
|
55
|
+
|
|
56
|
+
def _get_server_version_info(self, connection: Any) -> tuple[int, ...]:
|
|
57
|
+
"""Get Oracle server version."""
|
|
58
|
+
cursor = connection.connection.cursor()
|
|
59
|
+
try:
|
|
60
|
+
cursor.execute("SELECT version FROM v$instance")
|
|
61
|
+
version_string = cursor.fetchone()[0]
|
|
62
|
+
# Parse version like "19.0.0.0.0"
|
|
63
|
+
version_parts = version_string.split(".")
|
|
64
|
+
return tuple(int(p) for p in version_parts[:2])
|
|
65
|
+
except Exception:
|
|
66
|
+
# Fallback for restricted permissions
|
|
67
|
+
try:
|
|
68
|
+
cursor.execute("SELECT * FROM v$version WHERE banner LIKE 'Oracle%'")
|
|
69
|
+
banner = cursor.fetchone()[0]
|
|
70
|
+
# Extract version from banner
|
|
71
|
+
import re
|
|
72
|
+
|
|
73
|
+
match = re.search(r"Release (\d+)\.(\d+)", banner)
|
|
74
|
+
if match:
|
|
75
|
+
return (int(match.group(1)), int(match.group(2)))
|
|
76
|
+
except Exception:
|
|
77
|
+
pass
|
|
78
|
+
return (0, 0)
|
|
79
|
+
finally:
|
|
80
|
+
cursor.close()
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PostgreSQL ODBC dialect for SQLAlchemy.
|
|
3
|
+
|
|
4
|
+
Provides PostgreSQL database support via ODBC using the official PostgreSQL ODBC driver.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from sqlalchemy.dialects.postgresql import base as postgresql_base
|
|
13
|
+
|
|
14
|
+
from .odbc_base import ODBCDialect
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class PostgreSQLODBCDialect(ODBCDialect, postgresql_base.PGDialect):
|
|
20
|
+
"""
|
|
21
|
+
PostgreSQL ODBC dialect.
|
|
22
|
+
|
|
23
|
+
Supports PostgreSQL via ODBC using the PostgreSQL Unicode ODBC driver.
|
|
24
|
+
|
|
25
|
+
Recommended ODBC Driver:
|
|
26
|
+
- PostgreSQL Unicode (psqlODBC): Latest version
|
|
27
|
+
- Download: https://www.postgresql.org/ftp/odbc/versions/
|
|
28
|
+
|
|
29
|
+
Connection URL:
|
|
30
|
+
odbcapi+postgresql://user:password@host:5432/database
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
name = "postgresql"
|
|
34
|
+
driver = "odbcapi+postgresql"
|
|
35
|
+
|
|
36
|
+
# ODBC driver name for PostgreSQL
|
|
37
|
+
pyodbc_driver_name = "PostgreSQL Unicode"
|
|
38
|
+
|
|
39
|
+
default_schema_name = "public"
|
|
40
|
+
supports_sequences = True
|
|
41
|
+
supports_native_boolean = True
|
|
42
|
+
|
|
43
|
+
@classmethod
|
|
44
|
+
def import_dbapi(cls) -> Any:
|
|
45
|
+
"""Import pyodbc module."""
|
|
46
|
+
import pyodbc
|
|
47
|
+
|
|
48
|
+
return pyodbc
|
|
49
|
+
|
|
50
|
+
def _get_server_version_info(self, connection: Any) -> tuple[int, ...]:
|
|
51
|
+
"""Get PostgreSQL server version."""
|
|
52
|
+
cursor = connection.connection.cursor()
|
|
53
|
+
try:
|
|
54
|
+
cursor.execute("SELECT version()")
|
|
55
|
+
version_string = cursor.fetchone()[0]
|
|
56
|
+
# Parse version like "PostgreSQL 15.4 on ..."
|
|
57
|
+
parts = version_string.split()
|
|
58
|
+
if len(parts) >= 2:
|
|
59
|
+
version_parts = parts[1].split(".")
|
|
60
|
+
return tuple(int(p) for p in version_parts)
|
|
61
|
+
return (0, 0, 0)
|
|
62
|
+
finally:
|
|
63
|
+
cursor.close()
|