sqlspec 0.13.1__py3-none-any.whl → 0.16.2__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 (185) hide show
  1. sqlspec/__init__.py +71 -8
  2. sqlspec/__main__.py +12 -0
  3. sqlspec/__metadata__.py +1 -3
  4. sqlspec/_serialization.py +1 -2
  5. sqlspec/_sql.py +930 -136
  6. sqlspec/_typing.py +278 -142
  7. sqlspec/adapters/adbc/__init__.py +4 -3
  8. sqlspec/adapters/adbc/_types.py +12 -0
  9. sqlspec/adapters/adbc/config.py +116 -285
  10. sqlspec/adapters/adbc/driver.py +462 -340
  11. sqlspec/adapters/aiosqlite/__init__.py +18 -3
  12. sqlspec/adapters/aiosqlite/_types.py +13 -0
  13. sqlspec/adapters/aiosqlite/config.py +202 -150
  14. sqlspec/adapters/aiosqlite/driver.py +226 -247
  15. sqlspec/adapters/asyncmy/__init__.py +18 -3
  16. sqlspec/adapters/asyncmy/_types.py +12 -0
  17. sqlspec/adapters/asyncmy/config.py +80 -199
  18. sqlspec/adapters/asyncmy/driver.py +257 -215
  19. sqlspec/adapters/asyncpg/__init__.py +19 -4
  20. sqlspec/adapters/asyncpg/_types.py +17 -0
  21. sqlspec/adapters/asyncpg/config.py +81 -214
  22. sqlspec/adapters/asyncpg/driver.py +284 -359
  23. sqlspec/adapters/bigquery/__init__.py +17 -3
  24. sqlspec/adapters/bigquery/_types.py +12 -0
  25. sqlspec/adapters/bigquery/config.py +191 -299
  26. sqlspec/adapters/bigquery/driver.py +474 -634
  27. sqlspec/adapters/duckdb/__init__.py +14 -3
  28. sqlspec/adapters/duckdb/_types.py +12 -0
  29. sqlspec/adapters/duckdb/config.py +414 -397
  30. sqlspec/adapters/duckdb/driver.py +342 -393
  31. sqlspec/adapters/oracledb/__init__.py +19 -5
  32. sqlspec/adapters/oracledb/_types.py +14 -0
  33. sqlspec/adapters/oracledb/config.py +123 -458
  34. sqlspec/adapters/oracledb/driver.py +505 -531
  35. sqlspec/adapters/psqlpy/__init__.py +13 -3
  36. sqlspec/adapters/psqlpy/_types.py +11 -0
  37. sqlspec/adapters/psqlpy/config.py +93 -307
  38. sqlspec/adapters/psqlpy/driver.py +504 -213
  39. sqlspec/adapters/psycopg/__init__.py +19 -5
  40. sqlspec/adapters/psycopg/_types.py +17 -0
  41. sqlspec/adapters/psycopg/config.py +143 -472
  42. sqlspec/adapters/psycopg/driver.py +704 -825
  43. sqlspec/adapters/sqlite/__init__.py +14 -3
  44. sqlspec/adapters/sqlite/_types.py +11 -0
  45. sqlspec/adapters/sqlite/config.py +208 -142
  46. sqlspec/adapters/sqlite/driver.py +263 -278
  47. sqlspec/base.py +105 -9
  48. sqlspec/{statement/builder → builder}/__init__.py +12 -14
  49. sqlspec/{statement/builder/base.py → builder/_base.py} +184 -86
  50. sqlspec/{statement/builder/column.py → builder/_column.py} +97 -60
  51. sqlspec/{statement/builder/ddl.py → builder/_ddl.py} +61 -131
  52. sqlspec/{statement/builder → builder}/_ddl_utils.py +4 -10
  53. sqlspec/{statement/builder/delete.py → builder/_delete.py} +10 -30
  54. sqlspec/builder/_insert.py +421 -0
  55. sqlspec/builder/_merge.py +71 -0
  56. sqlspec/{statement/builder → builder}/_parsing_utils.py +49 -26
  57. sqlspec/builder/_select.py +170 -0
  58. sqlspec/{statement/builder/update.py → builder/_update.py} +16 -20
  59. sqlspec/builder/mixins/__init__.py +55 -0
  60. sqlspec/builder/mixins/_cte_and_set_ops.py +222 -0
  61. sqlspec/{statement/builder/mixins/_delete_from.py → builder/mixins/_delete_operations.py} +8 -1
  62. sqlspec/builder/mixins/_insert_operations.py +244 -0
  63. sqlspec/{statement/builder/mixins/_join.py → builder/mixins/_join_operations.py} +45 -13
  64. sqlspec/{statement/builder/mixins/_merge_clauses.py → builder/mixins/_merge_operations.py} +188 -30
  65. sqlspec/builder/mixins/_order_limit_operations.py +135 -0
  66. sqlspec/builder/mixins/_pivot_operations.py +153 -0
  67. sqlspec/builder/mixins/_select_operations.py +604 -0
  68. sqlspec/builder/mixins/_update_operations.py +202 -0
  69. sqlspec/builder/mixins/_where_clause.py +644 -0
  70. sqlspec/cli.py +247 -0
  71. sqlspec/config.py +183 -138
  72. sqlspec/core/__init__.py +63 -0
  73. sqlspec/core/cache.py +871 -0
  74. sqlspec/core/compiler.py +417 -0
  75. sqlspec/core/filters.py +830 -0
  76. sqlspec/core/hashing.py +310 -0
  77. sqlspec/core/parameters.py +1237 -0
  78. sqlspec/core/result.py +677 -0
  79. sqlspec/{statement → core}/splitter.py +321 -191
  80. sqlspec/core/statement.py +676 -0
  81. sqlspec/driver/__init__.py +7 -10
  82. sqlspec/driver/_async.py +422 -163
  83. sqlspec/driver/_common.py +545 -287
  84. sqlspec/driver/_sync.py +426 -160
  85. sqlspec/driver/mixins/__init__.py +2 -13
  86. sqlspec/driver/mixins/_result_tools.py +193 -0
  87. sqlspec/driver/mixins/_sql_translator.py +65 -14
  88. sqlspec/exceptions.py +5 -252
  89. sqlspec/extensions/aiosql/adapter.py +93 -96
  90. sqlspec/extensions/litestar/__init__.py +2 -1
  91. sqlspec/extensions/litestar/cli.py +48 -0
  92. sqlspec/extensions/litestar/config.py +0 -1
  93. sqlspec/extensions/litestar/handlers.py +15 -26
  94. sqlspec/extensions/litestar/plugin.py +21 -16
  95. sqlspec/extensions/litestar/providers.py +17 -52
  96. sqlspec/loader.py +423 -104
  97. sqlspec/migrations/__init__.py +35 -0
  98. sqlspec/migrations/base.py +414 -0
  99. sqlspec/migrations/commands.py +443 -0
  100. sqlspec/migrations/loaders.py +402 -0
  101. sqlspec/migrations/runner.py +213 -0
  102. sqlspec/migrations/tracker.py +140 -0
  103. sqlspec/migrations/utils.py +129 -0
  104. sqlspec/protocols.py +51 -186
  105. sqlspec/storage/__init__.py +1 -1
  106. sqlspec/storage/backends/base.py +37 -40
  107. sqlspec/storage/backends/fsspec.py +136 -112
  108. sqlspec/storage/backends/obstore.py +138 -160
  109. sqlspec/storage/capabilities.py +5 -4
  110. sqlspec/storage/registry.py +57 -106
  111. sqlspec/typing.py +136 -115
  112. sqlspec/utils/__init__.py +2 -2
  113. sqlspec/utils/correlation.py +0 -3
  114. sqlspec/utils/deprecation.py +6 -6
  115. sqlspec/utils/fixtures.py +6 -6
  116. sqlspec/utils/logging.py +0 -2
  117. sqlspec/utils/module_loader.py +7 -12
  118. sqlspec/utils/singleton.py +0 -1
  119. sqlspec/utils/sync_tools.py +17 -38
  120. sqlspec/utils/text.py +12 -51
  121. sqlspec/utils/type_guards.py +482 -235
  122. {sqlspec-0.13.1.dist-info → sqlspec-0.16.2.dist-info}/METADATA +7 -2
  123. sqlspec-0.16.2.dist-info/RECORD +134 -0
  124. sqlspec-0.16.2.dist-info/entry_points.txt +2 -0
  125. sqlspec/driver/connection.py +0 -207
  126. sqlspec/driver/mixins/_csv_writer.py +0 -91
  127. sqlspec/driver/mixins/_pipeline.py +0 -512
  128. sqlspec/driver/mixins/_result_utils.py +0 -140
  129. sqlspec/driver/mixins/_storage.py +0 -926
  130. sqlspec/driver/mixins/_type_coercion.py +0 -130
  131. sqlspec/driver/parameters.py +0 -138
  132. sqlspec/service/__init__.py +0 -4
  133. sqlspec/service/_util.py +0 -147
  134. sqlspec/service/base.py +0 -1131
  135. sqlspec/service/pagination.py +0 -26
  136. sqlspec/statement/__init__.py +0 -21
  137. sqlspec/statement/builder/insert.py +0 -288
  138. sqlspec/statement/builder/merge.py +0 -95
  139. sqlspec/statement/builder/mixins/__init__.py +0 -65
  140. sqlspec/statement/builder/mixins/_aggregate_functions.py +0 -250
  141. sqlspec/statement/builder/mixins/_case_builder.py +0 -91
  142. sqlspec/statement/builder/mixins/_common_table_expr.py +0 -90
  143. sqlspec/statement/builder/mixins/_from.py +0 -63
  144. sqlspec/statement/builder/mixins/_group_by.py +0 -118
  145. sqlspec/statement/builder/mixins/_having.py +0 -35
  146. sqlspec/statement/builder/mixins/_insert_from_select.py +0 -47
  147. sqlspec/statement/builder/mixins/_insert_into.py +0 -36
  148. sqlspec/statement/builder/mixins/_insert_values.py +0 -67
  149. sqlspec/statement/builder/mixins/_limit_offset.py +0 -53
  150. sqlspec/statement/builder/mixins/_order_by.py +0 -46
  151. sqlspec/statement/builder/mixins/_pivot.py +0 -79
  152. sqlspec/statement/builder/mixins/_returning.py +0 -37
  153. sqlspec/statement/builder/mixins/_select_columns.py +0 -61
  154. sqlspec/statement/builder/mixins/_set_ops.py +0 -122
  155. sqlspec/statement/builder/mixins/_unpivot.py +0 -77
  156. sqlspec/statement/builder/mixins/_update_from.py +0 -55
  157. sqlspec/statement/builder/mixins/_update_set.py +0 -94
  158. sqlspec/statement/builder/mixins/_update_table.py +0 -29
  159. sqlspec/statement/builder/mixins/_where.py +0 -401
  160. sqlspec/statement/builder/mixins/_window_functions.py +0 -86
  161. sqlspec/statement/builder/select.py +0 -221
  162. sqlspec/statement/filters.py +0 -596
  163. sqlspec/statement/parameter_manager.py +0 -220
  164. sqlspec/statement/parameters.py +0 -867
  165. sqlspec/statement/pipelines/__init__.py +0 -210
  166. sqlspec/statement/pipelines/analyzers/__init__.py +0 -9
  167. sqlspec/statement/pipelines/analyzers/_analyzer.py +0 -646
  168. sqlspec/statement/pipelines/context.py +0 -115
  169. sqlspec/statement/pipelines/transformers/__init__.py +0 -7
  170. sqlspec/statement/pipelines/transformers/_expression_simplifier.py +0 -88
  171. sqlspec/statement/pipelines/transformers/_literal_parameterizer.py +0 -1247
  172. sqlspec/statement/pipelines/transformers/_remove_comments_and_hints.py +0 -76
  173. sqlspec/statement/pipelines/validators/__init__.py +0 -23
  174. sqlspec/statement/pipelines/validators/_dml_safety.py +0 -290
  175. sqlspec/statement/pipelines/validators/_parameter_style.py +0 -370
  176. sqlspec/statement/pipelines/validators/_performance.py +0 -718
  177. sqlspec/statement/pipelines/validators/_security.py +0 -967
  178. sqlspec/statement/result.py +0 -435
  179. sqlspec/statement/sql.py +0 -1704
  180. sqlspec/statement/sql_compiler.py +0 -140
  181. sqlspec/utils/cached_property.py +0 -25
  182. sqlspec-0.13.1.dist-info/RECORD +0 -150
  183. {sqlspec-0.13.1.dist-info → sqlspec-0.16.2.dist-info}/WHEEL +0 -0
  184. {sqlspec-0.13.1.dist-info → sqlspec-0.16.2.dist-info}/licenses/LICENSE +0 -0
  185. {sqlspec-0.13.1.dist-info → sqlspec-0.16.2.dist-info}/licenses/NOTICE +0 -0
sqlspec/driver/_async.py CHANGED
@@ -1,243 +1,502 @@
1
- """Asynchronous driver protocol implementation."""
2
-
3
- from abc import ABC, abstractmethod
4
- from dataclasses import replace
5
- from typing import TYPE_CHECKING, Any, Optional, Union, overload
6
-
7
- from sqlspec.driver._common import CommonDriverAttributesMixin
8
- from sqlspec.driver.parameters import process_execute_many_parameters
9
- from sqlspec.statement.builder import Delete, Insert, QueryBuilder, Select, Update
10
- from sqlspec.statement.result import SQLResult
11
- from sqlspec.statement.sql import SQL, SQLConfig, Statement
12
- from sqlspec.typing import ConnectionT, DictRow, ModelDTOT, RowT, StatementParameters
1
+ """Asynchronous driver protocol implementation.
2
+
3
+ This module provides the async driver infrastructure for database adapters,
4
+ including connection management, transaction support, and result processing.
5
+ """
6
+
7
+ from abc import abstractmethod
8
+ from typing import TYPE_CHECKING, Any, Final, NoReturn, Optional, Union, cast, overload
9
+
10
+ from sqlspec.core import SQL, Statement
11
+ from sqlspec.driver._common import CommonDriverAttributesMixin, ExecutionResult
12
+ from sqlspec.driver.mixins import SQLTranslatorMixin, ToSchemaMixin
13
+ from sqlspec.exceptions import NotFoundError
13
14
  from sqlspec.utils.logging import get_logger
14
- from sqlspec.utils.type_guards import can_convert_to_schema
15
+ from sqlspec.utils.type_guards import is_dict_row, is_indexable_row
15
16
 
16
17
  if TYPE_CHECKING:
17
- from sqlspec.statement.filters import StatementFilter
18
+ from collections.abc import Sequence
19
+ from contextlib import AbstractAsyncContextManager
20
+
21
+ from sqlspec.builder import QueryBuilder
22
+ from sqlspec.core import SQLResult, StatementConfig, StatementFilter
23
+ from sqlspec.typing import ModelDTOT, StatementParameters
24
+
25
+ _LOGGER_NAME: Final[str] = "sqlspec"
26
+ logger = get_logger(_LOGGER_NAME)
18
27
 
19
- logger = get_logger("sqlspec")
28
+ __all__ = ("AsyncDriverAdapterBase",)
20
29
 
21
- __all__ = ("AsyncDriverAdapterProtocol",)
22
30
 
31
+ EMPTY_FILTERS: Final["list[StatementFilter]"] = []
23
32
 
24
- EMPTY_FILTERS: "list[StatementFilter]" = []
25
33
 
34
+ class AsyncDriverAdapterBase(CommonDriverAttributesMixin, SQLTranslatorMixin, ToSchemaMixin):
35
+ """Base class for asynchronous database drivers.
36
+
37
+ Provides the foundation for async database adapters, including connection management,
38
+ transaction support, and SQL execution methods. All database operations are performed
39
+ asynchronously and support context manager patterns for proper resource cleanup.
40
+ """
26
41
 
27
- class AsyncDriverAdapterProtocol(CommonDriverAttributesMixin[ConnectionT, RowT], ABC):
28
42
  __slots__ = ()
29
43
 
30
- def __init__(
31
- self,
32
- connection: "ConnectionT",
33
- config: "Optional[SQLConfig]" = None,
34
- default_row_type: "type[DictRow]" = DictRow,
35
- ) -> None:
36
- """Initialize async driver adapter.
44
+ async def dispatch_statement_execution(self, statement: "SQL", connection: "Any") -> "SQLResult":
45
+ """Central execution dispatcher using the Template Method Pattern.
46
+
47
+ Orchestrates the common execution flow, delegating database-specific steps
48
+ to abstract methods that concrete adapters must implement.
49
+ All database operations are wrapped in exception handling.
50
+
51
+ Args:
52
+ statement: The SQL statement to execute
53
+ connection: The database connection to use
54
+
55
+ Returns:
56
+ The result of the SQL execution
57
+ """
58
+ async with self.handle_database_exceptions(), self.with_cursor(connection) as cursor:
59
+ special_result = await self._try_special_handling(cursor, statement)
60
+ if special_result is not None:
61
+ return special_result
62
+
63
+ if statement.is_script:
64
+ execution_result = await self._execute_script(cursor, statement)
65
+ elif statement.is_many:
66
+ execution_result = await self._execute_many(cursor, statement)
67
+ else:
68
+ execution_result = await self._execute_statement(cursor, statement)
69
+
70
+ return self.build_statement_result(statement, execution_result)
71
+
72
+ @abstractmethod
73
+ def with_cursor(self, connection: Any) -> Any:
74
+ """Create and return an async context manager for cursor acquisition and cleanup.
75
+
76
+ Returns an async context manager that yields a cursor for database operations.
77
+ Concrete implementations handle database-specific cursor creation and cleanup.
78
+ """
79
+
80
+ @abstractmethod
81
+ def handle_database_exceptions(self) -> "AbstractAsyncContextManager[None]":
82
+ """Handle database-specific exceptions and wrap them appropriately.
83
+
84
+ Returns:
85
+ AsyncContextManager that can be used in async with statements
86
+ """
87
+
88
+ @abstractmethod
89
+ async def begin(self) -> None:
90
+ """Begin a database transaction on the current connection."""
91
+
92
+ @abstractmethod
93
+ async def rollback(self) -> None:
94
+ """Rollback the current transaction on the current connection."""
95
+
96
+ @abstractmethod
97
+ async def commit(self) -> None:
98
+ """Commit the current transaction on the current connection."""
99
+
100
+ @abstractmethod
101
+ async def _try_special_handling(self, cursor: Any, statement: "SQL") -> "Optional[SQLResult]":
102
+ """Hook for database-specific special operations (e.g., PostgreSQL COPY, bulk operations).
103
+
104
+ This method is called first in dispatch_statement_execution() to allow drivers to handle
105
+ special operations that don't follow the standard SQL execution pattern.
106
+
107
+ Args:
108
+ cursor: Database cursor/connection object
109
+ statement: SQL statement to analyze
110
+
111
+ Returns:
112
+ SQLResult if the special operation was handled and completed,
113
+ None if standard execution should proceed
114
+ """
115
+
116
+ async def _execute_script(self, cursor: Any, statement: "SQL") -> ExecutionResult:
117
+ """Execute a SQL script containing multiple statements.
118
+
119
+ Default implementation splits the script and executes statements individually.
120
+ Drivers can override for database-specific script execution methods.
121
+
122
+ Args:
123
+ cursor: Database cursor/connection object
124
+ statement: SQL statement object with all necessary data and configuration
125
+
126
+ Returns:
127
+ ExecutionResult with script execution data including statement counts
128
+ """
129
+ sql, prepared_parameters = self._get_compiled_sql(statement, self.statement_config)
130
+ statements = self.split_script_statements(sql, self.statement_config, strip_trailing_semicolon=True)
131
+
132
+ statement_count: int = len(statements)
133
+ successful_count: int = 0
134
+
135
+ for stmt in statements:
136
+ single_stmt = statement.copy(statement=stmt, parameters=prepared_parameters)
137
+ await self._execute_statement(cursor, single_stmt)
138
+ successful_count += 1
139
+
140
+ return self.create_execution_result(
141
+ cursor, statement_count=statement_count, successful_statements=successful_count, is_script_result=True
142
+ )
143
+
144
+ @abstractmethod
145
+ async def _execute_many(self, cursor: Any, statement: "SQL") -> ExecutionResult:
146
+ """Execute SQL with multiple parameter sets (executemany).
147
+
148
+ Must be implemented by each driver for database-specific executemany logic.
149
+
150
+ Args:
151
+ cursor: Database cursor/connection object
152
+ statement: SQL statement object with all necessary data and configuration
153
+
154
+ Returns:
155
+ ExecutionResult with execution data for the many operation
156
+ """
157
+
158
+ @abstractmethod
159
+ async def _execute_statement(self, cursor: Any, statement: "SQL") -> ExecutionResult:
160
+ """Execute a single SQL statement.
161
+
162
+ Must be implemented by each driver for database-specific execution logic.
37
163
 
38
164
  Args:
39
- connection: The database connection
40
- config: SQL statement configuration
41
- default_row_type: Default row type for results (DictRow, TupleRow, etc.)
165
+ cursor: Database cursor/connection object
166
+ statement: SQL statement object with all necessary data and configuration
167
+
168
+ Returns:
169
+ ExecutionResult with execution data
42
170
  """
43
- super().__init__(connection=connection, config=config, default_row_type=default_row_type)
44
171
 
45
- def _build_statement(
172
+ async def execute(
46
173
  self,
47
- statement: "Union[Statement, QueryBuilder[Any]]",
174
+ statement: "Union[SQL, Statement, QueryBuilder]",
175
+ /,
48
176
  *parameters: "Union[StatementParameters, StatementFilter]",
49
- _config: "Optional[SQLConfig]" = None,
177
+ statement_config: "Optional[StatementConfig]" = None,
178
+ **kwargs: Any,
179
+ ) -> "SQLResult":
180
+ """Execute a statement with parameter handling."""
181
+ sql_statement = self.prepare_statement(
182
+ statement, parameters, statement_config=statement_config or self.statement_config, kwargs=kwargs
183
+ )
184
+ return await self.dispatch_statement_execution(statement=sql_statement, connection=self.connection)
185
+
186
+ async def execute_many(
187
+ self,
188
+ statement: "Union[SQL, Statement, QueryBuilder]",
189
+ /,
190
+ parameters: "Sequence[StatementParameters]",
191
+ *filters: "Union[StatementParameters, StatementFilter]",
192
+ statement_config: "Optional[StatementConfig]" = None,
50
193
  **kwargs: Any,
51
- ) -> "SQL":
52
- # Use driver's config if none provided
53
- _config = _config or self.config
194
+ ) -> "SQLResult":
195
+ """Execute statement multiple times with different parameters.
196
+
197
+ Parameters passed will be used as the batch execution sequence.
198
+ """
199
+ config = statement_config or self.statement_config
54
200
 
55
- if isinstance(statement, QueryBuilder):
56
- return statement.to_statement(config=_config)
57
- # If statement is already a SQL object, handle additional parameters
58
201
  if isinstance(statement, SQL):
59
- if parameters or kwargs:
60
- new_config = _config
61
- if self.dialect and not new_config.dialect:
62
- new_config = replace(new_config, dialect=self.dialect)
63
- # Use raw SQL if available to ensure proper parsing with dialect
64
- sql_source = statement._raw_sql or statement._statement
65
- # Preserve filters and state when creating new SQL object
66
- existing_state = {
67
- "is_many": statement._is_many,
68
- "is_script": statement._is_script,
69
- "original_parameters": statement._original_parameters,
70
- "filters": statement._filters,
71
- "positional_params": statement._positional_params,
72
- "named_params": statement._named_params,
73
- }
74
- return SQL(sql_source, *parameters, config=new_config, _existing_state=existing_state, **kwargs)
75
- # Even without additional parameters, ensure dialect is set
76
- if self.dialect and (not statement._config.dialect or statement._config.dialect != self.dialect):
77
- new_config = replace(statement._config, dialect=self.dialect)
78
- # Use raw SQL if available to ensure proper parsing with dialect
79
- sql_source = statement._raw_sql or statement._statement
80
- # Preserve parameters and state when creating new SQL object
81
- # Use the public parameters property which always has the right value
82
- existing_state = {
83
- "is_many": statement._is_many,
84
- "is_script": statement._is_script,
85
- "original_parameters": statement._original_parameters,
86
- "filters": statement._filters,
87
- "positional_params": statement._positional_params,
88
- "named_params": statement._named_params,
89
- }
90
- if statement.parameters:
91
- return SQL(
92
- sql_source, parameters=statement.parameters, config=new_config, _existing_state=existing_state
93
- )
94
- return SQL(sql_source, config=new_config, _existing_state=existing_state)
95
- return statement
96
- new_config = _config
97
- if self.dialect and not new_config.dialect:
98
- new_config = replace(new_config, dialect=self.dialect)
99
- return SQL(statement, *parameters, config=new_config, **kwargs)
202
+ sql_statement = SQL(statement._raw_sql, parameters, statement_config=config, is_many=True, **kwargs)
203
+ else:
204
+ base_statement = self.prepare_statement(statement, filters, statement_config=config, kwargs=kwargs)
205
+ sql_statement = SQL(base_statement._raw_sql, parameters, statement_config=config, is_many=True, **kwargs)
100
206
 
101
- @abstractmethod
102
- async def _execute_statement(
103
- self, statement: "SQL", connection: "Optional[ConnectionT]" = None, **kwargs: Any
104
- ) -> "SQLResult[RowT]":
105
- """Actual execution implementation by concrete drivers, using the raw connection.
207
+ return await self.dispatch_statement_execution(statement=sql_statement, connection=self.connection)
208
+
209
+ async def execute_script(
210
+ self,
211
+ statement: "Union[str, SQL]",
212
+ /,
213
+ *parameters: "Union[StatementParameters, StatementFilter]",
214
+ statement_config: "Optional[StatementConfig]" = None,
215
+ **kwargs: Any,
216
+ ) -> "SQLResult":
217
+ """Execute a multi-statement script.
106
218
 
107
- Returns SQLResult directly based on the statement type.
219
+ By default, validates each statement and logs warnings for dangerous
220
+ operations. Use suppress_warnings=True for migrations and admin scripts.
108
221
  """
109
- raise NotImplementedError
222
+ config = statement_config or self.statement_config
223
+ sql_statement = self.prepare_statement(statement, parameters, statement_config=config, kwargs=kwargs)
224
+
225
+ return await self.dispatch_statement_execution(statement=sql_statement.as_script(), connection=self.connection)
110
226
 
111
227
  @overload
112
- async def execute(
228
+ async def select_one(
113
229
  self,
114
- statement: "Select",
230
+ statement: "Union[Statement, QueryBuilder]",
115
231
  /,
116
232
  *parameters: "Union[StatementParameters, StatementFilter]",
117
233
  schema_type: "type[ModelDTOT]",
118
- _connection: "Optional[ConnectionT]" = None,
119
- _config: "Optional[SQLConfig]" = None,
234
+ statement_config: "Optional[StatementConfig]" = None,
120
235
  **kwargs: Any,
121
- ) -> "SQLResult[ModelDTOT]": ...
236
+ ) -> "ModelDTOT": ...
122
237
 
123
238
  @overload
124
- async def execute(
239
+ async def select_one(
125
240
  self,
126
- statement: "Select",
241
+ statement: "Union[Statement, QueryBuilder]",
127
242
  /,
128
243
  *parameters: "Union[StatementParameters, StatementFilter]",
129
244
  schema_type: None = None,
130
- _connection: "Optional[ConnectionT]" = None,
131
- _config: "Optional[SQLConfig]" = None,
245
+ statement_config: "Optional[StatementConfig]" = None,
132
246
  **kwargs: Any,
133
- ) -> "SQLResult[RowT]": ...
247
+ ) -> "dict[str, Any]": ...
134
248
 
135
- @overload
136
- async def execute(
249
+ async def select_one(
137
250
  self,
138
- statement: "Union[Insert, Update, Delete]",
251
+ statement: "Union[Statement, QueryBuilder]",
139
252
  /,
140
253
  *parameters: "Union[StatementParameters, StatementFilter]",
141
- _connection: "Optional[ConnectionT]" = None,
142
- _config: "Optional[SQLConfig]" = None,
254
+ schema_type: "Optional[type[ModelDTOT]]" = None,
255
+ statement_config: "Optional[StatementConfig]" = None,
143
256
  **kwargs: Any,
144
- ) -> "SQLResult[RowT]": ...
257
+ ) -> "Union[dict[str, Any], ModelDTOT]":
258
+ """Execute a select statement and return exactly one row.
259
+
260
+ Raises an exception if no rows or more than one row is returned.
261
+ """
262
+ result = await self.execute(statement, *parameters, statement_config=statement_config, **kwargs)
263
+ data = result.get_data()
264
+ data_len: int = len(data)
265
+ if data_len == 0:
266
+ self._raise_no_rows_found()
267
+ if data_len > 1:
268
+ self._raise_expected_one_row(data_len)
269
+ first_row = data[0]
270
+ return self.to_schema(first_row, schema_type=schema_type) if schema_type else first_row
145
271
 
146
272
  @overload
147
- async def execute(
273
+ async def select_one_or_none(
148
274
  self,
149
- statement: "Union[str, SQL]", # exp.Expression
275
+ statement: "Union[Statement, QueryBuilder]",
150
276
  /,
151
277
  *parameters: "Union[StatementParameters, StatementFilter]",
152
278
  schema_type: "type[ModelDTOT]",
153
- _connection: "Optional[ConnectionT]" = None,
154
- _config: "Optional[SQLConfig]" = None,
279
+ statement_config: "Optional[StatementConfig]" = None,
155
280
  **kwargs: Any,
156
- ) -> "SQLResult[ModelDTOT]": ...
281
+ ) -> "Optional[ModelDTOT]": ...
157
282
 
158
283
  @overload
159
- async def execute(
284
+ async def select_one_or_none(
160
285
  self,
161
- statement: "Union[str, SQL]",
286
+ statement: "Union[Statement, QueryBuilder]",
162
287
  /,
163
288
  *parameters: "Union[StatementParameters, StatementFilter]",
164
289
  schema_type: None = None,
165
- _connection: "Optional[ConnectionT]" = None,
166
- _config: "Optional[SQLConfig]" = None,
290
+ statement_config: "Optional[StatementConfig]" = None,
167
291
  **kwargs: Any,
168
- ) -> "SQLResult[RowT]": ...
292
+ ) -> "Optional[dict[str, Any]]": ...
169
293
 
170
- async def execute(
294
+ async def select_one_or_none(
171
295
  self,
172
- statement: "Union[SQL, Statement, QueryBuilder[Any]]",
296
+ statement: "Union[Statement, QueryBuilder]",
173
297
  /,
174
298
  *parameters: "Union[StatementParameters, StatementFilter]",
175
299
  schema_type: "Optional[type[ModelDTOT]]" = None,
176
- _connection: "Optional[ConnectionT]" = None,
177
- _config: "Optional[SQLConfig]" = None,
300
+ statement_config: "Optional[StatementConfig]" = None,
178
301
  **kwargs: Any,
179
- ) -> "Union[SQLResult[ModelDTOT], SQLResult[RowT]]":
180
- sql_statement = self._build_statement(statement, *parameters, _config=_config or self.config, **kwargs)
181
- result = await self._execute_statement(
182
- statement=sql_statement, connection=self._connection(_connection), **kwargs
302
+ ) -> "Optional[Union[dict[str, Any], ModelDTOT]]":
303
+ """Execute a select statement and return at most one row.
304
+
305
+ Returns None if no rows are found.
306
+ Raises an exception if more than one row is returned.
307
+ """
308
+ result = await self.execute(statement, *parameters, statement_config=statement_config, **kwargs)
309
+ data = result.get_data()
310
+ data_len: int = len(data)
311
+ if data_len == 0:
312
+ return None
313
+ if data_len > 1:
314
+ self._raise_expected_at_most_one_row(data_len)
315
+ first_row = data[0]
316
+ return cast(
317
+ "Optional[Union[dict[str, Any], ModelDTOT]]",
318
+ self.to_schema(first_row, schema_type=schema_type) if schema_type else first_row,
183
319
  )
184
320
 
185
- # If schema_type is provided and we have data, convert it
186
- if schema_type and result.data and can_convert_to_schema(self):
187
- converted_data = list(self.to_schema(data=result.data, schema_type=schema_type))
188
- return SQLResult[ModelDTOT](
189
- statement=result.statement,
190
- data=converted_data,
191
- column_names=result.column_names,
192
- rows_affected=result.rows_affected,
193
- operation_type=result.operation_type,
194
- last_inserted_id=result.last_inserted_id,
195
- execution_time=result.execution_time,
196
- metadata=result.metadata,
197
- )
198
-
199
- return result
321
+ @overload
322
+ async def select(
323
+ self,
324
+ statement: "Union[Statement, QueryBuilder]",
325
+ /,
326
+ *parameters: "Union[StatementParameters, StatementFilter]",
327
+ schema_type: "type[ModelDTOT]",
328
+ statement_config: "Optional[StatementConfig]" = None,
329
+ **kwargs: Any,
330
+ ) -> "list[ModelDTOT]": ...
331
+
332
+ @overload
333
+ async def select(
334
+ self,
335
+ statement: "Union[Statement, QueryBuilder]",
336
+ /,
337
+ *parameters: "Union[StatementParameters, StatementFilter]",
338
+ schema_type: None = None,
339
+ statement_config: "Optional[StatementConfig]" = None,
340
+ **kwargs: Any,
341
+ ) -> "list[dict[str, Any]]": ...
200
342
 
201
- async def execute_many(
343
+ async def select(
344
+ self,
345
+ statement: "Union[Statement, QueryBuilder]",
346
+ /,
347
+ *parameters: "Union[StatementParameters, StatementFilter]",
348
+ schema_type: "Optional[type[ModelDTOT]]" = None,
349
+ statement_config: "Optional[StatementConfig]" = None,
350
+ **kwargs: Any,
351
+ ) -> "Union[list[dict[str, Any]], list[ModelDTOT]]":
352
+ """Execute a select statement and return all rows."""
353
+ result = await self.execute(statement, *parameters, statement_config=statement_config, **kwargs)
354
+ return cast(
355
+ "Union[list[dict[str, Any]], list[ModelDTOT]]", self.to_schema(result.get_data(), schema_type=schema_type)
356
+ )
357
+
358
+ async def select_value(
202
359
  self,
203
- statement: "Union[SQL, Statement, QueryBuilder[Any]]",
360
+ statement: "Union[Statement, QueryBuilder]",
204
361
  /,
205
362
  *parameters: "Union[StatementParameters, StatementFilter]",
206
- _connection: "Optional[ConnectionT]" = None,
207
- _config: "Optional[SQLConfig]" = None,
363
+ statement_config: "Optional[StatementConfig]" = None,
208
364
  **kwargs: Any,
209
- ) -> "SQLResult[RowT]":
210
- _filters, param_sequence = process_execute_many_parameters(parameters)
365
+ ) -> Any:
366
+ """Execute a select statement and return a single scalar value.
211
367
 
212
- # For execute_many, disable transformations to prevent literal extraction
213
- # since the SQL already has placeholders for bulk operations
214
- many_config = _config or self.config
215
- if many_config.enable_transformations:
216
- from dataclasses import replace
368
+ Expects exactly one row with one column.
369
+ Raises an exception if no rows or more than one row/column is returned.
370
+ """
371
+ result = await self.execute(statement, *parameters, statement_config=statement_config, **kwargs)
372
+ try:
373
+ row = result.one()
374
+ except ValueError as e:
375
+ self._raise_no_rows_found_from_exception(e)
376
+ if not row:
377
+ self._raise_no_rows_found()
378
+ if is_dict_row(row):
379
+ if not row:
380
+ self._raise_row_no_columns()
381
+ return next(iter(row.values()))
382
+ if is_indexable_row(row):
383
+ if not row:
384
+ self._raise_row_no_columns()
385
+ return row[0]
386
+ self._raise_unexpected_row_type(type(row))
387
+ return None
217
388
 
218
- many_config = replace(many_config, enable_transformations=False)
389
+ async def select_value_or_none(
390
+ self,
391
+ statement: "Union[Statement, QueryBuilder]",
392
+ /,
393
+ *parameters: "Union[StatementParameters, StatementFilter]",
394
+ statement_config: "Optional[StatementConfig]" = None,
395
+ **kwargs: Any,
396
+ ) -> Any:
397
+ """Execute a select statement and return a single scalar value or None.
219
398
 
220
- sql_statement = self._build_statement(statement, _config=many_config, **kwargs).as_many(param_sequence)
399
+ Returns None if no rows are found.
400
+ Expects at most one row with one column.
401
+ Raises an exception if more than one row is returned.
402
+ """
403
+ result = await self.execute(statement, *parameters, statement_config=statement_config, **kwargs)
404
+ data = result.get_data()
405
+ data_len: int = len(data)
406
+ if data_len == 0:
407
+ return None
408
+ if data_len > 1:
409
+ self._raise_expected_at_most_one_row(data_len)
410
+ row = data[0]
411
+ if is_dict_row(row):
412
+ if not row:
413
+ return None
414
+ return next(iter(row.values()))
415
+ if is_indexable_row(row):
416
+ return row[0]
417
+ self._raise_cannot_extract_value_from_row_type(type(row).__name__)
418
+ return None
221
419
 
222
- return await self._execute_statement(
223
- statement=sql_statement, connection=self._connection(_connection), **kwargs
224
- )
420
+ @overload
421
+ async def select_with_total(
422
+ self,
423
+ statement: "Union[Statement, QueryBuilder]",
424
+ /,
425
+ *parameters: "Union[StatementParameters, StatementFilter]",
426
+ schema_type: "type[ModelDTOT]",
427
+ statement_config: "Optional[StatementConfig]" = None,
428
+ **kwargs: Any,
429
+ ) -> "tuple[list[ModelDTOT], int]": ...
225
430
 
226
- async def execute_script(
431
+ @overload
432
+ async def select_with_total(
227
433
  self,
228
- statement: "Union[str, SQL]",
434
+ statement: "Union[Statement, QueryBuilder]",
435
+ /,
436
+ *parameters: "Union[StatementParameters, StatementFilter]",
437
+ schema_type: None = None,
438
+ statement_config: "Optional[StatementConfig]" = None,
439
+ **kwargs: Any,
440
+ ) -> "tuple[list[dict[str, Any]], int]": ...
441
+
442
+ async def select_with_total(
443
+ self,
444
+ statement: "Union[Statement, QueryBuilder]",
229
445
  /,
230
446
  *parameters: "Union[StatementParameters, StatementFilter]",
231
- _connection: "Optional[ConnectionT]" = None,
232
- _config: "Optional[SQLConfig]" = None,
447
+ schema_type: "Optional[type[ModelDTOT]]" = None,
448
+ statement_config: "Optional[StatementConfig]" = None,
233
449
  **kwargs: Any,
234
- ) -> "SQLResult[RowT]":
235
- script_config = _config or self.config
236
- if script_config.enable_validation:
237
- script_config = replace(script_config, enable_validation=False, strict_mode=False)
450
+ ) -> "tuple[Union[list[dict[str, Any]], list[ModelDTOT]], int]":
451
+ """Execute a select statement and return both the data and total count.
452
+
453
+ This method is designed for pagination scenarios where you need both
454
+ the current page of data and the total number of rows that match the query.
455
+
456
+ Args:
457
+ statement: The SQL statement, QueryBuilder, or raw SQL string
458
+ *parameters: Parameters for the SQL statement
459
+ schema_type: Optional schema type for data transformation
460
+ statement_config: Optional SQL configuration
461
+ **kwargs: Additional keyword arguments
238
462
 
239
- sql_statement = self._build_statement(statement, *parameters, _config=script_config, **kwargs)
240
- sql_statement = sql_statement.as_script()
241
- return await self._execute_statement(
242
- statement=sql_statement, connection=self._connection(_connection), **kwargs
463
+ Returns:
464
+ A tuple containing:
465
+ - List of data rows (transformed by schema_type if provided)
466
+ - Total count of rows matching the query (ignoring LIMIT/OFFSET)
467
+ """
468
+ sql_statement = self.prepare_statement(
469
+ statement, parameters, statement_config=statement_config or self.statement_config, kwargs=kwargs
243
470
  )
471
+ count_result = await self.dispatch_statement_execution(self._create_count_query(sql_statement), self.connection)
472
+ select_result = await self.execute(sql_statement)
473
+
474
+ return (self.to_schema(select_result.get_data(), schema_type=schema_type), count_result.scalar())
475
+
476
+ def _raise_no_rows_found(self) -> NoReturn:
477
+ msg = "No rows found"
478
+ raise NotFoundError(msg)
479
+
480
+ def _raise_no_rows_found_from_exception(self, e: ValueError) -> NoReturn:
481
+ msg = "No rows found"
482
+ raise NotFoundError(msg) from e
483
+
484
+ def _raise_expected_one_row(self, data_len: int) -> NoReturn:
485
+ msg = f"Expected exactly one row, found {data_len}"
486
+ raise ValueError(msg)
487
+
488
+ def _raise_expected_at_most_one_row(self, data_len: int) -> NoReturn:
489
+ msg = f"Expected at most one row, found {data_len}"
490
+ raise ValueError(msg)
491
+
492
+ def _raise_row_no_columns(self) -> NoReturn:
493
+ msg = "Row has no columns"
494
+ raise ValueError(msg)
495
+
496
+ def _raise_unexpected_row_type(self, row_type: type) -> NoReturn:
497
+ msg = f"Unexpected row type: {row_type}"
498
+ raise ValueError(msg)
499
+
500
+ def _raise_cannot_extract_value_from_row_type(self, type_name: str) -> NoReturn:
501
+ msg = f"Cannot extract value from row type {type_name}"
502
+ raise TypeError(msg)