sqlspec 0.26.0__py3-none-any.whl → 0.28.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of sqlspec might be problematic. Click here for more details.

Files changed (212) hide show
  1. sqlspec/__init__.py +7 -15
  2. sqlspec/_serialization.py +55 -25
  3. sqlspec/_typing.py +155 -52
  4. sqlspec/adapters/adbc/_types.py +1 -1
  5. sqlspec/adapters/adbc/adk/__init__.py +5 -0
  6. sqlspec/adapters/adbc/adk/store.py +880 -0
  7. sqlspec/adapters/adbc/config.py +62 -12
  8. sqlspec/adapters/adbc/data_dictionary.py +74 -2
  9. sqlspec/adapters/adbc/driver.py +226 -58
  10. sqlspec/adapters/adbc/litestar/__init__.py +5 -0
  11. sqlspec/adapters/adbc/litestar/store.py +504 -0
  12. sqlspec/adapters/adbc/type_converter.py +44 -50
  13. sqlspec/adapters/aiosqlite/_types.py +1 -1
  14. sqlspec/adapters/aiosqlite/adk/__init__.py +5 -0
  15. sqlspec/adapters/aiosqlite/adk/store.py +536 -0
  16. sqlspec/adapters/aiosqlite/config.py +86 -16
  17. sqlspec/adapters/aiosqlite/data_dictionary.py +34 -2
  18. sqlspec/adapters/aiosqlite/driver.py +127 -38
  19. sqlspec/adapters/aiosqlite/litestar/__init__.py +5 -0
  20. sqlspec/adapters/aiosqlite/litestar/store.py +281 -0
  21. sqlspec/adapters/aiosqlite/pool.py +7 -7
  22. sqlspec/adapters/asyncmy/__init__.py +7 -1
  23. sqlspec/adapters/asyncmy/_types.py +1 -1
  24. sqlspec/adapters/asyncmy/adk/__init__.py +5 -0
  25. sqlspec/adapters/asyncmy/adk/store.py +503 -0
  26. sqlspec/adapters/asyncmy/config.py +59 -17
  27. sqlspec/adapters/asyncmy/data_dictionary.py +41 -2
  28. sqlspec/adapters/asyncmy/driver.py +293 -62
  29. sqlspec/adapters/asyncmy/litestar/__init__.py +5 -0
  30. sqlspec/adapters/asyncmy/litestar/store.py +296 -0
  31. sqlspec/adapters/asyncpg/__init__.py +2 -1
  32. sqlspec/adapters/asyncpg/_type_handlers.py +71 -0
  33. sqlspec/adapters/asyncpg/_types.py +11 -7
  34. sqlspec/adapters/asyncpg/adk/__init__.py +5 -0
  35. sqlspec/adapters/asyncpg/adk/store.py +460 -0
  36. sqlspec/adapters/asyncpg/config.py +57 -36
  37. sqlspec/adapters/asyncpg/data_dictionary.py +48 -2
  38. sqlspec/adapters/asyncpg/driver.py +153 -23
  39. sqlspec/adapters/asyncpg/litestar/__init__.py +5 -0
  40. sqlspec/adapters/asyncpg/litestar/store.py +253 -0
  41. sqlspec/adapters/bigquery/_types.py +1 -1
  42. sqlspec/adapters/bigquery/adk/__init__.py +5 -0
  43. sqlspec/adapters/bigquery/adk/store.py +585 -0
  44. sqlspec/adapters/bigquery/config.py +36 -11
  45. sqlspec/adapters/bigquery/data_dictionary.py +42 -2
  46. sqlspec/adapters/bigquery/driver.py +489 -144
  47. sqlspec/adapters/bigquery/litestar/__init__.py +5 -0
  48. sqlspec/adapters/bigquery/litestar/store.py +327 -0
  49. sqlspec/adapters/bigquery/type_converter.py +55 -23
  50. sqlspec/adapters/duckdb/_types.py +2 -2
  51. sqlspec/adapters/duckdb/adk/__init__.py +14 -0
  52. sqlspec/adapters/duckdb/adk/store.py +563 -0
  53. sqlspec/adapters/duckdb/config.py +79 -21
  54. sqlspec/adapters/duckdb/data_dictionary.py +41 -2
  55. sqlspec/adapters/duckdb/driver.py +225 -44
  56. sqlspec/adapters/duckdb/litestar/__init__.py +5 -0
  57. sqlspec/adapters/duckdb/litestar/store.py +332 -0
  58. sqlspec/adapters/duckdb/pool.py +5 -5
  59. sqlspec/adapters/duckdb/type_converter.py +51 -21
  60. sqlspec/adapters/oracledb/_numpy_handlers.py +133 -0
  61. sqlspec/adapters/oracledb/_types.py +20 -2
  62. sqlspec/adapters/oracledb/adk/__init__.py +5 -0
  63. sqlspec/adapters/oracledb/adk/store.py +1628 -0
  64. sqlspec/adapters/oracledb/config.py +120 -36
  65. sqlspec/adapters/oracledb/data_dictionary.py +87 -20
  66. sqlspec/adapters/oracledb/driver.py +475 -86
  67. sqlspec/adapters/oracledb/litestar/__init__.py +5 -0
  68. sqlspec/adapters/oracledb/litestar/store.py +765 -0
  69. sqlspec/adapters/oracledb/migrations.py +316 -25
  70. sqlspec/adapters/oracledb/type_converter.py +91 -16
  71. sqlspec/adapters/psqlpy/_type_handlers.py +44 -0
  72. sqlspec/adapters/psqlpy/_types.py +2 -1
  73. sqlspec/adapters/psqlpy/adk/__init__.py +5 -0
  74. sqlspec/adapters/psqlpy/adk/store.py +483 -0
  75. sqlspec/adapters/psqlpy/config.py +45 -19
  76. sqlspec/adapters/psqlpy/data_dictionary.py +48 -2
  77. sqlspec/adapters/psqlpy/driver.py +108 -41
  78. sqlspec/adapters/psqlpy/litestar/__init__.py +5 -0
  79. sqlspec/adapters/psqlpy/litestar/store.py +272 -0
  80. sqlspec/adapters/psqlpy/type_converter.py +40 -11
  81. sqlspec/adapters/psycopg/_type_handlers.py +80 -0
  82. sqlspec/adapters/psycopg/_types.py +2 -1
  83. sqlspec/adapters/psycopg/adk/__init__.py +5 -0
  84. sqlspec/adapters/psycopg/adk/store.py +962 -0
  85. sqlspec/adapters/psycopg/config.py +65 -37
  86. sqlspec/adapters/psycopg/data_dictionary.py +91 -3
  87. sqlspec/adapters/psycopg/driver.py +200 -78
  88. sqlspec/adapters/psycopg/litestar/__init__.py +5 -0
  89. sqlspec/adapters/psycopg/litestar/store.py +554 -0
  90. sqlspec/adapters/sqlite/__init__.py +2 -1
  91. sqlspec/adapters/sqlite/_type_handlers.py +86 -0
  92. sqlspec/adapters/sqlite/_types.py +1 -1
  93. sqlspec/adapters/sqlite/adk/__init__.py +5 -0
  94. sqlspec/adapters/sqlite/adk/store.py +582 -0
  95. sqlspec/adapters/sqlite/config.py +85 -16
  96. sqlspec/adapters/sqlite/data_dictionary.py +34 -2
  97. sqlspec/adapters/sqlite/driver.py +120 -52
  98. sqlspec/adapters/sqlite/litestar/__init__.py +5 -0
  99. sqlspec/adapters/sqlite/litestar/store.py +318 -0
  100. sqlspec/adapters/sqlite/pool.py +5 -5
  101. sqlspec/base.py +45 -26
  102. sqlspec/builder/__init__.py +73 -4
  103. sqlspec/builder/_base.py +91 -58
  104. sqlspec/builder/_column.py +5 -5
  105. sqlspec/builder/_ddl.py +98 -89
  106. sqlspec/builder/_delete.py +5 -4
  107. sqlspec/builder/_dml.py +388 -0
  108. sqlspec/{_sql.py → builder/_factory.py} +41 -44
  109. sqlspec/builder/_insert.py +5 -82
  110. sqlspec/builder/{mixins/_join_operations.py → _join.py} +145 -143
  111. sqlspec/builder/_merge.py +446 -11
  112. sqlspec/builder/_parsing_utils.py +9 -11
  113. sqlspec/builder/_select.py +1313 -25
  114. sqlspec/builder/_update.py +11 -42
  115. sqlspec/cli.py +76 -69
  116. sqlspec/config.py +331 -62
  117. sqlspec/core/__init__.py +5 -4
  118. sqlspec/core/cache.py +18 -18
  119. sqlspec/core/compiler.py +6 -8
  120. sqlspec/core/filters.py +55 -47
  121. sqlspec/core/hashing.py +9 -9
  122. sqlspec/core/parameters.py +76 -45
  123. sqlspec/core/result.py +234 -47
  124. sqlspec/core/splitter.py +16 -17
  125. sqlspec/core/statement.py +32 -31
  126. sqlspec/core/type_conversion.py +3 -2
  127. sqlspec/driver/__init__.py +1 -3
  128. sqlspec/driver/_async.py +183 -160
  129. sqlspec/driver/_common.py +197 -109
  130. sqlspec/driver/_sync.py +189 -161
  131. sqlspec/driver/mixins/_result_tools.py +20 -236
  132. sqlspec/driver/mixins/_sql_translator.py +4 -4
  133. sqlspec/exceptions.py +70 -7
  134. sqlspec/extensions/adk/__init__.py +53 -0
  135. sqlspec/extensions/adk/_types.py +51 -0
  136. sqlspec/extensions/adk/converters.py +172 -0
  137. sqlspec/extensions/adk/migrations/0001_create_adk_tables.py +144 -0
  138. sqlspec/extensions/adk/migrations/__init__.py +0 -0
  139. sqlspec/extensions/adk/service.py +181 -0
  140. sqlspec/extensions/adk/store.py +536 -0
  141. sqlspec/extensions/aiosql/adapter.py +69 -61
  142. sqlspec/extensions/fastapi/__init__.py +21 -0
  143. sqlspec/extensions/fastapi/extension.py +331 -0
  144. sqlspec/extensions/fastapi/providers.py +543 -0
  145. sqlspec/extensions/flask/__init__.py +36 -0
  146. sqlspec/extensions/flask/_state.py +71 -0
  147. sqlspec/extensions/flask/_utils.py +40 -0
  148. sqlspec/extensions/flask/extension.py +389 -0
  149. sqlspec/extensions/litestar/__init__.py +21 -4
  150. sqlspec/extensions/litestar/cli.py +54 -10
  151. sqlspec/extensions/litestar/config.py +56 -266
  152. sqlspec/extensions/litestar/handlers.py +46 -17
  153. sqlspec/extensions/litestar/migrations/0001_create_session_table.py +137 -0
  154. sqlspec/extensions/litestar/migrations/__init__.py +3 -0
  155. sqlspec/extensions/litestar/plugin.py +349 -224
  156. sqlspec/extensions/litestar/providers.py +25 -25
  157. sqlspec/extensions/litestar/store.py +265 -0
  158. sqlspec/extensions/starlette/__init__.py +10 -0
  159. sqlspec/extensions/starlette/_state.py +25 -0
  160. sqlspec/extensions/starlette/_utils.py +52 -0
  161. sqlspec/extensions/starlette/extension.py +254 -0
  162. sqlspec/extensions/starlette/middleware.py +154 -0
  163. sqlspec/loader.py +30 -49
  164. sqlspec/migrations/base.py +200 -76
  165. sqlspec/migrations/commands.py +591 -62
  166. sqlspec/migrations/context.py +6 -9
  167. sqlspec/migrations/fix.py +199 -0
  168. sqlspec/migrations/loaders.py +47 -19
  169. sqlspec/migrations/runner.py +241 -75
  170. sqlspec/migrations/tracker.py +237 -21
  171. sqlspec/migrations/utils.py +51 -3
  172. sqlspec/migrations/validation.py +177 -0
  173. sqlspec/protocols.py +106 -36
  174. sqlspec/storage/_utils.py +85 -0
  175. sqlspec/storage/backends/fsspec.py +133 -107
  176. sqlspec/storage/backends/local.py +78 -51
  177. sqlspec/storage/backends/obstore.py +276 -168
  178. sqlspec/storage/registry.py +75 -39
  179. sqlspec/typing.py +30 -84
  180. sqlspec/utils/__init__.py +25 -4
  181. sqlspec/utils/arrow_helpers.py +81 -0
  182. sqlspec/utils/config_resolver.py +6 -6
  183. sqlspec/utils/correlation.py +4 -5
  184. sqlspec/utils/data_transformation.py +3 -2
  185. sqlspec/utils/deprecation.py +9 -8
  186. sqlspec/utils/fixtures.py +4 -4
  187. sqlspec/utils/logging.py +46 -6
  188. sqlspec/utils/module_loader.py +205 -5
  189. sqlspec/utils/portal.py +311 -0
  190. sqlspec/utils/schema.py +288 -0
  191. sqlspec/utils/serializers.py +113 -4
  192. sqlspec/utils/sync_tools.py +36 -22
  193. sqlspec/utils/text.py +1 -2
  194. sqlspec/utils/type_guards.py +136 -20
  195. sqlspec/utils/version.py +433 -0
  196. {sqlspec-0.26.0.dist-info → sqlspec-0.28.0.dist-info}/METADATA +41 -22
  197. sqlspec-0.28.0.dist-info/RECORD +221 -0
  198. sqlspec/builder/mixins/__init__.py +0 -55
  199. sqlspec/builder/mixins/_cte_and_set_ops.py +0 -253
  200. sqlspec/builder/mixins/_delete_operations.py +0 -50
  201. sqlspec/builder/mixins/_insert_operations.py +0 -282
  202. sqlspec/builder/mixins/_merge_operations.py +0 -698
  203. sqlspec/builder/mixins/_order_limit_operations.py +0 -145
  204. sqlspec/builder/mixins/_pivot_operations.py +0 -157
  205. sqlspec/builder/mixins/_select_operations.py +0 -930
  206. sqlspec/builder/mixins/_update_operations.py +0 -199
  207. sqlspec/builder/mixins/_where_clause.py +0 -1298
  208. sqlspec-0.26.0.dist-info/RECORD +0 -157
  209. sqlspec-0.26.0.dist-info/licenses/NOTICE +0 -29
  210. {sqlspec-0.26.0.dist-info → sqlspec-0.28.0.dist-info}/WHEEL +0 -0
  211. {sqlspec-0.26.0.dist-info → sqlspec-0.28.0.dist-info}/entry_points.txt +0 -0
  212. {sqlspec-0.26.0.dist-info → sqlspec-0.28.0.dist-info}/licenses/LICENSE +0 -0
sqlspec/driver/_async.py CHANGED
@@ -1,14 +1,22 @@
1
1
  """Asynchronous driver protocol implementation."""
2
2
 
3
3
  from abc import abstractmethod
4
- from typing import TYPE_CHECKING, Any, Final, NoReturn, Optional, TypeVar, Union, cast, overload
4
+ from typing import TYPE_CHECKING, Any, Final, TypeVar, overload
5
5
 
6
6
  from sqlspec.core import SQL, Statement
7
- from sqlspec.driver._common import CommonDriverAttributesMixin, DataDictionaryMixin, ExecutionResult, VersionInfo
8
- from sqlspec.driver.mixins import SQLTranslatorMixin, ToSchemaMixin
9
- from sqlspec.exceptions import NotFoundError
7
+ from sqlspec.core.result import create_arrow_result
8
+ from sqlspec.driver._common import (
9
+ CommonDriverAttributesMixin,
10
+ DataDictionaryMixin,
11
+ ExecutionResult,
12
+ VersionInfo,
13
+ handle_single_row_error,
14
+ )
15
+ from sqlspec.driver.mixins import SQLTranslatorMixin
16
+ from sqlspec.exceptions import ImproperConfigurationError
17
+ from sqlspec.utils.arrow_helpers import convert_dict_to_arrow
10
18
  from sqlspec.utils.logging import get_logger
11
- from sqlspec.utils.type_guards import is_dict_row, is_indexable_row
19
+ from sqlspec.utils.module_loader import ensure_pyarrow
12
20
 
13
21
  if TYPE_CHECKING:
14
22
  from collections.abc import Sequence
@@ -16,7 +24,7 @@ if TYPE_CHECKING:
16
24
 
17
25
  from sqlspec.builder import QueryBuilder
18
26
  from sqlspec.core import SQLResult, StatementConfig, StatementFilter
19
- from sqlspec.typing import ModelDTOT, StatementParameters
27
+ from sqlspec.typing import SchemaT, StatementParameters
20
28
 
21
29
  _LOGGER_NAME: Final[str] = "sqlspec"
22
30
  logger = get_logger(_LOGGER_NAME)
@@ -29,7 +37,7 @@ EMPTY_FILTERS: Final["list[StatementFilter]"] = []
29
37
  AsyncDriverT = TypeVar("AsyncDriverT", bound="AsyncDriverAdapterBase")
30
38
 
31
39
 
32
- class AsyncDriverAdapterBase(CommonDriverAttributesMixin, SQLTranslatorMixin, ToSchemaMixin):
40
+ class AsyncDriverAdapterBase(CommonDriverAttributesMixin, SQLTranslatorMixin):
33
41
  """Base class for asynchronous database drivers."""
34
42
 
35
43
  __slots__ = ()
@@ -96,7 +104,7 @@ class AsyncDriverAdapterBase(CommonDriverAttributesMixin, SQLTranslatorMixin, To
96
104
  """Commit the current transaction on the current connection."""
97
105
 
98
106
  @abstractmethod
99
- async def _try_special_handling(self, cursor: Any, statement: "SQL") -> "Optional[SQLResult]":
107
+ async def _try_special_handling(self, cursor: Any, statement: "SQL") -> "SQLResult | None":
100
108
  """Hook for database-specific special operations (e.g., PostgreSQL COPY, bulk operations).
101
109
 
102
110
  This method is called first in dispatch_statement_execution() to allow drivers to handle
@@ -169,10 +177,10 @@ class AsyncDriverAdapterBase(CommonDriverAttributesMixin, SQLTranslatorMixin, To
169
177
 
170
178
  async def execute(
171
179
  self,
172
- statement: "Union[SQL, Statement, QueryBuilder]",
180
+ statement: "SQL | Statement | QueryBuilder",
173
181
  /,
174
- *parameters: "Union[StatementParameters, StatementFilter]",
175
- statement_config: "Optional[StatementConfig]" = None,
182
+ *parameters: "StatementParameters | StatementFilter",
183
+ statement_config: "StatementConfig | None" = None,
176
184
  **kwargs: Any,
177
185
  ) -> "SQLResult":
178
186
  """Execute a statement with parameter handling."""
@@ -183,11 +191,11 @@ class AsyncDriverAdapterBase(CommonDriverAttributesMixin, SQLTranslatorMixin, To
183
191
 
184
192
  async def execute_many(
185
193
  self,
186
- statement: "Union[SQL, Statement, QueryBuilder]",
194
+ statement: "SQL | Statement | QueryBuilder",
187
195
  /,
188
196
  parameters: "Sequence[StatementParameters]",
189
- *filters: "Union[StatementParameters, StatementFilter]",
190
- statement_config: "Optional[StatementConfig]" = None,
197
+ *filters: "StatementParameters | StatementFilter",
198
+ statement_config: "StatementConfig | None" = None,
191
199
  **kwargs: Any,
192
200
  ) -> "SQLResult":
193
201
  """Execute statement multiple times with different parameters.
@@ -206,10 +214,10 @@ class AsyncDriverAdapterBase(CommonDriverAttributesMixin, SQLTranslatorMixin, To
206
214
 
207
215
  async def execute_script(
208
216
  self,
209
- statement: "Union[str, SQL]",
217
+ statement: "str | SQL",
210
218
  /,
211
- *parameters: "Union[StatementParameters, StatementFilter]",
212
- statement_config: "Optional[StatementConfig]" = None,
219
+ *parameters: "StatementParameters | StatementFilter",
220
+ statement_config: "StatementConfig | None" = None,
213
221
  **kwargs: Any,
214
222
  ) -> "SQLResult":
215
223
  """Execute a multi-statement script.
@@ -225,140 +233,209 @@ class AsyncDriverAdapterBase(CommonDriverAttributesMixin, SQLTranslatorMixin, To
225
233
  @overload
226
234
  async def select_one(
227
235
  self,
228
- statement: "Union[Statement, QueryBuilder]",
236
+ statement: "Statement | QueryBuilder",
229
237
  /,
230
- *parameters: "Union[StatementParameters, StatementFilter]",
231
- schema_type: "type[ModelDTOT]",
232
- statement_config: "Optional[StatementConfig]" = None,
238
+ *parameters: "StatementParameters | StatementFilter",
239
+ schema_type: "type[SchemaT]",
240
+ statement_config: "StatementConfig | None" = None,
233
241
  **kwargs: Any,
234
- ) -> "ModelDTOT": ...
242
+ ) -> "SchemaT": ...
235
243
 
236
244
  @overload
237
245
  async def select_one(
238
246
  self,
239
- statement: "Union[Statement, QueryBuilder]",
247
+ statement: "Statement | QueryBuilder",
240
248
  /,
241
- *parameters: "Union[StatementParameters, StatementFilter]",
249
+ *parameters: "StatementParameters | StatementFilter",
242
250
  schema_type: None = None,
243
- statement_config: "Optional[StatementConfig]" = None,
251
+ statement_config: "StatementConfig | None" = None,
244
252
  **kwargs: Any,
245
253
  ) -> "dict[str, Any]": ...
246
254
 
247
255
  async def select_one(
248
256
  self,
249
- statement: "Union[Statement, QueryBuilder]",
257
+ statement: "Statement | QueryBuilder",
250
258
  /,
251
- *parameters: "Union[StatementParameters, StatementFilter]",
252
- schema_type: "Optional[type[ModelDTOT]]" = None,
253
- statement_config: "Optional[StatementConfig]" = None,
259
+ *parameters: "StatementParameters | StatementFilter",
260
+ schema_type: "type[SchemaT] | None" = None,
261
+ statement_config: "StatementConfig | None" = None,
254
262
  **kwargs: Any,
255
- ) -> "Union[dict[str, Any], ModelDTOT]":
263
+ ) -> "SchemaT | dict[str, Any]":
256
264
  """Execute a select statement and return exactly one row.
257
265
 
258
266
  Raises an exception if no rows or more than one row is returned.
259
267
  """
260
268
  result = await self.execute(statement, *parameters, statement_config=statement_config, **kwargs)
261
- data = result.get_data()
262
- data_len: int = len(data)
263
- if data_len == 0:
264
- self._raise_no_rows_found()
265
- if data_len > 1:
266
- self._raise_expected_one_row(data_len)
267
- first_row = data[0]
268
- return self.to_schema(first_row, schema_type=schema_type) if schema_type else first_row
269
+ try:
270
+ return result.one(schema_type=schema_type)
271
+ except ValueError as error:
272
+ handle_single_row_error(error)
269
273
 
270
274
  @overload
271
275
  async def select_one_or_none(
272
276
  self,
273
- statement: "Union[Statement, QueryBuilder]",
277
+ statement: "Statement | QueryBuilder",
274
278
  /,
275
- *parameters: "Union[StatementParameters, StatementFilter]",
276
- schema_type: "type[ModelDTOT]",
277
- statement_config: "Optional[StatementConfig]" = None,
279
+ *parameters: "StatementParameters | StatementFilter",
280
+ schema_type: "type[SchemaT]",
281
+ statement_config: "StatementConfig | None" = None,
278
282
  **kwargs: Any,
279
- ) -> "Optional[ModelDTOT]": ...
283
+ ) -> "SchemaT | None": ...
280
284
 
281
285
  @overload
282
286
  async def select_one_or_none(
283
287
  self,
284
- statement: "Union[Statement, QueryBuilder]",
288
+ statement: "Statement | QueryBuilder",
285
289
  /,
286
- *parameters: "Union[StatementParameters, StatementFilter]",
290
+ *parameters: "StatementParameters | StatementFilter",
287
291
  schema_type: None = None,
288
- statement_config: "Optional[StatementConfig]" = None,
292
+ statement_config: "StatementConfig | None" = None,
289
293
  **kwargs: Any,
290
- ) -> "Optional[dict[str, Any]]": ...
294
+ ) -> "dict[str, Any] | None": ...
291
295
 
292
296
  async def select_one_or_none(
293
297
  self,
294
- statement: "Union[Statement, QueryBuilder]",
298
+ statement: "Statement | QueryBuilder",
295
299
  /,
296
- *parameters: "Union[StatementParameters, StatementFilter]",
297
- schema_type: "Optional[type[ModelDTOT]]" = None,
298
- statement_config: "Optional[StatementConfig]" = None,
300
+ *parameters: "StatementParameters | StatementFilter",
301
+ schema_type: "type[SchemaT] | None" = None,
302
+ statement_config: "StatementConfig | None" = None,
299
303
  **kwargs: Any,
300
- ) -> "Optional[Union[dict[str, Any], ModelDTOT]]":
304
+ ) -> "SchemaT | dict[str, Any] | None":
301
305
  """Execute a select statement and return at most one row.
302
306
 
303
307
  Returns None if no rows are found.
304
308
  Raises an exception if more than one row is returned.
305
309
  """
306
310
  result = await self.execute(statement, *parameters, statement_config=statement_config, **kwargs)
307
- data = result.get_data()
308
- data_len: int = len(data)
309
- if data_len == 0:
310
- return None
311
- if data_len > 1:
312
- self._raise_expected_at_most_one_row(data_len)
313
- first_row = data[0]
314
- return cast(
315
- "Optional[Union[dict[str, Any], ModelDTOT]]",
316
- self.to_schema(first_row, schema_type=schema_type) if schema_type else first_row,
317
- )
311
+ return result.one_or_none(schema_type=schema_type)
318
312
 
319
313
  @overload
320
314
  async def select(
321
315
  self,
322
- statement: "Union[Statement, QueryBuilder]",
316
+ statement: "Statement | QueryBuilder",
323
317
  /,
324
- *parameters: "Union[StatementParameters, StatementFilter]",
325
- schema_type: "type[ModelDTOT]",
326
- statement_config: "Optional[StatementConfig]" = None,
318
+ *parameters: "StatementParameters | StatementFilter",
319
+ schema_type: "type[SchemaT]",
320
+ statement_config: "StatementConfig | None" = None,
327
321
  **kwargs: Any,
328
- ) -> "list[ModelDTOT]": ...
322
+ ) -> "list[SchemaT]": ...
329
323
 
330
324
  @overload
331
325
  async def select(
332
326
  self,
333
- statement: "Union[Statement, QueryBuilder]",
327
+ statement: "Statement | QueryBuilder",
334
328
  /,
335
- *parameters: "Union[StatementParameters, StatementFilter]",
329
+ *parameters: "StatementParameters | StatementFilter",
336
330
  schema_type: None = None,
337
- statement_config: "Optional[StatementConfig]" = None,
331
+ statement_config: "StatementConfig | None" = None,
338
332
  **kwargs: Any,
339
333
  ) -> "list[dict[str, Any]]": ...
340
334
 
341
335
  async def select(
342
336
  self,
343
- statement: "Union[Statement, QueryBuilder]",
337
+ statement: "Statement | QueryBuilder",
344
338
  /,
345
- *parameters: "Union[StatementParameters, StatementFilter]",
346
- schema_type: "Optional[type[ModelDTOT]]" = None,
347
- statement_config: "Optional[StatementConfig]" = None,
339
+ *parameters: "StatementParameters | StatementFilter",
340
+ schema_type: "type[SchemaT] | None" = None,
341
+ statement_config: "StatementConfig | None" = None,
348
342
  **kwargs: Any,
349
- ) -> "Union[list[dict[str, Any]], list[ModelDTOT]]":
343
+ ) -> "list[SchemaT] | list[dict[str, Any]]":
350
344
  """Execute a select statement and return all rows."""
351
345
  result = await self.execute(statement, *parameters, statement_config=statement_config, **kwargs)
352
- return cast(
353
- "Union[list[dict[str, Any]], list[ModelDTOT]]", self.to_schema(result.get_data(), schema_type=schema_type)
346
+ return result.get_data(schema_type=schema_type)
347
+
348
+ async def select_to_arrow(
349
+ self,
350
+ statement: "Statement | QueryBuilder",
351
+ /,
352
+ *parameters: "StatementParameters | StatementFilter",
353
+ statement_config: "StatementConfig | None" = None,
354
+ return_format: str = "table",
355
+ native_only: bool = False,
356
+ batch_size: int | None = None,
357
+ arrow_schema: Any = None,
358
+ **kwargs: Any,
359
+ ) -> "Any":
360
+ """Execute query and return results as Apache Arrow format (async).
361
+
362
+ This base implementation uses the conversion path: execute() → dict → Arrow.
363
+ Adapters with native Arrow support (ADBC, DuckDB, BigQuery) override this
364
+ method to use zero-copy native paths for 5-10x performance improvement.
365
+
366
+ Args:
367
+ statement: SQL query string, Statement, or QueryBuilder
368
+ *parameters: Query parameters (same format as execute()/select())
369
+ statement_config: Optional statement configuration override
370
+ return_format: "table" for pyarrow.Table (default), "reader" for RecordBatchReader,
371
+ "batches" for iterator of RecordBatches
372
+ native_only: If True, raise error if native Arrow unavailable (default: False)
373
+ batch_size: Rows per batch for "batches" format (default: None = all rows)
374
+ arrow_schema: Optional pyarrow.Schema for type casting
375
+ **kwargs: Additional keyword arguments
376
+
377
+ Returns:
378
+ ArrowResult containing pyarrow.Table, RecordBatchReader, or RecordBatches
379
+
380
+ Raises:
381
+ ImproperConfigurationError: If native_only=True and adapter doesn't support native Arrow
382
+
383
+ Examples:
384
+ >>> result = await driver.select_to_arrow(
385
+ ... "SELECT * FROM users WHERE age > ?", 18
386
+ ... )
387
+ >>> df = result.to_pandas()
388
+ >>> print(df.head())
389
+
390
+ >>> # Force native Arrow path (raises error if unavailable)
391
+ >>> result = await driver.select_to_arrow(
392
+ ... "SELECT * FROM users", native_only=True
393
+ ... )
394
+ """
395
+ # Check pyarrow is available
396
+ ensure_pyarrow()
397
+
398
+ # Check if native_only requested but not supported
399
+ if native_only:
400
+ msg = (
401
+ f"Adapter '{self.__class__.__name__}' does not support native Arrow results. "
402
+ f"Use native_only=False to allow conversion path, or switch to an adapter "
403
+ f"with native Arrow support (ADBC, DuckDB, BigQuery)."
404
+ )
405
+ raise ImproperConfigurationError(msg)
406
+
407
+ # Execute query using standard path
408
+ result = await self.execute(statement, *parameters, statement_config=statement_config, **kwargs)
409
+
410
+ # Convert dict results to Arrow
411
+ arrow_data = convert_dict_to_arrow(
412
+ result.data,
413
+ return_format=return_format, # type: ignore[arg-type]
414
+ batch_size=batch_size,
415
+ )
416
+ if arrow_schema is not None:
417
+ import pyarrow as pa
418
+
419
+ if not isinstance(arrow_schema, pa.Schema):
420
+ msg = f"arrow_schema must be a pyarrow.Schema, got {type(arrow_schema).__name__}"
421
+ raise TypeError(msg)
422
+
423
+ arrow_data = arrow_data.cast(arrow_schema)
424
+ return create_arrow_result(
425
+ statement=result.statement,
426
+ data=arrow_data,
427
+ rows_affected=result.rows_affected,
428
+ last_inserted_id=result.last_inserted_id,
429
+ execution_time=result.execution_time,
430
+ metadata=result.metadata,
354
431
  )
355
432
 
356
433
  async def select_value(
357
434
  self,
358
- statement: "Union[Statement, QueryBuilder]",
435
+ statement: "Statement | QueryBuilder",
359
436
  /,
360
- *parameters: "Union[StatementParameters, StatementFilter]",
361
- statement_config: "Optional[StatementConfig]" = None,
437
+ *parameters: "StatementParameters | StatementFilter",
438
+ statement_config: "StatementConfig | None" = None,
362
439
  **kwargs: Any,
363
440
  ) -> Any:
364
441
  """Execute a select statement and return a single scalar value.
@@ -368,28 +445,16 @@ class AsyncDriverAdapterBase(CommonDriverAttributesMixin, SQLTranslatorMixin, To
368
445
  """
369
446
  result = await self.execute(statement, *parameters, statement_config=statement_config, **kwargs)
370
447
  try:
371
- row = result.one()
372
- except ValueError as e:
373
- self._raise_no_rows_found_from_exception(e)
374
- if not row:
375
- self._raise_no_rows_found()
376
- if is_dict_row(row):
377
- if not row:
378
- self._raise_row_no_columns()
379
- return next(iter(row.values()))
380
- if is_indexable_row(row):
381
- if not row:
382
- self._raise_row_no_columns()
383
- return row[0]
384
- self._raise_unexpected_row_type(type(row))
385
- return None
448
+ return result.scalar()
449
+ except ValueError as error:
450
+ handle_single_row_error(error)
386
451
 
387
452
  async def select_value_or_none(
388
453
  self,
389
- statement: "Union[Statement, QueryBuilder]",
454
+ statement: "Statement | QueryBuilder",
390
455
  /,
391
- *parameters: "Union[StatementParameters, StatementFilter]",
392
- statement_config: "Optional[StatementConfig]" = None,
456
+ *parameters: "StatementParameters | StatementFilter",
457
+ statement_config: "StatementConfig | None" = None,
393
458
  **kwargs: Any,
394
459
  ) -> Any:
395
460
  """Execute a select statement and return a single scalar value or None.
@@ -399,53 +464,39 @@ class AsyncDriverAdapterBase(CommonDriverAttributesMixin, SQLTranslatorMixin, To
399
464
  Raises an exception if more than one row is returned.
400
465
  """
401
466
  result = await self.execute(statement, *parameters, statement_config=statement_config, **kwargs)
402
- data = result.get_data()
403
- data_len: int = len(data)
404
- if data_len == 0:
405
- return None
406
- if data_len > 1:
407
- self._raise_expected_at_most_one_row(data_len)
408
- row = data[0]
409
- if is_dict_row(row):
410
- if not row:
411
- return None
412
- return next(iter(row.values()))
413
- if is_indexable_row(row):
414
- return row[0]
415
- self._raise_cannot_extract_value_from_row_type(type(row).__name__)
416
- return None
467
+ return result.scalar_or_none()
417
468
 
418
469
  @overload
419
470
  async def select_with_total(
420
471
  self,
421
- statement: "Union[Statement, QueryBuilder]",
472
+ statement: "Statement | QueryBuilder",
422
473
  /,
423
- *parameters: "Union[StatementParameters, StatementFilter]",
424
- schema_type: "type[ModelDTOT]",
425
- statement_config: "Optional[StatementConfig]" = None,
474
+ *parameters: "StatementParameters | StatementFilter",
475
+ schema_type: "type[SchemaT]",
476
+ statement_config: "StatementConfig | None" = None,
426
477
  **kwargs: Any,
427
- ) -> "tuple[list[ModelDTOT], int]": ...
478
+ ) -> "tuple[list[SchemaT], int]": ...
428
479
 
429
480
  @overload
430
481
  async def select_with_total(
431
482
  self,
432
- statement: "Union[Statement, QueryBuilder]",
483
+ statement: "Statement | QueryBuilder",
433
484
  /,
434
- *parameters: "Union[StatementParameters, StatementFilter]",
485
+ *parameters: "StatementParameters | StatementFilter",
435
486
  schema_type: None = None,
436
- statement_config: "Optional[StatementConfig]" = None,
487
+ statement_config: "StatementConfig | None" = None,
437
488
  **kwargs: Any,
438
489
  ) -> "tuple[list[dict[str, Any]], int]": ...
439
490
 
440
491
  async def select_with_total(
441
492
  self,
442
- statement: "Union[Statement, QueryBuilder]",
493
+ statement: "Statement | QueryBuilder",
443
494
  /,
444
- *parameters: "Union[StatementParameters, StatementFilter]",
445
- schema_type: "Optional[type[ModelDTOT]]" = None,
446
- statement_config: "Optional[StatementConfig]" = None,
495
+ *parameters: "StatementParameters | StatementFilter",
496
+ schema_type: "type[SchemaT] | None" = None,
497
+ statement_config: "StatementConfig | None" = None,
447
498
  **kwargs: Any,
448
- ) -> "tuple[Union[list[dict[str, Any]], list[ModelDTOT]], int]":
499
+ ) -> "tuple[list[SchemaT] | list[dict[str, Any]], int]":
449
500
  """Execute a select statement and return both the data and total count.
450
501
 
451
502
  This method is designed for pagination scenarios where you need both
@@ -469,42 +520,14 @@ class AsyncDriverAdapterBase(CommonDriverAttributesMixin, SQLTranslatorMixin, To
469
520
  count_result = await self.dispatch_statement_execution(self._create_count_query(sql_statement), self.connection)
470
521
  select_result = await self.execute(sql_statement)
471
522
 
472
- return (self.to_schema(select_result.get_data(), schema_type=schema_type), count_result.scalar())
473
-
474
- def _raise_no_rows_found(self) -> NoReturn:
475
- msg = "No rows found"
476
- raise NotFoundError(msg)
477
-
478
- def _raise_no_rows_found_from_exception(self, e: ValueError) -> NoReturn:
479
- msg = "No rows found"
480
- raise NotFoundError(msg) from e
481
-
482
- def _raise_expected_one_row(self, data_len: int) -> NoReturn:
483
- msg = f"Expected exactly one row, found {data_len}"
484
- raise ValueError(msg)
485
-
486
- def _raise_expected_at_most_one_row(self, data_len: int) -> NoReturn:
487
- msg = f"Expected at most one row, found {data_len}"
488
- raise ValueError(msg)
489
-
490
- def _raise_row_no_columns(self) -> NoReturn:
491
- msg = "Row has no columns"
492
- raise ValueError(msg)
493
-
494
- def _raise_unexpected_row_type(self, row_type: type) -> NoReturn:
495
- msg = f"Unexpected row type: {row_type}"
496
- raise ValueError(msg)
497
-
498
- def _raise_cannot_extract_value_from_row_type(self, type_name: str) -> NoReturn:
499
- msg = f"Cannot extract value from row type {type_name}"
500
- raise TypeError(msg)
523
+ return (select_result.get_data(schema_type=schema_type), count_result.scalar())
501
524
 
502
525
 
503
526
  class AsyncDataDictionaryBase(DataDictionaryMixin):
504
527
  """Base class for asynchronous data dictionary implementations."""
505
528
 
506
529
  @abstractmethod
507
- async def get_version(self, driver: "AsyncDriverAdapterBase") -> "Optional[VersionInfo]":
530
+ async def get_version(self, driver: "AsyncDriverAdapterBase") -> "VersionInfo | None":
508
531
  """Get database version information.
509
532
 
510
533
  Args:
@@ -538,7 +561,7 @@ class AsyncDataDictionaryBase(DataDictionaryMixin):
538
561
  Database-specific type name
539
562
  """
540
563
 
541
- async def get_tables(self, driver: "AsyncDriverAdapterBase", schema: "Optional[str]" = None) -> "list[str]":
564
+ async def get_tables(self, driver: "AsyncDriverAdapterBase", schema: "str | None" = None) -> "list[str]":
542
565
  """Get list of tables in schema.
543
566
 
544
567
  Args:
@@ -552,7 +575,7 @@ class AsyncDataDictionaryBase(DataDictionaryMixin):
552
575
  return []
553
576
 
554
577
  async def get_columns(
555
- self, driver: "AsyncDriverAdapterBase", table: str, schema: "Optional[str]" = None
578
+ self, driver: "AsyncDriverAdapterBase", table: str, schema: "str | None" = None
556
579
  ) -> "list[dict[str, Any]]":
557
580
  """Get column information for a table.
558
581
 
@@ -568,7 +591,7 @@ class AsyncDataDictionaryBase(DataDictionaryMixin):
568
591
  return []
569
592
 
570
593
  async def get_indexes(
571
- self, driver: "AsyncDriverAdapterBase", table: str, schema: "Optional[str]" = None
594
+ self, driver: "AsyncDriverAdapterBase", table: str, schema: "str | None" = None
572
595
  ) -> "list[dict[str, Any]]":
573
596
  """Get index information for a table.
574
597