sqlspec 0.26.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 (197) hide show
  1. sqlspec/__init__.py +7 -15
  2. sqlspec/_serialization.py +55 -25
  3. sqlspec/_typing.py +62 -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 +62 -12
  8. sqlspec/adapters/adbc/data_dictionary.py +52 -2
  9. sqlspec/adapters/adbc/driver.py +144 -45
  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 +44 -50
  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 +86 -16
  17. sqlspec/adapters/aiosqlite/data_dictionary.py +34 -2
  18. sqlspec/adapters/aiosqlite/driver.py +127 -38
  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 +1 -1
  24. sqlspec/adapters/asyncmy/adk/__init__.py +5 -0
  25. sqlspec/adapters/asyncmy/adk/store.py +493 -0
  26. sqlspec/adapters/asyncmy/config.py +59 -17
  27. sqlspec/adapters/asyncmy/data_dictionary.py +41 -2
  28. sqlspec/adapters/asyncmy/driver.py +293 -62
  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 +57 -36
  37. sqlspec/adapters/asyncpg/data_dictionary.py +41 -2
  38. sqlspec/adapters/asyncpg/driver.py +153 -23
  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 +25 -11
  45. sqlspec/adapters/bigquery/data_dictionary.py +42 -2
  46. sqlspec/adapters/bigquery/driver.py +352 -144
  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 +55 -23
  50. sqlspec/adapters/duckdb/_types.py +2 -2
  51. sqlspec/adapters/duckdb/adk/__init__.py +14 -0
  52. sqlspec/adapters/duckdb/adk/store.py +553 -0
  53. sqlspec/adapters/duckdb/config.py +79 -21
  54. sqlspec/adapters/duckdb/data_dictionary.py +41 -2
  55. sqlspec/adapters/duckdb/driver.py +138 -43
  56. sqlspec/adapters/duckdb/litestar/__init__.py +5 -0
  57. sqlspec/adapters/duckdb/litestar/store.py +332 -0
  58. sqlspec/adapters/duckdb/pool.py +5 -5
  59. sqlspec/adapters/duckdb/type_converter.py +51 -21
  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 +120 -36
  65. sqlspec/adapters/oracledb/data_dictionary.py +87 -20
  66. sqlspec/adapters/oracledb/driver.py +292 -84
  67. sqlspec/adapters/oracledb/litestar/__init__.py +5 -0
  68. sqlspec/adapters/oracledb/litestar/store.py +767 -0
  69. sqlspec/adapters/oracledb/migrations.py +316 -25
  70. sqlspec/adapters/oracledb/type_converter.py +91 -16
  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 +45 -19
  76. sqlspec/adapters/psqlpy/data_dictionary.py +41 -2
  77. sqlspec/adapters/psqlpy/driver.py +101 -31
  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 +40 -11
  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 +65 -37
  86. sqlspec/adapters/psycopg/data_dictionary.py +77 -3
  87. sqlspec/adapters/psycopg/driver.py +200 -78
  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 +85 -16
  96. sqlspec/adapters/sqlite/data_dictionary.py +34 -2
  97. sqlspec/adapters/sqlite/driver.py +120 -52
  98. sqlspec/adapters/sqlite/litestar/__init__.py +5 -0
  99. sqlspec/adapters/sqlite/litestar/store.py +318 -0
  100. sqlspec/adapters/sqlite/pool.py +5 -5
  101. sqlspec/base.py +45 -26
  102. sqlspec/builder/__init__.py +73 -4
  103. sqlspec/builder/_base.py +91 -58
  104. sqlspec/builder/_column.py +5 -5
  105. sqlspec/builder/_ddl.py +98 -89
  106. sqlspec/builder/_delete.py +5 -4
  107. sqlspec/builder/_dml.py +388 -0
  108. sqlspec/{_sql.py → builder/_factory.py} +41 -44
  109. sqlspec/builder/_insert.py +5 -82
  110. sqlspec/builder/{mixins/_join_operations.py → _join.py} +145 -143
  111. sqlspec/builder/_merge.py +446 -11
  112. sqlspec/builder/_parsing_utils.py +9 -11
  113. sqlspec/builder/_select.py +1313 -25
  114. sqlspec/builder/_update.py +11 -42
  115. sqlspec/cli.py +76 -69
  116. sqlspec/config.py +231 -60
  117. sqlspec/core/__init__.py +5 -4
  118. sqlspec/core/cache.py +18 -18
  119. sqlspec/core/compiler.py +6 -8
  120. sqlspec/core/filters.py +37 -37
  121. sqlspec/core/hashing.py +9 -9
  122. sqlspec/core/parameters.py +76 -45
  123. sqlspec/core/result.py +102 -46
  124. sqlspec/core/splitter.py +16 -17
  125. sqlspec/core/statement.py +32 -31
  126. sqlspec/core/type_conversion.py +3 -2
  127. sqlspec/driver/__init__.py +1 -3
  128. sqlspec/driver/_async.py +95 -161
  129. sqlspec/driver/_common.py +133 -80
  130. sqlspec/driver/_sync.py +95 -162
  131. sqlspec/driver/mixins/_result_tools.py +20 -236
  132. sqlspec/driver/mixins/_sql_translator.py +4 -4
  133. sqlspec/exceptions.py +70 -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/base.py +200 -76
  153. sqlspec/migrations/commands.py +591 -62
  154. sqlspec/migrations/context.py +6 -9
  155. sqlspec/migrations/fix.py +199 -0
  156. sqlspec/migrations/loaders.py +47 -19
  157. sqlspec/migrations/runner.py +241 -75
  158. sqlspec/migrations/tracker.py +237 -21
  159. sqlspec/migrations/utils.py +51 -3
  160. sqlspec/migrations/validation.py +177 -0
  161. sqlspec/protocols.py +66 -36
  162. sqlspec/storage/_utils.py +98 -0
  163. sqlspec/storage/backends/fsspec.py +134 -106
  164. sqlspec/storage/backends/local.py +78 -51
  165. sqlspec/storage/backends/obstore.py +278 -162
  166. sqlspec/storage/registry.py +75 -39
  167. sqlspec/typing.py +14 -84
  168. sqlspec/utils/config_resolver.py +6 -6
  169. sqlspec/utils/correlation.py +4 -5
  170. sqlspec/utils/data_transformation.py +3 -2
  171. sqlspec/utils/deprecation.py +9 -8
  172. sqlspec/utils/fixtures.py +4 -4
  173. sqlspec/utils/logging.py +46 -6
  174. sqlspec/utils/module_loader.py +2 -2
  175. sqlspec/utils/schema.py +288 -0
  176. sqlspec/utils/serializers.py +3 -3
  177. sqlspec/utils/sync_tools.py +21 -17
  178. sqlspec/utils/text.py +1 -2
  179. sqlspec/utils/type_guards.py +111 -20
  180. sqlspec/utils/version.py +433 -0
  181. {sqlspec-0.26.0.dist-info → sqlspec-0.27.0.dist-info}/METADATA +40 -21
  182. sqlspec-0.27.0.dist-info/RECORD +207 -0
  183. sqlspec/builder/mixins/__init__.py +0 -55
  184. sqlspec/builder/mixins/_cte_and_set_ops.py +0 -253
  185. sqlspec/builder/mixins/_delete_operations.py +0 -50
  186. sqlspec/builder/mixins/_insert_operations.py +0 -282
  187. sqlspec/builder/mixins/_merge_operations.py +0 -698
  188. sqlspec/builder/mixins/_order_limit_operations.py +0 -145
  189. sqlspec/builder/mixins/_pivot_operations.py +0 -157
  190. sqlspec/builder/mixins/_select_operations.py +0 -930
  191. sqlspec/builder/mixins/_update_operations.py +0 -199
  192. sqlspec/builder/mixins/_where_clause.py +0 -1298
  193. sqlspec-0.26.0.dist-info/RECORD +0 -157
  194. sqlspec-0.26.0.dist-info/licenses/NOTICE +0 -29
  195. {sqlspec-0.26.0.dist-info → sqlspec-0.27.0.dist-info}/WHEEL +0 -0
  196. {sqlspec-0.26.0.dist-info → sqlspec-0.27.0.dist-info}/entry_points.txt +0 -0
  197. {sqlspec-0.26.0.dist-info → sqlspec-0.27.0.dist-info}/licenses/LICENSE +0 -0
@@ -2,7 +2,8 @@
2
2
 
3
3
  import contextlib
4
4
  import logging
5
- from typing import TYPE_CHECKING, Any, Optional
5
+ import re
6
+ from typing import TYPE_CHECKING, Any, Final
6
7
 
7
8
  import oracledb
8
9
  from oracledb import AsyncCursor, Cursor
@@ -19,7 +20,19 @@ from sqlspec.driver import (
19
20
  SyncDataDictionaryBase,
20
21
  SyncDriverAdapterBase,
21
22
  )
22
- from sqlspec.exceptions import SQLParsingError, SQLSpecError
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
+ )
23
36
  from sqlspec.utils.serializers import to_json
24
37
 
25
38
  if TYPE_CHECKING:
@@ -36,6 +49,9 @@ LARGE_STRING_THRESHOLD = 3000 # Threshold for large string parameters to avoid
36
49
 
37
50
  _type_converter = OracleTypeConverter()
38
51
 
52
+ IMPLICIT_UPPER_COLUMN_PATTERN: Final[re.Pattern[str]] = re.compile(r"^(?!\d)(?:[A-Z0-9_]+)$")
53
+
54
+
39
55
  __all__ = (
40
56
  "OracleAsyncDriver",
41
57
  "OracleAsyncExceptionHandler",
@@ -45,6 +61,83 @@ __all__ = (
45
61
  )
46
62
 
47
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
+
48
141
  oracledb_statement_config = StatementConfig(
49
142
  dialect="oracle",
50
143
  parameter_config=ParameterStyleConfig(
@@ -71,7 +164,7 @@ class OracleSyncCursor:
71
164
 
72
165
  def __init__(self, connection: OracleSyncConnection) -> None:
73
166
  self.connection = connection
74
- self.cursor: Optional[Cursor] = None
167
+ self.cursor: Cursor | None = None
75
168
 
76
169
  def __enter__(self) -> Cursor:
77
170
  self.cursor = self.connection.cursor()
@@ -89,7 +182,7 @@ class OracleAsyncCursor:
89
182
 
90
183
  def __init__(self, connection: OracleAsyncConnection) -> None:
91
184
  self.connection = connection
92
- self.cursor: Optional[AsyncCursor] = None
185
+ self.cursor: AsyncCursor | None = None
93
186
 
94
187
  async def __aenter__(self) -> AsyncCursor:
95
188
  self.cursor = self.connection.cursor()
@@ -105,7 +198,11 @@ class OracleAsyncCursor:
105
198
 
106
199
 
107
200
  class OracleSyncExceptionHandler:
108
- """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
+ """
109
206
 
110
207
  __slots__ = ()
111
208
 
@@ -113,46 +210,101 @@ class OracleSyncExceptionHandler:
113
210
  return None
114
211
 
115
212
  def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
116
- _ = exc_tb # Mark as intentionally unused
213
+ _ = exc_tb
117
214
  if exc_type is None:
118
215
  return
119
-
120
- if issubclass(exc_type, oracledb.IntegrityError):
121
- e = exc_val
122
- msg = f"Oracle integrity constraint violation: {e}"
123
- raise SQLSpecError(msg) from e
124
- if issubclass(exc_type, oracledb.ProgrammingError):
125
- e = exc_val
126
- error_msg = str(e).lower()
127
- if "syntax" in error_msg or "parse" in error_msg:
128
- msg = f"Oracle SQL syntax error: {e}"
129
- raise SQLParsingError(msg) from e
130
- msg = f"Oracle programming error: {e}"
131
- raise SQLSpecError(msg) from e
132
- if issubclass(exc_type, oracledb.OperationalError):
133
- e = exc_val
134
- msg = f"Oracle operational error: {e}"
135
- raise SQLSpecError(msg) from e
136
216
  if issubclass(exc_type, oracledb.DatabaseError):
137
- e = exc_val
138
- msg = f"Oracle database error: {e}"
139
- raise SQLSpecError(msg) from e
140
- if issubclass(exc_type, oracledb.Error):
141
- e = exc_val
142
- msg = f"Oracle error: {e}"
143
- raise SQLSpecError(msg) from e
144
- if issubclass(exc_type, Exception):
145
- e = exc_val
146
- error_msg = str(e).lower()
147
- if "parse" in error_msg or "syntax" in error_msg:
148
- msg = f"SQL parsing failed: {e}"
149
- raise SQLParsingError(msg) from e
150
- msg = f"Unexpected database operation error: {e}"
151
- 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
152
300
 
153
301
 
154
302
  class OracleAsyncExceptionHandler:
155
- """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
+ """
156
308
 
157
309
  __slots__ = ()
158
310
 
@@ -160,42 +312,93 @@ class OracleAsyncExceptionHandler:
160
312
  return None
161
313
 
162
314
  async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
163
- _ = exc_tb # Mark as intentionally unused
315
+ _ = exc_tb
164
316
  if exc_type is None:
165
317
  return
166
-
167
- if issubclass(exc_type, oracledb.IntegrityError):
168
- e = exc_val
169
- msg = f"Oracle integrity constraint violation: {e}"
170
- raise SQLSpecError(msg) from e
171
- if issubclass(exc_type, oracledb.ProgrammingError):
172
- e = exc_val
173
- error_msg = str(e).lower()
174
- if "syntax" in error_msg or "parse" in error_msg:
175
- msg = f"Oracle SQL syntax error: {e}"
176
- raise SQLParsingError(msg) from e
177
- msg = f"Oracle programming error: {e}"
178
- raise SQLSpecError(msg) from e
179
- if issubclass(exc_type, oracledb.OperationalError):
180
- e = exc_val
181
- msg = f"Oracle operational error: {e}"
182
- raise SQLSpecError(msg) from e
183
318
  if issubclass(exc_type, oracledb.DatabaseError):
184
- e = exc_val
185
- msg = f"Oracle database error: {e}"
186
- raise SQLSpecError(msg) from e
187
- if issubclass(exc_type, oracledb.Error):
188
- e = exc_val
189
- msg = f"Oracle error: {e}"
190
- raise SQLSpecError(msg) from e
191
- if issubclass(exc_type, Exception):
192
- e = exc_val
193
- error_msg = str(e).lower()
194
- if "parse" in error_msg or "syntax" in error_msg:
195
- msg = f"SQL parsing failed: {e}"
196
- raise SQLParsingError(msg) from e
197
- msg = f"Unexpected async database operation error: {e}"
198
- 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
199
402
 
200
403
 
201
404
  class OracleSyncDriver(SyncDriverAdapterBase):
@@ -211,8 +414,8 @@ class OracleSyncDriver(SyncDriverAdapterBase):
211
414
  def __init__(
212
415
  self,
213
416
  connection: OracleSyncConnection,
214
- statement_config: "Optional[StatementConfig]" = None,
215
- driver_features: "Optional[dict[str, Any]]" = None,
417
+ statement_config: "StatementConfig | None" = None,
418
+ driver_features: "dict[str, Any] | None" = None,
216
419
  ) -> None:
217
420
  if statement_config is None:
218
421
  cache_config = get_cache_config()
@@ -224,7 +427,7 @@ class OracleSyncDriver(SyncDriverAdapterBase):
224
427
  )
225
428
 
226
429
  super().__init__(connection=connection, statement_config=statement_config, driver_features=driver_features)
227
- self._data_dictionary: Optional[SyncDataDictionaryBase] = None
430
+ self._data_dictionary: SyncDataDictionaryBase | None = None
228
431
 
229
432
  def with_cursor(self, connection: OracleSyncConnection) -> OracleSyncCursor:
230
433
  """Create context manager for Oracle cursor.
@@ -241,7 +444,7 @@ class OracleSyncDriver(SyncDriverAdapterBase):
241
444
  """Handle database-specific exceptions and wrap them appropriately."""
242
445
  return OracleSyncExceptionHandler()
243
446
 
244
- def _try_special_handling(self, cursor: Any, statement: "SQL") -> "Optional[SQLResult]":
447
+ def _try_special_handling(self, cursor: Any, statement: "SQL") -> "SQLResult | None":
245
448
  """Hook for Oracle-specific special operations.
246
449
 
247
450
  Oracle doesn't have complex special operations like PostgreSQL COPY,
@@ -339,9 +542,10 @@ class OracleSyncDriver(SyncDriverAdapterBase):
339
542
  if statement.returns_rows():
340
543
  fetched_data = cursor.fetchall()
341
544
  column_names = [col[0] for col in cursor.description or []]
545
+ column_names = _normalize_column_names(column_names, self.driver_features)
342
546
 
343
- # Oracle returns tuples - convert to consistent dict format
344
- 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]
345
549
 
346
550
  return self.create_execution_result(
347
551
  cursor, selected_data=data, column_names=column_names, data_row_count=len(data), is_select_result=True
@@ -408,8 +612,8 @@ class OracleAsyncDriver(AsyncDriverAdapterBase):
408
612
  def __init__(
409
613
  self,
410
614
  connection: OracleAsyncConnection,
411
- statement_config: "Optional[StatementConfig]" = None,
412
- driver_features: "Optional[dict[str, Any]]" = None,
615
+ statement_config: "StatementConfig | None" = None,
616
+ driver_features: "dict[str, Any] | None" = None,
413
617
  ) -> None:
414
618
  if statement_config is None:
415
619
  cache_config = get_cache_config()
@@ -421,7 +625,7 @@ class OracleAsyncDriver(AsyncDriverAdapterBase):
421
625
  )
422
626
 
423
627
  super().__init__(connection=connection, statement_config=statement_config, driver_features=driver_features)
424
- self._data_dictionary: Optional[AsyncDataDictionaryBase] = None
628
+ self._data_dictionary: AsyncDataDictionaryBase | None = None
425
629
 
426
630
  def with_cursor(self, connection: OracleAsyncConnection) -> OracleAsyncCursor:
427
631
  """Create context manager for Oracle cursor.
@@ -438,7 +642,7 @@ class OracleAsyncDriver(AsyncDriverAdapterBase):
438
642
  """Handle database-specific exceptions and wrap them appropriately."""
439
643
  return OracleAsyncExceptionHandler()
440
644
 
441
- 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":
442
646
  """Hook for Oracle-specific special operations.
443
647
 
444
648
  Oracle doesn't have complex special operations like PostgreSQL COPY,
@@ -531,9 +735,13 @@ class OracleAsyncDriver(AsyncDriverAdapterBase):
531
735
  if statement.returns_rows():
532
736
  fetched_data = await cursor.fetchall()
533
737
  column_names = [col[0] for col in cursor.description or []]
738
+ column_names = _normalize_column_names(column_names, self.driver_features)
534
739
 
535
- # Oracle returns tuples - convert to consistent dict format
536
- 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)))
537
745
 
538
746
  return self.create_execution_result(
539
747
  cursor, selected_data=data, column_names=column_names, data_row_count=len(data), is_select_result=True
@@ -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")