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.
- sqlspec/__init__.py +7 -15
- sqlspec/_serialization.py +55 -25
- sqlspec/_typing.py +155 -52
- sqlspec/adapters/adbc/_types.py +1 -1
- sqlspec/adapters/adbc/adk/__init__.py +5 -0
- sqlspec/adapters/adbc/adk/store.py +880 -0
- sqlspec/adapters/adbc/config.py +62 -12
- sqlspec/adapters/adbc/data_dictionary.py +74 -2
- sqlspec/adapters/adbc/driver.py +226 -58
- sqlspec/adapters/adbc/litestar/__init__.py +5 -0
- sqlspec/adapters/adbc/litestar/store.py +504 -0
- sqlspec/adapters/adbc/type_converter.py +44 -50
- sqlspec/adapters/aiosqlite/_types.py +1 -1
- sqlspec/adapters/aiosqlite/adk/__init__.py +5 -0
- sqlspec/adapters/aiosqlite/adk/store.py +536 -0
- sqlspec/adapters/aiosqlite/config.py +86 -16
- sqlspec/adapters/aiosqlite/data_dictionary.py +34 -2
- sqlspec/adapters/aiosqlite/driver.py +127 -38
- sqlspec/adapters/aiosqlite/litestar/__init__.py +5 -0
- sqlspec/adapters/aiosqlite/litestar/store.py +281 -0
- sqlspec/adapters/aiosqlite/pool.py +7 -7
- sqlspec/adapters/asyncmy/__init__.py +7 -1
- sqlspec/adapters/asyncmy/_types.py +1 -1
- sqlspec/adapters/asyncmy/adk/__init__.py +5 -0
- sqlspec/adapters/asyncmy/adk/store.py +503 -0
- sqlspec/adapters/asyncmy/config.py +59 -17
- sqlspec/adapters/asyncmy/data_dictionary.py +41 -2
- sqlspec/adapters/asyncmy/driver.py +293 -62
- sqlspec/adapters/asyncmy/litestar/__init__.py +5 -0
- sqlspec/adapters/asyncmy/litestar/store.py +296 -0
- sqlspec/adapters/asyncpg/__init__.py +2 -1
- sqlspec/adapters/asyncpg/_type_handlers.py +71 -0
- sqlspec/adapters/asyncpg/_types.py +11 -7
- sqlspec/adapters/asyncpg/adk/__init__.py +5 -0
- sqlspec/adapters/asyncpg/adk/store.py +460 -0
- sqlspec/adapters/asyncpg/config.py +57 -36
- sqlspec/adapters/asyncpg/data_dictionary.py +48 -2
- sqlspec/adapters/asyncpg/driver.py +153 -23
- sqlspec/adapters/asyncpg/litestar/__init__.py +5 -0
- sqlspec/adapters/asyncpg/litestar/store.py +253 -0
- sqlspec/adapters/bigquery/_types.py +1 -1
- sqlspec/adapters/bigquery/adk/__init__.py +5 -0
- sqlspec/adapters/bigquery/adk/store.py +585 -0
- sqlspec/adapters/bigquery/config.py +36 -11
- sqlspec/adapters/bigquery/data_dictionary.py +42 -2
- sqlspec/adapters/bigquery/driver.py +489 -144
- sqlspec/adapters/bigquery/litestar/__init__.py +5 -0
- sqlspec/adapters/bigquery/litestar/store.py +327 -0
- sqlspec/adapters/bigquery/type_converter.py +55 -23
- sqlspec/adapters/duckdb/_types.py +2 -2
- sqlspec/adapters/duckdb/adk/__init__.py +14 -0
- sqlspec/adapters/duckdb/adk/store.py +563 -0
- sqlspec/adapters/duckdb/config.py +79 -21
- sqlspec/adapters/duckdb/data_dictionary.py +41 -2
- sqlspec/adapters/duckdb/driver.py +225 -44
- sqlspec/adapters/duckdb/litestar/__init__.py +5 -0
- sqlspec/adapters/duckdb/litestar/store.py +332 -0
- sqlspec/adapters/duckdb/pool.py +5 -5
- sqlspec/adapters/duckdb/type_converter.py +51 -21
- sqlspec/adapters/oracledb/_numpy_handlers.py +133 -0
- sqlspec/adapters/oracledb/_types.py +20 -2
- sqlspec/adapters/oracledb/adk/__init__.py +5 -0
- sqlspec/adapters/oracledb/adk/store.py +1628 -0
- sqlspec/adapters/oracledb/config.py +120 -36
- sqlspec/adapters/oracledb/data_dictionary.py +87 -20
- sqlspec/adapters/oracledb/driver.py +475 -86
- sqlspec/adapters/oracledb/litestar/__init__.py +5 -0
- sqlspec/adapters/oracledb/litestar/store.py +765 -0
- sqlspec/adapters/oracledb/migrations.py +316 -25
- sqlspec/adapters/oracledb/type_converter.py +91 -16
- sqlspec/adapters/psqlpy/_type_handlers.py +44 -0
- sqlspec/adapters/psqlpy/_types.py +2 -1
- sqlspec/adapters/psqlpy/adk/__init__.py +5 -0
- sqlspec/adapters/psqlpy/adk/store.py +483 -0
- sqlspec/adapters/psqlpy/config.py +45 -19
- sqlspec/adapters/psqlpy/data_dictionary.py +48 -2
- sqlspec/adapters/psqlpy/driver.py +108 -41
- sqlspec/adapters/psqlpy/litestar/__init__.py +5 -0
- sqlspec/adapters/psqlpy/litestar/store.py +272 -0
- sqlspec/adapters/psqlpy/type_converter.py +40 -11
- sqlspec/adapters/psycopg/_type_handlers.py +80 -0
- sqlspec/adapters/psycopg/_types.py +2 -1
- sqlspec/adapters/psycopg/adk/__init__.py +5 -0
- sqlspec/adapters/psycopg/adk/store.py +962 -0
- sqlspec/adapters/psycopg/config.py +65 -37
- sqlspec/adapters/psycopg/data_dictionary.py +91 -3
- sqlspec/adapters/psycopg/driver.py +200 -78
- sqlspec/adapters/psycopg/litestar/__init__.py +5 -0
- sqlspec/adapters/psycopg/litestar/store.py +554 -0
- sqlspec/adapters/sqlite/__init__.py +2 -1
- sqlspec/adapters/sqlite/_type_handlers.py +86 -0
- sqlspec/adapters/sqlite/_types.py +1 -1
- sqlspec/adapters/sqlite/adk/__init__.py +5 -0
- sqlspec/adapters/sqlite/adk/store.py +582 -0
- sqlspec/adapters/sqlite/config.py +85 -16
- sqlspec/adapters/sqlite/data_dictionary.py +34 -2
- sqlspec/adapters/sqlite/driver.py +120 -52
- sqlspec/adapters/sqlite/litestar/__init__.py +5 -0
- sqlspec/adapters/sqlite/litestar/store.py +318 -0
- sqlspec/adapters/sqlite/pool.py +5 -5
- sqlspec/base.py +45 -26
- sqlspec/builder/__init__.py +73 -4
- sqlspec/builder/_base.py +91 -58
- sqlspec/builder/_column.py +5 -5
- sqlspec/builder/_ddl.py +98 -89
- sqlspec/builder/_delete.py +5 -4
- sqlspec/builder/_dml.py +388 -0
- sqlspec/{_sql.py → builder/_factory.py} +41 -44
- sqlspec/builder/_insert.py +5 -82
- sqlspec/builder/{mixins/_join_operations.py → _join.py} +145 -143
- sqlspec/builder/_merge.py +446 -11
- sqlspec/builder/_parsing_utils.py +9 -11
- sqlspec/builder/_select.py +1313 -25
- sqlspec/builder/_update.py +11 -42
- sqlspec/cli.py +76 -69
- sqlspec/config.py +331 -62
- sqlspec/core/__init__.py +5 -4
- sqlspec/core/cache.py +18 -18
- sqlspec/core/compiler.py +6 -8
- sqlspec/core/filters.py +55 -47
- sqlspec/core/hashing.py +9 -9
- sqlspec/core/parameters.py +76 -45
- sqlspec/core/result.py +234 -47
- sqlspec/core/splitter.py +16 -17
- sqlspec/core/statement.py +32 -31
- sqlspec/core/type_conversion.py +3 -2
- sqlspec/driver/__init__.py +1 -3
- sqlspec/driver/_async.py +183 -160
- sqlspec/driver/_common.py +197 -109
- sqlspec/driver/_sync.py +189 -161
- sqlspec/driver/mixins/_result_tools.py +20 -236
- sqlspec/driver/mixins/_sql_translator.py +4 -4
- sqlspec/exceptions.py +70 -7
- sqlspec/extensions/adk/__init__.py +53 -0
- sqlspec/extensions/adk/_types.py +51 -0
- sqlspec/extensions/adk/converters.py +172 -0
- sqlspec/extensions/adk/migrations/0001_create_adk_tables.py +144 -0
- sqlspec/extensions/adk/migrations/__init__.py +0 -0
- sqlspec/extensions/adk/service.py +181 -0
- sqlspec/extensions/adk/store.py +536 -0
- sqlspec/extensions/aiosql/adapter.py +69 -61
- sqlspec/extensions/fastapi/__init__.py +21 -0
- sqlspec/extensions/fastapi/extension.py +331 -0
- sqlspec/extensions/fastapi/providers.py +543 -0
- sqlspec/extensions/flask/__init__.py +36 -0
- sqlspec/extensions/flask/_state.py +71 -0
- sqlspec/extensions/flask/_utils.py +40 -0
- sqlspec/extensions/flask/extension.py +389 -0
- sqlspec/extensions/litestar/__init__.py +21 -4
- sqlspec/extensions/litestar/cli.py +54 -10
- sqlspec/extensions/litestar/config.py +56 -266
- sqlspec/extensions/litestar/handlers.py +46 -17
- sqlspec/extensions/litestar/migrations/0001_create_session_table.py +137 -0
- sqlspec/extensions/litestar/migrations/__init__.py +3 -0
- sqlspec/extensions/litestar/plugin.py +349 -224
- sqlspec/extensions/litestar/providers.py +25 -25
- sqlspec/extensions/litestar/store.py +265 -0
- sqlspec/extensions/starlette/__init__.py +10 -0
- sqlspec/extensions/starlette/_state.py +25 -0
- sqlspec/extensions/starlette/_utils.py +52 -0
- sqlspec/extensions/starlette/extension.py +254 -0
- sqlspec/extensions/starlette/middleware.py +154 -0
- sqlspec/loader.py +30 -49
- sqlspec/migrations/base.py +200 -76
- sqlspec/migrations/commands.py +591 -62
- sqlspec/migrations/context.py +6 -9
- sqlspec/migrations/fix.py +199 -0
- sqlspec/migrations/loaders.py +47 -19
- sqlspec/migrations/runner.py +241 -75
- sqlspec/migrations/tracker.py +237 -21
- sqlspec/migrations/utils.py +51 -3
- sqlspec/migrations/validation.py +177 -0
- sqlspec/protocols.py +106 -36
- sqlspec/storage/_utils.py +85 -0
- sqlspec/storage/backends/fsspec.py +133 -107
- sqlspec/storage/backends/local.py +78 -51
- sqlspec/storage/backends/obstore.py +276 -168
- sqlspec/storage/registry.py +75 -39
- sqlspec/typing.py +30 -84
- sqlspec/utils/__init__.py +25 -4
- sqlspec/utils/arrow_helpers.py +81 -0
- sqlspec/utils/config_resolver.py +6 -6
- sqlspec/utils/correlation.py +4 -5
- sqlspec/utils/data_transformation.py +3 -2
- sqlspec/utils/deprecation.py +9 -8
- sqlspec/utils/fixtures.py +4 -4
- sqlspec/utils/logging.py +46 -6
- sqlspec/utils/module_loader.py +205 -5
- sqlspec/utils/portal.py +311 -0
- sqlspec/utils/schema.py +288 -0
- sqlspec/utils/serializers.py +113 -4
- sqlspec/utils/sync_tools.py +36 -22
- sqlspec/utils/text.py +1 -2
- sqlspec/utils/type_guards.py +136 -20
- sqlspec/utils/version.py +433 -0
- {sqlspec-0.26.0.dist-info → sqlspec-0.28.0.dist-info}/METADATA +41 -22
- sqlspec-0.28.0.dist-info/RECORD +221 -0
- sqlspec/builder/mixins/__init__.py +0 -55
- sqlspec/builder/mixins/_cte_and_set_ops.py +0 -253
- sqlspec/builder/mixins/_delete_operations.py +0 -50
- sqlspec/builder/mixins/_insert_operations.py +0 -282
- sqlspec/builder/mixins/_merge_operations.py +0 -698
- sqlspec/builder/mixins/_order_limit_operations.py +0 -145
- sqlspec/builder/mixins/_pivot_operations.py +0 -157
- sqlspec/builder/mixins/_select_operations.py +0 -930
- sqlspec/builder/mixins/_update_operations.py +0 -199
- sqlspec/builder/mixins/_where_clause.py +0 -1298
- sqlspec-0.26.0.dist-info/RECORD +0 -157
- sqlspec-0.26.0.dist-info/licenses/NOTICE +0 -29
- {sqlspec-0.26.0.dist-info → sqlspec-0.28.0.dist-info}/WHEEL +0 -0
- {sqlspec-0.26.0.dist-info → sqlspec-0.28.0.dist-info}/entry_points.txt +0 -0
- {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
|
-
|
|
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.
|
|
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
|
|
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
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
"""
|
|
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
|
|
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
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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: "
|
|
215
|
-
driver_features: "
|
|
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:
|
|
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") -> "
|
|
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: "
|
|
412
|
-
driver_features: "
|
|
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:
|
|
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") -> "
|
|
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 = [
|
|
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.
|