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,134 @@
|
|
|
1
|
+
"""
|
|
2
|
+
IBM DB2 JDBC dialect for SQLAlchemy.
|
|
3
|
+
|
|
4
|
+
Provides support for IBM DB2 for Linux, Unix, Windows, and z/OS.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
import re
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from sqlalchemy import exc, sql
|
|
14
|
+
from sqlalchemy.engine import Connection, Dialect
|
|
15
|
+
|
|
16
|
+
from .base import BaseJDBCDialect, JDBCDriverConfig
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
# Try to import DB2 dialect if available
|
|
21
|
+
try:
|
|
22
|
+
from sqlalchemy.dialects.db2.base import DB2Dialect as BaseDB2Dialect
|
|
23
|
+
|
|
24
|
+
HAS_DB2_DIALECT = True
|
|
25
|
+
except ImportError:
|
|
26
|
+
# Fallback to generic dialect
|
|
27
|
+
BaseDB2Dialect = Dialect # type: ignore
|
|
28
|
+
HAS_DB2_DIALECT = False
|
|
29
|
+
logger.debug("IBM DB2 dialect not available, using generic implementation")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class DB2Dialect(BaseJDBCDialect, BaseDB2Dialect): # type: ignore
|
|
33
|
+
"""
|
|
34
|
+
IBM DB2 dialect using JDBC driver.
|
|
35
|
+
|
|
36
|
+
Supports DB2-specific features including:
|
|
37
|
+
- Sequences
|
|
38
|
+
- Identity columns
|
|
39
|
+
- Generated columns
|
|
40
|
+
- Temporal tables (DB2 10+)
|
|
41
|
+
- JSON support (DB2 11+)
|
|
42
|
+
|
|
43
|
+
Connection URL format:
|
|
44
|
+
jdbcapi+db2://user:password@host:50000/database
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
name = "db2"
|
|
48
|
+
driver = "jdbcapi"
|
|
49
|
+
|
|
50
|
+
# DB2 capabilities
|
|
51
|
+
supports_native_boolean = False # DB2 < 11.1 doesn't support BOOLEAN
|
|
52
|
+
supports_sequences = True
|
|
53
|
+
supports_identity_columns = True
|
|
54
|
+
supports_native_enum = False
|
|
55
|
+
|
|
56
|
+
@classmethod
|
|
57
|
+
def get_driver_config(cls) -> JDBCDriverConfig:
|
|
58
|
+
"""Get DB2 JDBC driver configuration."""
|
|
59
|
+
return JDBCDriverConfig(
|
|
60
|
+
driver_class="com.ibm.db2.jcc.DB2Driver",
|
|
61
|
+
jdbc_url_template="jdbc:db2://{host}:{port}/{database}",
|
|
62
|
+
default_port=50000,
|
|
63
|
+
supports_transactions=True,
|
|
64
|
+
supports_schemas=True,
|
|
65
|
+
supports_sequences=True,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
def initialize(self, connection: Connection) -> None:
|
|
69
|
+
"""Initialize DB2 connection."""
|
|
70
|
+
if HAS_DB2_DIALECT:
|
|
71
|
+
super().initialize(connection)
|
|
72
|
+
# Basic initialization
|
|
73
|
+
elif not hasattr(self, "_server_version_info"):
|
|
74
|
+
self._server_version_info = self._get_server_version_info(connection)
|
|
75
|
+
|
|
76
|
+
logger.debug("Initialized DB2 JDBC dialect")
|
|
77
|
+
|
|
78
|
+
def _get_server_version_info(self, connection: Connection) -> tuple[int, ...]:
|
|
79
|
+
"""
|
|
80
|
+
Get DB2 server version.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
Tuple of version numbers (e.g., (11, 5, 8))
|
|
84
|
+
"""
|
|
85
|
+
try:
|
|
86
|
+
# Try DB2-specific version query
|
|
87
|
+
result = connection.execute(
|
|
88
|
+
sql.text("SELECT SERVICE_LEVEL FROM SYSIBMADM.ENV_INST_INFO")
|
|
89
|
+
).scalar()
|
|
90
|
+
|
|
91
|
+
if result:
|
|
92
|
+
# Parse version from string like:
|
|
93
|
+
# "DB2 v11.5.8.0"
|
|
94
|
+
match = re.search(r"v(\d+)\.(\d+)\.(\d+)", result)
|
|
95
|
+
if match:
|
|
96
|
+
major = int(match.group(1))
|
|
97
|
+
minor = int(match.group(2))
|
|
98
|
+
patch = int(match.group(3))
|
|
99
|
+
return (major, minor, patch)
|
|
100
|
+
|
|
101
|
+
except exc.DBAPIError as e:
|
|
102
|
+
logger.warning(f"Failed to get DB2 server version: {e}")
|
|
103
|
+
|
|
104
|
+
# Fallback: try alternative query
|
|
105
|
+
try:
|
|
106
|
+
result = connection.execute(
|
|
107
|
+
sql.text("VALUES (SYSPROC.VERSION())")
|
|
108
|
+
).scalar()
|
|
109
|
+
|
|
110
|
+
if result:
|
|
111
|
+
match = re.search(r"(\d+)\.(\d+)\.(\d+)", result)
|
|
112
|
+
if match:
|
|
113
|
+
return tuple(int(match.group(i)) for i in range(1, 4))
|
|
114
|
+
|
|
115
|
+
except exc.DBAPIError:
|
|
116
|
+
pass
|
|
117
|
+
|
|
118
|
+
# Default fallback
|
|
119
|
+
return (11, 1, 0)
|
|
120
|
+
|
|
121
|
+
def do_ping(self, dbapi_connection: Any) -> bool:
|
|
122
|
+
"""Check if DB2 connection is alive."""
|
|
123
|
+
try:
|
|
124
|
+
cursor = dbapi_connection.cursor()
|
|
125
|
+
cursor.execute("SELECT 1 FROM SYSIBM.SYSDUMMY1")
|
|
126
|
+
cursor.close()
|
|
127
|
+
return True
|
|
128
|
+
except Exception as e:
|
|
129
|
+
logger.debug(f"DB2 ping failed: {e}")
|
|
130
|
+
return False
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
# Export the dialect
|
|
134
|
+
dialect = DB2Dialect
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Microsoft SQL Server JDBC dialect for SQLAlchemy.
|
|
3
|
+
|
|
4
|
+
Provides full SQL Server support through JDBC.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
import re
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from sqlalchemy import exc, sql
|
|
14
|
+
from sqlalchemy.dialects.mssql.base import MSDialect
|
|
15
|
+
from sqlalchemy.engine import Connection
|
|
16
|
+
|
|
17
|
+
from .base import BaseJDBCDialect, JDBCDriverConfig
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class MSSQLDialect(BaseJDBCDialect, MSDialect):
|
|
23
|
+
"""
|
|
24
|
+
Microsoft SQL Server dialect using JDBC driver.
|
|
25
|
+
|
|
26
|
+
Supports SQL Server-specific features including:
|
|
27
|
+
- T-SQL extensions
|
|
28
|
+
- TOP clause
|
|
29
|
+
- OUTPUT clause
|
|
30
|
+
- Common Table Expressions (CTEs)
|
|
31
|
+
- Window functions
|
|
32
|
+
- JSON support (SQL Server 2016+)
|
|
33
|
+
|
|
34
|
+
Connection URL formats:
|
|
35
|
+
jdbcapi+mssql://user:password@host:1433/database
|
|
36
|
+
jdbcapi+sqlserver://user:password@host:1433/database # Alias
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
name = "mssql"
|
|
40
|
+
driver = "jdbcapi"
|
|
41
|
+
|
|
42
|
+
# SQL Server capabilities
|
|
43
|
+
supports_native_boolean = False # SQL Server uses BIT
|
|
44
|
+
supports_sequences = True # SQL Server 2012+
|
|
45
|
+
supports_native_enum = False
|
|
46
|
+
|
|
47
|
+
@classmethod
|
|
48
|
+
def get_driver_config(cls) -> JDBCDriverConfig:
|
|
49
|
+
"""Get SQL Server JDBC driver configuration."""
|
|
50
|
+
return JDBCDriverConfig(
|
|
51
|
+
driver_class="com.microsoft.sqlserver.jdbc.SQLServerDriver",
|
|
52
|
+
jdbc_url_template="jdbc:sqlserver://{host}:{port};databaseName={database}",
|
|
53
|
+
default_port=1433,
|
|
54
|
+
supports_transactions=True,
|
|
55
|
+
supports_schemas=True,
|
|
56
|
+
supports_sequences=True,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
def initialize(self, connection: Connection) -> None:
|
|
60
|
+
"""Initialize SQL Server connection."""
|
|
61
|
+
super().initialize(connection)
|
|
62
|
+
logger.debug("Initialized SQL Server JDBC dialect")
|
|
63
|
+
|
|
64
|
+
def _get_server_version_info(self, connection: Connection) -> tuple[int, ...]:
|
|
65
|
+
"""
|
|
66
|
+
Get SQL Server version.
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
Tuple of version numbers (e.g., (15, 0, 4236))
|
|
70
|
+
"""
|
|
71
|
+
try:
|
|
72
|
+
result = connection.execute(sql.text("SELECT @@VERSION")).scalar()
|
|
73
|
+
|
|
74
|
+
if result:
|
|
75
|
+
# Parse version from string like:
|
|
76
|
+
# "Microsoft SQL Server 2019 (RTM-CU15) - 15.0.4236.7 ..."
|
|
77
|
+
match = re.search(r"- (\d+)\.(\d+)\.(\d+)", result)
|
|
78
|
+
if match:
|
|
79
|
+
major = int(match.group(1))
|
|
80
|
+
minor = int(match.group(2))
|
|
81
|
+
build = int(match.group(3))
|
|
82
|
+
return (major, minor, build)
|
|
83
|
+
|
|
84
|
+
# Fallback: try to extract major version from name
|
|
85
|
+
version_names = {
|
|
86
|
+
"2022": (16, 0, 0),
|
|
87
|
+
"2019": (15, 0, 0),
|
|
88
|
+
"2017": (14, 0, 0),
|
|
89
|
+
"2016": (13, 0, 0),
|
|
90
|
+
"2014": (12, 0, 0),
|
|
91
|
+
"2012": (11, 0, 0),
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
for name, version in version_names.items():
|
|
95
|
+
if name in result:
|
|
96
|
+
return version
|
|
97
|
+
|
|
98
|
+
except exc.DBAPIError as e:
|
|
99
|
+
logger.warning(f"Failed to get SQL Server version: {e}")
|
|
100
|
+
|
|
101
|
+
# Default fallback
|
|
102
|
+
return (13, 0, 0)
|
|
103
|
+
|
|
104
|
+
def do_ping(self, dbapi_connection: Any) -> bool:
|
|
105
|
+
"""Check if SQL Server connection is alive."""
|
|
106
|
+
try:
|
|
107
|
+
cursor = dbapi_connection.cursor()
|
|
108
|
+
cursor.execute("SELECT 1")
|
|
109
|
+
cursor.close()
|
|
110
|
+
return True
|
|
111
|
+
except Exception as e:
|
|
112
|
+
logger.debug(f"SQL Server ping failed: {e}")
|
|
113
|
+
return False
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
# Export the dialect
|
|
117
|
+
dialect = MSSQLDialect
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MySQL and MariaDB JDBC dialects for SQLAlchemy.
|
|
3
|
+
|
|
4
|
+
Provides support for MySQL and MariaDB through JDBC.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
import re
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from sqlalchemy import exc, sql
|
|
14
|
+
from sqlalchemy.dialects.mysql.base import MySQLDialect as BaseMySQLDialect
|
|
15
|
+
from sqlalchemy.engine import Connection
|
|
16
|
+
|
|
17
|
+
from .base import BaseJDBCDialect, JDBCDriverConfig
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class MySQLDialect(BaseJDBCDialect, BaseMySQLDialect):
|
|
23
|
+
"""
|
|
24
|
+
MySQL dialect using JDBC driver.
|
|
25
|
+
|
|
26
|
+
Supports MySQL-specific features including:
|
|
27
|
+
- AUTO_INCREMENT
|
|
28
|
+
- Full-text indexes
|
|
29
|
+
- JSON columns (MySQL 5.7+)
|
|
30
|
+
- Spatial types
|
|
31
|
+
|
|
32
|
+
Connection URL format:
|
|
33
|
+
jdbcapi+mysql://user:password@host:3306/database
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
name = "mysql"
|
|
37
|
+
driver = "jdbcapi"
|
|
38
|
+
|
|
39
|
+
# MySQL capabilities
|
|
40
|
+
supports_native_boolean = False # MySQL uses TINYINT(1)
|
|
41
|
+
supports_native_enum = True
|
|
42
|
+
supports_sequences = False # MySQL < 8.0 doesn't support sequences
|
|
43
|
+
|
|
44
|
+
@classmethod
|
|
45
|
+
def get_driver_config(cls) -> JDBCDriverConfig:
|
|
46
|
+
"""Get MySQL JDBC driver configuration."""
|
|
47
|
+
return JDBCDriverConfig(
|
|
48
|
+
driver_class="com.mysql.cj.jdbc.Driver", # MySQL Connector/J 8.0+
|
|
49
|
+
jdbc_url_template="jdbc:mysql://{host}:{port}/{database}",
|
|
50
|
+
default_port=3306,
|
|
51
|
+
supports_transactions=True,
|
|
52
|
+
supports_schemas=True,
|
|
53
|
+
supports_sequences=False,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
def initialize(self, connection: Connection) -> None:
|
|
57
|
+
"""Initialize MySQL connection."""
|
|
58
|
+
super().initialize(connection)
|
|
59
|
+
logger.debug("Initialized MySQL JDBC dialect")
|
|
60
|
+
|
|
61
|
+
def _get_server_version_info(self, connection: Connection) -> tuple[int, ...]:
|
|
62
|
+
"""
|
|
63
|
+
Get MySQL server version.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
Tuple of version numbers (e.g., (8, 0, 32))
|
|
67
|
+
"""
|
|
68
|
+
try:
|
|
69
|
+
result = connection.execute(sql.text("SELECT VERSION()")).scalar()
|
|
70
|
+
|
|
71
|
+
if result:
|
|
72
|
+
# Parse version from string like:
|
|
73
|
+
# "8.0.32" or "5.7.40-log"
|
|
74
|
+
match = re.search(r"(\d+)\.(\d+)\.(\d+)", result)
|
|
75
|
+
if match:
|
|
76
|
+
major = int(match.group(1))
|
|
77
|
+
minor = int(match.group(2))
|
|
78
|
+
patch = int(match.group(3))
|
|
79
|
+
return (major, minor, patch)
|
|
80
|
+
|
|
81
|
+
except exc.DBAPIError as e:
|
|
82
|
+
logger.warning(f"Failed to get MySQL server version: {e}")
|
|
83
|
+
|
|
84
|
+
# Default fallback
|
|
85
|
+
return (5, 7, 0)
|
|
86
|
+
|
|
87
|
+
def do_ping(self, dbapi_connection: Any) -> bool:
|
|
88
|
+
"""Check if MySQL connection is alive."""
|
|
89
|
+
try:
|
|
90
|
+
cursor = dbapi_connection.cursor()
|
|
91
|
+
cursor.execute("SELECT 1")
|
|
92
|
+
cursor.close()
|
|
93
|
+
return True
|
|
94
|
+
except Exception as e:
|
|
95
|
+
logger.debug(f"MySQL ping failed: {e}")
|
|
96
|
+
return False
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class MariaDBDialect(MySQLDialect):
|
|
100
|
+
"""
|
|
101
|
+
MariaDB dialect using JDBC driver.
|
|
102
|
+
|
|
103
|
+
MariaDB is a MySQL fork with additional features.
|
|
104
|
+
This dialect extends MySQL with MariaDB-specific capabilities.
|
|
105
|
+
|
|
106
|
+
Connection URL format:
|
|
107
|
+
jdbcapi+mariadb://user:password@host:3306/database
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
name = "mariadb"
|
|
111
|
+
|
|
112
|
+
@classmethod
|
|
113
|
+
def get_driver_config(cls) -> JDBCDriverConfig:
|
|
114
|
+
"""Get MariaDB JDBC driver configuration."""
|
|
115
|
+
return JDBCDriverConfig(
|
|
116
|
+
driver_class="org.mariadb.jdbc.Driver",
|
|
117
|
+
jdbc_url_template="jdbc:mariadb://{host}:{port}/{database}",
|
|
118
|
+
default_port=3306,
|
|
119
|
+
supports_transactions=True,
|
|
120
|
+
supports_schemas=True,
|
|
121
|
+
supports_sequences=True, # MariaDB 10.3+ supports sequences
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
def _get_server_version_info(self, connection: Connection) -> tuple[int, ...]:
|
|
125
|
+
"""
|
|
126
|
+
Get MariaDB server version.
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
Tuple of version numbers (e.g., (10, 11, 2))
|
|
130
|
+
"""
|
|
131
|
+
try:
|
|
132
|
+
result = connection.execute(sql.text("SELECT VERSION()")).scalar()
|
|
133
|
+
|
|
134
|
+
if result:
|
|
135
|
+
# Parse version from string like:
|
|
136
|
+
# "10.11.2-MariaDB" or "10.6.12-MariaDB-log"
|
|
137
|
+
match = re.search(r"(\d+)\.(\d+)\.(\d+)", result)
|
|
138
|
+
if match:
|
|
139
|
+
major = int(match.group(1))
|
|
140
|
+
minor = int(match.group(2))
|
|
141
|
+
patch = int(match.group(3))
|
|
142
|
+
return (major, minor, patch)
|
|
143
|
+
|
|
144
|
+
except exc.DBAPIError as e:
|
|
145
|
+
logger.warning(f"Failed to get MariaDB server version: {e}")
|
|
146
|
+
|
|
147
|
+
# Default fallback
|
|
148
|
+
return (10, 6, 0)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
# Export dialects
|
|
152
|
+
dialect = MySQLDialect
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OceanBase JDBC dialect for SQLAlchemy.
|
|
3
|
+
|
|
4
|
+
OceanBase is an enterprise distributed relational database with Oracle compatibility mode.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
import re
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from sqlalchemy import TIMESTAMP, TypeDecorator, exc, sql, util
|
|
15
|
+
from sqlalchemy.dialects.oracle.base import OracleDialect
|
|
16
|
+
from sqlalchemy.engine import Connection
|
|
17
|
+
from sqlalchemy.engine.url import URL, make_url
|
|
18
|
+
|
|
19
|
+
from .base import BaseJDBCDialect, JDBCDriverConfig
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ObTimestamp(TypeDecorator):
|
|
25
|
+
"""
|
|
26
|
+
Custom timestamp type for OceanBase.
|
|
27
|
+
|
|
28
|
+
Handles conversion between Python datetime and OceanBase Timestamp.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
impl = TIMESTAMP
|
|
32
|
+
cache_ok = True
|
|
33
|
+
|
|
34
|
+
def process_bind_param(self, value: Any, dialect: Any) -> Any:
|
|
35
|
+
"""Convert Python datetime to OceanBase timestamp."""
|
|
36
|
+
if isinstance(value, datetime):
|
|
37
|
+
try:
|
|
38
|
+
# Try to use JPype to create Java Timestamp
|
|
39
|
+
import jpype
|
|
40
|
+
|
|
41
|
+
if jpype.isJVMStarted():
|
|
42
|
+
Timestamp = jpype.JClass("java.sql.Timestamp")
|
|
43
|
+
return Timestamp.valueOf(
|
|
44
|
+
value.strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
|
|
45
|
+
)
|
|
46
|
+
except Exception as e:
|
|
47
|
+
logger.debug(f"Failed to create Java Timestamp: {e}")
|
|
48
|
+
# Fall back to string representation
|
|
49
|
+
return value
|
|
50
|
+
return value
|
|
51
|
+
|
|
52
|
+
def process_result_value(self, value: Any, dialect: Any) -> datetime | None:
|
|
53
|
+
"""Convert OceanBase timestamp to Python datetime."""
|
|
54
|
+
if value is not None:
|
|
55
|
+
if isinstance(value, datetime):
|
|
56
|
+
return value
|
|
57
|
+
if isinstance(value, str):
|
|
58
|
+
# Parse string timestamp
|
|
59
|
+
try:
|
|
60
|
+
from dateutil import parser
|
|
61
|
+
|
|
62
|
+
return parser.parse(value)
|
|
63
|
+
except Exception as e:
|
|
64
|
+
logger.warning(f"Failed to parse timestamp: {e}")
|
|
65
|
+
return None
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class OceanBaseDialect(BaseJDBCDialect, OracleDialect): # type: ignore
|
|
70
|
+
"""
|
|
71
|
+
OceanBase dialect using JDBC driver (Oracle compatibility mode).
|
|
72
|
+
|
|
73
|
+
OceanBase is a distributed database that supports both MySQL and Oracle
|
|
74
|
+
compatibility modes. This dialect implements Oracle mode.
|
|
75
|
+
|
|
76
|
+
Connection URL format:
|
|
77
|
+
jdbcapi+oceanbase://user@tenant#cluster:password@host:2881/database
|
|
78
|
+
jdbcapi+oceanbasejdbc://user:password@host:2881/database # Alias
|
|
79
|
+
|
|
80
|
+
Note: OceanBase uses a special user format: username@tenant#cluster
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
name = "oceanbase"
|
|
84
|
+
driver = "jdbcapi"
|
|
85
|
+
|
|
86
|
+
# OceanBase capabilities
|
|
87
|
+
supports_native_decimal = True
|
|
88
|
+
supports_sane_rowcount = False
|
|
89
|
+
supports_sane_multi_rowcount = False
|
|
90
|
+
supports_unicode_binds = True
|
|
91
|
+
supports_statement_cache = True
|
|
92
|
+
supports_sequences = True
|
|
93
|
+
|
|
94
|
+
# Custom column specifications
|
|
95
|
+
colspecs = util.update_copy(
|
|
96
|
+
OracleDialect.colspecs, # type: ignore
|
|
97
|
+
{TIMESTAMP: ObTimestamp},
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
@classmethod
|
|
101
|
+
def get_driver_config(cls) -> JDBCDriverConfig:
|
|
102
|
+
"""Get OceanBase JDBC driver configuration."""
|
|
103
|
+
return JDBCDriverConfig(
|
|
104
|
+
driver_class="com.oceanbase.jdbc.Driver",
|
|
105
|
+
jdbc_url_template="jdbc:oceanbase://{host}:{port}/{database}",
|
|
106
|
+
default_port=2881,
|
|
107
|
+
supports_transactions=True,
|
|
108
|
+
supports_schemas=True,
|
|
109
|
+
supports_sequences=True,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
def create_connect_args(self, url: URL | str) -> tuple[list[Any], dict[str, Any]]:
|
|
113
|
+
"""
|
|
114
|
+
Create connection arguments for OceanBase.
|
|
115
|
+
|
|
116
|
+
Handles special OceanBase user format and connection properties.
|
|
117
|
+
"""
|
|
118
|
+
if isinstance(url, str):
|
|
119
|
+
url = make_url(url)
|
|
120
|
+
|
|
121
|
+
config = self.get_driver_config()
|
|
122
|
+
|
|
123
|
+
# Build JDBC URL
|
|
124
|
+
jdbc_url = config.format_jdbc_url(
|
|
125
|
+
host=url.host or "localhost",
|
|
126
|
+
port=url.port,
|
|
127
|
+
database=url.database,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
logger.debug(f"Creating OceanBase connection to: {jdbc_url}")
|
|
131
|
+
|
|
132
|
+
# Build connection properties
|
|
133
|
+
connect_args: dict[str, Any] = {}
|
|
134
|
+
|
|
135
|
+
if url.username:
|
|
136
|
+
connect_args["user"] = url.username
|
|
137
|
+
if url.password:
|
|
138
|
+
connect_args["password"] = url.password
|
|
139
|
+
|
|
140
|
+
# Add query parameters as connection properties
|
|
141
|
+
if url.query:
|
|
142
|
+
connect_args.update(url.query)
|
|
143
|
+
|
|
144
|
+
kwargs = {
|
|
145
|
+
"jclassname": config.driver_class,
|
|
146
|
+
"url": jdbc_url,
|
|
147
|
+
"driver_args": connect_args,
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return ([], kwargs)
|
|
151
|
+
|
|
152
|
+
def initialize(self, connection: Connection) -> None:
|
|
153
|
+
"""Initialize OceanBase connection."""
|
|
154
|
+
super().initialize(connection)
|
|
155
|
+
logger.debug("Initialized OceanBase JDBC dialect")
|
|
156
|
+
|
|
157
|
+
def _get_server_version_info(self, connection: Connection) -> tuple[int, ...]:
|
|
158
|
+
"""
|
|
159
|
+
Get OceanBase server version.
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
Tuple of version numbers (e.g., (4, 0, 0))
|
|
163
|
+
"""
|
|
164
|
+
try:
|
|
165
|
+
ver_sql = sql.text("SELECT BANNER FROM v$version")
|
|
166
|
+
banner = connection.execute(ver_sql).scalar()
|
|
167
|
+
|
|
168
|
+
if banner:
|
|
169
|
+
# Parse version from string like:
|
|
170
|
+
# "OceanBase 4.0.0.0 (r10100032022071511-36b8cb0cebc8c2662e8d0c252603c8f2281bb5cc)"
|
|
171
|
+
match = re.search(r"OceanBase ([\d+\.]+\d+)", banner)
|
|
172
|
+
if match:
|
|
173
|
+
version_str = match.group(1)
|
|
174
|
+
parts = version_str.split(".")
|
|
175
|
+
return tuple(int(p) for p in parts[:3])
|
|
176
|
+
|
|
177
|
+
except exc.DBAPIError as e:
|
|
178
|
+
logger.warning(f"Failed to get OceanBase server version: {e}")
|
|
179
|
+
|
|
180
|
+
# Default fallback
|
|
181
|
+
return (3, 0, 0)
|
|
182
|
+
|
|
183
|
+
@property
|
|
184
|
+
def _is_oracle_8(self) -> bool:
|
|
185
|
+
"""OceanBase is never Oracle 8."""
|
|
186
|
+
return False
|
|
187
|
+
|
|
188
|
+
def _check_max_identifier_length(self, connection: Connection) -> int | None:
|
|
189
|
+
"""OceanBase uses Oracle-compatible identifier lengths."""
|
|
190
|
+
return 128
|
|
191
|
+
|
|
192
|
+
def do_rollback(self, dbapi_connection: Any) -> None:
|
|
193
|
+
"""
|
|
194
|
+
Perform rollback on OceanBase connection.
|
|
195
|
+
|
|
196
|
+
OceanBase sometimes has issues with rollback, this provides
|
|
197
|
+
error handling.
|
|
198
|
+
"""
|
|
199
|
+
try:
|
|
200
|
+
if dbapi_connection and not dbapi_connection.closed:
|
|
201
|
+
dbapi_connection.rollback()
|
|
202
|
+
except Exception as e:
|
|
203
|
+
logger.warning(f"OceanBase rollback failed: {e}")
|
|
204
|
+
|
|
205
|
+
def do_ping(self, dbapi_connection: Any) -> bool:
|
|
206
|
+
"""Check if OceanBase connection is alive."""
|
|
207
|
+
try:
|
|
208
|
+
cursor = dbapi_connection.cursor()
|
|
209
|
+
cursor.execute("SELECT 1 FROM DUAL")
|
|
210
|
+
cursor.close()
|
|
211
|
+
return True
|
|
212
|
+
except Exception as e:
|
|
213
|
+
logger.debug(f"OceanBase ping failed: {e}")
|
|
214
|
+
return False
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
# Export the dialect
|
|
218
|
+
dialect = OceanBaseDialect
|