sqlspec 0.11.1__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 -621
  8. sqlspec/adapters/aiosqlite/__init__.py +2 -6
  9. sqlspec/adapters/aiosqlite/config.py +143 -57
  10. sqlspec/adapters/aiosqlite/driver.py +269 -431
  11. sqlspec/adapters/asyncmy/__init__.py +3 -8
  12. sqlspec/adapters/asyncmy/config.py +247 -202
  13. sqlspec/adapters/asyncmy/driver.py +218 -436
  14. sqlspec/adapters/asyncpg/__init__.py +4 -7
  15. sqlspec/adapters/asyncpg/config.py +329 -176
  16. sqlspec/adapters/asyncpg/driver.py +417 -487
  17. sqlspec/adapters/bigquery/__init__.py +2 -2
  18. sqlspec/adapters/bigquery/config.py +407 -0
  19. sqlspec/adapters/bigquery/driver.py +600 -553
  20. sqlspec/adapters/duckdb/__init__.py +4 -1
  21. sqlspec/adapters/duckdb/config.py +432 -321
  22. sqlspec/adapters/duckdb/driver.py +392 -406
  23. sqlspec/adapters/oracledb/__init__.py +3 -8
  24. sqlspec/adapters/oracledb/config.py +625 -0
  25. sqlspec/adapters/oracledb/driver.py +548 -921
  26. sqlspec/adapters/psqlpy/__init__.py +4 -7
  27. sqlspec/adapters/psqlpy/config.py +372 -203
  28. sqlspec/adapters/psqlpy/driver.py +197 -533
  29. sqlspec/adapters/psycopg/__init__.py +3 -8
  30. sqlspec/adapters/psycopg/config.py +741 -0
  31. sqlspec/adapters/psycopg/driver.py +734 -694
  32. sqlspec/adapters/sqlite/__init__.py +2 -6
  33. sqlspec/adapters/sqlite/config.py +146 -81
  34. sqlspec/adapters/sqlite/driver.py +242 -405
  35. sqlspec/base.py +220 -784
  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.1.dist-info → sqlspec-0.12.0.dist-info}/METADATA +97 -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 -331
  150. sqlspec/mixins.py +0 -305
  151. sqlspec/statement.py +0 -378
  152. sqlspec-0.11.1.dist-info/RECORD +0 -69
  153. {sqlspec-0.11.1.dist-info → sqlspec-0.12.0.dist-info}/WHEEL +0 -0
  154. {sqlspec-0.11.1.dist-info → sqlspec-0.12.0.dist-info}/licenses/LICENSE +0 -0
  155. {sqlspec-0.11.1.dist-info → sqlspec-0.12.0.dist-info}/licenses/NOTICE +0 -0
@@ -1,954 +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.filters import StatementFilter
9
- from sqlspec.mixins import (
10
- AsyncArrowBulkOperationsMixin,
11
- ResultConverter,
8
+ from sqlspec.driver import AsyncDriverAdapterProtocol, SyncDriverAdapterProtocol
9
+ from sqlspec.driver.mixins import (
10
+ AsyncPipelinedExecutionMixin,
11
+ AsyncStorageMixin,
12
12
  SQLTranslatorMixin,
13
- SyncArrowBulkOperationsMixin,
13
+ SyncPipelinedExecutionMixin,
14
+ SyncStorageMixin,
15
+ ToSchemaMixin,
16
+ TypeCoercionMixin,
14
17
  )
15
- from sqlspec.statement import SQLStatement
16
- from sqlspec.typing import ArrowTable, StatementParameterType, T
17
-
18
- if TYPE_CHECKING:
19
- from collections.abc import AsyncGenerator, Generator, Mapping, Sequence
20
-
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
- *filters: "StatementFilter",
41
- **kwargs: Any,
42
- ) -> "tuple[str, Optional[Union[tuple[Any, ...], dict[str, Any]]]]":
43
- """Process SQL and parameters using SQLStatement with dialect support.
44
-
45
- Args:
46
- sql: The SQL statement to process.
47
- parameters: The parameters to bind to the statement.
48
- *filters: Statement filters to apply.
49
- **kwargs: Additional keyword arguments.
50
-
51
- Returns:
52
- A tuple of (sql, parameters) ready for execution.
53
- """
54
- data_params_for_statement: Optional[Union[Mapping[str, Any], Sequence[Any]]] = None
55
- combined_filters_list: list[StatementFilter] = list(filters)
56
-
57
- if parameters is not None:
58
- if isinstance(parameters, StatementFilter):
59
- combined_filters_list.insert(0, parameters)
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])
60
61
  else:
61
- data_params_for_statement = parameters
62
- if data_params_for_statement is not None and not isinstance(data_params_for_statement, (list, tuple, dict)):
63
- data_params_for_statement = (data_params_for_statement,)
64
-
65
- if isinstance(data_params_for_statement, dict) and not data_params_for_statement and not kwargs:
66
- return sql, None
67
-
68
- statement = SQLStatement(sql, data_params_for_statement, kwargs=kwargs, dialect=self.dialect)
69
- for filter_obj in combined_filters_list:
70
- statement = statement.apply_filter(filter_obj)
71
-
72
- processed_sql, processed_params, _ = statement.process()
73
- if processed_params is None:
74
- return processed_sql, None
75
- if isinstance(processed_params, dict):
76
- return processed_sql, processed_params
77
- if isinstance(processed_params, (list, tuple)):
78
- return processed_sql, tuple(processed_params)
79
- return processed_sql, (processed_params,) # type: ignore[unreachable]
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
80
69
 
81
70
 
82
71
  class OracleSyncDriver(
83
- OracleDriverBase,
84
- SyncArrowBulkOperationsMixin["OracleSyncConnection"],
85
- SQLTranslatorMixin["OracleSyncConnection"],
86
- SyncDriverAdapterProtocol["OracleSyncConnection"],
87
- ResultConverter,
72
+ SyncDriverAdapterProtocol[OracleSyncConnection, RowT],
73
+ SQLTranslatorMixin,
74
+ TypeCoercionMixin,
75
+ SyncStorageMixin,
76
+ SyncPipelinedExecutionMixin,
77
+ ToSchemaMixin,
88
78
  ):
89
- """Oracle Sync Driver Adapter."""
90
-
91
- connection: "OracleSyncConnection"
92
-
93
- def __init__(self, connection: "OracleSyncConnection") -> None:
94
- 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)
95
105
 
96
- @staticmethod
97
106
  @contextmanager
98
- def _with_cursor(connection: "OracleSyncConnection") -> "Generator[Cursor, None, None]":
99
- 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()
100
110
  try:
101
111
  yield cursor
102
112
  finally:
103
113
  cursor.close()
104
114
 
105
- # --- Public API Methods --- #
106
- @overload
107
- def select(
108
- self,
109
- sql: str,
110
- parameters: "Optional[StatementParameterType]" = None,
111
- *filters: "StatementFilter",
112
- connection: "Optional[OracleSyncConnection]" = None,
113
- schema_type: None = None,
114
- **kwargs: Any,
115
- ) -> "Sequence[dict[str, Any]]": ...
116
- @overload
117
- def select(
118
- self,
119
- sql: str,
120
- parameters: "Optional[StatementParameterType]" = None,
121
- *filters: "StatementFilter",
122
- connection: "Optional[OracleSyncConnection]" = None,
123
- schema_type: "type[ModelDTOT]",
124
- **kwargs: Any,
125
- ) -> "Sequence[ModelDTOT]": ...
126
- def select(
127
- self,
128
- sql: str,
129
- parameters: "Optional[StatementParameterType]" = None,
130
- *filters: "StatementFilter",
131
- connection: "Optional[OracleSyncConnection]" = None,
132
- schema_type: "Optional[type[ModelDTOT]]" = None,
133
- **kwargs: Any,
134
- ) -> "Sequence[Union[ModelDTOT, dict[str, Any]]]":
135
- """Fetch data from the database.
136
-
137
- Args:
138
- sql: The SQL query string.
139
- parameters: The parameters for the query (dict, tuple, list, or None).
140
- *filters: Statement filters to apply.
141
- connection: Optional connection override.
142
- schema_type: Optional schema class for the result.
143
- **kwargs: Additional keyword arguments to merge with parameters if parameters is a dict.
144
-
145
- Returns:
146
- List of row data as either model instances or dictionaries.
147
- """
148
- connection = self._connection(connection)
149
- sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
150
- with self._with_cursor(connection) as cursor:
151
- cursor.execute(sql, parameters) # pyright: ignore[reportUnknownMemberType]
152
- results = cursor.fetchall() # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType]
153
- if not results:
154
- return []
155
- # Get column names from description
156
- column_names = [col[0] for col in cursor.description or []] # pyright: ignore[reportUnknownMemberType,reportUnknownVariableType]
157
-
158
- return self.to_schema([dict(zip(column_names, row)) for row in results], schema_type=schema_type)
159
-
160
- @overload
161
- def select_one(
162
- self,
163
- sql: str,
164
- parameters: "Optional[StatementParameterType]" = None,
165
- *filters: "StatementFilter",
166
- connection: "Optional[OracleSyncConnection]" = None,
167
- schema_type: None = None,
168
- **kwargs: Any,
169
- ) -> "dict[str, Any]": ...
170
- @overload
171
- def select_one(
172
- self,
173
- sql: str,
174
- parameters: "Optional[StatementParameterType]" = None,
175
- *filters: "StatementFilter",
176
- connection: "Optional[OracleSyncConnection]" = None,
177
- schema_type: "type[ModelDTOT]",
178
- **kwargs: Any,
179
- ) -> "ModelDTOT": ...
180
- def select_one(
181
- self,
182
- sql: str,
183
- parameters: "Optional[StatementParameterType]" = None,
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
- *filters: "StatementFilter",
221
- connection: "Optional[OracleSyncConnection]" = None,
222
- schema_type: None = None,
223
- **kwargs: Any,
224
- ) -> "Optional[dict[str, Any]]": ...
225
- @overload
226
- def select_one_or_none(
227
- self,
228
- sql: str,
229
- parameters: "Optional[StatementParameterType]" = None,
230
- *filters: "StatementFilter",
231
- connection: "Optional[OracleSyncConnection]" = None,
232
- schema_type: "type[ModelDTOT]",
233
- **kwargs: Any,
234
- ) -> "Optional[ModelDTOT]": ...
235
- def select_one_or_none(
236
- self,
237
- sql: str,
238
- parameters: "Optional[StatementParameterType]" = None,
239
- *filters: "StatementFilter",
240
- connection: "Optional[OracleSyncConnection]" = None,
241
- schema_type: "Optional[type[ModelDTOT]]" = None,
242
- **kwargs: Any,
243
- ) -> "Optional[Union[ModelDTOT, dict[str, Any]]]":
244
- """Fetch one row from the database or return None if no rows found.
245
-
246
- Args:
247
- sql: The SQL query string.
248
- parameters: The parameters for the query (dict, tuple, list, or None).
249
- *filters: Statement filters to apply.
250
- connection: Optional connection override.
251
- schema_type: Optional schema class for the result.
252
- **kwargs: Additional keyword arguments to merge with parameters if parameters is a dict.
253
-
254
- Returns:
255
- The first row of the query results, or None if no results found.
256
- """
257
- connection = self._connection(connection)
258
- sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
259
-
260
- with self._with_cursor(connection) as cursor:
261
- cursor.execute(sql, parameters) # pyright: ignore[reportUnknownMemberType]
262
- result = cursor.fetchone() # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType]
263
- if result is None:
264
- return None
265
-
266
- # Get column names
267
- column_names = [col[0] for col in cursor.description or []] # pyright: ignore[reportUnknownMemberType,reportUnknownVariableType]
268
-
269
- return self.to_schema(dict(zip(column_names, result)), schema_type=schema_type)
270
-
271
- @overload
272
- def select_value(
273
- self,
274
- sql: str,
275
- parameters: "Optional[StatementParameterType]" = None,
276
- *filters: "StatementFilter",
277
- connection: "Optional[OracleSyncConnection]" = None,
278
- schema_type: None = None,
279
- **kwargs: Any,
280
- ) -> "Any": ...
281
- @overload
282
- def select_value(
283
- self,
284
- sql: str,
285
- parameters: "Optional[StatementParameterType]" = None,
286
- *filters: "StatementFilter",
287
- connection: "Optional[OracleSyncConnection]" = None,
288
- schema_type: "type[T]",
289
- **kwargs: Any,
290
- ) -> "T": ...
291
- def select_value(
292
- self,
293
- sql: str,
294
- parameters: "Optional[StatementParameterType]" = None,
295
- *filters: "StatementFilter",
296
- connection: "Optional[OracleSyncConnection]" = None,
297
- schema_type: "Optional[type[T]]" = None,
298
- **kwargs: Any,
299
- ) -> "Union[T, Any]":
300
- """Fetch a single value from the database.
301
-
302
- Args:
303
- sql: The SQL query string.
304
- parameters: The parameters for the query (dict, tuple, list, or None).
305
- *filters: Statement filters to apply.
306
- connection: Optional connection override.
307
- schema_type: Optional type to convert the result to.
308
- **kwargs: Additional keyword arguments to merge with parameters if parameters is a dict.
309
-
310
- Returns:
311
- The first value of the first row of the query results.
312
- """
313
- connection = self._connection(connection)
314
- sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
315
-
316
- with self._with_cursor(connection) as cursor:
317
- cursor.execute(sql, parameters) # pyright: ignore[reportUnknownMemberType]
318
- result = cursor.fetchone() # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType]
319
- result = self.check_not_found(result) # pyright: ignore[reportUnknownArgumentType]
320
-
321
- if schema_type is None:
322
- return result[0] # pyright: ignore[reportUnknownArgumentType]
323
- return schema_type(result[0]) # type: ignore[call-arg]
324
-
325
- @overload
326
- def select_value_or_none(
327
- self,
328
- sql: str,
329
- parameters: "Optional[StatementParameterType]" = None,
330
- *filters: "StatementFilter",
331
- connection: "Optional[OracleSyncConnection]" = None,
332
- schema_type: None = None,
333
- **kwargs: Any,
334
- ) -> "Optional[Any]": ...
335
- @overload
336
- def select_value_or_none(
337
- self,
338
- sql: str,
339
- parameters: "Optional[StatementParameterType]" = None,
340
- *filters: "StatementFilter",
341
- connection: "Optional[OracleSyncConnection]" = None,
342
- schema_type: "type[T]",
343
- **kwargs: Any,
344
- ) -> "Optional[T]": ...
345
- def select_value_or_none(
346
- self,
347
- sql: str,
348
- parameters: "Optional[StatementParameterType]" = None,
349
- *filters: "StatementFilter",
350
- connection: "Optional[OracleSyncConnection]" = None,
351
- schema_type: "Optional[type[T]]" = None,
352
- **kwargs: Any,
353
- ) -> "Optional[Union[T, Any]]":
354
- """Fetch a single value or None if not found.
355
-
356
- Args:
357
- sql: The SQL query string.
358
- parameters: The parameters for the query (dict, tuple, list, or None).
359
- *filters: Statement filters to apply.
360
- connection: Optional connection override.
361
- schema_type: Optional type to convert the result to.
362
- **kwargs: Additional keyword arguments to merge with parameters if parameters is a dict.
363
-
364
- Returns:
365
- The first value of the first row of the query results, or None if no results found.
366
- """
367
- connection = self._connection(connection)
368
- sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
369
-
370
- with self._with_cursor(connection) as cursor:
371
- cursor.execute(sql, parameters) # pyright: ignore[reportUnknownMemberType]
372
- result = cursor.fetchone() # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType]
373
- if result is None:
374
- return None
375
-
376
- if schema_type is None:
377
- return result[0] # pyright: ignore[reportUnknownArgumentType]
378
- return schema_type(result[0]) # type: ignore[call-arg]
379
-
380
- def insert_update_delete(
381
- self,
382
- sql: str,
383
- parameters: "Optional[StatementParameterType]" = None,
384
- *filters: "StatementFilter",
385
- connection: "Optional[OracleSyncConnection]" = None,
386
- **kwargs: Any,
387
- ) -> int:
388
- """Execute an insert, update, or delete statement.
389
-
390
- Args:
391
- sql: The SQL statement to execute.
392
- parameters: The parameters for the statement (dict, tuple, list, or None).
393
- *filters: Statement filters to apply.
394
- connection: Optional connection override.
395
- **kwargs: Additional keyword arguments to merge with parameters if parameters is a dict.
396
-
397
- Returns:
398
- The number of rows affected by the statement.
399
- """
400
- connection = self._connection(connection)
401
- sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
402
-
403
- with self._with_cursor(connection) as cursor:
404
- cursor.execute(sql, parameters) # pyright: ignore[reportUnknownMemberType]
405
- return cursor.rowcount # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType]
406
-
407
- @overload
408
- def insert_update_delete_returning(
409
- self,
410
- sql: str,
411
- parameters: "Optional[StatementParameterType]" = None,
412
- *filters: "StatementFilter",
413
- connection: "Optional[OracleSyncConnection]" = None,
414
- schema_type: None = None,
415
- **kwargs: Any,
416
- ) -> "dict[str, Any]": ...
417
- @overload
418
- def insert_update_delete_returning(
419
- self,
420
- sql: str,
421
- parameters: "Optional[StatementParameterType]" = None,
422
- *filters: "StatementFilter",
423
- connection: "Optional[OracleSyncConnection]" = None,
424
- schema_type: "type[ModelDTOT]",
425
- **kwargs: Any,
426
- ) -> "ModelDTOT": ...
427
- def insert_update_delete_returning(
428
- self,
429
- sql: str,
430
- parameters: "Optional[StatementParameterType]" = None,
431
- *filters: "StatementFilter",
432
- connection: "Optional[OracleSyncConnection]" = None,
433
- schema_type: "Optional[type[ModelDTOT]]" = None,
434
- **kwargs: Any,
435
- ) -> "Optional[Union[dict[str, Any], ModelDTOT]]":
436
- """Insert, update, or delete data from the database and return result.
437
-
438
- Returns:
439
- The first row of results.
440
- """
441
- connection = self._connection(connection)
442
- sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
443
-
444
- with self._with_cursor(connection) as cursor:
445
- cursor.execute(sql, parameters) # pyright: ignore[reportUnknownMemberType]
446
- result = cursor.fetchone() # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType]
447
-
448
- if result is None:
449
- return None
450
-
451
- # Get column names
452
- column_names = [col[0] for col in cursor.description or []] # pyright: ignore[reportUnknownMemberType,reportUnknownVariableType]
453
-
454
- if schema_type is not None:
455
- return cast("ModelDTOT", schema_type(**dict(zip(column_names, result)))) # pyright: ignore[reportUnknownArgumentType]
456
- # Always return dictionaries
457
- return dict(zip(column_names, result)) # pyright: ignore[reportUnknownArgumentType,reportUnknownVariableType]
458
-
459
- def execute_script(
460
- self,
461
- sql: str,
462
- parameters: "Optional[StatementParameterType]" = None,
463
- connection: "Optional[OracleSyncConnection]" = None,
464
- **kwargs: Any,
465
- ) -> str:
466
- """Execute a SQL script.
467
-
468
- Args:
469
- sql: The SQL script to execute.
470
- parameters: The parameters for the script (dict, tuple, list, or None).
471
- connection: Optional connection override.
472
- **kwargs: Additional keyword arguments to merge with parameters if parameters is a dict.
473
-
474
- Returns:
475
- A success message.
476
- """
477
- connection = self._connection(connection)
478
- sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
479
-
480
- with self._with_cursor(connection) as cursor:
481
- cursor.execute(sql, parameters) # pyright: ignore[reportUnknownMemberType]
482
- return str(cursor.rowcount) # pyright: ignore[reportUnknownMemberType]
483
-
484
- def select_arrow( # pyright: ignore[reportUnknownParameterType]
485
- self,
486
- sql: str,
487
- parameters: "Optional[StatementParameterType]" = None,
488
- *filters: "StatementFilter",
489
- connection: "Optional[OracleSyncConnection]" = None,
490
- **kwargs: Any,
491
- ) -> "ArrowTable": # pyright: ignore[reportUnknownVariableType]
492
- """Execute a SQL query and return results as an Apache Arrow Table.
493
-
494
- Returns:
495
- An Apache Arrow Table containing the query results.
496
- """
497
-
498
- connection = self._connection(connection)
499
- sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
500
- results = connection.fetch_df_all(sql, parameters)
501
- return cast("ArrowTable", ArrowTable.from_arrays(arrays=results.column_arrays(), names=results.column_names())) # pyright: ignore
502
-
503
- def _connection(self, connection: "Optional[OracleSyncConnection]" = None) -> "OracleSyncConnection":
504
- """Get the connection to use for the operation.
505
-
506
- Args:
507
- connection: Optional connection to use.
508
-
509
- Returns:
510
- The connection to use.
511
- """
512
- 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
+ )
513
314
 
514
315
 
515
316
  class OracleAsyncDriver(
516
- OracleDriverBase,
517
- AsyncArrowBulkOperationsMixin["OracleAsyncConnection"],
518
- SQLTranslatorMixin["OracleAsyncConnection"],
519
- AsyncDriverAdapterProtocol["OracleAsyncConnection"],
520
- ResultConverter,
317
+ AsyncDriverAdapterProtocol[OracleAsyncConnection, RowT],
318
+ SQLTranslatorMixin,
319
+ TypeCoercionMixin,
320
+ AsyncStorageMixin,
321
+ AsyncPipelinedExecutionMixin,
322
+ ToSchemaMixin,
521
323
  ):
522
- """Oracle Async Driver Adapter."""
523
-
524
- connection: "OracleAsyncConnection"
525
-
526
- def __init__(self, connection: "OracleAsyncConnection") -> None:
527
- 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)
528
351
 
529
- @staticmethod
530
352
  @asynccontextmanager
531
- async def _with_cursor(connection: "OracleAsyncConnection") -> "AsyncGenerator[AsyncCursor, None]":
532
- 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()
533
358
  try:
534
359
  yield cursor
535
360
  finally:
536
- cursor.close()
537
-
538
- # --- Public API Methods --- #
539
- @overload
540
- async def select(
541
- self,
542
- sql: str,
543
- parameters: "Optional[StatementParameterType]" = None,
544
- *filters: "StatementFilter",
545
- connection: "Optional[OracleAsyncConnection]" = None,
546
- schema_type: None = None,
547
- **kwargs: Any,
548
- ) -> "Sequence[dict[str, Any]]": ...
549
- @overload
550
- async def select(
551
- self,
552
- sql: str,
553
- parameters: "Optional[StatementParameterType]" = None,
554
- *filters: "StatementFilter",
555
- connection: "Optional[OracleAsyncConnection]" = None,
556
- schema_type: "type[ModelDTOT]",
557
- **kwargs: Any,
558
- ) -> "Sequence[ModelDTOT]": ...
559
- async def select(
560
- self,
561
- sql: str,
562
- parameters: "Optional[StatementParameterType]" = None,
563
- *filters: "StatementFilter",
564
- connection: "Optional[OracleAsyncConnection]" = None,
565
- schema_type: "Optional[type[ModelDTOT]]" = None,
566
- **kwargs: Any,
567
- ) -> "Sequence[Union[ModelDTOT, dict[str, Any]]]":
568
- """Fetch data from the database.
569
-
570
- Args:
571
- sql: The SQL query string.
572
- parameters: The parameters for the query (dict, tuple, list, or None).
573
- *filters: Statement filters to apply.
574
- connection: Optional connection override.
575
- schema_type: Optional schema class for the result.
576
- **kwargs: Additional keyword arguments to merge with parameters if parameters is a dict.
577
-
578
- Returns:
579
- List of row data as either model instances or dictionaries.
580
- """
581
- connection = self._connection(connection)
582
- sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
583
-
584
- async with self._with_cursor(connection) as cursor:
585
- await cursor.execute(sql, parameters) # pyright: ignore[reportUnknownMemberType]
586
- results = await cursor.fetchall() # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType]
587
- if not results:
588
- return []
589
- # Get column names from description
590
- column_names = [col[0] for col in cursor.description or []] # pyright: ignore[reportUnknownMemberType,reportUnknownVariableType]
591
-
592
- return self.to_schema([dict(zip(column_names, row)) for row in results], schema_type=schema_type)
593
-
594
- @overload
595
- async def select_one(
596
- self,
597
- sql: str,
598
- parameters: "Optional[StatementParameterType]" = None,
599
- *filters: "StatementFilter",
600
- connection: "Optional[OracleAsyncConnection]" = None,
601
- schema_type: None = None,
602
- **kwargs: Any,
603
- ) -> "dict[str, Any]": ...
604
- @overload
605
- async def select_one(
606
- self,
607
- sql: str,
608
- parameters: "Optional[StatementParameterType]" = None,
609
- *filters: "StatementFilter",
610
- connection: "Optional[OracleAsyncConnection]" = None,
611
- schema_type: "type[ModelDTOT]",
612
- **kwargs: Any,
613
- ) -> "ModelDTOT": ...
614
- async def select_one(
615
- self,
616
- sql: str,
617
- parameters: "Optional[StatementParameterType]" = None,
618
- *filters: "StatementFilter",
619
- connection: "Optional[OracleAsyncConnection]" = None,
620
- schema_type: "Optional[type[ModelDTOT]]" = None,
621
- **kwargs: Any,
622
- ) -> "Union[ModelDTOT, dict[str, Any]]":
623
- """Fetch one row from the database.
624
-
625
- Args:
626
- sql: The SQL query string.
627
- parameters: The parameters for the query (dict, tuple, list, or None).
628
- *filters: Statement filters to apply.
629
- connection: Optional connection override.
630
- schema_type: Optional schema class for the result.
631
- **kwargs: Additional keyword arguments to merge with parameters if parameters is a dict.
632
-
633
- Returns:
634
- The first row of the query results.
635
- """
636
- connection = self._connection(connection)
637
- sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
638
-
639
- async with self._with_cursor(connection) as cursor:
640
- await cursor.execute(sql, parameters) # pyright: ignore[reportUnknownMemberType]
641
- result = await cursor.fetchone() # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType]
642
- result = self.check_not_found(result) # pyright: ignore[reportUnknownArgumentType]
643
- column_names = [col[0] for col in cursor.description or []] # pyright: ignore[reportUnknownMemberType,reportUnknownVariableType]
644
-
645
- return self.to_schema(dict(zip(column_names, result)), schema_type=schema_type)
646
-
647
- @overload
648
- async def select_one_or_none(
649
- self,
650
- sql: str,
651
- parameters: "Optional[StatementParameterType]" = None,
652
- *filters: "StatementFilter",
653
- connection: "Optional[OracleAsyncConnection]" = None,
654
- schema_type: None = None,
655
- **kwargs: Any,
656
- ) -> "Optional[dict[str, Any]]": ...
657
- @overload
658
- async def select_one_or_none(
659
- self,
660
- sql: str,
661
- parameters: "Optional[StatementParameterType]" = None,
662
- *filters: "StatementFilter",
663
- connection: "Optional[OracleAsyncConnection]" = None,
664
- schema_type: "type[ModelDTOT]",
665
- **kwargs: Any,
666
- ) -> "Optional[ModelDTOT]": ...
667
- async def select_one_or_none(
668
- self,
669
- sql: str,
670
- parameters: "Optional[StatementParameterType]" = None,
671
- *filters: "StatementFilter",
672
- connection: "Optional[OracleAsyncConnection]" = None,
673
- schema_type: "Optional[type[ModelDTOT]]" = None,
674
- **kwargs: Any,
675
- ) -> "Optional[Union[ModelDTOT, dict[str, Any]]]":
676
- """Fetch one row from the database or return None if no rows found.
677
-
678
- Args:
679
- sql: The SQL query string.
680
- parameters: The parameters for the query (dict, tuple, list, or None).
681
- *filters: Statement filters to apply.
682
- connection: Optional connection override.
683
- schema_type: Optional schema class for the result.
684
- **kwargs: Additional keyword arguments to merge with parameters if parameters is a dict.
685
-
686
- Returns:
687
- The first row of the query results, or None if no results found.
688
- """
689
- connection = self._connection(connection)
690
- sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
691
-
692
- async with self._with_cursor(connection) as cursor:
693
- await cursor.execute(sql, parameters) # pyright: ignore[reportUnknownMemberType]
694
- result = await cursor.fetchone() # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType]
695
- if result is None:
696
- return None
697
- column_names = [col[0] for col in cursor.description or []] # pyright: ignore[reportUnknownMemberType,reportUnknownVariableType]
698
- return self.to_schema(dict(zip(column_names, result)), schema_type=schema_type)
699
-
700
- @overload
701
- async def select_value(
702
- self,
703
- sql: str,
704
- parameters: "Optional[StatementParameterType]" = None,
705
- *filters: "StatementFilter",
706
- connection: "Optional[OracleAsyncConnection]" = None,
707
- schema_type: None = None,
708
- **kwargs: Any,
709
- ) -> "Any": ...
710
- @overload
711
- async def select_value(
712
- self,
713
- sql: str,
714
- parameters: "Optional[StatementParameterType]" = None,
715
- *filters: "StatementFilter",
716
- connection: "Optional[OracleAsyncConnection]" = None,
717
- schema_type: "type[T]",
718
- **kwargs: Any,
719
- ) -> "T": ...
720
- async def select_value(
721
- self,
722
- sql: str,
723
- parameters: "Optional[StatementParameterType]" = None,
724
- *filters: "StatementFilter",
725
- connection: "Optional[OracleAsyncConnection]" = None,
726
- schema_type: "Optional[type[T]]" = None,
727
- **kwargs: Any,
728
- ) -> "Union[T, Any]":
729
- """Fetch a single value from the database.
730
-
731
- Args:
732
- sql: The SQL query string.
733
- parameters: The parameters for the query (dict, tuple, list, or None).
734
- *filters: Statement filters to apply.
735
- connection: Optional connection override.
736
- schema_type: Optional type to convert the result to.
737
- **kwargs: Additional keyword arguments to merge with parameters if parameters is a dict.
738
-
739
- Returns:
740
- The first value of the first row of the query results.
741
- """
742
- connection = self._connection(connection)
743
- sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
744
-
745
- async with self._with_cursor(connection) as cursor:
746
- await cursor.execute(sql, parameters) # pyright: ignore[reportUnknownMemberType]
747
- result = await cursor.fetchone() # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType]
748
- result = self.check_not_found(result) # pyright: ignore[reportUnknownArgumentType]
749
-
750
- if schema_type is None:
751
- return result[0] # pyright: ignore[reportUnknownArgumentType]
752
- return schema_type(result[0]) # type: ignore[call-arg]
753
-
754
- @overload
755
- async def select_value_or_none(
756
- self,
757
- sql: str,
758
- parameters: "Optional[StatementParameterType]" = None,
759
- *filters: "StatementFilter",
760
- connection: "Optional[OracleAsyncConnection]" = None,
761
- schema_type: None = None,
762
- **kwargs: Any,
763
- ) -> "Optional[Any]": ...
764
- @overload
765
- async def select_value_or_none(
766
- self,
767
- sql: str,
768
- parameters: "Optional[StatementParameterType]" = None,
769
- *filters: "StatementFilter",
770
- connection: "Optional[OracleAsyncConnection]" = None,
771
- schema_type: "type[T]",
772
- **kwargs: Any,
773
- ) -> "Optional[T]": ...
774
- async def select_value_or_none(
775
- self,
776
- sql: str,
777
- parameters: "Optional[StatementParameterType]" = None,
778
- *filters: "StatementFilter",
779
- connection: "Optional[OracleAsyncConnection]" = None,
780
- schema_type: "Optional[type[T]]" = None,
781
- **kwargs: Any,
782
- ) -> "Optional[Union[T, Any]]":
783
- """Fetch a single value or None if not found.
784
-
785
- Args:
786
- sql: The SQL query string.
787
- parameters: The parameters for the query (dict, tuple, list, or None).
788
- *filters: Statement filters to apply.
789
- connection: Optional connection override.
790
- schema_type: Optional type to convert the result to.
791
- **kwargs: Additional keyword arguments to merge with parameters if parameters is a dict.
792
-
793
- Returns:
794
- The first value of the first row of the query results, or None if no results found.
795
- """
796
- connection = self._connection(connection)
797
- sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
798
-
799
- async with self._with_cursor(connection) as cursor:
800
- await cursor.execute(sql, parameters) # pyright: ignore[reportUnknownMemberType]
801
- result = await cursor.fetchone() # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType]
802
- if result is None:
803
- return None
804
-
805
- if schema_type is None:
806
- return result[0] # pyright: ignore[reportUnknownArgumentType]
807
- return schema_type(result[0]) # type: ignore[call-arg]
808
-
809
- async def insert_update_delete(
810
- self,
811
- sql: str,
812
- parameters: "Optional[StatementParameterType]" = None,
813
- *filters: "StatementFilter",
814
- connection: "Optional[OracleAsyncConnection]" = None,
815
- **kwargs: Any,
816
- ) -> int:
817
- """Insert, update, or delete data from the database.
818
-
819
- Args:
820
- sql: The SQL statement to execute.
821
- parameters: The parameters for the statement (dict, tuple, list, or None).
822
- *filters: Statement filters to apply.
823
- connection: Optional connection override.
824
- **kwargs: Additional keyword arguments to merge with parameters if parameters is a dict.
825
-
826
- Returns:
827
- Row count affected by the operation.
828
- """
829
- connection = self._connection(connection)
830
- sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
831
-
832
- async with self._with_cursor(connection) as cursor:
833
- await cursor.execute(sql, parameters) # pyright: ignore[reportUnknownMemberType]
834
- return cursor.rowcount # pyright: ignore[reportUnknownMemberType,reportUnknownVariableType]
835
-
836
- @overload
837
- async def insert_update_delete_returning(
838
- self,
839
- sql: str,
840
- parameters: "Optional[StatementParameterType]" = None,
841
- *filters: "StatementFilter",
842
- connection: "Optional[OracleAsyncConnection]" = None,
843
- schema_type: None = None,
844
- **kwargs: Any,
845
- ) -> "dict[str, Any]": ...
846
- @overload
847
- async def insert_update_delete_returning(
848
- self,
849
- sql: str,
850
- parameters: "Optional[StatementParameterType]" = None,
851
- *filters: "StatementFilter",
852
- connection: "Optional[OracleAsyncConnection]" = None,
853
- schema_type: "type[ModelDTOT]",
854
- **kwargs: Any,
855
- ) -> "ModelDTOT": ...
856
- async def insert_update_delete_returning(
857
- self,
858
- sql: str,
859
- parameters: "Optional[StatementParameterType]" = None,
860
- *filters: "StatementFilter",
861
- connection: "Optional[OracleAsyncConnection]" = None,
862
- schema_type: "Optional[type[ModelDTOT]]" = None,
863
- **kwargs: Any,
864
- ) -> "Optional[Union[dict[str, Any], ModelDTOT]]":
865
- """Insert, update, or delete data from the database and return result.
866
-
867
- Args:
868
- sql: The SQL statement with RETURNING clause.
869
- parameters: The parameters for the statement (dict, tuple, list, or None).
870
- *filters: Statement filters to apply.
871
- connection: Optional connection override.
872
- schema_type: Optional schema class for the result.
873
- **kwargs: Additional keyword arguments to merge with parameters if parameters is a dict.
874
-
875
- Returns:
876
- The returned row data, as either a model instance or dictionary.
877
- """
878
- connection = self._connection(connection)
879
- sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
880
-
881
- async with self._with_cursor(connection) as cursor:
882
- await cursor.execute(sql, parameters) # pyright: ignore[reportUnknownMemberType]
883
- result = await cursor.fetchone() # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType]
884
- if result is None:
885
- return None
886
-
887
- # Get column names
888
- column_names = [col[0] for col in cursor.description or []] # pyright: ignore[reportUnknownMemberType,reportUnknownVariableType]
889
-
890
- if schema_type is not None:
891
- return cast("ModelDTOT", schema_type(**dict(zip(column_names, result)))) # pyright: ignore[reportUnknownArgumentType]
892
- return dict(zip(column_names, result)) # pyright: ignore[reportUnknownArgumentType,reportUnknownVariableType]
893
-
894
- async def execute_script(
895
- self,
896
- sql: str,
897
- parameters: "Optional[StatementParameterType]" = None,
898
- connection: "Optional[OracleAsyncConnection]" = None,
899
- **kwargs: Any,
900
- ) -> str:
901
- """Execute a SQL script.
902
-
903
- Args:
904
- sql: The SQL script to execute.
905
- parameters: The parameters for the script (dict, tuple, list, or None).
906
- connection: Optional connection override.
907
- **kwargs: Additional keyword arguments to merge with parameters if parameters is a dict.
908
-
909
- Returns:
910
- A success message.
911
- """
912
- connection = self._connection(connection)
913
- sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
914
-
915
- async with self._with_cursor(connection) as cursor:
916
- await cursor.execute(sql, parameters) # pyright: ignore[reportUnknownMemberType]
917
- return str(cursor.rowcount) # pyright: ignore[reportUnknownMemberType]
918
-
919
- async def select_arrow(
920
- self,
921
- sql: str,
922
- parameters: "Optional[StatementParameterType]" = None,
923
- *filters: "StatementFilter",
924
- connection: "Optional[OracleAsyncConnection]" = None,
925
- **kwargs: Any,
926
- ) -> "ArrowTable": # pyright: ignore[reportUnknownVariableType]
927
- """Execute a SQL query asynchronously and return results as an Apache Arrow Table.
928
-
929
- Args:
930
- sql: The SQL query string.
931
- parameters: Parameters for the query.
932
- filters: Statement filters to apply.
933
- connection: Optional connection override.
934
- **kwargs: Additional keyword arguments to merge with parameters if parameters is a dict.
935
-
936
- Returns:
937
- An Apache Arrow Table containing the query results.
938
- """
939
-
940
- connection = self._connection(connection)
941
- sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
942
- results = await connection.fetch_df_all(sql, parameters)
943
- return ArrowTable.from_arrays(arrays=results.column_arrays(), names=results.column_names()) # pyright: ignore
944
-
945
- def _connection(self, connection: "Optional[OracleAsyncConnection]" = None) -> "OracleAsyncConnection":
946
- """Get the connection to use for the operation.
947
-
948
- Args:
949
- connection: Optional connection to use.
950
-
951
- Returns:
952
- The connection to use.
953
- """
954
- 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
+ )