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