sqlspec 0.26.0__py3-none-any.whl → 0.28.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 (212) hide show
  1. sqlspec/__init__.py +7 -15
  2. sqlspec/_serialization.py +55 -25
  3. sqlspec/_typing.py +155 -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 +880 -0
  7. sqlspec/adapters/adbc/config.py +62 -12
  8. sqlspec/adapters/adbc/data_dictionary.py +74 -2
  9. sqlspec/adapters/adbc/driver.py +226 -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 +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 +536 -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 +503 -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 +460 -0
  36. sqlspec/adapters/asyncpg/config.py +57 -36
  37. sqlspec/adapters/asyncpg/data_dictionary.py +48 -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 +585 -0
  44. sqlspec/adapters/bigquery/config.py +36 -11
  45. sqlspec/adapters/bigquery/data_dictionary.py +42 -2
  46. sqlspec/adapters/bigquery/driver.py +489 -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 +563 -0
  53. sqlspec/adapters/duckdb/config.py +79 -21
  54. sqlspec/adapters/duckdb/data_dictionary.py +41 -2
  55. sqlspec/adapters/duckdb/driver.py +225 -44
  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 +1628 -0
  64. sqlspec/adapters/oracledb/config.py +120 -36
  65. sqlspec/adapters/oracledb/data_dictionary.py +87 -20
  66. sqlspec/adapters/oracledb/driver.py +475 -86
  67. sqlspec/adapters/oracledb/litestar/__init__.py +5 -0
  68. sqlspec/adapters/oracledb/litestar/store.py +765 -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 +483 -0
  75. sqlspec/adapters/psqlpy/config.py +45 -19
  76. sqlspec/adapters/psqlpy/data_dictionary.py +48 -2
  77. sqlspec/adapters/psqlpy/driver.py +108 -41
  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 +962 -0
  85. sqlspec/adapters/psycopg/config.py +65 -37
  86. sqlspec/adapters/psycopg/data_dictionary.py +91 -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 +582 -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 +331 -62
  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 +55 -47
  121. sqlspec/core/hashing.py +9 -9
  122. sqlspec/core/parameters.py +76 -45
  123. sqlspec/core/result.py +234 -47
  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 +183 -160
  129. sqlspec/driver/_common.py +197 -109
  130. sqlspec/driver/_sync.py +189 -161
  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 +69 -61
  142. sqlspec/extensions/fastapi/__init__.py +21 -0
  143. sqlspec/extensions/fastapi/extension.py +331 -0
  144. sqlspec/extensions/fastapi/providers.py +543 -0
  145. sqlspec/extensions/flask/__init__.py +36 -0
  146. sqlspec/extensions/flask/_state.py +71 -0
  147. sqlspec/extensions/flask/_utils.py +40 -0
  148. sqlspec/extensions/flask/extension.py +389 -0
  149. sqlspec/extensions/litestar/__init__.py +21 -4
  150. sqlspec/extensions/litestar/cli.py +54 -10
  151. sqlspec/extensions/litestar/config.py +56 -266
  152. sqlspec/extensions/litestar/handlers.py +46 -17
  153. sqlspec/extensions/litestar/migrations/0001_create_session_table.py +137 -0
  154. sqlspec/extensions/litestar/migrations/__init__.py +3 -0
  155. sqlspec/extensions/litestar/plugin.py +349 -224
  156. sqlspec/extensions/litestar/providers.py +25 -25
  157. sqlspec/extensions/litestar/store.py +265 -0
  158. sqlspec/extensions/starlette/__init__.py +10 -0
  159. sqlspec/extensions/starlette/_state.py +25 -0
  160. sqlspec/extensions/starlette/_utils.py +52 -0
  161. sqlspec/extensions/starlette/extension.py +254 -0
  162. sqlspec/extensions/starlette/middleware.py +154 -0
  163. sqlspec/loader.py +30 -49
  164. sqlspec/migrations/base.py +200 -76
  165. sqlspec/migrations/commands.py +591 -62
  166. sqlspec/migrations/context.py +6 -9
  167. sqlspec/migrations/fix.py +199 -0
  168. sqlspec/migrations/loaders.py +47 -19
  169. sqlspec/migrations/runner.py +241 -75
  170. sqlspec/migrations/tracker.py +237 -21
  171. sqlspec/migrations/utils.py +51 -3
  172. sqlspec/migrations/validation.py +177 -0
  173. sqlspec/protocols.py +106 -36
  174. sqlspec/storage/_utils.py +85 -0
  175. sqlspec/storage/backends/fsspec.py +133 -107
  176. sqlspec/storage/backends/local.py +78 -51
  177. sqlspec/storage/backends/obstore.py +276 -168
  178. sqlspec/storage/registry.py +75 -39
  179. sqlspec/typing.py +30 -84
  180. sqlspec/utils/__init__.py +25 -4
  181. sqlspec/utils/arrow_helpers.py +81 -0
  182. sqlspec/utils/config_resolver.py +6 -6
  183. sqlspec/utils/correlation.py +4 -5
  184. sqlspec/utils/data_transformation.py +3 -2
  185. sqlspec/utils/deprecation.py +9 -8
  186. sqlspec/utils/fixtures.py +4 -4
  187. sqlspec/utils/logging.py +46 -6
  188. sqlspec/utils/module_loader.py +205 -5
  189. sqlspec/utils/portal.py +311 -0
  190. sqlspec/utils/schema.py +288 -0
  191. sqlspec/utils/serializers.py +113 -4
  192. sqlspec/utils/sync_tools.py +36 -22
  193. sqlspec/utils/text.py +1 -2
  194. sqlspec/utils/type_guards.py +136 -20
  195. sqlspec/utils/version.py +433 -0
  196. {sqlspec-0.26.0.dist-info → sqlspec-0.28.0.dist-info}/METADATA +41 -22
  197. sqlspec-0.28.0.dist-info/RECORD +221 -0
  198. sqlspec/builder/mixins/__init__.py +0 -55
  199. sqlspec/builder/mixins/_cte_and_set_ops.py +0 -253
  200. sqlspec/builder/mixins/_delete_operations.py +0 -50
  201. sqlspec/builder/mixins/_insert_operations.py +0 -282
  202. sqlspec/builder/mixins/_merge_operations.py +0 -698
  203. sqlspec/builder/mixins/_order_limit_operations.py +0 -145
  204. sqlspec/builder/mixins/_pivot_operations.py +0 -157
  205. sqlspec/builder/mixins/_select_operations.py +0 -930
  206. sqlspec/builder/mixins/_update_operations.py +0 -199
  207. sqlspec/builder/mixins/_where_clause.py +0 -1298
  208. sqlspec-0.26.0.dist-info/RECORD +0 -157
  209. sqlspec-0.26.0.dist-info/licenses/NOTICE +0 -29
  210. {sqlspec-0.26.0.dist-info → sqlspec-0.28.0.dist-info}/WHEEL +0 -0
  211. {sqlspec-0.26.0.dist-info → sqlspec-0.28.0.dist-info}/entry_points.txt +0 -0
  212. {sqlspec-0.26.0.dist-info → sqlspec-0.28.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
@@ -12,22 +13,39 @@ from sqlspec.adapters.oracledb.data_dictionary import OracleAsyncDataDictionary,
12
13
  from sqlspec.adapters.oracledb.type_converter import OracleTypeConverter
13
14
  from sqlspec.core.cache import get_cache_config
14
15
  from sqlspec.core.parameters import ParameterStyle, ParameterStyleConfig
15
- from sqlspec.core.statement import StatementConfig
16
+ from sqlspec.core.result import create_arrow_result
17
+ from sqlspec.core.statement import SQL, StatementConfig
16
18
  from sqlspec.driver import (
17
19
  AsyncDataDictionaryBase,
18
20
  AsyncDriverAdapterBase,
19
21
  SyncDataDictionaryBase,
20
22
  SyncDriverAdapterBase,
21
23
  )
22
- from sqlspec.exceptions import SQLParsingError, SQLSpecError
24
+ from sqlspec.exceptions import (
25
+ CheckViolationError,
26
+ DatabaseConnectionError,
27
+ DataError,
28
+ ForeignKeyViolationError,
29
+ IntegrityError,
30
+ NotNullViolationError,
31
+ OperationalError,
32
+ SQLParsingError,
33
+ SQLSpecError,
34
+ TransactionError,
35
+ UniqueViolationError,
36
+ )
37
+ from sqlspec.utils.module_loader import ensure_pyarrow
23
38
  from sqlspec.utils.serializers import to_json
24
39
 
25
40
  if TYPE_CHECKING:
26
41
  from contextlib import AbstractAsyncContextManager, AbstractContextManager
27
42
 
43
+ from sqlspec.builder import QueryBuilder
44
+ from sqlspec.core import StatementFilter
28
45
  from sqlspec.core.result import SQLResult
29
- from sqlspec.core.statement import SQL
46
+ from sqlspec.core.statement import Statement
30
47
  from sqlspec.driver._common import ExecutionResult
48
+ from sqlspec.typing import StatementParameters
31
49
 
32
50
  logger = logging.getLogger(__name__)
33
51
 
@@ -36,6 +54,9 @@ LARGE_STRING_THRESHOLD = 3000 # Threshold for large string parameters to avoid
36
54
 
37
55
  _type_converter = OracleTypeConverter()
38
56
 
57
+ IMPLICIT_UPPER_COLUMN_PATTERN: Final[re.Pattern[str]] = re.compile(r"^(?!\d)(?:[A-Z0-9_]+)$")
58
+
59
+
39
60
  __all__ = (
40
61
  "OracleAsyncDriver",
41
62
  "OracleAsyncExceptionHandler",
@@ -45,6 +66,83 @@ __all__ = (
45
66
  )
46
67
 
47
68
 
69
+ def _normalize_column_names(column_names: "list[str]", driver_features: "dict[str, Any]") -> "list[str]":
70
+ should_lowercase = driver_features.get("enable_lowercase_column_names", False)
71
+ if not should_lowercase:
72
+ return column_names
73
+ normalized: list[str] = []
74
+ for name in column_names:
75
+ if name and IMPLICIT_UPPER_COLUMN_PATTERN.fullmatch(name):
76
+ normalized.append(name.lower())
77
+ else:
78
+ normalized.append(name)
79
+ return normalized
80
+
81
+
82
+ def _coerce_sync_row_values(row: "tuple[Any, ...]") -> "list[Any]":
83
+ """Coerce LOB handles to concrete values for synchronous execution.
84
+
85
+ Processes each value in the row, reading LOB objects and applying
86
+ type detection for JSON values stored in CLOBs.
87
+
88
+ Args:
89
+ row: Tuple of column values from database fetch.
90
+
91
+ Returns:
92
+ List of coerced values with LOBs read to strings/bytes.
93
+ """
94
+ coerced_values: list[Any] = []
95
+ for value in row:
96
+ if hasattr(value, "read"):
97
+ try:
98
+ processed_value = value.read()
99
+ except Exception:
100
+ coerced_values.append(value)
101
+ continue
102
+ if isinstance(processed_value, str):
103
+ processed_value = _type_converter.convert_if_detected(processed_value)
104
+ coerced_values.append(processed_value)
105
+ else:
106
+ coerced_values.append(value)
107
+ return coerced_values
108
+
109
+
110
+ async def _coerce_async_row_values(row: "tuple[Any, ...]") -> "list[Any]":
111
+ """Coerce LOB handles to concrete values for asynchronous execution.
112
+
113
+ Processes each value in the row, reading LOB objects asynchronously
114
+ and applying type detection for JSON values stored in CLOBs.
115
+
116
+ Args:
117
+ row: Tuple of column values from database fetch.
118
+
119
+ Returns:
120
+ List of coerced values with LOBs read to strings/bytes.
121
+ """
122
+ coerced_values: list[Any] = []
123
+ for value in row:
124
+ if hasattr(value, "read"):
125
+ try:
126
+ processed_value = await _type_converter.process_lob(value)
127
+ except Exception:
128
+ coerced_values.append(value)
129
+ continue
130
+ if isinstance(processed_value, str):
131
+ processed_value = _type_converter.convert_if_detected(processed_value)
132
+ coerced_values.append(processed_value)
133
+ else:
134
+ coerced_values.append(value)
135
+ return coerced_values
136
+
137
+
138
+ ORA_CHECK_CONSTRAINT = 2290
139
+ ORA_INTEGRITY_RANGE_START = 2200
140
+ ORA_INTEGRITY_RANGE_END = 2300
141
+ ORA_PARSING_RANGE_START = 900
142
+ ORA_PARSING_RANGE_END = 1000
143
+ ORA_TABLESPACE_FULL = 1652
144
+
145
+
48
146
  oracledb_statement_config = StatementConfig(
49
147
  dialect="oracle",
50
148
  parameter_config=ParameterStyleConfig(
@@ -71,7 +169,7 @@ class OracleSyncCursor:
71
169
 
72
170
  def __init__(self, connection: OracleSyncConnection) -> None:
73
171
  self.connection = connection
74
- self.cursor: Optional[Cursor] = None
172
+ self.cursor: Cursor | None = None
75
173
 
76
174
  def __enter__(self) -> Cursor:
77
175
  self.cursor = self.connection.cursor()
@@ -89,7 +187,7 @@ class OracleAsyncCursor:
89
187
 
90
188
  def __init__(self, connection: OracleAsyncConnection) -> None:
91
189
  self.connection = connection
92
- self.cursor: Optional[AsyncCursor] = None
190
+ self.cursor: AsyncCursor | None = None
93
191
 
94
192
  async def __aenter__(self) -> AsyncCursor:
95
193
  self.cursor = self.connection.cursor()
@@ -105,7 +203,11 @@ class OracleAsyncCursor:
105
203
 
106
204
 
107
205
  class OracleSyncExceptionHandler:
108
- """Context manager for handling Oracle database exceptions in synchronous operations."""
206
+ """Context manager for handling Oracle database exceptions.
207
+
208
+ Maps Oracle ORA-XXXXX error codes to specific SQLSpec exceptions
209
+ for better error handling in application code.
210
+ """
109
211
 
110
212
  __slots__ = ()
111
213
 
@@ -113,46 +215,101 @@ class OracleSyncExceptionHandler:
113
215
  return None
114
216
 
115
217
  def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
116
- _ = exc_tb # Mark as intentionally unused
218
+ _ = exc_tb
117
219
  if exc_type is None:
118
220
  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
221
  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
222
+ self._map_oracle_exception(exc_val)
223
+
224
+ def _map_oracle_exception(self, e: Any) -> None:
225
+ """Map Oracle exception to SQLSpec exception.
226
+
227
+ Args:
228
+ e: oracledb.DatabaseError instance
229
+ """
230
+ error_obj = e.args[0] if e.args else None
231
+ if not error_obj:
232
+ self._raise_generic_error(e, None)
233
+
234
+ error_code = getattr(error_obj, "code", None)
235
+
236
+ if not error_code:
237
+ self._raise_generic_error(e, None)
238
+
239
+ if error_code == 1:
240
+ self._raise_unique_violation(e, error_code)
241
+ elif error_code in {2291, 2292}:
242
+ self._raise_foreign_key_violation(e, error_code)
243
+ elif error_code == ORA_CHECK_CONSTRAINT:
244
+ self._raise_check_violation(e, error_code)
245
+ elif error_code in {1400, 1407}:
246
+ self._raise_not_null_violation(e, error_code)
247
+ elif error_code and ORA_INTEGRITY_RANGE_START <= error_code < ORA_INTEGRITY_RANGE_END:
248
+ self._raise_integrity_error(e, error_code)
249
+ elif error_code in {1017, 12154, 12541, 12545, 12514, 12505}:
250
+ self._raise_connection_error(e, error_code)
251
+ elif error_code in {60, 8176}:
252
+ self._raise_transaction_error(e, error_code)
253
+ elif error_code in {1722, 1858, 1840}:
254
+ self._raise_data_error(e, error_code)
255
+ elif error_code and ORA_PARSING_RANGE_START <= error_code < ORA_PARSING_RANGE_END:
256
+ self._raise_parsing_error(e, error_code)
257
+ elif error_code == ORA_TABLESPACE_FULL:
258
+ self._raise_operational_error(e, error_code)
259
+ else:
260
+ self._raise_generic_error(e, error_code)
261
+
262
+ def _raise_unique_violation(self, e: Any, code: int) -> None:
263
+ msg = f"Oracle unique constraint violation [ORA-{code:05d}]: {e}"
264
+ raise UniqueViolationError(msg) from e
265
+
266
+ def _raise_foreign_key_violation(self, e: Any, code: int) -> None:
267
+ msg = f"Oracle foreign key constraint violation [ORA-{code:05d}]: {e}"
268
+ raise ForeignKeyViolationError(msg) from e
269
+
270
+ def _raise_check_violation(self, e: Any, code: int) -> None:
271
+ msg = f"Oracle check constraint violation [ORA-{code:05d}]: {e}"
272
+ raise CheckViolationError(msg) from e
273
+
274
+ def _raise_not_null_violation(self, e: Any, code: int) -> None:
275
+ msg = f"Oracle not-null constraint violation [ORA-{code:05d}]: {e}"
276
+ raise NotNullViolationError(msg) from e
277
+
278
+ def _raise_integrity_error(self, e: Any, code: int) -> None:
279
+ msg = f"Oracle integrity constraint violation [ORA-{code:05d}]: {e}"
280
+ raise IntegrityError(msg) from e
281
+
282
+ def _raise_parsing_error(self, e: Any, code: int) -> None:
283
+ msg = f"Oracle SQL syntax error [ORA-{code:05d}]: {e}"
284
+ raise SQLParsingError(msg) from e
285
+
286
+ def _raise_connection_error(self, e: Any, code: int) -> None:
287
+ msg = f"Oracle connection error [ORA-{code:05d}]: {e}"
288
+ raise DatabaseConnectionError(msg) from e
289
+
290
+ def _raise_transaction_error(self, e: Any, code: int) -> None:
291
+ msg = f"Oracle transaction error [ORA-{code:05d}]: {e}"
292
+ raise TransactionError(msg) from e
293
+
294
+ def _raise_data_error(self, e: Any, code: int) -> None:
295
+ msg = f"Oracle data error [ORA-{code:05d}]: {e}"
296
+ raise DataError(msg) from e
297
+
298
+ def _raise_operational_error(self, e: Any, code: int) -> None:
299
+ msg = f"Oracle operational error [ORA-{code:05d}]: {e}"
300
+ raise OperationalError(msg) from e
301
+
302
+ def _raise_generic_error(self, e: Any, code: "int | None") -> None:
303
+ msg = f"Oracle database error [ORA-{code:05d}]: {e}" if code else f"Oracle database error: {e}"
304
+ raise SQLSpecError(msg) from e
152
305
 
153
306
 
154
307
  class OracleAsyncExceptionHandler:
155
- """Context manager for handling Oracle database exceptions in asynchronous operations."""
308
+ """Async context manager for handling Oracle database exceptions.
309
+
310
+ Maps Oracle ORA-XXXXX error codes to specific SQLSpec exceptions
311
+ for better error handling in application code.
312
+ """
156
313
 
157
314
  __slots__ = ()
158
315
 
@@ -160,42 +317,93 @@ class OracleAsyncExceptionHandler:
160
317
  return None
161
318
 
162
319
  async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
163
- _ = exc_tb # Mark as intentionally unused
320
+ _ = exc_tb
164
321
  if exc_type is None:
165
322
  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
323
  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
324
+ self._map_oracle_exception(exc_val)
325
+
326
+ def _map_oracle_exception(self, e: Any) -> None:
327
+ """Map Oracle exception to SQLSpec exception.
328
+
329
+ Args:
330
+ e: oracledb.DatabaseError instance
331
+ """
332
+ error_obj = e.args[0] if e.args else None
333
+ if not error_obj:
334
+ self._raise_generic_error(e, None)
335
+
336
+ error_code = getattr(error_obj, "code", None)
337
+
338
+ if not error_code:
339
+ self._raise_generic_error(e, None)
340
+
341
+ if error_code == 1:
342
+ self._raise_unique_violation(e, error_code)
343
+ elif error_code in {2291, 2292}:
344
+ self._raise_foreign_key_violation(e, error_code)
345
+ elif error_code == ORA_CHECK_CONSTRAINT:
346
+ self._raise_check_violation(e, error_code)
347
+ elif error_code in {1400, 1407}:
348
+ self._raise_not_null_violation(e, error_code)
349
+ elif error_code and ORA_INTEGRITY_RANGE_START <= error_code < ORA_INTEGRITY_RANGE_END:
350
+ self._raise_integrity_error(e, error_code)
351
+ elif error_code in {1017, 12154, 12541, 12545, 12514, 12505}:
352
+ self._raise_connection_error(e, error_code)
353
+ elif error_code in {60, 8176}:
354
+ self._raise_transaction_error(e, error_code)
355
+ elif error_code in {1722, 1858, 1840}:
356
+ self._raise_data_error(e, error_code)
357
+ elif error_code and ORA_PARSING_RANGE_START <= error_code < ORA_PARSING_RANGE_END:
358
+ self._raise_parsing_error(e, error_code)
359
+ elif error_code == ORA_TABLESPACE_FULL:
360
+ self._raise_operational_error(e, error_code)
361
+ else:
362
+ self._raise_generic_error(e, error_code)
363
+
364
+ def _raise_unique_violation(self, e: Any, code: int) -> None:
365
+ msg = f"Oracle unique constraint violation [ORA-{code:05d}]: {e}"
366
+ raise UniqueViolationError(msg) from e
367
+
368
+ def _raise_foreign_key_violation(self, e: Any, code: int) -> None:
369
+ msg = f"Oracle foreign key constraint violation [ORA-{code:05d}]: {e}"
370
+ raise ForeignKeyViolationError(msg) from e
371
+
372
+ def _raise_check_violation(self, e: Any, code: int) -> None:
373
+ msg = f"Oracle check constraint violation [ORA-{code:05d}]: {e}"
374
+ raise CheckViolationError(msg) from e
375
+
376
+ def _raise_not_null_violation(self, e: Any, code: int) -> None:
377
+ msg = f"Oracle not-null constraint violation [ORA-{code:05d}]: {e}"
378
+ raise NotNullViolationError(msg) from e
379
+
380
+ def _raise_integrity_error(self, e: Any, code: int) -> None:
381
+ msg = f"Oracle integrity constraint violation [ORA-{code:05d}]: {e}"
382
+ raise IntegrityError(msg) from e
383
+
384
+ def _raise_parsing_error(self, e: Any, code: int) -> None:
385
+ msg = f"Oracle SQL syntax error [ORA-{code:05d}]: {e}"
386
+ raise SQLParsingError(msg) from e
387
+
388
+ def _raise_connection_error(self, e: Any, code: int) -> None:
389
+ msg = f"Oracle connection error [ORA-{code:05d}]: {e}"
390
+ raise DatabaseConnectionError(msg) from e
391
+
392
+ def _raise_transaction_error(self, e: Any, code: int) -> None:
393
+ msg = f"Oracle transaction error [ORA-{code:05d}]: {e}"
394
+ raise TransactionError(msg) from e
395
+
396
+ def _raise_data_error(self, e: Any, code: int) -> None:
397
+ msg = f"Oracle data error [ORA-{code:05d}]: {e}"
398
+ raise DataError(msg) from e
399
+
400
+ def _raise_operational_error(self, e: Any, code: int) -> None:
401
+ msg = f"Oracle operational error [ORA-{code:05d}]: {e}"
402
+ raise OperationalError(msg) from e
403
+
404
+ def _raise_generic_error(self, e: Any, code: "int | None") -> None:
405
+ msg = f"Oracle database error [ORA-{code:05d}]: {e}" if code else f"Oracle database error: {e}"
406
+ raise SQLSpecError(msg) from e
199
407
 
200
408
 
201
409
  class OracleSyncDriver(SyncDriverAdapterBase):
@@ -211,8 +419,8 @@ class OracleSyncDriver(SyncDriverAdapterBase):
211
419
  def __init__(
212
420
  self,
213
421
  connection: OracleSyncConnection,
214
- statement_config: "Optional[StatementConfig]" = None,
215
- driver_features: "Optional[dict[str, Any]]" = None,
422
+ statement_config: "StatementConfig | None" = None,
423
+ driver_features: "dict[str, Any] | None" = None,
216
424
  ) -> None:
217
425
  if statement_config is None:
218
426
  cache_config = get_cache_config()
@@ -224,7 +432,7 @@ class OracleSyncDriver(SyncDriverAdapterBase):
224
432
  )
225
433
 
226
434
  super().__init__(connection=connection, statement_config=statement_config, driver_features=driver_features)
227
- self._data_dictionary: Optional[SyncDataDictionaryBase] = None
435
+ self._data_dictionary: SyncDataDictionaryBase | None = None
228
436
 
229
437
  def with_cursor(self, connection: OracleSyncConnection) -> OracleSyncCursor:
230
438
  """Create context manager for Oracle cursor.
@@ -241,7 +449,7 @@ class OracleSyncDriver(SyncDriverAdapterBase):
241
449
  """Handle database-specific exceptions and wrap them appropriately."""
242
450
  return OracleSyncExceptionHandler()
243
451
 
244
- def _try_special_handling(self, cursor: Any, statement: "SQL") -> "Optional[SQLResult]":
452
+ def _try_special_handling(self, cursor: Any, statement: "SQL") -> "SQLResult | None":
245
453
  """Hook for Oracle-specific special operations.
246
454
 
247
455
  Oracle doesn't have complex special operations like PostgreSQL COPY,
@@ -339,9 +547,10 @@ class OracleSyncDriver(SyncDriverAdapterBase):
339
547
  if statement.returns_rows():
340
548
  fetched_data = cursor.fetchall()
341
549
  column_names = [col[0] for col in cursor.description or []]
550
+ column_names = _normalize_column_names(column_names, self.driver_features)
342
551
 
343
- # Oracle returns tuples - convert to consistent dict format
344
- data = [dict(zip(column_names, row)) for row in fetched_data]
552
+ # Oracle returns tuples - convert to consistent dict format after LOB hydration
553
+ data = [dict(zip(column_names, _coerce_sync_row_values(row), strict=False)) for row in fetched_data]
345
554
 
346
555
  return self.create_execution_result(
347
556
  cursor, selected_data=data, column_names=column_names, data_row_count=len(data), is_select_result=True
@@ -383,6 +592,94 @@ class OracleSyncDriver(SyncDriverAdapterBase):
383
592
  msg = f"Failed to commit Oracle transaction: {e}"
384
593
  raise SQLSpecError(msg) from e
385
594
 
595
+ def select_to_arrow(
596
+ self,
597
+ statement: "Statement | QueryBuilder",
598
+ /,
599
+ *parameters: "StatementParameters | StatementFilter",
600
+ statement_config: "StatementConfig | None" = None,
601
+ return_format: str = "table",
602
+ native_only: bool = False,
603
+ batch_size: int | None = None,
604
+ arrow_schema: Any = None,
605
+ **kwargs: Any,
606
+ ) -> "Any":
607
+ """Execute query and return results as Apache Arrow format using Oracle native support.
608
+
609
+ This implementation uses Oracle's native fetch_df_all() method which returns
610
+ an OracleDataFrame with Arrow PyCapsule interface, providing zero-copy data
611
+ transfer and 5-10x performance improvement over dict conversion.
612
+
613
+ Args:
614
+ statement: SQL query string, Statement, or QueryBuilder
615
+ *parameters: Query parameters (same format as execute()/select())
616
+ statement_config: Optional statement configuration override
617
+ return_format: "table" for pyarrow.Table (default), "batches" for RecordBatch
618
+ native_only: If False, use base conversion path instead of native (default: False uses native)
619
+ batch_size: Rows per batch when using "batches" format
620
+ arrow_schema: Optional pyarrow.Schema for type casting
621
+ **kwargs: Additional keyword arguments
622
+
623
+ Returns:
624
+ ArrowResult containing pyarrow.Table or RecordBatch
625
+
626
+ Examples:
627
+ >>> result = driver.select_to_arrow(
628
+ ... "SELECT * FROM users WHERE age > :1", (18,)
629
+ ... )
630
+ >>> df = result.to_pandas()
631
+ >>> print(df.head())
632
+ """
633
+ # Check pyarrow is available
634
+ ensure_pyarrow()
635
+
636
+ # If native_only=False explicitly passed, use base conversion path
637
+ if native_only is False:
638
+ return super().select_to_arrow(
639
+ statement,
640
+ *parameters,
641
+ statement_config=statement_config,
642
+ return_format=return_format,
643
+ native_only=native_only,
644
+ batch_size=batch_size,
645
+ arrow_schema=arrow_schema,
646
+ **kwargs,
647
+ )
648
+
649
+ import pyarrow as pa
650
+
651
+ # Prepare statement with parameters
652
+ config = statement_config or self.statement_config
653
+ prepared_statement = self.prepare_statement(statement, parameters, statement_config=config, kwargs=kwargs)
654
+ sql, prepared_parameters = self._get_compiled_sql(prepared_statement, config)
655
+
656
+ # Use Oracle's native fetch_df_all() for zero-copy Arrow transfer
657
+ oracle_df = self.connection.fetch_df_all(
658
+ statement=sql, parameters=prepared_parameters or [], arraysize=batch_size or 1000
659
+ )
660
+
661
+ # Convert OracleDataFrame to PyArrow Table using PyCapsule interface
662
+ arrow_table = pa.table(oracle_df)
663
+
664
+ # Apply schema casting if provided
665
+ if arrow_schema is not None:
666
+ if not isinstance(arrow_schema, pa.Schema):
667
+ msg = f"arrow_schema must be a pyarrow.Schema, got {type(arrow_schema).__name__}"
668
+ raise TypeError(msg)
669
+ arrow_table = arrow_table.cast(arrow_schema)
670
+
671
+ # Convert to batches if requested
672
+ if return_format == "batches":
673
+ batches = arrow_table.to_batches()
674
+ arrow_data: Any = batches[0] if batches else pa.RecordBatch.from_pydict({})
675
+ else:
676
+ arrow_data = arrow_table
677
+
678
+ # Get row count
679
+ rows_affected = len(arrow_table)
680
+
681
+ return create_arrow_result(statement=prepared_statement, data=arrow_data, rows_affected=rows_affected)
682
+
386
683
  @property
387
684
  def data_dictionary(self) -> "SyncDataDictionaryBase":
388
685
  """Get the data dictionary for this driver.
@@ -408,8 +705,8 @@ class OracleAsyncDriver(AsyncDriverAdapterBase):
408
705
  def __init__(
409
706
  self,
410
707
  connection: OracleAsyncConnection,
411
- statement_config: "Optional[StatementConfig]" = None,
412
- driver_features: "Optional[dict[str, Any]]" = None,
708
+ statement_config: "StatementConfig | None" = None,
709
+ driver_features: "dict[str, Any] | None" = None,
413
710
  ) -> None:
414
711
  if statement_config is None:
415
712
  cache_config = get_cache_config()
@@ -421,7 +718,7 @@ class OracleAsyncDriver(AsyncDriverAdapterBase):
421
718
  )
422
719
 
423
720
  super().__init__(connection=connection, statement_config=statement_config, driver_features=driver_features)
424
- self._data_dictionary: Optional[AsyncDataDictionaryBase] = None
721
+ self._data_dictionary: AsyncDataDictionaryBase | None = None
425
722
 
426
723
  def with_cursor(self, connection: OracleAsyncConnection) -> OracleAsyncCursor:
427
724
  """Create context manager for Oracle cursor.
@@ -438,7 +735,7 @@ class OracleAsyncDriver(AsyncDriverAdapterBase):
438
735
  """Handle database-specific exceptions and wrap them appropriately."""
439
736
  return OracleAsyncExceptionHandler()
440
737
 
441
- async def _try_special_handling(self, cursor: Any, statement: "SQL") -> "Optional[SQLResult]":
738
+ async def _try_special_handling(self, cursor: Any, statement: "SQL") -> "SQLResult | None":
442
739
  """Hook for Oracle-specific special operations.
443
740
 
444
741
  Oracle doesn't have complex special operations like PostgreSQL COPY,
@@ -531,9 +828,13 @@ class OracleAsyncDriver(AsyncDriverAdapterBase):
531
828
  if statement.returns_rows():
532
829
  fetched_data = await cursor.fetchall()
533
830
  column_names = [col[0] for col in cursor.description or []]
831
+ column_names = _normalize_column_names(column_names, self.driver_features)
534
832
 
535
- # Oracle returns tuples - convert to consistent dict format
536
- data = [dict(zip(column_names, row)) for row in fetched_data]
833
+ # Oracle returns tuples - convert to consistent dict format after LOB hydration
834
+ data = []
835
+ for row in fetched_data:
836
+ coerced_row = await _coerce_async_row_values(row)
837
+ data.append(dict(zip(column_names, coerced_row, strict=False)))
537
838
 
538
839
  return self.create_execution_result(
539
840
  cursor, selected_data=data, column_names=column_names, data_row_count=len(data), is_select_result=True
@@ -575,6 +876,94 @@ class OracleAsyncDriver(AsyncDriverAdapterBase):
575
876
  msg = f"Failed to commit Oracle transaction: {e}"
576
877
  raise SQLSpecError(msg) from e
577
878
 
879
+ async def select_to_arrow(
880
+ self,
881
+ statement: "Statement | QueryBuilder",
882
+ /,
883
+ *parameters: "StatementParameters | StatementFilter",
884
+ statement_config: "StatementConfig | None" = None,
885
+ return_format: str = "table",
886
+ native_only: bool = False,
887
+ batch_size: int | None = None,
888
+ arrow_schema: Any = None,
889
+ **kwargs: Any,
890
+ ) -> "Any":
891
+ """Execute query and return results as Apache Arrow format using Oracle native support.
892
+
893
+ This implementation uses Oracle's native fetch_df_all() method which returns
894
+ an OracleDataFrame with Arrow PyCapsule interface, providing zero-copy data
895
+ transfer and 5-10x performance improvement over dict conversion.
896
+
897
+ Args:
898
+ statement: SQL query string, Statement, or QueryBuilder
899
+ *parameters: Query parameters (same format as execute()/select())
900
+ statement_config: Optional statement configuration override
901
+ return_format: "table" for pyarrow.Table (default), "batches" for RecordBatch
902
+ native_only: If False, use base conversion path instead of native (default: False uses native)
903
+ batch_size: Rows per batch when using "batches" format
904
+ arrow_schema: Optional pyarrow.Schema for type casting
905
+ **kwargs: Additional keyword arguments
906
+
907
+ Returns:
908
+ ArrowResult containing pyarrow.Table or RecordBatch
909
+
910
+ Examples:
911
+ >>> result = await driver.select_to_arrow(
912
+ ... "SELECT * FROM users WHERE age > :1", (18,)
913
+ ... )
914
+ >>> df = result.to_pandas()
915
+ >>> print(df.head())
916
+ """
917
+ # Check pyarrow is available
918
+ ensure_pyarrow()
919
+
920
+ # If native_only=False explicitly passed, use base conversion path
921
+ if native_only is False:
922
+ return await super().select_to_arrow(
923
+ statement,
924
+ *parameters,
925
+ statement_config=statement_config,
926
+ return_format=return_format,
927
+ native_only=native_only,
928
+ batch_size=batch_size,
929
+ arrow_schema=arrow_schema,
930
+ **kwargs,
931
+ )
932
+
933
+ import pyarrow as pa
934
+
935
+ # Prepare statement with parameters
936
+ config = statement_config or self.statement_config
937
+ prepared_statement = self.prepare_statement(statement, parameters, statement_config=config, kwargs=kwargs)
938
+ sql, prepared_parameters = self._get_compiled_sql(prepared_statement, config)
939
+
940
+ # Use Oracle's native fetch_df_all() for zero-copy Arrow transfer
941
+ oracle_df = await self.connection.fetch_df_all(
942
+ statement=sql, parameters=prepared_parameters or [], arraysize=batch_size or 1000
943
+ )
944
+
945
+ # Convert OracleDataFrame to PyArrow Table using PyCapsule interface
946
+ arrow_table = pa.table(oracle_df)
947
+
948
+ # Apply schema casting if provided
949
+ if arrow_schema is not None:
950
+ if not isinstance(arrow_schema, pa.Schema):
951
+ msg = f"arrow_schema must be a pyarrow.Schema, got {type(arrow_schema).__name__}"
952
+ raise TypeError(msg)
953
+ arrow_table = arrow_table.cast(arrow_schema)
954
+
955
+ # Convert to batches if requested
956
+ if return_format == "batches":
957
+ batches = arrow_table.to_batches()
958
+ arrow_data: Any = batches[0] if batches else pa.RecordBatch.from_pydict({})
959
+ else:
960
+ arrow_data = arrow_table
961
+
962
+ # Get row count
963
+ rows_affected = len(arrow_table)
964
+
965
+ return create_arrow_result(statement=prepared_statement, data=arrow_data, rows_affected=rows_affected)
966
+
578
967
  @property
579
968
  def data_dictionary(self) -> "AsyncDataDictionaryBase":
580
969
  """Get the data dictionary for this driver.