sqlspec 0.25.0__py3-none-any.whl → 0.27.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 (199) hide show
  1. sqlspec/__init__.py +7 -15
  2. sqlspec/_serialization.py +256 -24
  3. sqlspec/_typing.py +71 -52
  4. sqlspec/adapters/adbc/_types.py +1 -1
  5. sqlspec/adapters/adbc/adk/__init__.py +5 -0
  6. sqlspec/adapters/adbc/adk/store.py +870 -0
  7. sqlspec/adapters/adbc/config.py +69 -12
  8. sqlspec/adapters/adbc/data_dictionary.py +340 -0
  9. sqlspec/adapters/adbc/driver.py +266 -58
  10. sqlspec/adapters/adbc/litestar/__init__.py +5 -0
  11. sqlspec/adapters/adbc/litestar/store.py +504 -0
  12. sqlspec/adapters/adbc/type_converter.py +153 -0
  13. sqlspec/adapters/aiosqlite/_types.py +1 -1
  14. sqlspec/adapters/aiosqlite/adk/__init__.py +5 -0
  15. sqlspec/adapters/aiosqlite/adk/store.py +527 -0
  16. sqlspec/adapters/aiosqlite/config.py +88 -15
  17. sqlspec/adapters/aiosqlite/data_dictionary.py +149 -0
  18. sqlspec/adapters/aiosqlite/driver.py +143 -40
  19. sqlspec/adapters/aiosqlite/litestar/__init__.py +5 -0
  20. sqlspec/adapters/aiosqlite/litestar/store.py +281 -0
  21. sqlspec/adapters/aiosqlite/pool.py +7 -7
  22. sqlspec/adapters/asyncmy/__init__.py +7 -1
  23. sqlspec/adapters/asyncmy/_types.py +2 -2
  24. sqlspec/adapters/asyncmy/adk/__init__.py +5 -0
  25. sqlspec/adapters/asyncmy/adk/store.py +493 -0
  26. sqlspec/adapters/asyncmy/config.py +68 -23
  27. sqlspec/adapters/asyncmy/data_dictionary.py +161 -0
  28. sqlspec/adapters/asyncmy/driver.py +313 -58
  29. sqlspec/adapters/asyncmy/litestar/__init__.py +5 -0
  30. sqlspec/adapters/asyncmy/litestar/store.py +296 -0
  31. sqlspec/adapters/asyncpg/__init__.py +2 -1
  32. sqlspec/adapters/asyncpg/_type_handlers.py +71 -0
  33. sqlspec/adapters/asyncpg/_types.py +11 -7
  34. sqlspec/adapters/asyncpg/adk/__init__.py +5 -0
  35. sqlspec/adapters/asyncpg/adk/store.py +450 -0
  36. sqlspec/adapters/asyncpg/config.py +59 -35
  37. sqlspec/adapters/asyncpg/data_dictionary.py +173 -0
  38. sqlspec/adapters/asyncpg/driver.py +170 -25
  39. sqlspec/adapters/asyncpg/litestar/__init__.py +5 -0
  40. sqlspec/adapters/asyncpg/litestar/store.py +253 -0
  41. sqlspec/adapters/bigquery/_types.py +1 -1
  42. sqlspec/adapters/bigquery/adk/__init__.py +5 -0
  43. sqlspec/adapters/bigquery/adk/store.py +576 -0
  44. sqlspec/adapters/bigquery/config.py +27 -10
  45. sqlspec/adapters/bigquery/data_dictionary.py +149 -0
  46. sqlspec/adapters/bigquery/driver.py +368 -142
  47. sqlspec/adapters/bigquery/litestar/__init__.py +5 -0
  48. sqlspec/adapters/bigquery/litestar/store.py +327 -0
  49. sqlspec/adapters/bigquery/type_converter.py +125 -0
  50. sqlspec/adapters/duckdb/_types.py +1 -1
  51. sqlspec/adapters/duckdb/adk/__init__.py +14 -0
  52. sqlspec/adapters/duckdb/adk/store.py +553 -0
  53. sqlspec/adapters/duckdb/config.py +80 -20
  54. sqlspec/adapters/duckdb/data_dictionary.py +163 -0
  55. sqlspec/adapters/duckdb/driver.py +167 -45
  56. sqlspec/adapters/duckdb/litestar/__init__.py +5 -0
  57. sqlspec/adapters/duckdb/litestar/store.py +332 -0
  58. sqlspec/adapters/duckdb/pool.py +4 -4
  59. sqlspec/adapters/duckdb/type_converter.py +133 -0
  60. sqlspec/adapters/oracledb/_numpy_handlers.py +133 -0
  61. sqlspec/adapters/oracledb/_types.py +20 -2
  62. sqlspec/adapters/oracledb/adk/__init__.py +5 -0
  63. sqlspec/adapters/oracledb/adk/store.py +1745 -0
  64. sqlspec/adapters/oracledb/config.py +122 -32
  65. sqlspec/adapters/oracledb/data_dictionary.py +509 -0
  66. sqlspec/adapters/oracledb/driver.py +353 -91
  67. sqlspec/adapters/oracledb/litestar/__init__.py +5 -0
  68. sqlspec/adapters/oracledb/litestar/store.py +767 -0
  69. sqlspec/adapters/oracledb/migrations.py +348 -73
  70. sqlspec/adapters/oracledb/type_converter.py +207 -0
  71. sqlspec/adapters/psqlpy/_type_handlers.py +44 -0
  72. sqlspec/adapters/psqlpy/_types.py +2 -1
  73. sqlspec/adapters/psqlpy/adk/__init__.py +5 -0
  74. sqlspec/adapters/psqlpy/adk/store.py +482 -0
  75. sqlspec/adapters/psqlpy/config.py +46 -17
  76. sqlspec/adapters/psqlpy/data_dictionary.py +172 -0
  77. sqlspec/adapters/psqlpy/driver.py +123 -209
  78. sqlspec/adapters/psqlpy/litestar/__init__.py +5 -0
  79. sqlspec/adapters/psqlpy/litestar/store.py +272 -0
  80. sqlspec/adapters/psqlpy/type_converter.py +102 -0
  81. sqlspec/adapters/psycopg/_type_handlers.py +80 -0
  82. sqlspec/adapters/psycopg/_types.py +2 -1
  83. sqlspec/adapters/psycopg/adk/__init__.py +5 -0
  84. sqlspec/adapters/psycopg/adk/store.py +944 -0
  85. sqlspec/adapters/psycopg/config.py +69 -35
  86. sqlspec/adapters/psycopg/data_dictionary.py +331 -0
  87. sqlspec/adapters/psycopg/driver.py +238 -81
  88. sqlspec/adapters/psycopg/litestar/__init__.py +5 -0
  89. sqlspec/adapters/psycopg/litestar/store.py +554 -0
  90. sqlspec/adapters/sqlite/__init__.py +2 -1
  91. sqlspec/adapters/sqlite/_type_handlers.py +86 -0
  92. sqlspec/adapters/sqlite/_types.py +1 -1
  93. sqlspec/adapters/sqlite/adk/__init__.py +5 -0
  94. sqlspec/adapters/sqlite/adk/store.py +572 -0
  95. sqlspec/adapters/sqlite/config.py +87 -15
  96. sqlspec/adapters/sqlite/data_dictionary.py +149 -0
  97. sqlspec/adapters/sqlite/driver.py +137 -54
  98. sqlspec/adapters/sqlite/litestar/__init__.py +5 -0
  99. sqlspec/adapters/sqlite/litestar/store.py +318 -0
  100. sqlspec/adapters/sqlite/pool.py +18 -9
  101. sqlspec/base.py +45 -26
  102. sqlspec/builder/__init__.py +73 -4
  103. sqlspec/builder/_base.py +162 -89
  104. sqlspec/builder/_column.py +62 -29
  105. sqlspec/builder/_ddl.py +180 -121
  106. sqlspec/builder/_delete.py +5 -4
  107. sqlspec/builder/_dml.py +388 -0
  108. sqlspec/{_sql.py → builder/_factory.py} +53 -94
  109. sqlspec/builder/_insert.py +32 -131
  110. sqlspec/builder/_join.py +375 -0
  111. sqlspec/builder/_merge.py +446 -11
  112. sqlspec/builder/_parsing_utils.py +111 -17
  113. sqlspec/builder/_select.py +1457 -24
  114. sqlspec/builder/_update.py +11 -42
  115. sqlspec/cli.py +307 -194
  116. sqlspec/config.py +252 -67
  117. sqlspec/core/__init__.py +5 -4
  118. sqlspec/core/cache.py +17 -17
  119. sqlspec/core/compiler.py +62 -9
  120. sqlspec/core/filters.py +37 -37
  121. sqlspec/core/hashing.py +9 -9
  122. sqlspec/core/parameters.py +83 -48
  123. sqlspec/core/result.py +102 -46
  124. sqlspec/core/splitter.py +16 -17
  125. sqlspec/core/statement.py +36 -30
  126. sqlspec/core/type_conversion.py +235 -0
  127. sqlspec/driver/__init__.py +7 -6
  128. sqlspec/driver/_async.py +188 -151
  129. sqlspec/driver/_common.py +285 -80
  130. sqlspec/driver/_sync.py +188 -152
  131. sqlspec/driver/mixins/_result_tools.py +20 -236
  132. sqlspec/driver/mixins/_sql_translator.py +4 -4
  133. sqlspec/exceptions.py +75 -7
  134. sqlspec/extensions/adk/__init__.py +53 -0
  135. sqlspec/extensions/adk/_types.py +51 -0
  136. sqlspec/extensions/adk/converters.py +172 -0
  137. sqlspec/extensions/adk/migrations/0001_create_adk_tables.py +144 -0
  138. sqlspec/extensions/adk/migrations/__init__.py +0 -0
  139. sqlspec/extensions/adk/service.py +181 -0
  140. sqlspec/extensions/adk/store.py +536 -0
  141. sqlspec/extensions/aiosql/adapter.py +73 -53
  142. sqlspec/extensions/litestar/__init__.py +21 -4
  143. sqlspec/extensions/litestar/cli.py +54 -10
  144. sqlspec/extensions/litestar/config.py +59 -266
  145. sqlspec/extensions/litestar/handlers.py +46 -17
  146. sqlspec/extensions/litestar/migrations/0001_create_session_table.py +137 -0
  147. sqlspec/extensions/litestar/migrations/__init__.py +3 -0
  148. sqlspec/extensions/litestar/plugin.py +324 -223
  149. sqlspec/extensions/litestar/providers.py +25 -25
  150. sqlspec/extensions/litestar/store.py +265 -0
  151. sqlspec/loader.py +30 -49
  152. sqlspec/migrations/__init__.py +4 -3
  153. sqlspec/migrations/base.py +302 -39
  154. sqlspec/migrations/commands.py +611 -144
  155. sqlspec/migrations/context.py +142 -0
  156. sqlspec/migrations/fix.py +199 -0
  157. sqlspec/migrations/loaders.py +68 -23
  158. sqlspec/migrations/runner.py +543 -107
  159. sqlspec/migrations/tracker.py +237 -21
  160. sqlspec/migrations/utils.py +51 -3
  161. sqlspec/migrations/validation.py +177 -0
  162. sqlspec/protocols.py +66 -36
  163. sqlspec/storage/_utils.py +98 -0
  164. sqlspec/storage/backends/fsspec.py +134 -106
  165. sqlspec/storage/backends/local.py +78 -51
  166. sqlspec/storage/backends/obstore.py +278 -162
  167. sqlspec/storage/registry.py +75 -39
  168. sqlspec/typing.py +16 -84
  169. sqlspec/utils/config_resolver.py +153 -0
  170. sqlspec/utils/correlation.py +4 -5
  171. sqlspec/utils/data_transformation.py +3 -2
  172. sqlspec/utils/deprecation.py +9 -8
  173. sqlspec/utils/fixtures.py +4 -4
  174. sqlspec/utils/logging.py +46 -6
  175. sqlspec/utils/module_loader.py +2 -2
  176. sqlspec/utils/schema.py +288 -0
  177. sqlspec/utils/serializers.py +50 -2
  178. sqlspec/utils/sync_tools.py +21 -17
  179. sqlspec/utils/text.py +1 -2
  180. sqlspec/utils/type_guards.py +111 -20
  181. sqlspec/utils/version.py +433 -0
  182. {sqlspec-0.25.0.dist-info → sqlspec-0.27.0.dist-info}/METADATA +40 -21
  183. sqlspec-0.27.0.dist-info/RECORD +207 -0
  184. sqlspec/builder/mixins/__init__.py +0 -55
  185. sqlspec/builder/mixins/_cte_and_set_ops.py +0 -254
  186. sqlspec/builder/mixins/_delete_operations.py +0 -50
  187. sqlspec/builder/mixins/_insert_operations.py +0 -282
  188. sqlspec/builder/mixins/_join_operations.py +0 -389
  189. sqlspec/builder/mixins/_merge_operations.py +0 -592
  190. sqlspec/builder/mixins/_order_limit_operations.py +0 -152
  191. sqlspec/builder/mixins/_pivot_operations.py +0 -157
  192. sqlspec/builder/mixins/_select_operations.py +0 -936
  193. sqlspec/builder/mixins/_update_operations.py +0 -218
  194. sqlspec/builder/mixins/_where_clause.py +0 -1304
  195. sqlspec-0.25.0.dist-info/RECORD +0 -139
  196. sqlspec-0.25.0.dist-info/licenses/NOTICE +0 -29
  197. {sqlspec-0.25.0.dist-info → sqlspec-0.27.0.dist-info}/WHEEL +0 -0
  198. {sqlspec-0.25.0.dist-info → sqlspec-0.27.0.dist-info}/entry_points.txt +0 -0
  199. {sqlspec-0.25.0.dist-info → sqlspec-0.27.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,17 +1,39 @@
1
1
  """Oracle Driver"""
2
2
 
3
+ import contextlib
3
4
  import logging
4
- from typing import TYPE_CHECKING, Any, Optional
5
+ import re
6
+ from typing import TYPE_CHECKING, Any, Final
5
7
 
6
8
  import oracledb
7
9
  from oracledb import AsyncCursor, Cursor
8
10
 
9
11
  from sqlspec.adapters.oracledb._types import OracleAsyncConnection, OracleSyncConnection
12
+ from sqlspec.adapters.oracledb.data_dictionary import OracleAsyncDataDictionary, OracleSyncDataDictionary
13
+ from sqlspec.adapters.oracledb.type_converter import OracleTypeConverter
10
14
  from sqlspec.core.cache import get_cache_config
11
15
  from sqlspec.core.parameters import ParameterStyle, ParameterStyleConfig
12
16
  from sqlspec.core.statement import StatementConfig
13
- from sqlspec.driver import AsyncDriverAdapterBase, SyncDriverAdapterBase
14
- from sqlspec.exceptions import SQLParsingError, SQLSpecError
17
+ from sqlspec.driver import (
18
+ AsyncDataDictionaryBase,
19
+ AsyncDriverAdapterBase,
20
+ SyncDataDictionaryBase,
21
+ SyncDriverAdapterBase,
22
+ )
23
+ from sqlspec.exceptions import (
24
+ CheckViolationError,
25
+ DatabaseConnectionError,
26
+ DataError,
27
+ ForeignKeyViolationError,
28
+ IntegrityError,
29
+ NotNullViolationError,
30
+ OperationalError,
31
+ SQLParsingError,
32
+ SQLSpecError,
33
+ TransactionError,
34
+ UniqueViolationError,
35
+ )
36
+ from sqlspec.utils.serializers import to_json
15
37
 
16
38
  if TYPE_CHECKING:
17
39
  from contextlib import AbstractAsyncContextManager, AbstractContextManager
@@ -22,6 +44,14 @@ if TYPE_CHECKING:
22
44
 
23
45
  logger = logging.getLogger(__name__)
24
46
 
47
+ # Oracle-specific constants
48
+ LARGE_STRING_THRESHOLD = 3000 # Threshold for large string parameters to avoid ORA-01704
49
+
50
+ _type_converter = OracleTypeConverter()
51
+
52
+ IMPLICIT_UPPER_COLUMN_PATTERN: Final[re.Pattern[str]] = re.compile(r"^(?!\d)(?:[A-Z0-9_]+)$")
53
+
54
+
25
55
  __all__ = (
26
56
  "OracleAsyncDriver",
27
57
  "OracleAsyncExceptionHandler",
@@ -31,16 +61,93 @@ __all__ = (
31
61
  )
32
62
 
33
63
 
64
+ def _normalize_column_names(column_names: "list[str]", driver_features: "dict[str, Any]") -> "list[str]":
65
+ should_lowercase = driver_features.get("enable_lowercase_column_names", False)
66
+ if not should_lowercase:
67
+ return column_names
68
+ normalized: list[str] = []
69
+ for name in column_names:
70
+ if name and IMPLICIT_UPPER_COLUMN_PATTERN.fullmatch(name):
71
+ normalized.append(name.lower())
72
+ else:
73
+ normalized.append(name)
74
+ return normalized
75
+
76
+
77
+ def _coerce_sync_row_values(row: "tuple[Any, ...]") -> "list[Any]":
78
+ """Coerce LOB handles to concrete values for synchronous execution.
79
+
80
+ Processes each value in the row, reading LOB objects and applying
81
+ type detection for JSON values stored in CLOBs.
82
+
83
+ Args:
84
+ row: Tuple of column values from database fetch.
85
+
86
+ Returns:
87
+ List of coerced values with LOBs read to strings/bytes.
88
+ """
89
+ coerced_values: list[Any] = []
90
+ for value in row:
91
+ if hasattr(value, "read"):
92
+ try:
93
+ processed_value = value.read()
94
+ except Exception:
95
+ coerced_values.append(value)
96
+ continue
97
+ if isinstance(processed_value, str):
98
+ processed_value = _type_converter.convert_if_detected(processed_value)
99
+ coerced_values.append(processed_value)
100
+ else:
101
+ coerced_values.append(value)
102
+ return coerced_values
103
+
104
+
105
+ async def _coerce_async_row_values(row: "tuple[Any, ...]") -> "list[Any]":
106
+ """Coerce LOB handles to concrete values for asynchronous execution.
107
+
108
+ Processes each value in the row, reading LOB objects asynchronously
109
+ and applying type detection for JSON values stored in CLOBs.
110
+
111
+ Args:
112
+ row: Tuple of column values from database fetch.
113
+
114
+ Returns:
115
+ List of coerced values with LOBs read to strings/bytes.
116
+ """
117
+ coerced_values: list[Any] = []
118
+ for value in row:
119
+ if hasattr(value, "read"):
120
+ try:
121
+ processed_value = await _type_converter.process_lob(value)
122
+ except Exception:
123
+ coerced_values.append(value)
124
+ continue
125
+ if isinstance(processed_value, str):
126
+ processed_value = _type_converter.convert_if_detected(processed_value)
127
+ coerced_values.append(processed_value)
128
+ else:
129
+ coerced_values.append(value)
130
+ return coerced_values
131
+
132
+
133
+ ORA_CHECK_CONSTRAINT = 2290
134
+ ORA_INTEGRITY_RANGE_START = 2200
135
+ ORA_INTEGRITY_RANGE_END = 2300
136
+ ORA_PARSING_RANGE_START = 900
137
+ ORA_PARSING_RANGE_END = 1000
138
+ ORA_TABLESPACE_FULL = 1652
139
+
140
+
34
141
  oracledb_statement_config = StatementConfig(
35
142
  dialect="oracle",
36
143
  parameter_config=ParameterStyleConfig(
37
144
  default_parameter_style=ParameterStyle.POSITIONAL_COLON,
38
145
  supported_parameter_styles={ParameterStyle.NAMED_COLON, ParameterStyle.POSITIONAL_COLON, ParameterStyle.QMARK},
39
- default_execution_parameter_style=ParameterStyle.POSITIONAL_COLON,
146
+ default_execution_parameter_style=ParameterStyle.NAMED_COLON,
40
147
  supported_execution_parameter_styles={ParameterStyle.NAMED_COLON, ParameterStyle.POSITIONAL_COLON},
41
- type_coercion_map={},
148
+ type_coercion_map={dict: to_json, list: to_json},
42
149
  has_native_list_expansion=False,
43
- needs_static_script_compilation=True,
150
+ needs_static_script_compilation=False,
44
151
  preserve_parameter_format=True,
45
152
  ),
46
153
  enable_parsing=True,
@@ -57,14 +164,13 @@ class OracleSyncCursor:
57
164
 
58
165
  def __init__(self, connection: OracleSyncConnection) -> None:
59
166
  self.connection = connection
60
- self.cursor: Optional[Cursor] = None
167
+ self.cursor: Cursor | None = None
61
168
 
62
169
  def __enter__(self) -> Cursor:
63
170
  self.cursor = self.connection.cursor()
64
171
  return self.cursor
65
172
 
66
- def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
67
- _ = (exc_type, exc_val, exc_tb) # Mark as intentionally unused
173
+ def __exit__(self, *_: Any) -> None:
68
174
  if self.cursor is not None:
69
175
  self.cursor.close()
70
176
 
@@ -76,7 +182,7 @@ class OracleAsyncCursor:
76
182
 
77
183
  def __init__(self, connection: OracleAsyncConnection) -> None:
78
184
  self.connection = connection
79
- self.cursor: Optional[AsyncCursor] = None
185
+ self.cursor: AsyncCursor | None = None
80
186
 
81
187
  async def __aenter__(self) -> AsyncCursor:
82
188
  self.cursor = self.connection.cursor()
@@ -85,11 +191,18 @@ class OracleAsyncCursor:
85
191
  async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
86
192
  _ = (exc_type, exc_val, exc_tb) # Mark as intentionally unused
87
193
  if self.cursor is not None:
88
- self.cursor.close() # Synchronous method - do not await
194
+ with contextlib.suppress(Exception):
195
+ # Oracle async cursors have a synchronous close method
196
+ # but we need to ensure proper cleanup in the event loop context
197
+ self.cursor.close()
89
198
 
90
199
 
91
200
  class OracleSyncExceptionHandler:
92
- """Context manager for handling Oracle database exceptions in synchronous operations."""
201
+ """Context manager for handling Oracle database exceptions.
202
+
203
+ Maps Oracle ORA-XXXXX error codes to specific SQLSpec exceptions
204
+ for better error handling in application code.
205
+ """
93
206
 
94
207
  __slots__ = ()
95
208
 
@@ -97,46 +210,101 @@ class OracleSyncExceptionHandler:
97
210
  return None
98
211
 
99
212
  def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
100
- _ = exc_tb # Mark as intentionally unused
213
+ _ = exc_tb
101
214
  if exc_type is None:
102
215
  return
103
-
104
- if issubclass(exc_type, oracledb.IntegrityError):
105
- e = exc_val
106
- msg = f"Oracle integrity constraint violation: {e}"
107
- raise SQLSpecError(msg) from e
108
- if issubclass(exc_type, oracledb.ProgrammingError):
109
- e = exc_val
110
- error_msg = str(e).lower()
111
- if "syntax" in error_msg or "parse" in error_msg:
112
- msg = f"Oracle SQL syntax error: {e}"
113
- raise SQLParsingError(msg) from e
114
- msg = f"Oracle programming error: {e}"
115
- raise SQLSpecError(msg) from e
116
- if issubclass(exc_type, oracledb.OperationalError):
117
- e = exc_val
118
- msg = f"Oracle operational error: {e}"
119
- raise SQLSpecError(msg) from e
120
216
  if issubclass(exc_type, oracledb.DatabaseError):
121
- e = exc_val
122
- msg = f"Oracle database error: {e}"
123
- raise SQLSpecError(msg) from e
124
- if issubclass(exc_type, oracledb.Error):
125
- e = exc_val
126
- msg = f"Oracle error: {e}"
127
- raise SQLSpecError(msg) from e
128
- if issubclass(exc_type, Exception):
129
- e = exc_val
130
- error_msg = str(e).lower()
131
- if "parse" in error_msg or "syntax" in error_msg:
132
- msg = f"SQL parsing failed: {e}"
133
- raise SQLParsingError(msg) from e
134
- msg = f"Unexpected database operation error: {e}"
135
- raise SQLSpecError(msg) from e
217
+ self._map_oracle_exception(exc_val)
218
+
219
+ def _map_oracle_exception(self, e: Any) -> None:
220
+ """Map Oracle exception to SQLSpec exception.
221
+
222
+ Args:
223
+ e: oracledb.DatabaseError instance
224
+ """
225
+ error_obj = e.args[0] if e.args else None
226
+ if not error_obj:
227
+ self._raise_generic_error(e, None)
228
+
229
+ error_code = getattr(error_obj, "code", None)
230
+
231
+ if not error_code:
232
+ self._raise_generic_error(e, None)
233
+
234
+ if error_code == 1:
235
+ self._raise_unique_violation(e, error_code)
236
+ elif error_code in {2291, 2292}:
237
+ self._raise_foreign_key_violation(e, error_code)
238
+ elif error_code == ORA_CHECK_CONSTRAINT:
239
+ self._raise_check_violation(e, error_code)
240
+ elif error_code in {1400, 1407}:
241
+ self._raise_not_null_violation(e, error_code)
242
+ elif error_code and ORA_INTEGRITY_RANGE_START <= error_code < ORA_INTEGRITY_RANGE_END:
243
+ self._raise_integrity_error(e, error_code)
244
+ elif error_code in {1017, 12154, 12541, 12545, 12514, 12505}:
245
+ self._raise_connection_error(e, error_code)
246
+ elif error_code in {60, 8176}:
247
+ self._raise_transaction_error(e, error_code)
248
+ elif error_code in {1722, 1858, 1840}:
249
+ self._raise_data_error(e, error_code)
250
+ elif error_code and ORA_PARSING_RANGE_START <= error_code < ORA_PARSING_RANGE_END:
251
+ self._raise_parsing_error(e, error_code)
252
+ elif error_code == ORA_TABLESPACE_FULL:
253
+ self._raise_operational_error(e, error_code)
254
+ else:
255
+ self._raise_generic_error(e, error_code)
256
+
257
+ def _raise_unique_violation(self, e: Any, code: int) -> None:
258
+ msg = f"Oracle unique constraint violation [ORA-{code:05d}]: {e}"
259
+ raise UniqueViolationError(msg) from e
260
+
261
+ def _raise_foreign_key_violation(self, e: Any, code: int) -> None:
262
+ msg = f"Oracle foreign key constraint violation [ORA-{code:05d}]: {e}"
263
+ raise ForeignKeyViolationError(msg) from e
264
+
265
+ def _raise_check_violation(self, e: Any, code: int) -> None:
266
+ msg = f"Oracle check constraint violation [ORA-{code:05d}]: {e}"
267
+ raise CheckViolationError(msg) from e
268
+
269
+ def _raise_not_null_violation(self, e: Any, code: int) -> None:
270
+ msg = f"Oracle not-null constraint violation [ORA-{code:05d}]: {e}"
271
+ raise NotNullViolationError(msg) from e
272
+
273
+ def _raise_integrity_error(self, e: Any, code: int) -> None:
274
+ msg = f"Oracle integrity constraint violation [ORA-{code:05d}]: {e}"
275
+ raise IntegrityError(msg) from e
276
+
277
+ def _raise_parsing_error(self, e: Any, code: int) -> None:
278
+ msg = f"Oracle SQL syntax error [ORA-{code:05d}]: {e}"
279
+ raise SQLParsingError(msg) from e
280
+
281
+ def _raise_connection_error(self, e: Any, code: int) -> None:
282
+ msg = f"Oracle connection error [ORA-{code:05d}]: {e}"
283
+ raise DatabaseConnectionError(msg) from e
284
+
285
+ def _raise_transaction_error(self, e: Any, code: int) -> None:
286
+ msg = f"Oracle transaction error [ORA-{code:05d}]: {e}"
287
+ raise TransactionError(msg) from e
288
+
289
+ def _raise_data_error(self, e: Any, code: int) -> None:
290
+ msg = f"Oracle data error [ORA-{code:05d}]: {e}"
291
+ raise DataError(msg) from e
292
+
293
+ def _raise_operational_error(self, e: Any, code: int) -> None:
294
+ msg = f"Oracle operational error [ORA-{code:05d}]: {e}"
295
+ raise OperationalError(msg) from e
296
+
297
+ def _raise_generic_error(self, e: Any, code: "int | None") -> None:
298
+ msg = f"Oracle database error [ORA-{code:05d}]: {e}" if code else f"Oracle database error: {e}"
299
+ raise SQLSpecError(msg) from e
136
300
 
137
301
 
138
302
  class OracleAsyncExceptionHandler:
139
- """Context manager for handling Oracle database exceptions in asynchronous operations."""
303
+ """Async context manager for handling Oracle database exceptions.
304
+
305
+ Maps Oracle ORA-XXXXX error codes to specific SQLSpec exceptions
306
+ for better error handling in application code.
307
+ """
140
308
 
141
309
  __slots__ = ()
142
310
 
@@ -144,42 +312,93 @@ class OracleAsyncExceptionHandler:
144
312
  return None
145
313
 
146
314
  async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
147
- _ = exc_tb # Mark as intentionally unused
315
+ _ = exc_tb
148
316
  if exc_type is None:
149
317
  return
150
-
151
- if issubclass(exc_type, oracledb.IntegrityError):
152
- e = exc_val
153
- msg = f"Oracle integrity constraint violation: {e}"
154
- raise SQLSpecError(msg) from e
155
- if issubclass(exc_type, oracledb.ProgrammingError):
156
- e = exc_val
157
- error_msg = str(e).lower()
158
- if "syntax" in error_msg or "parse" in error_msg:
159
- msg = f"Oracle SQL syntax error: {e}"
160
- raise SQLParsingError(msg) from e
161
- msg = f"Oracle programming error: {e}"
162
- raise SQLSpecError(msg) from e
163
- if issubclass(exc_type, oracledb.OperationalError):
164
- e = exc_val
165
- msg = f"Oracle operational error: {e}"
166
- raise SQLSpecError(msg) from e
167
318
  if issubclass(exc_type, oracledb.DatabaseError):
168
- e = exc_val
169
- msg = f"Oracle database error: {e}"
170
- raise SQLSpecError(msg) from e
171
- if issubclass(exc_type, oracledb.Error):
172
- e = exc_val
173
- msg = f"Oracle error: {e}"
174
- raise SQLSpecError(msg) from e
175
- if issubclass(exc_type, Exception):
176
- e = exc_val
177
- error_msg = str(e).lower()
178
- if "parse" in error_msg or "syntax" in error_msg:
179
- msg = f"SQL parsing failed: {e}"
180
- raise SQLParsingError(msg) from e
181
- msg = f"Unexpected async database operation error: {e}"
182
- raise SQLSpecError(msg) from e
319
+ self._map_oracle_exception(exc_val)
320
+
321
+ def _map_oracle_exception(self, e: Any) -> None:
322
+ """Map Oracle exception to SQLSpec exception.
323
+
324
+ Args:
325
+ e: oracledb.DatabaseError instance
326
+ """
327
+ error_obj = e.args[0] if e.args else None
328
+ if not error_obj:
329
+ self._raise_generic_error(e, None)
330
+
331
+ error_code = getattr(error_obj, "code", None)
332
+
333
+ if not error_code:
334
+ self._raise_generic_error(e, None)
335
+
336
+ if error_code == 1:
337
+ self._raise_unique_violation(e, error_code)
338
+ elif error_code in {2291, 2292}:
339
+ self._raise_foreign_key_violation(e, error_code)
340
+ elif error_code == ORA_CHECK_CONSTRAINT:
341
+ self._raise_check_violation(e, error_code)
342
+ elif error_code in {1400, 1407}:
343
+ self._raise_not_null_violation(e, error_code)
344
+ elif error_code and ORA_INTEGRITY_RANGE_START <= error_code < ORA_INTEGRITY_RANGE_END:
345
+ self._raise_integrity_error(e, error_code)
346
+ elif error_code in {1017, 12154, 12541, 12545, 12514, 12505}:
347
+ self._raise_connection_error(e, error_code)
348
+ elif error_code in {60, 8176}:
349
+ self._raise_transaction_error(e, error_code)
350
+ elif error_code in {1722, 1858, 1840}:
351
+ self._raise_data_error(e, error_code)
352
+ elif error_code and ORA_PARSING_RANGE_START <= error_code < ORA_PARSING_RANGE_END:
353
+ self._raise_parsing_error(e, error_code)
354
+ elif error_code == ORA_TABLESPACE_FULL:
355
+ self._raise_operational_error(e, error_code)
356
+ else:
357
+ self._raise_generic_error(e, error_code)
358
+
359
+ def _raise_unique_violation(self, e: Any, code: int) -> None:
360
+ msg = f"Oracle unique constraint violation [ORA-{code:05d}]: {e}"
361
+ raise UniqueViolationError(msg) from e
362
+
363
+ def _raise_foreign_key_violation(self, e: Any, code: int) -> None:
364
+ msg = f"Oracle foreign key constraint violation [ORA-{code:05d}]: {e}"
365
+ raise ForeignKeyViolationError(msg) from e
366
+
367
+ def _raise_check_violation(self, e: Any, code: int) -> None:
368
+ msg = f"Oracle check constraint violation [ORA-{code:05d}]: {e}"
369
+ raise CheckViolationError(msg) from e
370
+
371
+ def _raise_not_null_violation(self, e: Any, code: int) -> None:
372
+ msg = f"Oracle not-null constraint violation [ORA-{code:05d}]: {e}"
373
+ raise NotNullViolationError(msg) from e
374
+
375
+ def _raise_integrity_error(self, e: Any, code: int) -> None:
376
+ msg = f"Oracle integrity constraint violation [ORA-{code:05d}]: {e}"
377
+ raise IntegrityError(msg) from e
378
+
379
+ def _raise_parsing_error(self, e: Any, code: int) -> None:
380
+ msg = f"Oracle SQL syntax error [ORA-{code:05d}]: {e}"
381
+ raise SQLParsingError(msg) from e
382
+
383
+ def _raise_connection_error(self, e: Any, code: int) -> None:
384
+ msg = f"Oracle connection error [ORA-{code:05d}]: {e}"
385
+ raise DatabaseConnectionError(msg) from e
386
+
387
+ def _raise_transaction_error(self, e: Any, code: int) -> None:
388
+ msg = f"Oracle transaction error [ORA-{code:05d}]: {e}"
389
+ raise TransactionError(msg) from e
390
+
391
+ def _raise_data_error(self, e: Any, code: int) -> None:
392
+ msg = f"Oracle data error [ORA-{code:05d}]: {e}"
393
+ raise DataError(msg) from e
394
+
395
+ def _raise_operational_error(self, e: Any, code: int) -> None:
396
+ msg = f"Oracle operational error [ORA-{code:05d}]: {e}"
397
+ raise OperationalError(msg) from e
398
+
399
+ def _raise_generic_error(self, e: Any, code: "int | None") -> None:
400
+ msg = f"Oracle database error [ORA-{code:05d}]: {e}" if code else f"Oracle database error: {e}"
401
+ raise SQLSpecError(msg) from e
183
402
 
184
403
 
185
404
  class OracleSyncDriver(SyncDriverAdapterBase):
@@ -189,14 +408,14 @@ class OracleSyncDriver(SyncDriverAdapterBase):
189
408
  error handling, and transaction management.
190
409
  """
191
410
 
192
- __slots__ = ()
411
+ __slots__ = ("_data_dictionary",)
193
412
  dialect = "oracle"
194
413
 
195
414
  def __init__(
196
415
  self,
197
416
  connection: OracleSyncConnection,
198
- statement_config: "Optional[StatementConfig]" = None,
199
- driver_features: "Optional[dict[str, Any]]" = None,
417
+ statement_config: "StatementConfig | None" = None,
418
+ driver_features: "dict[str, Any] | None" = None,
200
419
  ) -> None:
201
420
  if statement_config is None:
202
421
  cache_config = get_cache_config()
@@ -208,6 +427,7 @@ class OracleSyncDriver(SyncDriverAdapterBase):
208
427
  )
209
428
 
210
429
  super().__init__(connection=connection, statement_config=statement_config, driver_features=driver_features)
430
+ self._data_dictionary: SyncDataDictionaryBase | None = None
211
431
 
212
432
  def with_cursor(self, connection: OracleSyncConnection) -> OracleSyncCursor:
213
433
  """Create context manager for Oracle cursor.
@@ -224,7 +444,7 @@ class OracleSyncDriver(SyncDriverAdapterBase):
224
444
  """Handle database-specific exceptions and wrap them appropriately."""
225
445
  return OracleSyncExceptionHandler()
226
446
 
227
- def _try_special_handling(self, cursor: Any, statement: "SQL") -> "Optional[SQLResult]":
447
+ def _try_special_handling(self, cursor: Any, statement: "SQL") -> "SQLResult | None":
228
448
  """Hook for Oracle-specific special operations.
229
449
 
230
450
  Oracle doesn't have complex special operations like PostgreSQL COPY,
@@ -309,15 +529,23 @@ class OracleSyncDriver(SyncDriverAdapterBase):
309
529
  Execution result containing data for SELECT statements or row count for others
310
530
  """
311
531
  sql, prepared_parameters = self._get_compiled_sql(statement, self.statement_config)
532
+
533
+ # Oracle-specific: Use setinputsizes for large string parameters to avoid ORA-01704
534
+ if prepared_parameters and isinstance(prepared_parameters, dict):
535
+ for param_name, param_value in prepared_parameters.items():
536
+ if isinstance(param_value, str) and len(param_value) > LARGE_STRING_THRESHOLD:
537
+ cursor.setinputsizes(**{param_name: len(param_value)})
538
+
312
539
  cursor.execute(sql, prepared_parameters or {})
313
540
 
314
541
  # SELECT result processing for Oracle
315
542
  if statement.returns_rows():
316
543
  fetched_data = cursor.fetchall()
317
544
  column_names = [col[0] for col in cursor.description or []]
545
+ column_names = _normalize_column_names(column_names, self.driver_features)
318
546
 
319
- # Oracle returns tuples - convert to consistent dict format
320
- data = [dict(zip(column_names, row)) for row in fetched_data]
547
+ # Oracle returns tuples - convert to consistent dict format after LOB hydration
548
+ data = [dict(zip(column_names, _coerce_sync_row_values(row), strict=False)) for row in fetched_data]
321
549
 
322
550
  return self.create_execution_result(
323
551
  cursor, selected_data=data, column_names=column_names, data_row_count=len(data), is_select_result=True
@@ -359,6 +587,17 @@ class OracleSyncDriver(SyncDriverAdapterBase):
359
587
  msg = f"Failed to commit Oracle transaction: {e}"
360
588
  raise SQLSpecError(msg) from e
361
589
 
590
+ @property
591
+ def data_dictionary(self) -> "SyncDataDictionaryBase":
592
+ """Get the data dictionary for this driver.
593
+
594
+ Returns:
595
+ Data dictionary instance for metadata queries
596
+ """
597
+ if self._data_dictionary is None:
598
+ self._data_dictionary = OracleSyncDataDictionary()
599
+ return self._data_dictionary
600
+
362
601
 
363
602
  class OracleAsyncDriver(AsyncDriverAdapterBase):
364
603
  """Asynchronous Oracle Database driver.
@@ -367,14 +606,14 @@ class OracleAsyncDriver(AsyncDriverAdapterBase):
367
606
  error handling, and transaction management for async operations.
368
607
  """
369
608
 
370
- __slots__ = ()
609
+ __slots__ = ("_data_dictionary",)
371
610
  dialect = "oracle"
372
611
 
373
612
  def __init__(
374
613
  self,
375
614
  connection: OracleAsyncConnection,
376
- statement_config: "Optional[StatementConfig]" = None,
377
- driver_features: "Optional[dict[str, Any]]" = None,
615
+ statement_config: "StatementConfig | None" = None,
616
+ driver_features: "dict[str, Any] | None" = None,
378
617
  ) -> None:
379
618
  if statement_config is None:
380
619
  cache_config = get_cache_config()
@@ -386,6 +625,7 @@ class OracleAsyncDriver(AsyncDriverAdapterBase):
386
625
  )
387
626
 
388
627
  super().__init__(connection=connection, statement_config=statement_config, driver_features=driver_features)
628
+ self._data_dictionary: AsyncDataDictionaryBase | None = None
389
629
 
390
630
  def with_cursor(self, connection: OracleAsyncConnection) -> OracleAsyncCursor:
391
631
  """Create context manager for Oracle cursor.
@@ -402,7 +642,7 @@ class OracleAsyncDriver(AsyncDriverAdapterBase):
402
642
  """Handle database-specific exceptions and wrap them appropriately."""
403
643
  return OracleAsyncExceptionHandler()
404
644
 
405
- async def _try_special_handling(self, cursor: Any, statement: "SQL") -> "Optional[SQLResult]":
645
+ async def _try_special_handling(self, cursor: Any, statement: "SQL") -> "SQLResult | None":
406
646
  """Hook for Oracle-specific special operations.
407
647
 
408
648
  Oracle doesn't have complex special operations like PostgreSQL COPY,
@@ -482,15 +722,26 @@ class OracleAsyncDriver(AsyncDriverAdapterBase):
482
722
  Execution result containing data for SELECT statements or row count for others
483
723
  """
484
724
  sql, prepared_parameters = self._get_compiled_sql(statement, self.statement_config)
725
+
726
+ # Oracle-specific: Use setinputsizes for large string parameters to avoid ORA-01704
727
+ if prepared_parameters and isinstance(prepared_parameters, dict):
728
+ for param_name, param_value in prepared_parameters.items():
729
+ if isinstance(param_value, str) and len(param_value) > LARGE_STRING_THRESHOLD:
730
+ cursor.setinputsizes(**{param_name: len(param_value)})
731
+
485
732
  await cursor.execute(sql, prepared_parameters or {})
486
733
 
487
734
  # SELECT result processing for Oracle
488
735
  if statement.returns_rows():
489
736
  fetched_data = await cursor.fetchall()
490
737
  column_names = [col[0] for col in cursor.description or []]
738
+ column_names = _normalize_column_names(column_names, self.driver_features)
491
739
 
492
- # Oracle returns tuples - convert to consistent dict format
493
- data = [dict(zip(column_names, row)) for row in fetched_data]
740
+ # Oracle returns tuples - convert to consistent dict format after LOB hydration
741
+ data = []
742
+ for row in fetched_data:
743
+ coerced_row = await _coerce_async_row_values(row)
744
+ data.append(dict(zip(column_names, coerced_row, strict=False)))
494
745
 
495
746
  return self.create_execution_result(
496
747
  cursor, selected_data=data, column_names=column_names, data_row_count=len(data), is_select_result=True
@@ -531,3 +782,14 @@ class OracleAsyncDriver(AsyncDriverAdapterBase):
531
782
  except oracledb.Error as e:
532
783
  msg = f"Failed to commit Oracle transaction: {e}"
533
784
  raise SQLSpecError(msg) from e
785
+
786
+ @property
787
+ def data_dictionary(self) -> "AsyncDataDictionaryBase":
788
+ """Get the data dictionary for this driver.
789
+
790
+ Returns:
791
+ Data dictionary instance for metadata queries
792
+ """
793
+ if self._data_dictionary is None:
794
+ self._data_dictionary = OracleAsyncDataDictionary()
795
+ return self._data_dictionary
@@ -0,0 +1,5 @@
1
+ """Oracle Litestar integration exports."""
2
+
3
+ from sqlspec.adapters.oracledb.litestar.store import OracleAsyncStore, OracleSyncStore
4
+
5
+ __all__ = ("OracleAsyncStore", "OracleSyncStore")