sqlalchemy-jdbcapi 2.0.0.post2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. sqlalchemy_jdbcapi/__init__.py +128 -0
  2. sqlalchemy_jdbcapi/_version.py +34 -0
  3. sqlalchemy_jdbcapi/dialects/__init__.py +30 -0
  4. sqlalchemy_jdbcapi/dialects/base.py +879 -0
  5. sqlalchemy_jdbcapi/dialects/db2.py +134 -0
  6. sqlalchemy_jdbcapi/dialects/mssql.py +117 -0
  7. sqlalchemy_jdbcapi/dialects/mysql.py +152 -0
  8. sqlalchemy_jdbcapi/dialects/oceanbase.py +218 -0
  9. sqlalchemy_jdbcapi/dialects/odbc_base.py +389 -0
  10. sqlalchemy_jdbcapi/dialects/odbc_mssql.py +69 -0
  11. sqlalchemy_jdbcapi/dialects/odbc_mysql.py +101 -0
  12. sqlalchemy_jdbcapi/dialects/odbc_oracle.py +80 -0
  13. sqlalchemy_jdbcapi/dialects/odbc_postgresql.py +63 -0
  14. sqlalchemy_jdbcapi/dialects/oracle.py +180 -0
  15. sqlalchemy_jdbcapi/dialects/postgresql.py +110 -0
  16. sqlalchemy_jdbcapi/dialects/sqlite.py +141 -0
  17. sqlalchemy_jdbcapi/jdbc/__init__.py +98 -0
  18. sqlalchemy_jdbcapi/jdbc/connection.py +244 -0
  19. sqlalchemy_jdbcapi/jdbc/cursor.py +329 -0
  20. sqlalchemy_jdbcapi/jdbc/dataframe.py +198 -0
  21. sqlalchemy_jdbcapi/jdbc/driver_manager.py +353 -0
  22. sqlalchemy_jdbcapi/jdbc/exceptions.py +53 -0
  23. sqlalchemy_jdbcapi/jdbc/jvm.py +176 -0
  24. sqlalchemy_jdbcapi/jdbc/type_converter.py +292 -0
  25. sqlalchemy_jdbcapi/jdbc/types.py +72 -0
  26. sqlalchemy_jdbcapi/odbc/__init__.py +46 -0
  27. sqlalchemy_jdbcapi/odbc/connection.py +136 -0
  28. sqlalchemy_jdbcapi/odbc/exceptions.py +48 -0
  29. sqlalchemy_jdbcapi/py.typed +2 -0
  30. sqlalchemy_jdbcapi-2.0.0.post2.dist-info/METADATA +825 -0
  31. sqlalchemy_jdbcapi-2.0.0.post2.dist-info/RECORD +36 -0
  32. sqlalchemy_jdbcapi-2.0.0.post2.dist-info/WHEEL +5 -0
  33. sqlalchemy_jdbcapi-2.0.0.post2.dist-info/entry_points.txt +20 -0
  34. sqlalchemy_jdbcapi-2.0.0.post2.dist-info/licenses/AUTHORS +7 -0
  35. sqlalchemy_jdbcapi-2.0.0.post2.dist-info/licenses/LICENSE +13 -0
  36. sqlalchemy_jdbcapi-2.0.0.post2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,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()