sqlspec 0.24.1__py3-none-any.whl → 0.26.0__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.
Potentially problematic release.
This version of sqlspec might be problematic. Click here for more details.
- sqlspec/_serialization.py +223 -21
- sqlspec/_sql.py +20 -62
- sqlspec/_typing.py +11 -0
- sqlspec/adapters/adbc/config.py +8 -1
- sqlspec/adapters/adbc/data_dictionary.py +290 -0
- sqlspec/adapters/adbc/driver.py +129 -20
- sqlspec/adapters/adbc/type_converter.py +159 -0
- sqlspec/adapters/aiosqlite/config.py +3 -0
- sqlspec/adapters/aiosqlite/data_dictionary.py +117 -0
- sqlspec/adapters/aiosqlite/driver.py +17 -3
- sqlspec/adapters/asyncmy/_types.py +1 -1
- sqlspec/adapters/asyncmy/config.py +11 -8
- sqlspec/adapters/asyncmy/data_dictionary.py +122 -0
- sqlspec/adapters/asyncmy/driver.py +31 -7
- sqlspec/adapters/asyncpg/config.py +3 -0
- sqlspec/adapters/asyncpg/data_dictionary.py +134 -0
- sqlspec/adapters/asyncpg/driver.py +19 -4
- sqlspec/adapters/bigquery/config.py +3 -0
- sqlspec/adapters/bigquery/data_dictionary.py +109 -0
- sqlspec/adapters/bigquery/driver.py +21 -3
- sqlspec/adapters/bigquery/type_converter.py +93 -0
- sqlspec/adapters/duckdb/_types.py +1 -1
- sqlspec/adapters/duckdb/config.py +2 -0
- sqlspec/adapters/duckdb/data_dictionary.py +124 -0
- sqlspec/adapters/duckdb/driver.py +32 -5
- sqlspec/adapters/duckdb/pool.py +1 -1
- sqlspec/adapters/duckdb/type_converter.py +103 -0
- sqlspec/adapters/oracledb/config.py +6 -0
- sqlspec/adapters/oracledb/data_dictionary.py +442 -0
- sqlspec/adapters/oracledb/driver.py +68 -9
- sqlspec/adapters/oracledb/migrations.py +51 -67
- sqlspec/adapters/oracledb/type_converter.py +132 -0
- sqlspec/adapters/psqlpy/config.py +3 -0
- sqlspec/adapters/psqlpy/data_dictionary.py +133 -0
- sqlspec/adapters/psqlpy/driver.py +23 -179
- sqlspec/adapters/psqlpy/type_converter.py +73 -0
- sqlspec/adapters/psycopg/config.py +8 -4
- sqlspec/adapters/psycopg/data_dictionary.py +257 -0
- sqlspec/adapters/psycopg/driver.py +40 -5
- sqlspec/adapters/sqlite/config.py +3 -0
- sqlspec/adapters/sqlite/data_dictionary.py +117 -0
- sqlspec/adapters/sqlite/driver.py +18 -3
- sqlspec/adapters/sqlite/pool.py +13 -4
- sqlspec/base.py +3 -4
- sqlspec/builder/_base.py +130 -48
- sqlspec/builder/_column.py +66 -24
- sqlspec/builder/_ddl.py +91 -41
- sqlspec/builder/_insert.py +40 -58
- sqlspec/builder/_parsing_utils.py +127 -12
- sqlspec/builder/_select.py +147 -2
- sqlspec/builder/_update.py +1 -1
- sqlspec/builder/mixins/_cte_and_set_ops.py +31 -23
- sqlspec/builder/mixins/_delete_operations.py +12 -7
- sqlspec/builder/mixins/_insert_operations.py +50 -36
- sqlspec/builder/mixins/_join_operations.py +15 -30
- sqlspec/builder/mixins/_merge_operations.py +210 -78
- sqlspec/builder/mixins/_order_limit_operations.py +4 -10
- sqlspec/builder/mixins/_pivot_operations.py +1 -0
- sqlspec/builder/mixins/_select_operations.py +44 -22
- sqlspec/builder/mixins/_update_operations.py +30 -37
- sqlspec/builder/mixins/_where_clause.py +52 -70
- sqlspec/cli.py +246 -140
- sqlspec/config.py +33 -19
- sqlspec/core/__init__.py +3 -2
- sqlspec/core/cache.py +298 -352
- sqlspec/core/compiler.py +61 -4
- sqlspec/core/filters.py +246 -213
- sqlspec/core/hashing.py +9 -11
- sqlspec/core/parameters.py +27 -10
- sqlspec/core/statement.py +72 -12
- sqlspec/core/type_conversion.py +234 -0
- sqlspec/driver/__init__.py +6 -3
- sqlspec/driver/_async.py +108 -5
- sqlspec/driver/_common.py +186 -17
- sqlspec/driver/_sync.py +108 -5
- sqlspec/driver/mixins/_result_tools.py +60 -7
- sqlspec/exceptions.py +5 -0
- sqlspec/loader.py +8 -9
- sqlspec/migrations/__init__.py +4 -3
- sqlspec/migrations/base.py +153 -14
- sqlspec/migrations/commands.py +34 -96
- sqlspec/migrations/context.py +145 -0
- sqlspec/migrations/loaders.py +25 -8
- sqlspec/migrations/runner.py +352 -82
- sqlspec/storage/backends/fsspec.py +1 -0
- sqlspec/typing.py +4 -0
- sqlspec/utils/config_resolver.py +153 -0
- sqlspec/utils/serializers.py +50 -2
- {sqlspec-0.24.1.dist-info → sqlspec-0.26.0.dist-info}/METADATA +1 -1
- sqlspec-0.26.0.dist-info/RECORD +157 -0
- sqlspec-0.24.1.dist-info/RECORD +0 -139
- {sqlspec-0.24.1.dist-info → sqlspec-0.26.0.dist-info}/WHEEL +0 -0
- {sqlspec-0.24.1.dist-info → sqlspec-0.26.0.dist-info}/entry_points.txt +0 -0
- {sqlspec-0.24.1.dist-info → sqlspec-0.26.0.dist-info}/licenses/LICENSE +0 -0
- {sqlspec-0.24.1.dist-info → sqlspec-0.26.0.dist-info}/licenses/NOTICE +0 -0
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
"""DuckDB driver implementation."""
|
|
2
2
|
|
|
3
|
+
import datetime
|
|
4
|
+
from decimal import Decimal
|
|
3
5
|
from typing import TYPE_CHECKING, Any, Final, Optional
|
|
4
6
|
|
|
5
|
-
import duckdb
|
|
7
|
+
import duckdb # type: ignore[import-untyped]
|
|
6
8
|
from sqlglot import exp
|
|
7
9
|
|
|
10
|
+
from sqlspec.adapters.duckdb.data_dictionary import DuckDBSyncDataDictionary
|
|
11
|
+
from sqlspec.adapters.duckdb.type_converter import DuckDBTypeConverter
|
|
8
12
|
from sqlspec.core.cache import get_cache_config
|
|
9
13
|
from sqlspec.core.parameters import ParameterStyle, ParameterStyleConfig
|
|
10
14
|
from sqlspec.core.statement import SQL, StatementConfig
|
|
11
15
|
from sqlspec.driver import SyncDriverAdapterBase
|
|
12
16
|
from sqlspec.exceptions import SQLParsingError, SQLSpecError
|
|
13
17
|
from sqlspec.utils.logging import get_logger
|
|
18
|
+
from sqlspec.utils.serializers import to_json
|
|
14
19
|
|
|
15
20
|
if TYPE_CHECKING:
|
|
16
21
|
from contextlib import AbstractContextManager
|
|
@@ -18,11 +23,14 @@ if TYPE_CHECKING:
|
|
|
18
23
|
from sqlspec.adapters.duckdb._types import DuckDBConnection
|
|
19
24
|
from sqlspec.core.result import SQLResult
|
|
20
25
|
from sqlspec.driver import ExecutionResult
|
|
26
|
+
from sqlspec.driver._sync import SyncDataDictionaryBase
|
|
21
27
|
|
|
22
28
|
__all__ = ("DuckDBCursor", "DuckDBDriver", "DuckDBExceptionHandler", "duckdb_statement_config")
|
|
23
29
|
|
|
24
30
|
logger = get_logger("adapters.duckdb")
|
|
25
31
|
|
|
32
|
+
_type_converter = DuckDBTypeConverter()
|
|
33
|
+
|
|
26
34
|
|
|
27
35
|
duckdb_statement_config = StatementConfig(
|
|
28
36
|
dialect="duckdb",
|
|
@@ -31,7 +39,15 @@ duckdb_statement_config = StatementConfig(
|
|
|
31
39
|
supported_parameter_styles={ParameterStyle.QMARK, ParameterStyle.NUMERIC, ParameterStyle.NAMED_DOLLAR},
|
|
32
40
|
default_execution_parameter_style=ParameterStyle.QMARK,
|
|
33
41
|
supported_execution_parameter_styles={ParameterStyle.QMARK, ParameterStyle.NUMERIC},
|
|
34
|
-
type_coercion_map={
|
|
42
|
+
type_coercion_map={
|
|
43
|
+
bool: int,
|
|
44
|
+
datetime.datetime: lambda v: v.isoformat(),
|
|
45
|
+
datetime.date: lambda v: v.isoformat(),
|
|
46
|
+
Decimal: str,
|
|
47
|
+
dict: to_json,
|
|
48
|
+
list: to_json,
|
|
49
|
+
str: _type_converter.convert_if_detected,
|
|
50
|
+
},
|
|
35
51
|
has_native_list_expansion=True,
|
|
36
52
|
needs_static_script_compilation=False,
|
|
37
53
|
preserve_parameter_format=True,
|
|
@@ -60,8 +76,7 @@ class DuckDBCursor:
|
|
|
60
76
|
self.cursor = self.connection.cursor()
|
|
61
77
|
return self.cursor
|
|
62
78
|
|
|
63
|
-
def __exit__(self,
|
|
64
|
-
_ = (exc_type, exc_val, exc_tb)
|
|
79
|
+
def __exit__(self, *_: Any) -> None:
|
|
65
80
|
if self.cursor is not None:
|
|
66
81
|
self.cursor.close()
|
|
67
82
|
|
|
@@ -127,7 +142,7 @@ class DuckDBDriver(SyncDriverAdapterBase):
|
|
|
127
142
|
the sqlspec.core modules for statement processing and caching.
|
|
128
143
|
"""
|
|
129
144
|
|
|
130
|
-
__slots__ = ()
|
|
145
|
+
__slots__ = ("_data_dictionary",)
|
|
131
146
|
dialect = "duckdb"
|
|
132
147
|
|
|
133
148
|
def __init__(
|
|
@@ -147,6 +162,7 @@ class DuckDBDriver(SyncDriverAdapterBase):
|
|
|
147
162
|
statement_config = updated_config
|
|
148
163
|
|
|
149
164
|
super().__init__(connection=connection, statement_config=statement_config, driver_features=driver_features)
|
|
165
|
+
self._data_dictionary: Optional[SyncDataDictionaryBase] = None
|
|
150
166
|
|
|
151
167
|
def with_cursor(self, connection: "DuckDBConnection") -> "DuckDBCursor":
|
|
152
168
|
"""Create context manager for DuckDB cursor.
|
|
@@ -325,3 +341,14 @@ class DuckDBDriver(SyncDriverAdapterBase):
|
|
|
325
341
|
except duckdb.Error as e:
|
|
326
342
|
msg = f"Failed to commit DuckDB transaction: {e}"
|
|
327
343
|
raise SQLSpecError(msg) from e
|
|
344
|
+
|
|
345
|
+
@property
|
|
346
|
+
def data_dictionary(self) -> "SyncDataDictionaryBase":
|
|
347
|
+
"""Get the data dictionary for this driver.
|
|
348
|
+
|
|
349
|
+
Returns:
|
|
350
|
+
Data dictionary instance for metadata queries
|
|
351
|
+
"""
|
|
352
|
+
if self._data_dictionary is None:
|
|
353
|
+
self._data_dictionary = DuckDBSyncDataDictionary()
|
|
354
|
+
return self._data_dictionary
|
sqlspec/adapters/duckdb/pool.py
CHANGED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""DuckDB-specific type conversion with native UUID support.
|
|
2
|
+
|
|
3
|
+
Provides specialized type handling for DuckDB, including native UUID
|
|
4
|
+
support and standardized datetime formatting.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from typing import Any
|
|
9
|
+
from uuid import UUID
|
|
10
|
+
|
|
11
|
+
from sqlspec.core.type_conversion import BaseTypeConverter, convert_uuid, format_datetime_rfc3339
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class DuckDBTypeConverter(BaseTypeConverter):
|
|
15
|
+
"""DuckDB-specific type conversion with native UUID support.
|
|
16
|
+
|
|
17
|
+
Extends the base TypeDetector with DuckDB-specific functionality
|
|
18
|
+
including native UUID handling and standardized datetime formatting.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
__slots__ = ()
|
|
22
|
+
|
|
23
|
+
def handle_uuid(self, value: Any) -> Any:
|
|
24
|
+
"""Handle UUID conversion for DuckDB.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
value: Value that might be a UUID.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
UUID object if value is UUID-like, original value otherwise.
|
|
31
|
+
"""
|
|
32
|
+
if isinstance(value, UUID):
|
|
33
|
+
return value # DuckDB supports UUID natively
|
|
34
|
+
|
|
35
|
+
if isinstance(value, str):
|
|
36
|
+
detected_type = self.detect_type(value)
|
|
37
|
+
if detected_type == "uuid":
|
|
38
|
+
return convert_uuid(value)
|
|
39
|
+
|
|
40
|
+
return value
|
|
41
|
+
|
|
42
|
+
def format_datetime(self, dt: datetime) -> str:
|
|
43
|
+
"""Standardized datetime formatting for DuckDB.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
dt: datetime object to format.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
RFC 3339 formatted datetime string.
|
|
50
|
+
"""
|
|
51
|
+
return format_datetime_rfc3339(dt)
|
|
52
|
+
|
|
53
|
+
def convert_duckdb_value(self, value: Any) -> Any:
|
|
54
|
+
"""Convert value with DuckDB-specific handling.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
value: Value to convert.
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
Converted value appropriate for DuckDB.
|
|
61
|
+
"""
|
|
62
|
+
# Handle UUIDs
|
|
63
|
+
if isinstance(value, (str, UUID)):
|
|
64
|
+
uuid_value = self.handle_uuid(value)
|
|
65
|
+
if isinstance(uuid_value, UUID):
|
|
66
|
+
return uuid_value
|
|
67
|
+
|
|
68
|
+
# Handle other string types
|
|
69
|
+
if isinstance(value, str):
|
|
70
|
+
detected_type = self.detect_type(value)
|
|
71
|
+
if detected_type:
|
|
72
|
+
try:
|
|
73
|
+
return self.convert_value(value, detected_type)
|
|
74
|
+
except Exception:
|
|
75
|
+
# If conversion fails, return original value
|
|
76
|
+
return value
|
|
77
|
+
|
|
78
|
+
# Handle datetime formatting
|
|
79
|
+
if isinstance(value, datetime):
|
|
80
|
+
return self.format_datetime(value)
|
|
81
|
+
|
|
82
|
+
return value
|
|
83
|
+
|
|
84
|
+
def prepare_duckdb_parameter(self, value: Any) -> Any:
|
|
85
|
+
"""Prepare parameter for DuckDB execution.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
value: Parameter value to prepare.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
Value ready for DuckDB parameter binding.
|
|
92
|
+
"""
|
|
93
|
+
# DuckDB can handle most Python types natively
|
|
94
|
+
converted = self.convert_duckdb_value(value)
|
|
95
|
+
|
|
96
|
+
# Ensure UUIDs are properly handled
|
|
97
|
+
if isinstance(converted, UUID):
|
|
98
|
+
return converted # DuckDB native UUID support
|
|
99
|
+
|
|
100
|
+
return converted
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
__all__ = ("DuckDBTypeConverter",)
|
|
@@ -94,6 +94,7 @@ class OracleSyncConfig(SyncDatabaseConfig[OracleSyncConnection, "OracleSyncConne
|
|
|
94
94
|
migration_config: Optional[dict[str, Any]] = None,
|
|
95
95
|
statement_config: "Optional[StatementConfig]" = None,
|
|
96
96
|
driver_features: "Optional[dict[str, Any]]" = None,
|
|
97
|
+
bind_key: "Optional[str]" = None,
|
|
97
98
|
) -> None:
|
|
98
99
|
"""Initialize Oracle synchronous configuration.
|
|
99
100
|
|
|
@@ -103,6 +104,7 @@ class OracleSyncConfig(SyncDatabaseConfig[OracleSyncConnection, "OracleSyncConne
|
|
|
103
104
|
migration_config: Migration configuration
|
|
104
105
|
statement_config: Default SQL statement configuration
|
|
105
106
|
driver_features: Optional driver feature configuration
|
|
107
|
+
bind_key: Optional unique identifier for this configuration
|
|
106
108
|
"""
|
|
107
109
|
|
|
108
110
|
processed_pool_config: dict[str, Any] = dict(pool_config) if pool_config else {}
|
|
@@ -116,6 +118,7 @@ class OracleSyncConfig(SyncDatabaseConfig[OracleSyncConnection, "OracleSyncConne
|
|
|
116
118
|
migration_config=migration_config,
|
|
117
119
|
statement_config=statement_config,
|
|
118
120
|
driver_features=driver_features or {},
|
|
121
|
+
bind_key=bind_key,
|
|
119
122
|
)
|
|
120
123
|
|
|
121
124
|
def _create_pool(self) -> "OracleSyncConnectionPool":
|
|
@@ -220,6 +223,7 @@ class OracleAsyncConfig(AsyncDatabaseConfig[OracleAsyncConnection, "OracleAsyncC
|
|
|
220
223
|
migration_config: Optional[dict[str, Any]] = None,
|
|
221
224
|
statement_config: "Optional[StatementConfig]" = None,
|
|
222
225
|
driver_features: "Optional[dict[str, Any]]" = None,
|
|
226
|
+
bind_key: "Optional[str]" = None,
|
|
223
227
|
) -> None:
|
|
224
228
|
"""Initialize Oracle asynchronous configuration.
|
|
225
229
|
|
|
@@ -229,6 +233,7 @@ class OracleAsyncConfig(AsyncDatabaseConfig[OracleAsyncConnection, "OracleAsyncC
|
|
|
229
233
|
migration_config: Migration configuration
|
|
230
234
|
statement_config: Default SQL statement configuration
|
|
231
235
|
driver_features: Optional driver feature configuration
|
|
236
|
+
bind_key: Optional unique identifier for this configuration
|
|
232
237
|
"""
|
|
233
238
|
|
|
234
239
|
processed_pool_config: dict[str, Any] = dict(pool_config) if pool_config else {}
|
|
@@ -242,6 +247,7 @@ class OracleAsyncConfig(AsyncDatabaseConfig[OracleAsyncConnection, "OracleAsyncC
|
|
|
242
247
|
migration_config=migration_config,
|
|
243
248
|
statement_config=statement_config or oracledb_statement_config,
|
|
244
249
|
driver_features=driver_features or {},
|
|
250
|
+
bind_key=bind_key,
|
|
245
251
|
)
|
|
246
252
|
|
|
247
253
|
async def _create_pool(self) -> "OracleAsyncConnectionPool":
|
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
"""Oracle-specific data dictionary for metadata queries."""
|
|
2
|
+
# cspell:ignore pdbs
|
|
3
|
+
|
|
4
|
+
import re
|
|
5
|
+
from contextlib import suppress
|
|
6
|
+
from typing import TYPE_CHECKING, Callable, Optional, cast
|
|
7
|
+
|
|
8
|
+
from sqlspec.driver import (
|
|
9
|
+
AsyncDataDictionaryBase,
|
|
10
|
+
AsyncDriverAdapterBase,
|
|
11
|
+
SyncDataDictionaryBase,
|
|
12
|
+
SyncDriverAdapterBase,
|
|
13
|
+
VersionInfo,
|
|
14
|
+
)
|
|
15
|
+
from sqlspec.utils.logging import get_logger
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from sqlspec.adapters.oracledb.driver import OracleAsyncDriver, OracleSyncDriver
|
|
19
|
+
|
|
20
|
+
logger = get_logger("adapters.oracledb.data_dictionary")
|
|
21
|
+
|
|
22
|
+
# Oracle version constants
|
|
23
|
+
ORACLE_MIN_JSON_NATIVE_VERSION = 21
|
|
24
|
+
ORACLE_MIN_JSON_NATIVE_COMPATIBLE = 20
|
|
25
|
+
ORACLE_MIN_JSON_BLOB_VERSION = 12
|
|
26
|
+
ORACLE_MIN_OSON_VERSION = 19
|
|
27
|
+
|
|
28
|
+
# Compiled regex patterns
|
|
29
|
+
ORACLE_VERSION_PATTERN = re.compile(r"Oracle Database (\d+)c?.* Release (\d+)\.(\d+)\.(\d+)")
|
|
30
|
+
|
|
31
|
+
__all__ = ("OracleAsyncDataDictionary", "OracleSyncDataDictionary", "OracleVersionInfo")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class OracleVersionInfo(VersionInfo):
|
|
35
|
+
"""Oracle database version information."""
|
|
36
|
+
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
major: int,
|
|
40
|
+
minor: int = 0,
|
|
41
|
+
patch: int = 0,
|
|
42
|
+
compatible: "Optional[str]" = None,
|
|
43
|
+
is_autonomous: bool = False,
|
|
44
|
+
) -> None:
|
|
45
|
+
"""Initialize Oracle version info.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
major: Major version number (e.g., 19, 21, 23)
|
|
49
|
+
minor: Minor version number
|
|
50
|
+
patch: Patch version number
|
|
51
|
+
compatible: Compatible parameter value
|
|
52
|
+
is_autonomous: Whether this is an Autonomous Database
|
|
53
|
+
"""
|
|
54
|
+
super().__init__(major, minor, patch)
|
|
55
|
+
self.compatible = compatible
|
|
56
|
+
self.is_autonomous = is_autonomous
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def compatible_major(self) -> "Optional[int]":
|
|
60
|
+
"""Get major version from compatible parameter."""
|
|
61
|
+
if not self.compatible:
|
|
62
|
+
return None
|
|
63
|
+
parts = self.compatible.split(".")
|
|
64
|
+
if not parts:
|
|
65
|
+
return None
|
|
66
|
+
return int(parts[0])
|
|
67
|
+
|
|
68
|
+
def supports_native_json(self) -> bool:
|
|
69
|
+
"""Check if database supports native JSON data type.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
True if Oracle 21c+ with compatible >= 20
|
|
73
|
+
"""
|
|
74
|
+
return (
|
|
75
|
+
self.major >= ORACLE_MIN_JSON_NATIVE_VERSION
|
|
76
|
+
and (self.compatible_major or 0) >= ORACLE_MIN_JSON_NATIVE_COMPATIBLE
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
def supports_oson_blob(self) -> bool:
|
|
80
|
+
"""Check if database supports BLOB with OSON format.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
True if Oracle 19c+ (Autonomous) or 21c+
|
|
84
|
+
"""
|
|
85
|
+
if self.major >= ORACLE_MIN_JSON_NATIVE_VERSION:
|
|
86
|
+
return True
|
|
87
|
+
return self.major >= ORACLE_MIN_OSON_VERSION and self.is_autonomous
|
|
88
|
+
|
|
89
|
+
def supports_json_blob(self) -> bool:
|
|
90
|
+
"""Check if database supports BLOB with JSON validation.
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
True if Oracle 12c+
|
|
94
|
+
"""
|
|
95
|
+
return self.major >= ORACLE_MIN_JSON_BLOB_VERSION
|
|
96
|
+
|
|
97
|
+
def __str__(self) -> str:
|
|
98
|
+
"""String representation of version info."""
|
|
99
|
+
version_str = f"{self.major}.{self.minor}.{self.patch}"
|
|
100
|
+
if self.compatible:
|
|
101
|
+
version_str += f" (compatible={self.compatible})"
|
|
102
|
+
if self.is_autonomous:
|
|
103
|
+
version_str += " [Autonomous]"
|
|
104
|
+
return version_str
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class OracleDataDictionaryMixin:
|
|
108
|
+
"""Mixin providing Oracle-specific metadata queries."""
|
|
109
|
+
|
|
110
|
+
__slots__ = ()
|
|
111
|
+
|
|
112
|
+
def _get_oracle_version(self, driver: "OracleAsyncDriver | OracleSyncDriver") -> "Optional[OracleVersionInfo]":
|
|
113
|
+
"""Get Oracle database version information.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
driver: Database driver instance
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
Oracle version information or None if detection fails
|
|
120
|
+
"""
|
|
121
|
+
banner = driver.select_value("SELECT banner FROM v$version WHERE banner LIKE 'Oracle%'")
|
|
122
|
+
|
|
123
|
+
# Parse version from banner like "Oracle Database 21c Enterprise Edition Release 21.0.0.0.0 - Production"
|
|
124
|
+
# or "Oracle Database 19c Standard Edition 2 Release 19.0.0.0.0 - Production"
|
|
125
|
+
version_match = ORACLE_VERSION_PATTERN.search(str(banner))
|
|
126
|
+
|
|
127
|
+
if not version_match:
|
|
128
|
+
logger.warning("Could not parse Oracle version from banner: %s", banner)
|
|
129
|
+
return None
|
|
130
|
+
|
|
131
|
+
major = int(version_match.group(1))
|
|
132
|
+
release_major = int(version_match.group(2))
|
|
133
|
+
minor = int(version_match.group(3))
|
|
134
|
+
patch = int(version_match.group(4))
|
|
135
|
+
|
|
136
|
+
# For Oracle 21c+, the major version is in the first group
|
|
137
|
+
# For Oracle 19c and earlier, use the release version
|
|
138
|
+
if major >= ORACLE_MIN_JSON_NATIVE_VERSION:
|
|
139
|
+
version_info = OracleVersionInfo(major, minor, patch)
|
|
140
|
+
else:
|
|
141
|
+
version_info = OracleVersionInfo(release_major, minor, patch)
|
|
142
|
+
|
|
143
|
+
logger.debug("Detected Oracle version: %s", version_info)
|
|
144
|
+
return version_info
|
|
145
|
+
|
|
146
|
+
def _get_oracle_compatible(self, driver: "OracleAsyncDriver | OracleSyncDriver") -> "Optional[str]":
|
|
147
|
+
"""Get Oracle compatible parameter value.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
driver: Database driver instance
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
Compatible parameter value or None if detection fails
|
|
154
|
+
"""
|
|
155
|
+
try:
|
|
156
|
+
compatible = driver.select_value("SELECT value FROM v$parameter WHERE name = 'compatible'")
|
|
157
|
+
logger.debug("Detected Oracle compatible parameter: %s", compatible)
|
|
158
|
+
return str(compatible)
|
|
159
|
+
except Exception:
|
|
160
|
+
logger.warning("Compatible parameter not found")
|
|
161
|
+
return None
|
|
162
|
+
|
|
163
|
+
def _get_oracle_json_type(self, version_info: "Optional[OracleVersionInfo]") -> str:
|
|
164
|
+
"""Determine the appropriate JSON column type for Oracle.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
version_info: Oracle version information
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
Appropriate Oracle column type for JSON data
|
|
171
|
+
"""
|
|
172
|
+
if not version_info:
|
|
173
|
+
logger.warning("No version info provided, using CLOB fallback")
|
|
174
|
+
return "CLOB"
|
|
175
|
+
|
|
176
|
+
# Decision matrix for JSON column type
|
|
177
|
+
if version_info.supports_native_json():
|
|
178
|
+
logger.info("Using native JSON type for Oracle %s", version_info)
|
|
179
|
+
return "JSON"
|
|
180
|
+
if version_info.supports_oson_blob():
|
|
181
|
+
logger.info("Using BLOB with OSON format for Oracle %s", version_info)
|
|
182
|
+
return "BLOB CHECK (data IS JSON FORMAT OSON)"
|
|
183
|
+
if version_info.supports_json_blob():
|
|
184
|
+
logger.info("Using BLOB with JSON validation for Oracle %s", version_info)
|
|
185
|
+
return "BLOB CHECK (data IS JSON)"
|
|
186
|
+
logger.info("Using CLOB fallback for Oracle %s", version_info)
|
|
187
|
+
return "CLOB"
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
class OracleSyncDataDictionary(OracleDataDictionaryMixin, SyncDataDictionaryBase):
|
|
191
|
+
"""Oracle-specific sync data dictionary."""
|
|
192
|
+
|
|
193
|
+
def _is_oracle_autonomous(self, driver: "OracleSyncDriver") -> bool:
|
|
194
|
+
"""Check if this is an Oracle Autonomous Database.
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
driver: Database driver instance
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
True if this is an Autonomous Database, False otherwise
|
|
201
|
+
"""
|
|
202
|
+
result = driver.select_value_or_none("SELECT COUNT(*) as cnt FROM v$pdbs WHERE cloud_identity IS NOT NULL")
|
|
203
|
+
return bool(result and int(result) > 0)
|
|
204
|
+
|
|
205
|
+
def get_version(self, driver: SyncDriverAdapterBase) -> "Optional[OracleVersionInfo]":
|
|
206
|
+
"""Get Oracle database version information.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
driver: Database driver instance
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
Oracle version information or None if detection fails
|
|
213
|
+
"""
|
|
214
|
+
oracle_driver = cast("OracleSyncDriver", driver)
|
|
215
|
+
version_info = self._get_oracle_version(oracle_driver)
|
|
216
|
+
if version_info:
|
|
217
|
+
# Enhance with additional information
|
|
218
|
+
compatible = self._get_oracle_compatible(oracle_driver)
|
|
219
|
+
is_autonomous = self._is_oracle_autonomous(oracle_driver)
|
|
220
|
+
|
|
221
|
+
version_info.compatible = compatible
|
|
222
|
+
version_info.is_autonomous = is_autonomous
|
|
223
|
+
|
|
224
|
+
return version_info
|
|
225
|
+
|
|
226
|
+
def get_feature_flag(self, driver: SyncDriverAdapterBase, feature: str) -> bool:
|
|
227
|
+
"""Check if Oracle database supports a specific feature.
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
driver: Database driver instance
|
|
231
|
+
feature: Feature name to check
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
True if feature is supported, False otherwise
|
|
235
|
+
"""
|
|
236
|
+
if feature == "is_autonomous":
|
|
237
|
+
return self._is_oracle_autonomous(cast("OracleSyncDriver", driver))
|
|
238
|
+
|
|
239
|
+
version_info = self.get_version(driver)
|
|
240
|
+
if not version_info:
|
|
241
|
+
return False
|
|
242
|
+
|
|
243
|
+
feature_checks: dict[str, Callable[..., bool]] = {
|
|
244
|
+
"supports_native_json": version_info.supports_native_json,
|
|
245
|
+
"supports_oson_blob": version_info.supports_oson_blob,
|
|
246
|
+
"supports_json_blob": version_info.supports_json_blob,
|
|
247
|
+
"supports_json": version_info.supports_json_blob, # Any JSON support
|
|
248
|
+
"supports_transactions": lambda: True,
|
|
249
|
+
"supports_prepared_statements": lambda: True,
|
|
250
|
+
"supports_schemas": lambda: True,
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if feature in feature_checks:
|
|
254
|
+
return bool(feature_checks[feature]())
|
|
255
|
+
|
|
256
|
+
return False
|
|
257
|
+
|
|
258
|
+
def get_optimal_type(self, driver: SyncDriverAdapterBase, type_category: str) -> str:
|
|
259
|
+
"""Get optimal Oracle type for a category.
|
|
260
|
+
|
|
261
|
+
Args:
|
|
262
|
+
driver: Database driver instance
|
|
263
|
+
type_category: Type category
|
|
264
|
+
|
|
265
|
+
Returns:
|
|
266
|
+
Oracle-specific type name
|
|
267
|
+
"""
|
|
268
|
+
type_map = {
|
|
269
|
+
"json": self._get_oracle_json_type(self.get_version(driver)),
|
|
270
|
+
"uuid": "RAW(16)",
|
|
271
|
+
"boolean": "NUMBER(1)",
|
|
272
|
+
"timestamp": "TIMESTAMP",
|
|
273
|
+
"text": "CLOB",
|
|
274
|
+
"blob": "BLOB",
|
|
275
|
+
}
|
|
276
|
+
return type_map.get(type_category, "VARCHAR2(255)")
|
|
277
|
+
|
|
278
|
+
def list_available_features(self) -> "list[str]":
|
|
279
|
+
"""List available Oracle feature flags.
|
|
280
|
+
|
|
281
|
+
Returns:
|
|
282
|
+
List of supported feature names
|
|
283
|
+
"""
|
|
284
|
+
return [
|
|
285
|
+
"is_autonomous",
|
|
286
|
+
"supports_native_json",
|
|
287
|
+
"supports_oson_blob",
|
|
288
|
+
"supports_json_blob",
|
|
289
|
+
"supports_json",
|
|
290
|
+
"supports_transactions",
|
|
291
|
+
"supports_prepared_statements",
|
|
292
|
+
"supports_schemas",
|
|
293
|
+
]
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
class OracleAsyncDataDictionary(OracleDataDictionaryMixin, AsyncDataDictionaryBase):
|
|
297
|
+
"""Oracle-specific async data dictionary."""
|
|
298
|
+
|
|
299
|
+
async def get_version(self, driver: AsyncDriverAdapterBase) -> "Optional[OracleVersionInfo]":
|
|
300
|
+
"""Get Oracle database version information.
|
|
301
|
+
|
|
302
|
+
Args:
|
|
303
|
+
driver: Async database driver instance
|
|
304
|
+
|
|
305
|
+
Returns:
|
|
306
|
+
Oracle version information or None if detection fails
|
|
307
|
+
"""
|
|
308
|
+
banner = await cast("OracleAsyncDriver", driver).select_value(
|
|
309
|
+
"SELECT banner FROM v$version WHERE banner LIKE 'Oracle%'"
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
version_match = ORACLE_VERSION_PATTERN.search(str(banner))
|
|
313
|
+
|
|
314
|
+
if not version_match:
|
|
315
|
+
logger.warning("Could not parse Oracle version from banner: %s", banner)
|
|
316
|
+
return None
|
|
317
|
+
|
|
318
|
+
major = int(version_match.group(1))
|
|
319
|
+
release_major = int(version_match.group(2))
|
|
320
|
+
minor = int(version_match.group(3))
|
|
321
|
+
patch = int(version_match.group(4))
|
|
322
|
+
|
|
323
|
+
if major >= ORACLE_MIN_JSON_NATIVE_VERSION:
|
|
324
|
+
version_info = OracleVersionInfo(major, minor, patch)
|
|
325
|
+
else:
|
|
326
|
+
version_info = OracleVersionInfo(release_major, minor, patch)
|
|
327
|
+
|
|
328
|
+
# Enhance with additional information
|
|
329
|
+
oracle_driver = cast("OracleAsyncDriver", driver)
|
|
330
|
+
compatible = await self._get_oracle_compatible_async(oracle_driver)
|
|
331
|
+
is_autonomous = await self._is_oracle_autonomous_async(oracle_driver)
|
|
332
|
+
|
|
333
|
+
version_info.compatible = compatible
|
|
334
|
+
version_info.is_autonomous = is_autonomous
|
|
335
|
+
|
|
336
|
+
logger.debug("Detected Oracle version: %s", version_info)
|
|
337
|
+
return version_info
|
|
338
|
+
|
|
339
|
+
async def _get_oracle_compatible_async(self, driver: "OracleAsyncDriver") -> "Optional[str]":
|
|
340
|
+
"""Get Oracle compatible parameter value (async version).
|
|
341
|
+
|
|
342
|
+
Args:
|
|
343
|
+
driver: Async database driver instance
|
|
344
|
+
|
|
345
|
+
Returns:
|
|
346
|
+
Compatible parameter value or None if detection fails
|
|
347
|
+
"""
|
|
348
|
+
try:
|
|
349
|
+
compatible = await driver.select_value("SELECT value FROM v$parameter WHERE name = 'compatible'")
|
|
350
|
+
logger.debug("Detected Oracle compatible parameter: %s", compatible)
|
|
351
|
+
return str(compatible)
|
|
352
|
+
except Exception:
|
|
353
|
+
logger.warning("Compatible parameter not found")
|
|
354
|
+
return None
|
|
355
|
+
|
|
356
|
+
async def _is_oracle_autonomous_async(self, driver: "OracleAsyncDriver") -> bool:
|
|
357
|
+
"""Check if this is an Oracle Autonomous Database (async version).
|
|
358
|
+
|
|
359
|
+
Args:
|
|
360
|
+
driver: Async database driver instance
|
|
361
|
+
|
|
362
|
+
Returns:
|
|
363
|
+
True if this is an Autonomous Database, False otherwise
|
|
364
|
+
"""
|
|
365
|
+
# Check for cloud_identity in v$pdbs (most reliable for Autonomous)
|
|
366
|
+
with suppress(Exception):
|
|
367
|
+
result = await driver.execute("SELECT COUNT(*) as cnt FROM v$pdbs WHERE cloud_identity IS NOT NULL")
|
|
368
|
+
if result.data:
|
|
369
|
+
count = result.data[0]["cnt"] if isinstance(result.data[0], dict) else result.data[0][0]
|
|
370
|
+
if int(count) > 0:
|
|
371
|
+
logger.debug("Detected Oracle Autonomous Database via v$pdbs")
|
|
372
|
+
return True
|
|
373
|
+
|
|
374
|
+
logger.debug("Oracle Autonomous Database not detected")
|
|
375
|
+
return False
|
|
376
|
+
|
|
377
|
+
async def get_feature_flag(self, driver: AsyncDriverAdapterBase, feature: str) -> bool:
|
|
378
|
+
"""Check if Oracle database supports a specific feature.
|
|
379
|
+
|
|
380
|
+
Args:
|
|
381
|
+
driver: Async database driver instance
|
|
382
|
+
feature: Feature name to check
|
|
383
|
+
|
|
384
|
+
Returns:
|
|
385
|
+
True if feature is supported, False otherwise
|
|
386
|
+
"""
|
|
387
|
+
if feature == "is_autonomous":
|
|
388
|
+
return await self._is_oracle_autonomous_async(cast("OracleAsyncDriver", driver))
|
|
389
|
+
|
|
390
|
+
version_info = await self.get_version(driver)
|
|
391
|
+
if not version_info:
|
|
392
|
+
return False
|
|
393
|
+
|
|
394
|
+
feature_checks: dict[str, Callable[..., bool]] = {
|
|
395
|
+
"supports_native_json": version_info.supports_native_json,
|
|
396
|
+
"supports_oson_blob": version_info.supports_oson_blob,
|
|
397
|
+
"supports_json_blob": version_info.supports_json_blob,
|
|
398
|
+
"supports_json": version_info.supports_json_blob, # Any JSON support
|
|
399
|
+
"supports_transactions": lambda: True,
|
|
400
|
+
"supports_prepared_statements": lambda: True,
|
|
401
|
+
"supports_schemas": lambda: True,
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if feature in feature_checks:
|
|
405
|
+
return bool(feature_checks[feature]())
|
|
406
|
+
|
|
407
|
+
return False
|
|
408
|
+
|
|
409
|
+
async def get_optimal_type(self, driver: AsyncDriverAdapterBase, type_category: str) -> str:
|
|
410
|
+
"""Get optimal Oracle type for a category.
|
|
411
|
+
|
|
412
|
+
Args:
|
|
413
|
+
driver: Async database driver instance
|
|
414
|
+
type_category: Type category
|
|
415
|
+
|
|
416
|
+
Returns:
|
|
417
|
+
Oracle-specific type name
|
|
418
|
+
"""
|
|
419
|
+
if type_category == "json":
|
|
420
|
+
version_info = await self.get_version(driver)
|
|
421
|
+
return self._get_oracle_json_type(version_info)
|
|
422
|
+
|
|
423
|
+
# Other Oracle-specific type mappings
|
|
424
|
+
type_map = {"uuid": "RAW(16)", "boolean": "NUMBER(1)", "timestamp": "TIMESTAMP", "text": "CLOB", "blob": "BLOB"}
|
|
425
|
+
return type_map.get(type_category, "VARCHAR2(255)")
|
|
426
|
+
|
|
427
|
+
def list_available_features(self) -> "list[str]":
|
|
428
|
+
"""List available Oracle feature flags.
|
|
429
|
+
|
|
430
|
+
Returns:
|
|
431
|
+
List of supported feature names
|
|
432
|
+
"""
|
|
433
|
+
return [
|
|
434
|
+
"is_autonomous",
|
|
435
|
+
"supports_native_json",
|
|
436
|
+
"supports_oson_blob",
|
|
437
|
+
"supports_json_blob",
|
|
438
|
+
"supports_json",
|
|
439
|
+
"supports_transactions",
|
|
440
|
+
"supports_prepared_statements",
|
|
441
|
+
"supports_schemas",
|
|
442
|
+
]
|