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.

Files changed (95) hide show
  1. sqlspec/_serialization.py +223 -21
  2. sqlspec/_sql.py +20 -62
  3. sqlspec/_typing.py +11 -0
  4. sqlspec/adapters/adbc/config.py +8 -1
  5. sqlspec/adapters/adbc/data_dictionary.py +290 -0
  6. sqlspec/adapters/adbc/driver.py +129 -20
  7. sqlspec/adapters/adbc/type_converter.py +159 -0
  8. sqlspec/adapters/aiosqlite/config.py +3 -0
  9. sqlspec/adapters/aiosqlite/data_dictionary.py +117 -0
  10. sqlspec/adapters/aiosqlite/driver.py +17 -3
  11. sqlspec/adapters/asyncmy/_types.py +1 -1
  12. sqlspec/adapters/asyncmy/config.py +11 -8
  13. sqlspec/adapters/asyncmy/data_dictionary.py +122 -0
  14. sqlspec/adapters/asyncmy/driver.py +31 -7
  15. sqlspec/adapters/asyncpg/config.py +3 -0
  16. sqlspec/adapters/asyncpg/data_dictionary.py +134 -0
  17. sqlspec/adapters/asyncpg/driver.py +19 -4
  18. sqlspec/adapters/bigquery/config.py +3 -0
  19. sqlspec/adapters/bigquery/data_dictionary.py +109 -0
  20. sqlspec/adapters/bigquery/driver.py +21 -3
  21. sqlspec/adapters/bigquery/type_converter.py +93 -0
  22. sqlspec/adapters/duckdb/_types.py +1 -1
  23. sqlspec/adapters/duckdb/config.py +2 -0
  24. sqlspec/adapters/duckdb/data_dictionary.py +124 -0
  25. sqlspec/adapters/duckdb/driver.py +32 -5
  26. sqlspec/adapters/duckdb/pool.py +1 -1
  27. sqlspec/adapters/duckdb/type_converter.py +103 -0
  28. sqlspec/adapters/oracledb/config.py +6 -0
  29. sqlspec/adapters/oracledb/data_dictionary.py +442 -0
  30. sqlspec/adapters/oracledb/driver.py +68 -9
  31. sqlspec/adapters/oracledb/migrations.py +51 -67
  32. sqlspec/adapters/oracledb/type_converter.py +132 -0
  33. sqlspec/adapters/psqlpy/config.py +3 -0
  34. sqlspec/adapters/psqlpy/data_dictionary.py +133 -0
  35. sqlspec/adapters/psqlpy/driver.py +23 -179
  36. sqlspec/adapters/psqlpy/type_converter.py +73 -0
  37. sqlspec/adapters/psycopg/config.py +8 -4
  38. sqlspec/adapters/psycopg/data_dictionary.py +257 -0
  39. sqlspec/adapters/psycopg/driver.py +40 -5
  40. sqlspec/adapters/sqlite/config.py +3 -0
  41. sqlspec/adapters/sqlite/data_dictionary.py +117 -0
  42. sqlspec/adapters/sqlite/driver.py +18 -3
  43. sqlspec/adapters/sqlite/pool.py +13 -4
  44. sqlspec/base.py +3 -4
  45. sqlspec/builder/_base.py +130 -48
  46. sqlspec/builder/_column.py +66 -24
  47. sqlspec/builder/_ddl.py +91 -41
  48. sqlspec/builder/_insert.py +40 -58
  49. sqlspec/builder/_parsing_utils.py +127 -12
  50. sqlspec/builder/_select.py +147 -2
  51. sqlspec/builder/_update.py +1 -1
  52. sqlspec/builder/mixins/_cte_and_set_ops.py +31 -23
  53. sqlspec/builder/mixins/_delete_operations.py +12 -7
  54. sqlspec/builder/mixins/_insert_operations.py +50 -36
  55. sqlspec/builder/mixins/_join_operations.py +15 -30
  56. sqlspec/builder/mixins/_merge_operations.py +210 -78
  57. sqlspec/builder/mixins/_order_limit_operations.py +4 -10
  58. sqlspec/builder/mixins/_pivot_operations.py +1 -0
  59. sqlspec/builder/mixins/_select_operations.py +44 -22
  60. sqlspec/builder/mixins/_update_operations.py +30 -37
  61. sqlspec/builder/mixins/_where_clause.py +52 -70
  62. sqlspec/cli.py +246 -140
  63. sqlspec/config.py +33 -19
  64. sqlspec/core/__init__.py +3 -2
  65. sqlspec/core/cache.py +298 -352
  66. sqlspec/core/compiler.py +61 -4
  67. sqlspec/core/filters.py +246 -213
  68. sqlspec/core/hashing.py +9 -11
  69. sqlspec/core/parameters.py +27 -10
  70. sqlspec/core/statement.py +72 -12
  71. sqlspec/core/type_conversion.py +234 -0
  72. sqlspec/driver/__init__.py +6 -3
  73. sqlspec/driver/_async.py +108 -5
  74. sqlspec/driver/_common.py +186 -17
  75. sqlspec/driver/_sync.py +108 -5
  76. sqlspec/driver/mixins/_result_tools.py +60 -7
  77. sqlspec/exceptions.py +5 -0
  78. sqlspec/loader.py +8 -9
  79. sqlspec/migrations/__init__.py +4 -3
  80. sqlspec/migrations/base.py +153 -14
  81. sqlspec/migrations/commands.py +34 -96
  82. sqlspec/migrations/context.py +145 -0
  83. sqlspec/migrations/loaders.py +25 -8
  84. sqlspec/migrations/runner.py +352 -82
  85. sqlspec/storage/backends/fsspec.py +1 -0
  86. sqlspec/typing.py +4 -0
  87. sqlspec/utils/config_resolver.py +153 -0
  88. sqlspec/utils/serializers.py +50 -2
  89. {sqlspec-0.24.1.dist-info → sqlspec-0.26.0.dist-info}/METADATA +1 -1
  90. sqlspec-0.26.0.dist-info/RECORD +157 -0
  91. sqlspec-0.24.1.dist-info/RECORD +0 -139
  92. {sqlspec-0.24.1.dist-info → sqlspec-0.26.0.dist-info}/WHEEL +0 -0
  93. {sqlspec-0.24.1.dist-info → sqlspec-0.26.0.dist-info}/entry_points.txt +0 -0
  94. {sqlspec-0.24.1.dist-info → sqlspec-0.26.0.dist-info}/licenses/LICENSE +0 -0
  95. {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, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
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
@@ -6,7 +6,7 @@ import time
6
6
  from contextlib import contextmanager, suppress
7
7
  from typing import TYPE_CHECKING, Any, Final, Optional, cast
8
8
 
9
- import duckdb
9
+ import duckdb # type: ignore[import-untyped]
10
10
 
11
11
  from sqlspec.adapters.duckdb._types import DuckDBConnection
12
12
 
@@ -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
+ ]