sqlspec 0.11.0__py3-none-any.whl → 0.12.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 (155) hide show
  1. sqlspec/__init__.py +16 -3
  2. sqlspec/_serialization.py +3 -10
  3. sqlspec/_sql.py +1147 -0
  4. sqlspec/_typing.py +343 -41
  5. sqlspec/adapters/adbc/__init__.py +2 -6
  6. sqlspec/adapters/adbc/config.py +474 -149
  7. sqlspec/adapters/adbc/driver.py +330 -644
  8. sqlspec/adapters/aiosqlite/__init__.py +2 -6
  9. sqlspec/adapters/aiosqlite/config.py +143 -57
  10. sqlspec/adapters/aiosqlite/driver.py +269 -462
  11. sqlspec/adapters/asyncmy/__init__.py +3 -8
  12. sqlspec/adapters/asyncmy/config.py +247 -202
  13. sqlspec/adapters/asyncmy/driver.py +217 -451
  14. sqlspec/adapters/asyncpg/__init__.py +4 -7
  15. sqlspec/adapters/asyncpg/config.py +329 -176
  16. sqlspec/adapters/asyncpg/driver.py +418 -498
  17. sqlspec/adapters/bigquery/__init__.py +2 -2
  18. sqlspec/adapters/bigquery/config.py +407 -0
  19. sqlspec/adapters/bigquery/driver.py +592 -634
  20. sqlspec/adapters/duckdb/__init__.py +4 -1
  21. sqlspec/adapters/duckdb/config.py +432 -321
  22. sqlspec/adapters/duckdb/driver.py +393 -436
  23. sqlspec/adapters/oracledb/__init__.py +3 -8
  24. sqlspec/adapters/oracledb/config.py +625 -0
  25. sqlspec/adapters/oracledb/driver.py +549 -942
  26. sqlspec/adapters/psqlpy/__init__.py +4 -7
  27. sqlspec/adapters/psqlpy/config.py +372 -203
  28. sqlspec/adapters/psqlpy/driver.py +197 -550
  29. sqlspec/adapters/psycopg/__init__.py +3 -8
  30. sqlspec/adapters/psycopg/config.py +741 -0
  31. sqlspec/adapters/psycopg/driver.py +732 -733
  32. sqlspec/adapters/sqlite/__init__.py +2 -6
  33. sqlspec/adapters/sqlite/config.py +146 -81
  34. sqlspec/adapters/sqlite/driver.py +243 -426
  35. sqlspec/base.py +220 -825
  36. sqlspec/config.py +354 -0
  37. sqlspec/driver/__init__.py +22 -0
  38. sqlspec/driver/_async.py +252 -0
  39. sqlspec/driver/_common.py +338 -0
  40. sqlspec/driver/_sync.py +261 -0
  41. sqlspec/driver/mixins/__init__.py +17 -0
  42. sqlspec/driver/mixins/_pipeline.py +523 -0
  43. sqlspec/driver/mixins/_result_utils.py +122 -0
  44. sqlspec/driver/mixins/_sql_translator.py +35 -0
  45. sqlspec/driver/mixins/_storage.py +993 -0
  46. sqlspec/driver/mixins/_type_coercion.py +131 -0
  47. sqlspec/exceptions.py +299 -7
  48. sqlspec/extensions/aiosql/__init__.py +10 -0
  49. sqlspec/extensions/aiosql/adapter.py +474 -0
  50. sqlspec/extensions/litestar/__init__.py +1 -6
  51. sqlspec/extensions/litestar/_utils.py +1 -5
  52. sqlspec/extensions/litestar/config.py +5 -6
  53. sqlspec/extensions/litestar/handlers.py +13 -12
  54. sqlspec/extensions/litestar/plugin.py +22 -24
  55. sqlspec/extensions/litestar/providers.py +37 -55
  56. sqlspec/loader.py +528 -0
  57. sqlspec/service/__init__.py +3 -0
  58. sqlspec/service/base.py +24 -0
  59. sqlspec/service/pagination.py +26 -0
  60. sqlspec/statement/__init__.py +21 -0
  61. sqlspec/statement/builder/__init__.py +54 -0
  62. sqlspec/statement/builder/_ddl_utils.py +119 -0
  63. sqlspec/statement/builder/_parsing_utils.py +135 -0
  64. sqlspec/statement/builder/base.py +328 -0
  65. sqlspec/statement/builder/ddl.py +1379 -0
  66. sqlspec/statement/builder/delete.py +80 -0
  67. sqlspec/statement/builder/insert.py +274 -0
  68. sqlspec/statement/builder/merge.py +95 -0
  69. sqlspec/statement/builder/mixins/__init__.py +65 -0
  70. sqlspec/statement/builder/mixins/_aggregate_functions.py +151 -0
  71. sqlspec/statement/builder/mixins/_case_builder.py +91 -0
  72. sqlspec/statement/builder/mixins/_common_table_expr.py +91 -0
  73. sqlspec/statement/builder/mixins/_delete_from.py +34 -0
  74. sqlspec/statement/builder/mixins/_from.py +61 -0
  75. sqlspec/statement/builder/mixins/_group_by.py +119 -0
  76. sqlspec/statement/builder/mixins/_having.py +35 -0
  77. sqlspec/statement/builder/mixins/_insert_from_select.py +48 -0
  78. sqlspec/statement/builder/mixins/_insert_into.py +36 -0
  79. sqlspec/statement/builder/mixins/_insert_values.py +69 -0
  80. sqlspec/statement/builder/mixins/_join.py +110 -0
  81. sqlspec/statement/builder/mixins/_limit_offset.py +53 -0
  82. sqlspec/statement/builder/mixins/_merge_clauses.py +405 -0
  83. sqlspec/statement/builder/mixins/_order_by.py +46 -0
  84. sqlspec/statement/builder/mixins/_pivot.py +82 -0
  85. sqlspec/statement/builder/mixins/_returning.py +37 -0
  86. sqlspec/statement/builder/mixins/_select_columns.py +60 -0
  87. sqlspec/statement/builder/mixins/_set_ops.py +122 -0
  88. sqlspec/statement/builder/mixins/_unpivot.py +80 -0
  89. sqlspec/statement/builder/mixins/_update_from.py +54 -0
  90. sqlspec/statement/builder/mixins/_update_set.py +91 -0
  91. sqlspec/statement/builder/mixins/_update_table.py +29 -0
  92. sqlspec/statement/builder/mixins/_where.py +374 -0
  93. sqlspec/statement/builder/mixins/_window_functions.py +86 -0
  94. sqlspec/statement/builder/protocols.py +20 -0
  95. sqlspec/statement/builder/select.py +206 -0
  96. sqlspec/statement/builder/update.py +178 -0
  97. sqlspec/statement/filters.py +571 -0
  98. sqlspec/statement/parameters.py +736 -0
  99. sqlspec/statement/pipelines/__init__.py +67 -0
  100. sqlspec/statement/pipelines/analyzers/__init__.py +9 -0
  101. sqlspec/statement/pipelines/analyzers/_analyzer.py +649 -0
  102. sqlspec/statement/pipelines/base.py +315 -0
  103. sqlspec/statement/pipelines/context.py +119 -0
  104. sqlspec/statement/pipelines/result_types.py +41 -0
  105. sqlspec/statement/pipelines/transformers/__init__.py +8 -0
  106. sqlspec/statement/pipelines/transformers/_expression_simplifier.py +256 -0
  107. sqlspec/statement/pipelines/transformers/_literal_parameterizer.py +623 -0
  108. sqlspec/statement/pipelines/transformers/_remove_comments.py +66 -0
  109. sqlspec/statement/pipelines/transformers/_remove_hints.py +81 -0
  110. sqlspec/statement/pipelines/validators/__init__.py +23 -0
  111. sqlspec/statement/pipelines/validators/_dml_safety.py +275 -0
  112. sqlspec/statement/pipelines/validators/_parameter_style.py +297 -0
  113. sqlspec/statement/pipelines/validators/_performance.py +703 -0
  114. sqlspec/statement/pipelines/validators/_security.py +990 -0
  115. sqlspec/statement/pipelines/validators/base.py +67 -0
  116. sqlspec/statement/result.py +527 -0
  117. sqlspec/statement/splitter.py +701 -0
  118. sqlspec/statement/sql.py +1198 -0
  119. sqlspec/storage/__init__.py +15 -0
  120. sqlspec/storage/backends/__init__.py +0 -0
  121. sqlspec/storage/backends/base.py +166 -0
  122. sqlspec/storage/backends/fsspec.py +315 -0
  123. sqlspec/storage/backends/obstore.py +464 -0
  124. sqlspec/storage/protocol.py +170 -0
  125. sqlspec/storage/registry.py +315 -0
  126. sqlspec/typing.py +157 -36
  127. sqlspec/utils/correlation.py +155 -0
  128. sqlspec/utils/deprecation.py +3 -6
  129. sqlspec/utils/fixtures.py +6 -11
  130. sqlspec/utils/logging.py +135 -0
  131. sqlspec/utils/module_loader.py +45 -43
  132. sqlspec/utils/serializers.py +4 -0
  133. sqlspec/utils/singleton.py +6 -8
  134. sqlspec/utils/sync_tools.py +15 -27
  135. sqlspec/utils/text.py +58 -26
  136. {sqlspec-0.11.0.dist-info → sqlspec-0.12.0.dist-info}/METADATA +100 -26
  137. sqlspec-0.12.0.dist-info/RECORD +145 -0
  138. sqlspec/adapters/bigquery/config/__init__.py +0 -3
  139. sqlspec/adapters/bigquery/config/_common.py +0 -40
  140. sqlspec/adapters/bigquery/config/_sync.py +0 -87
  141. sqlspec/adapters/oracledb/config/__init__.py +0 -9
  142. sqlspec/adapters/oracledb/config/_asyncio.py +0 -186
  143. sqlspec/adapters/oracledb/config/_common.py +0 -131
  144. sqlspec/adapters/oracledb/config/_sync.py +0 -186
  145. sqlspec/adapters/psycopg/config/__init__.py +0 -19
  146. sqlspec/adapters/psycopg/config/_async.py +0 -169
  147. sqlspec/adapters/psycopg/config/_common.py +0 -56
  148. sqlspec/adapters/psycopg/config/_sync.py +0 -168
  149. sqlspec/filters.py +0 -330
  150. sqlspec/mixins.py +0 -306
  151. sqlspec/statement.py +0 -378
  152. sqlspec-0.11.0.dist-info/RECORD +0 -69
  153. {sqlspec-0.11.0.dist-info → sqlspec-0.12.0.dist-info}/WHEEL +0 -0
  154. {sqlspec-0.11.0.dist-info → sqlspec-0.12.0.dist-info}/licenses/LICENSE +0 -0
  155. {sqlspec-0.11.0.dist-info → sqlspec-0.12.0.dist-info}/licenses/NOTICE +0 -0
@@ -1,974 +1,581 @@
1
- import logging
1
+ from collections.abc import AsyncGenerator, Generator
2
2
  from contextlib import asynccontextmanager, contextmanager
3
- from typing import TYPE_CHECKING, Any, Optional, Union, cast, overload
3
+ from typing import Any, ClassVar, Optional, Union, cast
4
4
 
5
5
  from oracledb import AsyncConnection, AsyncCursor, Connection, Cursor
6
+ from sqlglot.dialects.dialect import DialectType
6
7
 
7
- from sqlspec.base import AsyncDriverAdapterProtocol, SyncDriverAdapterProtocol
8
- from sqlspec.mixins import (
9
- AsyncArrowBulkOperationsMixin,
10
- ResultConverter,
8
+ from sqlspec.driver import AsyncDriverAdapterProtocol, SyncDriverAdapterProtocol
9
+ from sqlspec.driver.mixins import (
10
+ AsyncPipelinedExecutionMixin,
11
+ AsyncStorageMixin,
11
12
  SQLTranslatorMixin,
12
- SyncArrowBulkOperationsMixin,
13
+ SyncPipelinedExecutionMixin,
14
+ SyncStorageMixin,
15
+ ToSchemaMixin,
16
+ TypeCoercionMixin,
13
17
  )
14
- from sqlspec.statement import SQLStatement
15
- from sqlspec.typing import ArrowTable, StatementParameterType, T
16
-
17
- if TYPE_CHECKING:
18
- from collections.abc import AsyncGenerator, Generator, Sequence
19
-
20
- from sqlspec.filters import StatementFilter
21
- from sqlspec.typing import ModelDTOT
18
+ from sqlspec.statement.parameters import ParameterStyle
19
+ from sqlspec.statement.result import ArrowResult, DMLResultDict, ScriptResultDict, SelectResultDict, SQLResult
20
+ from sqlspec.statement.sql import SQL, SQLConfig
21
+ from sqlspec.typing import DictRow, ModelDTOT, RowT, SQLParameterType
22
+ from sqlspec.utils.logging import get_logger
23
+ from sqlspec.utils.sync_tools import ensure_async_
22
24
 
23
25
  __all__ = ("OracleAsyncConnection", "OracleAsyncDriver", "OracleSyncConnection", "OracleSyncDriver")
24
26
 
25
27
  OracleSyncConnection = Connection
26
28
  OracleAsyncConnection = AsyncConnection
27
29
 
28
- logger = logging.getLogger("sqlspec")
29
-
30
-
31
- class OracleDriverBase:
32
- """Base class for Oracle drivers with common functionality."""
33
-
34
- dialect: str = "oracle"
35
-
36
- def _process_sql_params(
37
- self,
38
- sql: str,
39
- parameters: "Optional[StatementParameterType]" = None,
40
- /,
41
- *filters: "StatementFilter",
42
- **kwargs: Any,
43
- ) -> "tuple[str, Optional[Union[tuple[Any, ...], dict[str, Any]]]]":
44
- """Process SQL and parameters using SQLStatement with dialect support.
45
-
46
- Args:
47
- sql: The SQL statement to process.
48
- parameters: The parameters to bind to the statement.
49
- *filters: Statement filters to apply.
50
- **kwargs: Additional keyword arguments.
51
-
52
- Returns:
53
- A tuple of (sql, parameters) ready for execution.
54
- """
55
- # Special case: Oracle treats empty dicts as None
56
- if isinstance(parameters, dict) and not parameters and not kwargs:
57
- return sql, None
58
-
59
- # Create a SQLStatement with appropriate dialect
60
- statement = SQLStatement(sql, parameters, kwargs=kwargs, dialect=self.dialect)
61
-
62
- # Apply any filters
63
- for filter_obj in filters:
64
- statement = statement.apply_filter(filter_obj)
65
-
66
- processed_sql, processed_params, _ = statement.process()
67
- if processed_params is None:
68
- return processed_sql, None
69
- if isinstance(processed_params, dict):
70
- return processed_sql, processed_params
71
- if isinstance(processed_params, (list, tuple)):
72
- return processed_sql, tuple(processed_params)
73
- return processed_sql, (processed_params,) # type: ignore[unreachable]
30
+ logger = get_logger("adapters.oracledb")
31
+
32
+
33
+ def _process_oracle_parameters(params: Any) -> Any:
34
+ """Process parameters to handle Oracle-specific requirements.
35
+
36
+ - Extract values from TypedParameter objects
37
+ - Convert tuples to lists (Oracle doesn't support tuples)
38
+ """
39
+ from sqlspec.statement.parameters import TypedParameter
40
+
41
+ if params is None:
42
+ return None
43
+
44
+ # Handle TypedParameter objects
45
+ if isinstance(params, TypedParameter):
46
+ return _process_oracle_parameters(params.value)
47
+
48
+ if isinstance(params, tuple):
49
+ # Convert single tuple to list and process each element
50
+ return [_process_oracle_parameters(item) for item in params]
51
+ if isinstance(params, list):
52
+ # Process list of parameter sets
53
+ processed = []
54
+ for param_set in params:
55
+ if isinstance(param_set, tuple):
56
+ # Convert tuple to list and process each element
57
+ processed.append([_process_oracle_parameters(item) for item in param_set])
58
+ elif isinstance(param_set, list):
59
+ # Process each element in the list
60
+ processed.append([_process_oracle_parameters(item) for item in param_set])
61
+ else:
62
+ processed.append(_process_oracle_parameters(param_set))
63
+ return processed
64
+ if isinstance(params, dict):
65
+ # Process dict values
66
+ return {key: _process_oracle_parameters(value) for key, value in params.items()}
67
+ # Return as-is for other types
68
+ return params
74
69
 
75
70
 
76
71
  class OracleSyncDriver(
77
- OracleDriverBase,
78
- SyncArrowBulkOperationsMixin["OracleSyncConnection"],
79
- SQLTranslatorMixin["OracleSyncConnection"],
80
- SyncDriverAdapterProtocol["OracleSyncConnection"],
81
- ResultConverter,
72
+ SyncDriverAdapterProtocol[OracleSyncConnection, RowT],
73
+ SQLTranslatorMixin,
74
+ TypeCoercionMixin,
75
+ SyncStorageMixin,
76
+ SyncPipelinedExecutionMixin,
77
+ ToSchemaMixin,
82
78
  ):
83
- """Oracle Sync Driver Adapter."""
84
-
85
- connection: "OracleSyncConnection"
86
-
87
- def __init__(self, connection: "OracleSyncConnection") -> None:
88
- self.connection = connection
79
+ """Oracle Sync Driver Adapter. Refactored for new protocol."""
80
+
81
+ dialect: "DialectType" = "oracle"
82
+ supported_parameter_styles: "tuple[ParameterStyle, ...]" = (
83
+ ParameterStyle.NAMED_COLON,
84
+ ParameterStyle.POSITIONAL_COLON,
85
+ )
86
+ default_parameter_style: ParameterStyle = ParameterStyle.NAMED_COLON
87
+ support_native_arrow_export = True
88
+ __slots__ = ()
89
+
90
+ def __init__(
91
+ self,
92
+ connection: OracleSyncConnection,
93
+ config: Optional[SQLConfig] = None,
94
+ default_row_type: type[DictRow] = DictRow,
95
+ ) -> None:
96
+ super().__init__(connection=connection, config=config, default_row_type=default_row_type)
97
+
98
+ def _process_parameters(self, parameters: "SQLParameterType") -> "SQLParameterType":
99
+ """Process parameters to handle Oracle-specific requirements.
100
+
101
+ - Extract values from TypedParameter objects
102
+ - Convert tuples to lists (Oracle doesn't support tuples)
103
+ """
104
+ return _process_oracle_parameters(parameters)
89
105
 
90
- @staticmethod
91
106
  @contextmanager
92
- def _with_cursor(connection: "OracleSyncConnection") -> "Generator[Cursor, None, None]":
93
- cursor = connection.cursor()
107
+ def _get_cursor(self, connection: Optional[OracleSyncConnection] = None) -> Generator[Cursor, None, None]:
108
+ conn_to_use = connection or self.connection
109
+ cursor: Cursor = conn_to_use.cursor()
94
110
  try:
95
111
  yield cursor
96
112
  finally:
97
113
  cursor.close()
98
114
 
99
- # --- Public API Methods --- #
100
- @overload
101
- def select(
102
- self,
103
- sql: str,
104
- parameters: "Optional[StatementParameterType]" = None,
105
- /,
106
- *filters: "StatementFilter",
107
- connection: "Optional[OracleSyncConnection]" = None,
108
- schema_type: None = None,
109
- **kwargs: Any,
110
- ) -> "Sequence[dict[str, Any]]": ...
111
- @overload
112
- def select(
113
- self,
114
- sql: str,
115
- parameters: "Optional[StatementParameterType]" = None,
116
- /,
117
- *filters: "StatementFilter",
118
- connection: "Optional[OracleSyncConnection]" = None,
119
- schema_type: "type[ModelDTOT]",
120
- **kwargs: Any,
121
- ) -> "Sequence[ModelDTOT]": ...
122
- def select(
123
- self,
124
- sql: str,
125
- parameters: "Optional[StatementParameterType]" = None,
126
- /,
127
- *filters: "StatementFilter",
128
- connection: "Optional[OracleSyncConnection]" = None,
129
- schema_type: "Optional[type[ModelDTOT]]" = None,
130
- **kwargs: Any,
131
- ) -> "Sequence[Union[ModelDTOT, dict[str, Any]]]":
132
- """Fetch data from the database.
133
-
134
- Args:
135
- sql: The SQL query string.
136
- parameters: The parameters for the query (dict, tuple, list, or None).
137
- *filters: Statement filters to apply.
138
- connection: Optional connection override.
139
- schema_type: Optional schema class for the result.
140
- **kwargs: Additional keyword arguments to merge with parameters if parameters is a dict.
141
-
142
- Returns:
143
- List of row data as either model instances or dictionaries.
144
- """
145
- connection = self._connection(connection)
146
- sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
147
- with self._with_cursor(connection) as cursor:
148
- cursor.execute(sql, parameters) # pyright: ignore[reportUnknownMemberType]
149
- results = cursor.fetchall() # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType]
150
- if not results:
151
- return []
152
- # Get column names
153
- column_names = [col[0] for col in cursor.description or []] # pyright: ignore[reportUnknownMemberType,reportUnknownVariableType]
154
-
155
- return self.to_schema([dict(zip(column_names, row)) for row in results], schema_type=schema_type)
156
-
157
- @overload
158
- def select_one(
159
- self,
160
- sql: str,
161
- parameters: "Optional[StatementParameterType]" = None,
162
- /,
163
- *filters: "StatementFilter",
164
- connection: "Optional[OracleSyncConnection]" = None,
165
- schema_type: None = None,
166
- **kwargs: Any,
167
- ) -> "dict[str, Any]": ...
168
- @overload
169
- def select_one(
170
- self,
171
- sql: str,
172
- parameters: "Optional[StatementParameterType]" = None,
173
- /,
174
- *filters: "StatementFilter",
175
- connection: "Optional[OracleSyncConnection]" = None,
176
- schema_type: "type[ModelDTOT]",
177
- **kwargs: Any,
178
- ) -> "ModelDTOT": ...
179
- def select_one(
180
- self,
181
- sql: str,
182
- parameters: "Optional[StatementParameterType]" = None,
183
- /,
184
- *filters: "StatementFilter",
185
- connection: "Optional[OracleSyncConnection]" = None,
186
- schema_type: "Optional[type[ModelDTOT]]" = None,
187
- **kwargs: Any,
188
- ) -> "Union[ModelDTOT, dict[str, Any]]":
189
- """Fetch one row from the database.
190
-
191
- Args:
192
- sql: The SQL query string.
193
- parameters: The parameters for the query (dict, tuple, list, or None).
194
- *filters: Statement filters to apply.
195
- connection: Optional connection override.
196
- schema_type: Optional schema class for the result.
197
- **kwargs: Additional keyword arguments to merge with parameters if parameters is a dict.
198
-
199
- Returns:
200
- The first row of the query results.
201
- """
202
- connection = self._connection(connection)
203
- sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
204
-
205
- with self._with_cursor(connection) as cursor:
206
- cursor.execute(sql, parameters) # pyright: ignore[reportUnknownMemberType]
207
- result = cursor.fetchone() # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType]
208
- result = self.check_not_found(result) # pyright: ignore[reportUnknownArgumentType]
209
-
210
- # Get column names
211
- column_names = [col[0] for col in cursor.description or []] # pyright: ignore[reportUnknownMemberType,reportUnknownVariableType]
212
-
213
- return self.to_schema(dict(zip(column_names, result)), schema_type=schema_type)
214
-
215
- @overload
216
- def select_one_or_none(
217
- self,
218
- sql: str,
219
- parameters: "Optional[StatementParameterType]" = None,
220
- /,
221
- *filters: "StatementFilter",
222
- connection: "Optional[OracleSyncConnection]" = None,
223
- schema_type: None = None,
224
- **kwargs: Any,
225
- ) -> "Optional[dict[str, Any]]": ...
226
- @overload
227
- def select_one_or_none(
228
- self,
229
- sql: str,
230
- parameters: "Optional[StatementParameterType]" = None,
231
- /,
232
- *filters: "StatementFilter",
233
- connection: "Optional[OracleSyncConnection]" = None,
234
- schema_type: "type[ModelDTOT]",
235
- **kwargs: Any,
236
- ) -> "Optional[ModelDTOT]": ...
237
- def select_one_or_none(
238
- self,
239
- sql: str,
240
- parameters: "Optional[StatementParameterType]" = None,
241
- /,
242
- *filters: "StatementFilter",
243
- connection: "Optional[OracleSyncConnection]" = None,
244
- schema_type: "Optional[type[ModelDTOT]]" = None,
245
- **kwargs: Any,
246
- ) -> "Optional[Union[ModelDTOT, dict[str, Any]]]":
247
- """Fetch one row from the database or return None if no rows found.
248
-
249
- Args:
250
- sql: The SQL query string.
251
- parameters: The parameters for the query (dict, tuple, list, or None).
252
- *filters: Statement filters to apply.
253
- connection: Optional connection override.
254
- schema_type: Optional schema class for the result.
255
- **kwargs: Additional keyword arguments to merge with parameters if parameters is a dict.
256
-
257
- Returns:
258
- The first row of the query results, or None if no results found.
259
- """
260
- connection = self._connection(connection)
261
- sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
262
-
263
- with self._with_cursor(connection) as cursor:
264
- cursor.execute(sql, parameters) # pyright: ignore[reportUnknownMemberType]
265
- result = cursor.fetchone() # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType]
266
- if result is None:
267
- return None
268
-
269
- # Get column names
270
- column_names = [col[0] for col in cursor.description or []] # pyright: ignore[reportUnknownMemberType,reportUnknownVariableType]
271
-
272
- return self.to_schema(dict(zip(column_names, result)), schema_type=schema_type)
273
-
274
- @overload
275
- def select_value(
276
- self,
277
- sql: str,
278
- parameters: "Optional[StatementParameterType]" = None,
279
- /,
280
- *filters: "StatementFilter",
281
- connection: "Optional[OracleSyncConnection]" = None,
282
- schema_type: None = None,
283
- **kwargs: Any,
284
- ) -> "Any": ...
285
- @overload
286
- def select_value(
287
- self,
288
- sql: str,
289
- parameters: "Optional[StatementParameterType]" = None,
290
- /,
291
- *filters: "StatementFilter",
292
- connection: "Optional[OracleSyncConnection]" = None,
293
- schema_type: "type[T]",
294
- **kwargs: Any,
295
- ) -> "T": ...
296
- def select_value(
297
- self,
298
- sql: str,
299
- parameters: "Optional[StatementParameterType]" = None,
300
- /,
301
- *filters: "StatementFilter",
302
- connection: "Optional[OracleSyncConnection]" = None,
303
- schema_type: "Optional[type[T]]" = None,
304
- **kwargs: Any,
305
- ) -> "Union[T, Any]":
306
- """Fetch a single value from the database.
307
-
308
- Args:
309
- sql: The SQL query string.
310
- parameters: The parameters for the query (dict, tuple, list, or None).
311
- *filters: Statement filters to apply.
312
- connection: Optional connection override.
313
- schema_type: Optional type to convert the result to.
314
- **kwargs: Additional keyword arguments to merge with parameters if parameters is a dict.
315
-
316
- Returns:
317
- The first value of the first row of the query results.
318
- """
319
- connection = self._connection(connection)
320
- sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
321
-
322
- with self._with_cursor(connection) as cursor:
323
- cursor.execute(sql, parameters) # pyright: ignore[reportUnknownMemberType]
324
- result = cursor.fetchone() # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType]
325
- result = self.check_not_found(result) # pyright: ignore[reportUnknownArgumentType]
326
-
327
- if schema_type is None:
328
- return result[0] # pyright: ignore[reportUnknownArgumentType]
329
- return schema_type(result[0]) # type: ignore[call-arg]
330
-
331
- @overload
332
- def select_value_or_none(
333
- self,
334
- sql: str,
335
- parameters: "Optional[StatementParameterType]" = None,
336
- /,
337
- *filters: "StatementFilter",
338
- connection: "Optional[OracleSyncConnection]" = None,
339
- schema_type: None = None,
340
- **kwargs: Any,
341
- ) -> "Optional[Any]": ...
342
- @overload
343
- def select_value_or_none(
344
- self,
345
- sql: str,
346
- parameters: "Optional[StatementParameterType]" = None,
347
- /,
348
- *filters: "StatementFilter",
349
- connection: "Optional[OracleSyncConnection]" = None,
350
- schema_type: "type[T]",
351
- **kwargs: Any,
352
- ) -> "Optional[T]": ...
353
- def select_value_or_none(
354
- self,
355
- sql: str,
356
- parameters: "Optional[StatementParameterType]" = None,
357
- /,
358
- *filters: "StatementFilter",
359
- connection: "Optional[OracleSyncConnection]" = None,
360
- schema_type: "Optional[type[T]]" = None,
361
- **kwargs: Any,
362
- ) -> "Optional[Union[T, Any]]":
363
- """Fetch a single value or None if not found.
364
-
365
- Args:
366
- sql: The SQL query string.
367
- parameters: The parameters for the query (dict, tuple, list, or None).
368
- *filters: Statement filters to apply.
369
- connection: Optional connection override.
370
- schema_type: Optional type to convert the result to.
371
- **kwargs: Additional keyword arguments to merge with parameters if parameters is a dict.
372
-
373
- Returns:
374
- The first value of the first row of the query results, or None if no results found.
375
- """
376
- connection = self._connection(connection)
377
- sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
378
-
379
- with self._with_cursor(connection) as cursor:
380
- cursor.execute(sql, parameters) # pyright: ignore[reportUnknownMemberType]
381
- result = cursor.fetchone() # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType]
382
- if result is None:
383
- return None
384
-
385
- if schema_type is None:
386
- return result[0] # pyright: ignore[reportUnknownArgumentType]
387
- return schema_type(result[0]) # type: ignore[call-arg]
388
-
389
- def insert_update_delete(
390
- self,
391
- sql: str,
392
- parameters: "Optional[StatementParameterType]" = None,
393
- /,
394
- *filters: "StatementFilter",
395
- connection: "Optional[OracleSyncConnection]" = None,
396
- **kwargs: Any,
397
- ) -> int:
398
- """Execute an insert, update, or delete statement.
399
-
400
- Args:
401
- sql: The SQL statement to execute.
402
- parameters: The parameters for the statement (dict, tuple, list, or None).
403
- *filters: Statement filters to apply.
404
- connection: Optional connection override.
405
- **kwargs: Additional keyword arguments to merge with parameters if parameters is a dict.
406
-
407
- Returns:
408
- The number of rows affected by the statement.
409
- """
410
- connection = self._connection(connection)
411
- sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
412
-
413
- with self._with_cursor(connection) as cursor:
414
- cursor.execute(sql, parameters) # pyright: ignore[reportUnknownMemberType]
415
- return cursor.rowcount # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType]
416
-
417
- @overload
418
- def insert_update_delete_returning(
419
- self,
420
- sql: str,
421
- parameters: "Optional[StatementParameterType]" = None,
422
- /,
423
- *filters: "StatementFilter",
424
- connection: "Optional[OracleSyncConnection]" = None,
425
- schema_type: None = None,
426
- **kwargs: Any,
427
- ) -> "dict[str, Any]": ...
428
- @overload
429
- def insert_update_delete_returning(
430
- self,
431
- sql: str,
432
- parameters: "Optional[StatementParameterType]" = None,
433
- /,
434
- *filters: "StatementFilter",
435
- connection: "Optional[OracleSyncConnection]" = None,
436
- schema_type: "type[ModelDTOT]",
437
- **kwargs: Any,
438
- ) -> "ModelDTOT": ...
439
- def insert_update_delete_returning(
440
- self,
441
- sql: str,
442
- parameters: "Optional[StatementParameterType]" = None,
443
- /,
444
- *filters: "StatementFilter",
445
- connection: "Optional[OracleSyncConnection]" = None,
446
- schema_type: "Optional[type[ModelDTOT]]" = None,
447
- **kwargs: Any,
448
- ) -> "Optional[Union[dict[str, Any], ModelDTOT]]":
449
- """Insert, update, or delete data from the database and return result.
450
-
451
- Returns:
452
- The first row of results.
453
- """
454
- connection = self._connection(connection)
455
- sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
456
-
457
- with self._with_cursor(connection) as cursor:
458
- cursor.execute(sql, parameters) # pyright: ignore[reportUnknownMemberType]
459
- result = cursor.fetchone() # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType]
460
-
461
- if result is None:
462
- return None
463
-
464
- # Get column names
465
- column_names = [col[0] for col in cursor.description or []] # pyright: ignore[reportUnknownMemberType,reportUnknownVariableType]
466
-
467
- if schema_type is not None:
468
- return cast("ModelDTOT", schema_type(**dict(zip(column_names, result)))) # pyright: ignore[reportUnknownArgumentType]
469
- # Always return dictionaries
470
- return dict(zip(column_names, result)) # pyright: ignore[reportUnknownArgumentType,reportUnknownVariableType]
471
-
472
- def execute_script(
473
- self,
474
- sql: str,
475
- parameters: "Optional[StatementParameterType]" = None,
476
- /,
477
- connection: "Optional[OracleSyncConnection]" = None,
478
- **kwargs: Any,
479
- ) -> str:
480
- """Execute a SQL script.
481
-
482
- Args:
483
- sql: The SQL script to execute.
484
- parameters: The parameters for the script (dict, tuple, list, or None).
485
- *filters: Statement filters to apply.
486
- connection: Optional connection override.
487
- **kwargs: Additional keyword arguments to merge with parameters if parameters is a dict.
488
-
489
- Returns:
490
- A success message.
491
- """
492
- connection = self._connection(connection)
493
- sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
494
-
495
- with self._with_cursor(connection) as cursor:
496
- cursor.execute(sql, parameters) # pyright: ignore[reportUnknownMemberType]
497
- return str(cursor.rowcount) # pyright: ignore[reportUnknownMemberType]
498
-
499
- def select_arrow( # pyright: ignore[reportUnknownParameterType]
500
- self,
501
- sql: str,
502
- parameters: "Optional[StatementParameterType]" = None,
503
- /,
504
- *,
505
- connection: "Optional[OracleSyncConnection]" = None,
506
- **kwargs: Any,
507
- ) -> "ArrowTable": # pyright: ignore[reportUnknownVariableType]
508
- """Execute a SQL query and return results as an Apache Arrow Table.
509
-
510
- Returns:
511
- An Apache Arrow Table containing the query results.
512
- """
513
-
514
- connection = self._connection(connection)
515
- sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
516
- results = connection.fetch_df_all(sql, parameters)
517
- return cast("ArrowTable", ArrowTable.from_arrays(arrays=results.column_arrays(), names=results.column_names())) # pyright: ignore
518
-
519
- def _connection(self, connection: "Optional[OracleSyncConnection]" = None) -> "OracleSyncConnection":
520
- """Get the connection to use for the operation.
521
-
522
- Args:
523
- connection: Optional connection to use.
524
-
525
- Returns:
526
- The connection to use.
527
- """
528
- return connection or self.connection
115
+ def _execute_statement(
116
+ self, statement: SQL, connection: Optional[OracleSyncConnection] = None, **kwargs: Any
117
+ ) -> Union[SelectResultDict, DMLResultDict, ScriptResultDict]:
118
+ if statement.is_script:
119
+ sql, _ = statement.compile(placeholder_style=ParameterStyle.STATIC)
120
+ return self._execute_script(sql, connection=connection, **kwargs)
121
+
122
+ # Determine if we need to convert parameter style
123
+ detected_styles = {p.style for p in statement.parameter_info}
124
+ target_style = self.default_parameter_style
125
+
126
+ # Check if any detected style is not supported
127
+ unsupported_styles = detected_styles - set(self.supported_parameter_styles)
128
+ if unsupported_styles:
129
+ # Convert to default style if we have unsupported styles
130
+ target_style = self.default_parameter_style
131
+ elif detected_styles:
132
+ # Use the first detected style if all are supported
133
+ # Prefer the first supported style found
134
+ for style in detected_styles:
135
+ if style in self.supported_parameter_styles:
136
+ target_style = style
137
+ break
138
+
139
+ if statement.is_many:
140
+ sql, params = statement.compile(placeholder_style=target_style)
141
+ # Process parameters to convert tuples to lists for Oracle
142
+ params = self._process_parameters(params)
143
+ # Oracle doesn't like underscores in bind parameter names
144
+ if isinstance(params, list) and params and isinstance(params[0], dict):
145
+ # Fix the SQL and parameters
146
+ for key in list(params[0].keys()):
147
+ if key.startswith("_arg_"):
148
+ # Remove leading underscore: _arg_0 -> arg0
149
+ new_key = key[1:].replace("_", "")
150
+ sql = sql.replace(f":{key}", f":{new_key}")
151
+ # Update all parameter sets
152
+ for param_set in params:
153
+ if isinstance(param_set, dict) and key in param_set:
154
+ param_set[new_key] = param_set.pop(key)
155
+ return self._execute_many(sql, params, connection=connection, **kwargs)
156
+
157
+ sql, params = statement.compile(placeholder_style=target_style)
158
+ # Oracle doesn't like underscores in bind parameter names
159
+ if isinstance(params, dict):
160
+ # Fix the SQL and parameters
161
+ for key in list(params.keys()):
162
+ if key.startswith("_arg_"):
163
+ # Remove leading underscore: _arg_0 -> arg0
164
+ new_key = key[1:].replace("_", "")
165
+ sql = sql.replace(f":{key}", f":{new_key}")
166
+ params[new_key] = params.pop(key)
167
+ return self._execute(sql, params, statement, connection=connection, **kwargs)
168
+
169
+ def _execute(
170
+ self,
171
+ sql: str,
172
+ parameters: Any,
173
+ statement: SQL,
174
+ connection: Optional[OracleSyncConnection] = None,
175
+ **kwargs: Any,
176
+ ) -> Union[SelectResultDict, DMLResultDict]:
177
+ conn = self._connection(connection)
178
+ with self._get_cursor(conn) as cursor:
179
+ # Process parameters to extract values from TypedParameter objects
180
+ processed_params = self._process_parameters(parameters) if parameters else []
181
+ cursor.execute(sql, processed_params)
182
+
183
+ if self.returns_rows(statement.expression):
184
+ fetched_data = cursor.fetchall()
185
+ column_names = [col[0] for col in cursor.description or []]
186
+ return {"data": fetched_data, "column_names": column_names, "rows_affected": cursor.rowcount}
187
+
188
+ return {"rows_affected": cursor.rowcount, "status_message": "OK"}
189
+
190
+ def _execute_many(
191
+ self, sql: str, param_list: Any, connection: Optional[OracleSyncConnection] = None, **kwargs: Any
192
+ ) -> DMLResultDict:
193
+ conn = self._connection(connection)
194
+ with self._get_cursor(conn) as cursor:
195
+ # Handle None or empty param_list
196
+ if param_list is None:
197
+ param_list = []
198
+ # Ensure param_list is a list of parameter sets
199
+ elif param_list and not isinstance(param_list, list):
200
+ # Single parameter set, wrap it
201
+ param_list = [param_list]
202
+ elif param_list and not isinstance(param_list[0], (list, tuple, dict)):
203
+ # Already a flat list, likely from incorrect usage
204
+ param_list = [param_list]
205
+ # Parameters have already been processed in _execute_statement
206
+ cursor.executemany(sql, param_list)
207
+ return {"rows_affected": cursor.rowcount, "status_message": "OK"}
208
+
209
+ def _execute_script(
210
+ self, script: str, connection: Optional[OracleSyncConnection] = None, **kwargs: Any
211
+ ) -> ScriptResultDict:
212
+ conn = self._connection(connection)
213
+ statements = self._split_script_statements(script, strip_trailing_semicolon=True)
214
+ with self._get_cursor(conn) as cursor:
215
+ for statement in statements:
216
+ if statement and statement.strip():
217
+ cursor.execute(statement.strip())
218
+
219
+ return {"statements_executed": len(statements), "status_message": "SCRIPT EXECUTED"}
220
+
221
+ def _fetch_arrow_table(self, sql: SQL, connection: "Optional[Any]" = None, **kwargs: Any) -> "ArrowResult":
222
+ self._ensure_pyarrow_installed()
223
+ conn = self._connection(connection)
224
+
225
+ # Get SQL and parameters using compile to ensure they match
226
+ # For fetch_arrow_table, we need to use POSITIONAL_COLON style since the SQL has :1 placeholders
227
+ sql_str, params = sql.compile(placeholder_style=ParameterStyle.POSITIONAL_COLON)
228
+ if params is None:
229
+ params = []
230
+
231
+ # Process parameters to extract values from TypedParameter objects
232
+ processed_params = self._process_parameters(params) if params else []
233
+
234
+ oracle_df = conn.fetch_df_all(sql_str, processed_params)
235
+ from pyarrow.interchange.from_dataframe import from_dataframe
236
+
237
+ arrow_table = from_dataframe(oracle_df)
238
+
239
+ return ArrowResult(statement=sql, data=arrow_table)
240
+
241
+ def _ingest_arrow_table(self, table: "Any", table_name: str, mode: str = "append", **options: Any) -> int:
242
+ self._ensure_pyarrow_installed()
243
+ conn = self._connection(None)
244
+
245
+ with self._get_cursor(conn) as cursor:
246
+ if mode == "replace":
247
+ cursor.execute(f"TRUNCATE TABLE {table_name}")
248
+ elif mode == "create":
249
+ msg = "'create' mode is not supported for oracledb ingestion."
250
+ raise NotImplementedError(msg)
251
+
252
+ data_for_ingest = table.to_pylist()
253
+ if not data_for_ingest:
254
+ return 0
255
+
256
+ # Generate column placeholders: :1, :2, etc.
257
+ num_columns = len(data_for_ingest[0])
258
+ placeholders = ", ".join(f":{i + 1}" for i in range(num_columns))
259
+ sql = f"INSERT INTO {table_name} VALUES ({placeholders})"
260
+ cursor.executemany(sql, data_for_ingest)
261
+ return cursor.rowcount
262
+
263
+ def _wrap_select_result(
264
+ self, statement: SQL, result: SelectResultDict, schema_type: Optional[type[ModelDTOT]] = None, **kwargs: Any
265
+ ) -> Union[SQLResult[ModelDTOT], SQLResult[RowT]]:
266
+ fetched_tuples = result.get("data", [])
267
+ column_names = result.get("column_names", [])
268
+
269
+ if not fetched_tuples:
270
+ return SQLResult[RowT](statement=statement, data=[], column_names=column_names, operation_type="SELECT")
271
+
272
+ rows_as_dicts: list[dict[str, Any]] = [dict(zip(column_names, row_tuple)) for row_tuple in fetched_tuples]
273
+
274
+ if schema_type:
275
+ converted_data = self.to_schema(rows_as_dicts, schema_type=schema_type)
276
+ return SQLResult[ModelDTOT](
277
+ statement=statement, data=list(converted_data), column_names=column_names, operation_type="SELECT"
278
+ )
279
+
280
+ return SQLResult[RowT](
281
+ statement=statement, data=rows_as_dicts, column_names=column_names, operation_type="SELECT"
282
+ )
283
+
284
+ def _wrap_execute_result(
285
+ self, statement: SQL, result: Union[DMLResultDict, ScriptResultDict], **kwargs: Any
286
+ ) -> SQLResult[RowT]:
287
+ operation_type = "UNKNOWN"
288
+ if statement.expression:
289
+ operation_type = str(statement.expression.key).upper()
290
+
291
+ if "statements_executed" in result:
292
+ script_result = cast("ScriptResultDict", result)
293
+ return SQLResult[RowT](
294
+ statement=statement,
295
+ data=[],
296
+ rows_affected=0,
297
+ operation_type="SCRIPT",
298
+ metadata={
299
+ "status_message": script_result.get("status_message", ""),
300
+ "statements_executed": script_result.get("statements_executed", -1),
301
+ },
302
+ )
303
+
304
+ dml_result = cast("DMLResultDict", result)
305
+ rows_affected = dml_result.get("rows_affected", -1)
306
+ status_message = dml_result.get("status_message", "")
307
+ return SQLResult[RowT](
308
+ statement=statement,
309
+ data=[],
310
+ rows_affected=rows_affected,
311
+ operation_type=operation_type,
312
+ metadata={"status_message": status_message},
313
+ )
529
314
 
530
315
 
531
316
  class OracleAsyncDriver(
532
- OracleDriverBase,
533
- AsyncArrowBulkOperationsMixin["OracleAsyncConnection"],
534
- SQLTranslatorMixin["OracleAsyncConnection"],
535
- AsyncDriverAdapterProtocol["OracleAsyncConnection"],
536
- ResultConverter,
317
+ AsyncDriverAdapterProtocol[OracleAsyncConnection, RowT],
318
+ SQLTranslatorMixin,
319
+ TypeCoercionMixin,
320
+ AsyncStorageMixin,
321
+ AsyncPipelinedExecutionMixin,
322
+ ToSchemaMixin,
537
323
  ):
538
- """Oracle Async Driver Adapter."""
539
-
540
- connection: "OracleAsyncConnection"
541
-
542
- def __init__(self, connection: "OracleAsyncConnection") -> None:
543
- self.connection = connection
324
+ """Oracle Async Driver Adapter. Refactored for new protocol."""
325
+
326
+ dialect: DialectType = "oracle"
327
+ supported_parameter_styles: "tuple[ParameterStyle, ...]" = (
328
+ ParameterStyle.NAMED_COLON,
329
+ ParameterStyle.POSITIONAL_COLON,
330
+ )
331
+ default_parameter_style: ParameterStyle = ParameterStyle.NAMED_COLON
332
+ __supports_arrow__: ClassVar[bool] = True
333
+ __supports_parquet__: ClassVar[bool] = False
334
+ __slots__ = ()
335
+
336
+ def __init__(
337
+ self,
338
+ connection: OracleAsyncConnection,
339
+ config: "Optional[SQLConfig]" = None,
340
+ default_row_type: "type[DictRow]" = DictRow,
341
+ ) -> None:
342
+ super().__init__(connection=connection, config=config, default_row_type=default_row_type)
343
+
344
+ def _process_parameters(self, parameters: "SQLParameterType") -> "SQLParameterType":
345
+ """Process parameters to handle Oracle-specific requirements.
346
+
347
+ - Extract values from TypedParameter objects
348
+ - Convert tuples to lists (Oracle doesn't support tuples)
349
+ """
350
+ return _process_oracle_parameters(parameters)
544
351
 
545
- @staticmethod
546
352
  @asynccontextmanager
547
- async def _with_cursor(connection: "OracleAsyncConnection") -> "AsyncGenerator[AsyncCursor, None]":
548
- cursor = connection.cursor()
353
+ async def _get_cursor(
354
+ self, connection: Optional[OracleAsyncConnection] = None
355
+ ) -> AsyncGenerator[AsyncCursor, None]:
356
+ conn_to_use = connection or self.connection
357
+ cursor: AsyncCursor = conn_to_use.cursor()
549
358
  try:
550
359
  yield cursor
551
360
  finally:
552
- cursor.close()
553
-
554
- # --- Public API Methods --- #
555
- @overload
556
- async def select(
557
- self,
558
- sql: str,
559
- parameters: "Optional[StatementParameterType]" = None,
560
- /,
561
- *filters: "StatementFilter",
562
- connection: "Optional[OracleAsyncConnection]" = None,
563
- schema_type: None = None,
564
- **kwargs: Any,
565
- ) -> "Sequence[dict[str, Any]]": ...
566
- @overload
567
- async def select(
568
- self,
569
- sql: str,
570
- parameters: "Optional[StatementParameterType]" = None,
571
- /,
572
- *filters: "StatementFilter",
573
- connection: "Optional[OracleAsyncConnection]" = None,
574
- schema_type: "type[ModelDTOT]",
575
- **kwargs: Any,
576
- ) -> "Sequence[ModelDTOT]": ...
577
- async def select(
578
- self,
579
- sql: str,
580
- parameters: "Optional[StatementParameterType]" = None,
581
- /,
582
- *filters: "StatementFilter",
583
- connection: "Optional[OracleAsyncConnection]" = None,
584
- schema_type: "Optional[type[ModelDTOT]]" = None,
585
- **kwargs: Any,
586
- ) -> "Sequence[Union[ModelDTOT, dict[str, Any]]]":
587
- """Fetch data from the database.
588
-
589
- Returns:
590
- List of row data as either model instances or dictionaries.
591
- """
592
- connection = self._connection(connection)
593
- sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
594
-
595
- async with self._with_cursor(connection) as cursor:
596
- await cursor.execute(sql, parameters) # pyright: ignore[reportUnknownMemberType]
597
- results = await cursor.fetchall() # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType]
598
- if not results:
599
- return []
600
- # Get column names
601
- column_names = [col[0] for col in cursor.description or []] # pyright: ignore[reportUnknownMemberType,reportUnknownVariableType]
602
-
603
- if schema_type:
604
- return [cast("ModelDTOT", schema_type(**dict(zip(column_names, row)))) for row in results] # pyright: ignore
605
-
606
- return [dict(zip(column_names, row)) for row in results] # pyright: ignore
607
-
608
- @overload
609
- async def select_one(
610
- self,
611
- sql: str,
612
- parameters: "Optional[StatementParameterType]" = None,
613
- /,
614
- *filters: "StatementFilter",
615
- connection: "Optional[OracleAsyncConnection]" = None,
616
- schema_type: None = None,
617
- **kwargs: Any,
618
- ) -> "dict[str, Any]": ...
619
- @overload
620
- async def select_one(
621
- self,
622
- sql: str,
623
- parameters: "Optional[StatementParameterType]" = None,
624
- /,
625
- *filters: "StatementFilter",
626
- connection: "Optional[OracleAsyncConnection]" = None,
627
- schema_type: "type[ModelDTOT]",
628
- **kwargs: Any,
629
- ) -> "ModelDTOT": ...
630
- async def select_one(
631
- self,
632
- sql: str,
633
- parameters: "Optional[StatementParameterType]" = None,
634
- /,
635
- *filters: "StatementFilter",
636
- connection: "Optional[OracleAsyncConnection]" = None,
637
- schema_type: "Optional[type[ModelDTOT]]" = None,
638
- **kwargs: Any,
639
- ) -> "Union[ModelDTOT, dict[str, Any]]":
640
- """Fetch one row from the database.
641
-
642
- Returns:
643
- The first row of the query results.
644
- """
645
- connection = self._connection(connection)
646
- sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
647
-
648
- async with self._with_cursor(connection) as cursor:
649
- await cursor.execute(sql, parameters) # pyright: ignore[reportUnknownMemberType]
650
- result = await cursor.fetchone() # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType]
651
- result = self.check_not_found(result) # pyright: ignore[reportUnknownArgumentType]
652
- # Get column names
653
- column_names = [col[0] for col in cursor.description or []] # pyright: ignore[reportUnknownMemberType,reportUnknownVariableType]
654
-
655
- if schema_type is not None:
656
- return cast("ModelDTOT", schema_type(**dict(zip(column_names, result)))) # pyright: ignore[reportUnknownArgumentType]
657
- # Always return dictionaries
658
- return dict(zip(column_names, result)) # pyright: ignore[reportUnknownArgumentType,reportUnknownVariableType]
659
-
660
- @overload
661
- async def select_one_or_none(
662
- self,
663
- sql: str,
664
- parameters: "Optional[StatementParameterType]" = None,
665
- /,
666
- *filters: "StatementFilter",
667
- connection: "Optional[OracleAsyncConnection]" = None,
668
- schema_type: None = None,
669
- **kwargs: Any,
670
- ) -> "Optional[dict[str, Any]]": ...
671
- @overload
672
- async def select_one_or_none(
673
- self,
674
- sql: str,
675
- parameters: "Optional[StatementParameterType]" = None,
676
- /,
677
- *filters: "StatementFilter",
678
- connection: "Optional[OracleAsyncConnection]" = None,
679
- schema_type: "type[ModelDTOT]",
680
- **kwargs: Any,
681
- ) -> "Optional[ModelDTOT]": ...
682
- async def select_one_or_none(
683
- self,
684
- sql: str,
685
- parameters: "Optional[StatementParameterType]" = None,
686
- /,
687
- *filters: "StatementFilter",
688
- connection: "Optional[OracleAsyncConnection]" = None,
689
- schema_type: "Optional[type[ModelDTOT]]" = None,
690
- **kwargs: Any,
691
- ) -> "Optional[Union[ModelDTOT, dict[str, Any]]]":
692
- """Fetch one row from the database.
693
-
694
- Returns:
695
- The first row of the query results.
696
- """
697
- connection = self._connection(connection)
698
- sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
699
-
700
- async with self._with_cursor(connection) as cursor:
701
- await cursor.execute(sql, parameters) # pyright: ignore[reportUnknownMemberType]
702
- result = await cursor.fetchone() # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType]
703
-
704
- if result is None:
705
- return None
706
-
707
- # Get column names
708
- column_names = [col[0] for col in cursor.description or []] # pyright: ignore[reportUnknownMemberType,reportUnknownVariableType]
709
-
710
- if schema_type is not None:
711
- return cast("ModelDTOT", schema_type(**dict(zip(column_names, result)))) # pyright: ignore[reportUnknownArgumentType]
712
- # Always return dictionaries
713
- return dict(zip(column_names, result)) # pyright: ignore[reportUnknownArgumentType,reportUnknownVariableType]
714
-
715
- @overload
716
- async def select_value(
717
- self,
718
- sql: str,
719
- parameters: "Optional[StatementParameterType]" = None,
720
- /,
721
- *filters: "StatementFilter",
722
- connection: "Optional[OracleAsyncConnection]" = None,
723
- schema_type: None = None,
724
- **kwargs: Any,
725
- ) -> "Any": ...
726
- @overload
727
- async def select_value(
728
- self,
729
- sql: str,
730
- parameters: "Optional[StatementParameterType]" = None,
731
- /,
732
- *filters: "StatementFilter",
733
- connection: "Optional[OracleAsyncConnection]" = None,
734
- schema_type: "type[T]",
735
- **kwargs: Any,
736
- ) -> "T": ...
737
- async def select_value(
738
- self,
739
- sql: str,
740
- parameters: "Optional[StatementParameterType]" = None,
741
- /,
742
- *filters: "StatementFilter",
743
- connection: "Optional[OracleAsyncConnection]" = None,
744
- schema_type: "Optional[type[T]]" = None,
745
- **kwargs: Any,
746
- ) -> "Union[T, Any]":
747
- """Fetch a single value from the database.
748
-
749
- Args:
750
- sql: The SQL query string.
751
- parameters: The parameters for the query (dict, tuple, list, or None).
752
- *filters: Statement filters to apply.
753
- connection: Optional connection override.
754
- schema_type: Optional type to convert the result to.
755
- **kwargs: Additional keyword arguments to merge with parameters if parameters is a dict.
756
-
757
- Returns:
758
- The first value of the first row of the query results.
759
- """
760
- connection = self._connection(connection)
761
- sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
762
-
763
- async with self._with_cursor(connection) as cursor:
764
- await cursor.execute(sql, parameters) # pyright: ignore[reportUnknownMemberType]
765
- result = await cursor.fetchone() # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType]
766
- result = self.check_not_found(result)
767
-
768
- if schema_type is None:
769
- return result[0] # pyright: ignore[reportUnknownArgumentType]
770
- return schema_type(result[0]) # type: ignore[call-arg]
771
-
772
- @overload
773
- async def select_value_or_none(
774
- self,
775
- sql: str,
776
- parameters: "Optional[StatementParameterType]" = None,
777
- /,
778
- *filters: "StatementFilter",
779
- connection: "Optional[OracleAsyncConnection]" = None,
780
- schema_type: None = None,
781
- **kwargs: Any,
782
- ) -> "Optional[Any]": ...
783
- @overload
784
- async def select_value_or_none(
785
- self,
786
- sql: str,
787
- parameters: "Optional[StatementParameterType]" = None,
788
- /,
789
- *filters: "StatementFilter",
790
- connection: "Optional[OracleAsyncConnection]" = None,
791
- schema_type: "type[T]",
792
- **kwargs: Any,
793
- ) -> "Optional[T]": ...
794
- async def select_value_or_none(
795
- self,
796
- sql: str,
797
- parameters: "Optional[StatementParameterType]" = None,
798
- /,
799
- *filters: "StatementFilter",
800
- connection: "Optional[OracleAsyncConnection]" = None,
801
- schema_type: "Optional[type[T]]" = None,
802
- **kwargs: Any,
803
- ) -> "Optional[Union[T, Any]]":
804
- """Fetch a single value or None if not found.
805
-
806
- Args:
807
- sql: The SQL query string.
808
- parameters: The parameters for the query (dict, tuple, list, or None).
809
- *filters: Statement filters to apply.
810
- connection: Optional connection override.
811
- schema_type: Optional type to convert the result to.
812
- **kwargs: Additional keyword arguments to merge with parameters if parameters is a dict.
813
-
814
- Returns:
815
- The first value of the first row of the query results, or None if no results found.
816
- """
817
- connection = self._connection(connection)
818
- sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
819
-
820
- async with self._with_cursor(connection) as cursor:
821
- await cursor.execute(sql, parameters) # pyright: ignore[reportUnknownMemberType]
822
- result = await cursor.fetchone() # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType]
823
- if result is None:
824
- return None
825
-
826
- if schema_type is None:
827
- return result[0] # pyright: ignore[reportUnknownArgumentType]
828
- return schema_type(result[0]) # type: ignore[call-arg]
829
-
830
- async def insert_update_delete(
831
- self,
832
- sql: str,
833
- parameters: "Optional[StatementParameterType]" = None,
834
- /,
835
- *filters: "StatementFilter",
836
- connection: "Optional[OracleAsyncConnection]" = None,
837
- **kwargs: Any,
838
- ) -> int:
839
- """Execute an insert, update, or delete statement.
840
-
841
- Args:
842
- sql: The SQL statement to execute.
843
- parameters: The parameters for the statement (dict, tuple, list, or None).
844
- *filters: Statement filters to apply.
845
- connection: Optional connection override.
846
- **kwargs: Additional keyword arguments to merge with parameters if parameters is a dict.
847
-
848
- Returns:
849
- The number of rows affected by the statement.
850
- """
851
- connection = self._connection(connection)
852
- sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
853
-
854
- async with self._with_cursor(connection) as cursor:
855
- await cursor.execute(sql, parameters) # pyright: ignore[reportUnknownMemberType]
856
- return cursor.rowcount # pyright: ignore[reportUnknownMemberType]
857
-
858
- @overload
859
- async def insert_update_delete_returning(
860
- self,
861
- sql: str,
862
- parameters: "Optional[StatementParameterType]" = None,
863
- /,
864
- *filters: "StatementFilter",
865
- connection: "Optional[OracleAsyncConnection]" = None,
866
- schema_type: None = None,
867
- **kwargs: Any,
868
- ) -> "dict[str, Any]": ...
869
- @overload
870
- async def insert_update_delete_returning(
871
- self,
872
- sql: str,
873
- parameters: "Optional[StatementParameterType]" = None,
874
- /,
875
- *filters: "StatementFilter",
876
- connection: "Optional[OracleAsyncConnection]" = None,
877
- schema_type: "type[ModelDTOT]",
878
- **kwargs: Any,
879
- ) -> "ModelDTOT": ...
880
- async def insert_update_delete_returning(
881
- self,
882
- sql: str,
883
- parameters: "Optional[StatementParameterType]" = None,
884
- /,
885
- *filters: "StatementFilter",
886
- connection: "Optional[OracleAsyncConnection]" = None,
887
- schema_type: "Optional[type[ModelDTOT]]" = None,
888
- **kwargs: Any,
889
- ) -> "Optional[Union[dict[str, Any], ModelDTOT]]":
890
- """Insert, update, or delete data from the database and return result.
891
-
892
- Returns:
893
- The first row of results.
894
- """
895
- connection = self._connection(connection)
896
- sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
897
-
898
- async with self._with_cursor(connection) as cursor:
899
- await cursor.execute(sql, parameters) # pyright: ignore[reportUnknownMemberType]
900
- result = await cursor.fetchone() # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType]
901
-
902
- if result is None:
903
- return None
904
-
905
- # Get column names
906
- column_names = [col[0] for col in cursor.description or []] # pyright: ignore[reportUnknownMemberType,reportUnknownVariableType]
907
-
908
- if schema_type is not None:
909
- return cast("ModelDTOT", schema_type(**dict(zip(column_names, result)))) # pyright: ignore[reportUnknownArgumentType]
910
- # Always return dictionaries
911
- return dict(zip(column_names, result)) # pyright: ignore[reportUnknownArgumentType,reportUnknownVariableType]
912
-
913
- async def execute_script(
914
- self,
915
- sql: str,
916
- parameters: "Optional[StatementParameterType]" = None,
917
- /,
918
- connection: "Optional[OracleAsyncConnection]" = None,
919
- **kwargs: Any,
920
- ) -> str:
921
- """Execute a SQL script.
922
-
923
- Args:
924
- sql: The SQL script to execute.
925
- parameters: The parameters for the script (dict, tuple, list, or None).
926
- connection: Optional connection override.
927
- **kwargs: Additional keyword arguments to merge with parameters if parameters is a dict.
928
-
929
- Returns:
930
- A success message.
931
- """
932
- connection = self._connection(connection)
933
- sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
934
-
935
- async with self._with_cursor(connection) as cursor:
936
- await cursor.execute(sql, parameters) # pyright: ignore[reportUnknownMemberType]
937
- return str(cursor.rowcount) # pyright: ignore[reportUnknownMemberType]
938
-
939
- async def select_arrow( # pyright: ignore[reportUnknownParameterType]
940
- self,
941
- sql: str,
942
- parameters: "Optional[StatementParameterType]" = None,
943
- /,
944
- *,
945
- connection: "Optional[OracleAsyncConnection]" = None,
946
- **kwargs: Any,
947
- ) -> "ArrowTable": # pyright: ignore[reportUnknownVariableType]
948
- """Execute a SQL query asynchronously and return results as an Apache Arrow Table.
949
-
950
- Args:
951
- sql: The SQL query string.
952
- parameters: Parameters for the query.
953
- connection: Optional connection override.
954
- **kwargs: Additional keyword arguments to merge with parameters if parameters is a dict.
955
-
956
- Returns:
957
- An Apache Arrow Table containing the query results.
958
- """
959
-
960
- connection = self._connection(connection)
961
- sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
962
- results = await connection.fetch_df_all(sql, parameters)
963
- return ArrowTable.from_arrays(arrays=results.column_arrays(), names=results.column_names()) # pyright: ignore
964
-
965
- def _connection(self, connection: "Optional[OracleAsyncConnection]" = None) -> "OracleAsyncConnection":
966
- """Get the connection to use for the operation.
967
-
968
- Args:
969
- connection: Optional connection to use.
970
-
971
- Returns:
972
- The connection to use.
973
- """
974
- return connection or self.connection
361
+ await ensure_async_(cursor.close)()
362
+
363
+ async def _execute_statement(
364
+ self, statement: SQL, connection: Optional[OracleAsyncConnection] = None, **kwargs: Any
365
+ ) -> Union[SelectResultDict, DMLResultDict, ScriptResultDict]:
366
+ if statement.is_script:
367
+ sql, _ = statement.compile(placeholder_style=ParameterStyle.STATIC)
368
+ return await self._execute_script(sql, connection=connection, **kwargs)
369
+
370
+ # Determine if we need to convert parameter style
371
+ detected_styles = {p.style for p in statement.parameter_info}
372
+ target_style = self.default_parameter_style
373
+
374
+ # Check if any detected style is not supported
375
+ unsupported_styles = detected_styles - set(self.supported_parameter_styles)
376
+ if unsupported_styles:
377
+ # Convert to default style if we have unsupported styles
378
+ target_style = self.default_parameter_style
379
+ elif detected_styles:
380
+ # Use the first detected style if all are supported
381
+ # Prefer the first supported style found
382
+ for style in detected_styles:
383
+ if style in self.supported_parameter_styles:
384
+ target_style = style
385
+ break
386
+
387
+ if statement.is_many:
388
+ sql, params = statement.compile(placeholder_style=target_style)
389
+ # Process parameters to convert tuples to lists for Oracle
390
+ params = self._process_parameters(params)
391
+ # Oracle doesn't like underscores in bind parameter names
392
+ if isinstance(params, list) and params and isinstance(params[0], dict):
393
+ # Fix the SQL and parameters
394
+ for key in list(params[0].keys()):
395
+ if key.startswith("_arg_"):
396
+ # Remove leading underscore: _arg_0 -> arg0
397
+ new_key = key[1:].replace("_", "")
398
+ sql = sql.replace(f":{key}", f":{new_key}")
399
+ # Update all parameter sets
400
+ for param_set in params:
401
+ if isinstance(param_set, dict) and key in param_set:
402
+ param_set[new_key] = param_set.pop(key)
403
+ return await self._execute_many(sql, params, connection=connection, **kwargs)
404
+
405
+ sql, params = statement.compile(placeholder_style=target_style)
406
+ # Oracle doesn't like underscores in bind parameter names
407
+ if isinstance(params, dict):
408
+ # Fix the SQL and parameters
409
+ for key in list(params.keys()):
410
+ if key.startswith("_arg_"):
411
+ # Remove leading underscore: _arg_0 -> arg0
412
+ new_key = key[1:].replace("_", "")
413
+ sql = sql.replace(f":{key}", f":{new_key}")
414
+ params[new_key] = params.pop(key)
415
+ return await self._execute(sql, params, statement, connection=connection, **kwargs)
416
+
417
+ async def _execute(
418
+ self,
419
+ sql: str,
420
+ parameters: Any,
421
+ statement: SQL,
422
+ connection: Optional[OracleAsyncConnection] = None,
423
+ **kwargs: Any,
424
+ ) -> Union[SelectResultDict, DMLResultDict]:
425
+ conn = self._connection(connection)
426
+ async with self._get_cursor(conn) as cursor:
427
+ if parameters is None:
428
+ await cursor.execute(sql)
429
+ else:
430
+ # Process parameters to extract values from TypedParameter objects
431
+ processed_params = self._process_parameters(parameters)
432
+ await cursor.execute(sql, processed_params)
433
+
434
+ # For SELECT statements, extract data while cursor is open
435
+ if self.returns_rows(statement.expression):
436
+ fetched_data = await cursor.fetchall()
437
+ column_names = [col[0] for col in cursor.description or []]
438
+ result: SelectResultDict = {
439
+ "data": fetched_data,
440
+ "column_names": column_names,
441
+ "rows_affected": cursor.rowcount,
442
+ }
443
+ return result
444
+ dml_result: DMLResultDict = {"rows_affected": cursor.rowcount, "status_message": "OK"}
445
+ return dml_result
446
+
447
+ async def _execute_many(
448
+ self, sql: str, param_list: Any, connection: Optional[OracleAsyncConnection] = None, **kwargs: Any
449
+ ) -> DMLResultDict:
450
+ conn = self._connection(connection)
451
+ async with self._get_cursor(conn) as cursor:
452
+ # Handle None or empty param_list
453
+ if param_list is None:
454
+ param_list = []
455
+ # Ensure param_list is a list of parameter sets
456
+ elif param_list and not isinstance(param_list, list):
457
+ # Single parameter set, wrap it
458
+ param_list = [param_list]
459
+ elif param_list and not isinstance(param_list[0], (list, tuple, dict)):
460
+ # Already a flat list, likely from incorrect usage
461
+ param_list = [param_list]
462
+ # Parameters have already been processed in _execute_statement
463
+ await cursor.executemany(sql, param_list)
464
+ result: DMLResultDict = {"rows_affected": cursor.rowcount, "status_message": "OK"}
465
+ return result
466
+
467
+ async def _execute_script(
468
+ self, script: str, connection: Optional[OracleAsyncConnection] = None, **kwargs: Any
469
+ ) -> ScriptResultDict:
470
+ conn = self._connection(connection)
471
+ # Oracle doesn't support multi-statement scripts in a single execute
472
+ # The splitter now handles PL/SQL blocks correctly when strip_trailing_semicolon=True
473
+ statements = self._split_script_statements(script, strip_trailing_semicolon=True)
474
+
475
+ async with self._get_cursor(conn) as cursor:
476
+ for statement in statements:
477
+ if statement and statement.strip():
478
+ await cursor.execute(statement.strip())
479
+
480
+ result: ScriptResultDict = {"statements_executed": len(statements), "status_message": "SCRIPT EXECUTED"}
481
+ return result
482
+
483
+ async def _fetch_arrow_table(self, sql: SQL, connection: "Optional[Any]" = None, **kwargs: Any) -> "ArrowResult":
484
+ self._ensure_pyarrow_installed()
485
+ conn = self._connection(connection)
486
+
487
+ # Get SQL and parameters using compile to ensure they match
488
+ # For fetch_arrow_table, we need to use POSITIONAL_COLON style since the SQL has :1 placeholders
489
+ sql_str, params = sql.compile(placeholder_style=ParameterStyle.POSITIONAL_COLON)
490
+ if params is None:
491
+ params = []
492
+
493
+ # Process parameters to extract values from TypedParameter objects
494
+ processed_params = self._process_parameters(params) if params else []
495
+
496
+ oracle_df = await conn.fetch_df_all(sql_str, processed_params)
497
+ from pyarrow.interchange.from_dataframe import from_dataframe
498
+
499
+ arrow_table = from_dataframe(oracle_df)
500
+
501
+ return ArrowResult(statement=sql, data=arrow_table)
502
+
503
+ async def _ingest_arrow_table(self, table: "Any", table_name: str, mode: str = "append", **options: Any) -> int:
504
+ self._ensure_pyarrow_installed()
505
+ conn = self._connection(None)
506
+
507
+ async with self._get_cursor(conn) as cursor:
508
+ if mode == "replace":
509
+ await cursor.execute(f"TRUNCATE TABLE {table_name}")
510
+ elif mode == "create":
511
+ msg = "'create' mode is not supported for oracledb ingestion."
512
+ raise NotImplementedError(msg)
513
+
514
+ data_for_ingest = table.to_pylist()
515
+ if not data_for_ingest:
516
+ return 0
517
+
518
+ # Generate column placeholders: :1, :2, etc.
519
+ num_columns = len(data_for_ingest[0])
520
+ placeholders = ", ".join(f":{i + 1}" for i in range(num_columns))
521
+ sql = f"INSERT INTO {table_name} VALUES ({placeholders})"
522
+ await cursor.executemany(sql, data_for_ingest)
523
+ return cursor.rowcount
524
+
525
+ async def _wrap_select_result(
526
+ self,
527
+ statement: SQL,
528
+ result: SelectResultDict,
529
+ schema_type: Optional[type[ModelDTOT]] = None,
530
+ **kwargs: Any, # pyright: ignore[reportUnusedParameter]
531
+ ) -> Union[SQLResult[ModelDTOT], SQLResult[RowT]]:
532
+ fetched_tuples = result["data"]
533
+ column_names = result["column_names"]
534
+
535
+ if not fetched_tuples:
536
+ return SQLResult[RowT](statement=statement, data=[], column_names=column_names, operation_type="SELECT")
537
+
538
+ rows_as_dicts: list[dict[str, Any]] = [dict(zip(column_names, row_tuple)) for row_tuple in fetched_tuples]
539
+
540
+ if schema_type:
541
+ converted_data = self.to_schema(rows_as_dicts, schema_type=schema_type)
542
+ return SQLResult[ModelDTOT](
543
+ statement=statement, data=list(converted_data), column_names=column_names, operation_type="SELECT"
544
+ )
545
+ return SQLResult[RowT](
546
+ statement=statement, data=rows_as_dicts, column_names=column_names, operation_type="SELECT"
547
+ )
548
+
549
+ async def _wrap_execute_result(
550
+ self,
551
+ statement: SQL,
552
+ result: Union[DMLResultDict, ScriptResultDict],
553
+ **kwargs: Any, # pyright: ignore[reportUnusedParameter]
554
+ ) -> SQLResult[RowT]:
555
+ operation_type = "UNKNOWN"
556
+ if statement.expression:
557
+ operation_type = str(statement.expression.key).upper()
558
+
559
+ if "statements_executed" in result:
560
+ script_result = cast("ScriptResultDict", result)
561
+ return SQLResult[RowT](
562
+ statement=statement,
563
+ data=[],
564
+ rows_affected=0,
565
+ operation_type="SCRIPT",
566
+ metadata={
567
+ "status_message": script_result.get("status_message", ""),
568
+ "statements_executed": script_result.get("statements_executed", -1),
569
+ },
570
+ )
571
+
572
+ dml_result = cast("DMLResultDict", result)
573
+ rows_affected = dml_result.get("rows_affected", -1)
574
+ status_message = dml_result.get("status_message", "")
575
+ return SQLResult[RowT](
576
+ statement=statement,
577
+ data=[],
578
+ rows_affected=rows_affected,
579
+ operation_type=operation_type,
580
+ metadata={"status_message": status_message},
581
+ )