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
@@ -1,796 +0,0 @@
1
- # pyright: reportCallIssue=false, reportAttributeAccessIssue=false, reportArgumentType=false
2
- import logging
3
- from abc import ABC
4
- from typing import TYPE_CHECKING, Any, Generic, Optional, Union, overload
5
-
6
- from sqlglot import exp, parse_one
7
- from typing_extensions import Self
8
-
9
- from sqlspec.exceptions import NotFoundError
10
- from sqlspec.statement.filters import LimitOffsetFilter, OffsetPagination
11
- from sqlspec.statement.sql import SQL
12
- from sqlspec.typing import ConnectionT, ModelDTOT, RowT
13
- from sqlspec.utils.type_guards import (
14
- is_dict_row,
15
- is_indexable_row,
16
- is_limit_offset_filter,
17
- is_select_builder,
18
- is_statement_filter,
19
- )
20
-
21
- if TYPE_CHECKING:
22
- from sqlglot.dialects.dialect import DialectType
23
-
24
- from sqlspec.statement import Statement, StatementFilter
25
- from sqlspec.statement.builder import Select
26
- from sqlspec.statement.sql import SQLConfig
27
- from sqlspec.typing import StatementParameters
28
-
29
- __all__ = ("AsyncQueryMixin", "SyncQueryMixin")
30
-
31
- logger = logging.getLogger(__name__)
32
-
33
- WINDOWS_PATH_MIN_LENGTH = 3
34
-
35
-
36
- class QueryBase(ABC, Generic[ConnectionT]):
37
- """Base class with common query functionality."""
38
-
39
- config: Any
40
- _connection: Any
41
- dialect: "DialectType"
42
-
43
- @property
44
- def connection(self) -> "ConnectionT":
45
- """Get the connection instance."""
46
- return self._connection # type: ignore[no-any-return]
47
-
48
- @classmethod
49
- def new(cls, connection: "ConnectionT") -> Self:
50
- return cls(connection) # type: ignore[call-arg]
51
-
52
- def _transform_to_sql(
53
- self,
54
- statement: "Union[Statement, Select]",
55
- params: "Optional[dict[str, Any]]" = None,
56
- config: "Optional[SQLConfig]" = None,
57
- ) -> "SQL":
58
- """Normalize a statement of any supported type into a SQL object.
59
-
60
- Args:
61
- statement: The statement to normalize (str, Expression, SQL, or Select)
62
- params: Optional parameters (ignored for Select and SQL objects)
63
- config: Optional SQL configuration
64
-
65
- Returns:
66
- A converted SQL object
67
- """
68
-
69
- if is_select_builder(statement):
70
- # Select has its own parameters via build(), ignore external params
71
- safe_query = statement.build()
72
- return SQL(safe_query.sql, parameters=safe_query.parameters, config=config)
73
-
74
- if isinstance(statement, SQL):
75
- # SQL object is already complete, ignore external params
76
- return statement
77
-
78
- if isinstance(statement, (str, exp.Expression)):
79
- return SQL(statement, parameters=params, config=config)
80
-
81
- # Fallback for type safety
82
- msg = f"Unsupported statement type: {type(statement).__name__}"
83
- raise TypeError(msg)
84
-
85
-
86
- class SyncQueryMixin(QueryBase[ConnectionT]):
87
- """Unified storage operations for synchronous drivers."""
88
-
89
- @overload
90
- def select_one(
91
- self,
92
- statement: "Union[Statement, Select]",
93
- /,
94
- *parameters: "Union[StatementParameters, StatementFilter]",
95
- schema_type: "type[ModelDTOT]",
96
- _connection: "Optional[ConnectionT]" = None,
97
- _config: "Optional[SQLConfig]" = None,
98
- **kwargs: Any,
99
- ) -> "ModelDTOT": ...
100
-
101
- @overload
102
- def select_one(
103
- self,
104
- statement: "Union[Statement, Select]",
105
- /,
106
- *parameters: "Union[StatementParameters, StatementFilter]",
107
- schema_type: None = None,
108
- _connection: "Optional[ConnectionT]" = None,
109
- _config: "Optional[SQLConfig]" = None,
110
- **kwargs: Any,
111
- ) -> "RowT": ... # type: ignore[type-var]
112
-
113
- def select_one(
114
- self,
115
- statement: "Union[Statement, Select]",
116
- /,
117
- *parameters: "Union[StatementParameters, StatementFilter]",
118
- schema_type: "Optional[type[ModelDTOT]]" = None,
119
- _connection: "Optional[ConnectionT]" = None,
120
- _config: "Optional[SQLConfig]" = None,
121
- **kwargs: Any,
122
- ) -> "Union[RowT, ModelDTOT]":
123
- """Execute a select statement and return exactly one row.
124
-
125
- Raises an exception if no rows or more than one row is returned.
126
- """
127
- result = self.execute( # type: ignore[attr-defined]
128
- statement, *parameters, schema_type=schema_type, _connection=_connection, _config=_config, **kwargs
129
- )
130
- data = result.get_data()
131
- if not isinstance(data, list):
132
- msg = "Expected list result from select operation"
133
- raise TypeError(msg)
134
- if not data:
135
- msg = "No rows found"
136
- raise NotFoundError(msg)
137
- if len(data) > 1:
138
- msg = f"Expected exactly one row, found {len(data)}"
139
- raise ValueError(msg)
140
- return data[0] # type: ignore[no-any-return]
141
-
142
- @overload
143
- def select_one_or_none(
144
- self,
145
- statement: "Union[Statement, Select]",
146
- /,
147
- *parameters: "Union[StatementParameters, StatementFilter]",
148
- schema_type: "type[ModelDTOT]",
149
- _connection: "Optional[ConnectionT]" = None,
150
- _config: "Optional[SQLConfig]" = None,
151
- **kwargs: Any,
152
- ) -> "Optional[ModelDTOT]": ...
153
-
154
- @overload
155
- def select_one_or_none(
156
- self,
157
- statement: "Union[Statement, Select]",
158
- /,
159
- *parameters: "Union[StatementParameters, StatementFilter]",
160
- schema_type: None = None,
161
- _connection: "Optional[ConnectionT]" = None,
162
- _config: "Optional[SQLConfig]" = None,
163
- **kwargs: Any,
164
- ) -> "Optional[RowT]": ...
165
-
166
- def select_one_or_none(
167
- self,
168
- statement: "Union[Statement, Select]",
169
- /,
170
- *parameters: "Union[StatementParameters, StatementFilter]",
171
- schema_type: "Optional[type[ModelDTOT]]" = None,
172
- _connection: "Optional[ConnectionT]" = None,
173
- _config: "Optional[SQLConfig]" = None,
174
- **kwargs: "Optional[Union[RowT, ModelDTOT]]",
175
- ) -> Any:
176
- """Execute a select statement and return at most one row.
177
-
178
- Returns None if no rows are found.
179
- Raises an exception if more than one row is returned.
180
- """
181
- result = self.execute( # type: ignore[attr-defined]
182
- statement, *parameters, schema_type=schema_type, _connection=_connection, _config=_config, **kwargs
183
- )
184
- data = result.get_data()
185
- # For select operations, data should be a list
186
- if not isinstance(data, list):
187
- msg = "Expected list result from select operation"
188
- raise TypeError(msg)
189
- if not data:
190
- return None
191
- if len(data) > 1:
192
- msg = f"Expected at most one row, found {len(data)}"
193
- raise ValueError(msg)
194
- return data[0]
195
-
196
- @overload
197
- def select(
198
- self,
199
- statement: "Union[Statement, Select]",
200
- /,
201
- *parameters: "Union[StatementParameters, StatementFilter]",
202
- schema_type: "type[ModelDTOT]",
203
- _connection: "Optional[ConnectionT]" = None,
204
- _config: "Optional[SQLConfig]" = None,
205
- **kwargs: Any,
206
- ) -> "list[ModelDTOT]": ...
207
-
208
- @overload
209
- def select(
210
- self,
211
- statement: "Union[Statement, Select]",
212
- /,
213
- *parameters: "Union[StatementParameters, StatementFilter]",
214
- schema_type: None = None,
215
- _connection: "Optional[ConnectionT]" = None,
216
- _config: "Optional[SQLConfig]" = None,
217
- **kwargs: Any,
218
- ) -> "list[RowT]": ...
219
-
220
- def select(
221
- self,
222
- statement: "Union[Statement, Select]",
223
- /,
224
- *parameters: "Union[StatementParameters, StatementFilter]",
225
- schema_type: "Optional[type[ModelDTOT]]" = None,
226
- _connection: "Optional[ConnectionT]" = None,
227
- _config: "Optional[SQLConfig]" = None,
228
- **kwargs: Any,
229
- ) -> Union[list[RowT], list[ModelDTOT]]:
230
- """Execute a select statement and return all rows."""
231
- result = self.execute( # type: ignore[attr-defined]
232
- statement, *parameters, schema_type=schema_type, _connection=_connection, _config=_config, **kwargs
233
- )
234
- data = result.get_data()
235
- # For select operations, data should be a list
236
- if not isinstance(data, list):
237
- msg = "Expected list result from select operation"
238
- raise TypeError(msg)
239
- return data
240
-
241
- def select_value(
242
- self,
243
- statement: "Union[Statement, Select]",
244
- /,
245
- *parameters: "Union[StatementParameters, StatementFilter]",
246
- _connection: "Optional[ConnectionT]" = None,
247
- _config: "Optional[SQLConfig]" = None,
248
- **kwargs: Any,
249
- ) -> Any:
250
- """Execute a select statement and return a single scalar value.
251
-
252
- Expects exactly one row with one column.
253
- Raises an exception if no rows or more than one row/column is returned.
254
- """
255
- result = self.execute(statement, *parameters, _connection=_connection, _config=_config, **kwargs) # type: ignore[attr-defined]
256
- data = result.get_data()
257
- # For select operations, data should be a list
258
- if not isinstance(data, list):
259
- msg = "Expected list result from select operation"
260
- raise TypeError(msg)
261
- if not data:
262
- msg = "No rows found"
263
- raise NotFoundError(msg)
264
- if len(data) > 1:
265
- msg = f"Expected exactly one row, found {len(data)}"
266
- raise ValueError(msg)
267
- row = data[0]
268
- if is_dict_row(row):
269
- if not row:
270
- msg = "Row has no columns"
271
- raise ValueError(msg)
272
- return next(iter(row.values()))
273
- if is_indexable_row(row):
274
- if not row:
275
- msg = "Row has no columns"
276
- raise ValueError(msg)
277
- return row[0]
278
- msg = f"Unexpected row type: {type(row)}"
279
- raise ValueError(msg)
280
-
281
- def select_value_or_none(
282
- self,
283
- statement: "Union[Statement, Select]",
284
- /,
285
- *parameters: "Union[StatementParameters, StatementFilter]",
286
- _connection: "Optional[ConnectionT]" = None,
287
- _config: "Optional[SQLConfig]" = None,
288
- **kwargs: Any,
289
- ) -> Any:
290
- """Execute a select statement and return a single scalar value or None.
291
-
292
- Returns None if no rows are found.
293
- Expects at most one row with one column.
294
- Raises an exception if more than one row is returned.
295
- """
296
- result = self.execute(statement, *parameters, _connection=_connection, _config=_config, **kwargs) # type: ignore[attr-defined]
297
- data = result.get_data()
298
- # For select operations, data should be a list
299
- if not isinstance(data, list):
300
- msg = "Expected list result from select operation"
301
- raise TypeError(msg)
302
- if not data:
303
- return None
304
- if len(data) > 1:
305
- msg = f"Expected at most one row, found {len(data)}"
306
- raise ValueError(msg)
307
- row = data[0]
308
- if isinstance(row, dict):
309
- if not row:
310
- return None
311
- return next(iter(row.values()))
312
- if isinstance(row, (tuple, list)):
313
- # Tuple or list-like row
314
- return row[0]
315
- try:
316
- return row[0]
317
- except (TypeError, IndexError) as e:
318
- msg = f"Cannot extract value from row type {type(row).__name__}: {e}"
319
- raise TypeError(msg) from e
320
-
321
- @overload
322
- def paginate(
323
- self,
324
- statement: "Union[Statement, Select]",
325
- /,
326
- *parameters: "Union[StatementParameters, StatementFilter]",
327
- schema_type: "type[ModelDTOT]",
328
- _connection: "Optional[ConnectionT]" = None,
329
- _config: "Optional[SQLConfig]" = None,
330
- **kwargs: Any,
331
- ) -> "OffsetPagination[ModelDTOT]": ...
332
-
333
- @overload
334
- def paginate(
335
- self,
336
- statement: "Union[Statement, Select]",
337
- /,
338
- *parameters: "Union[StatementParameters, StatementFilter]",
339
- schema_type: None = None,
340
- _connection: "Optional[ConnectionT]" = None,
341
- _config: "Optional[SQLConfig]" = None,
342
- **kwargs: Any,
343
- ) -> "OffsetPagination[RowT]": ...
344
-
345
- def paginate(
346
- self,
347
- statement: "Union[Statement, Select]",
348
- /,
349
- *parameters: "Union[StatementParameters, StatementFilter]",
350
- schema_type: "Optional[type[ModelDTOT]]" = None,
351
- _connection: "Optional[ConnectionT]" = None,
352
- _config: "Optional[SQLConfig]" = None,
353
- **kwargs: Any,
354
- ) -> "Union[OffsetPagination[RowT], OffsetPagination[ModelDTOT]]":
355
- """Execute a paginated query with automatic counting.
356
-
357
- This method performs two queries:
358
- 1. A count query to get the total number of results
359
- 2. A data query with limit/offset applied
360
-
361
- Pagination can be specified either via LimitOffsetFilter in parameters
362
- or via 'limit' and 'offset' in kwargs.
363
-
364
- Args:
365
- statement: The SELECT statement to paginate
366
- *parameters: Statement parameters and filters (can include LimitOffsetFilter)
367
- schema_type: Optional model type for automatic schema conversion
368
- _connection: Optional connection to use
369
- _config: Optional SQL configuration
370
- **kwargs: Additional driver-specific arguments. Can include 'limit' and 'offset'
371
- if LimitOffsetFilter is not provided
372
-
373
- Returns:
374
- OffsetPagination object containing items, limit, offset, and total count
375
-
376
- Raises:
377
- ValueError: If neither LimitOffsetFilter nor limit/offset kwargs are provided
378
-
379
- Example:
380
- >>> # Using LimitOffsetFilter (recommended)
381
- >>> from sqlspec.statement.filters import LimitOffsetFilter
382
- >>> result = service.paginate(
383
- ... sql.select("*").from_("users"),
384
- ... LimitOffsetFilter(limit=10, offset=20),
385
- ... )
386
- >>> print(
387
- ... f"Showing {len(result.items)} of {result.total} users"
388
- ... )
389
-
390
- >>> # Using kwargs (convenience)
391
- >>> result = service.paginate(
392
- ... sql.select("*").from_("users"), limit=10, offset=20
393
- ... )
394
-
395
- >>> # With schema conversion
396
- >>> result = service.paginate(
397
- ... sql.select("*").from_("users"),
398
- ... LimitOffsetFilter(limit=10, offset=0),
399
- ... schema_type=User,
400
- ... )
401
- >>> # result.items is list[User] with proper type inference
402
-
403
- >>> # With multiple filters
404
- >>> from sqlspec.statement.filters import (
405
- ... LimitOffsetFilter,
406
- ... OrderByFilter,
407
- ... )
408
- >>> result = service.paginate(
409
- ... sql.select("*").from_("users"),
410
- ... OrderByFilter("created_at", "desc"),
411
- ... LimitOffsetFilter(limit=20, offset=40),
412
- ... schema_type=User,
413
- ... )
414
- """
415
-
416
- # Separate filters from parameters
417
- filters: list[StatementFilter] = []
418
- params: list[Any] = []
419
-
420
- for p in parameters:
421
- if is_statement_filter(p):
422
- filters.append(p)
423
- else:
424
- params.append(p)
425
-
426
- # Check for LimitOffsetFilter in filters
427
- limit_offset_filter = None
428
- other_filters = []
429
- for f in filters:
430
- if is_limit_offset_filter(f):
431
- limit_offset_filter = f
432
- else:
433
- other_filters.append(f)
434
-
435
- if limit_offset_filter is not None:
436
- limit = limit_offset_filter.limit
437
- offset = limit_offset_filter.offset
438
- elif "limit" in kwargs and "offset" in kwargs:
439
- limit = kwargs.pop("limit")
440
- offset = kwargs.pop("offset")
441
- else:
442
- msg = "Pagination requires either a LimitOffsetFilter in parameters or 'limit' and 'offset' in kwargs."
443
- raise ValueError(msg)
444
-
445
- base_stmt = self._transform_to_sql(statement, params, _config) # type: ignore[arg-type]
446
-
447
- filtered_stmt = base_stmt
448
- for filter_obj in other_filters:
449
- filtered_stmt = filter_obj.append_to_statement(filtered_stmt)
450
-
451
- sql_str = filtered_stmt.to_sql()
452
- parsed = parse_one(sql_str)
453
-
454
- # Using exp.Subquery to properly wrap the parsed expression
455
- subquery = exp.Subquery(this=parsed, alias="_count_subquery")
456
- count_ast = exp.Select().select(exp.func("COUNT", exp.Star()).as_("total")).from_(subquery)
457
-
458
- # Preserve parameters from the original statement
459
- count_stmt = SQL(count_ast, parameters=filtered_stmt.parameters, _config=_config)
460
-
461
- # Execute count query
462
- total = self.select_value(count_stmt, _connection=_connection, _config=_config, **kwargs)
463
-
464
- data_stmt = self._transform_to_sql(statement, params, _config) # type: ignore[arg-type]
465
-
466
- for filter_obj in other_filters:
467
- data_stmt = filter_obj.append_to_statement(data_stmt)
468
-
469
- # Apply limit and offset using LimitOffsetFilter
470
- from sqlspec.statement.filters import LimitOffsetFilter
471
-
472
- limit_offset = LimitOffsetFilter(limit=limit, offset=offset)
473
- data_stmt = limit_offset.append_to_statement(data_stmt)
474
-
475
- # Execute data query
476
- items = self.select(
477
- data_stmt, params, schema_type=schema_type, _connection=_connection, _config=_config, **kwargs
478
- )
479
-
480
- return OffsetPagination(items=items, limit=limit, offset=offset, total=total) # pyright: ignore
481
-
482
-
483
- class AsyncQueryMixin(QueryBase[ConnectionT]):
484
- """Unified query operations for asynchronous drivers."""
485
-
486
- @overload
487
- async def select_one(
488
- self,
489
- statement: "Union[Statement, Select]",
490
- /,
491
- *parameters: "Union[StatementParameters, StatementFilter]",
492
- schema_type: "type[ModelDTOT]",
493
- _connection: "Optional[ConnectionT]" = None,
494
- _config: "Optional[SQLConfig]" = None,
495
- **kwargs: Any,
496
- ) -> "ModelDTOT": ...
497
-
498
- @overload
499
- async def select_one(
500
- self,
501
- statement: "Union[Statement, Select]",
502
- /,
503
- *parameters: "Union[StatementParameters, StatementFilter]",
504
- schema_type: None = None,
505
- _connection: "Optional[ConnectionT]" = None,
506
- _config: "Optional[SQLConfig]" = None,
507
- **kwargs: Any,
508
- ) -> "RowT": ...
509
-
510
- async def select_one(
511
- self,
512
- statement: "Union[Statement, Select]",
513
- /,
514
- *parameters: "Union[StatementParameters, StatementFilter]",
515
- schema_type: "Optional[type[ModelDTOT]]" = None,
516
- _connection: "Optional[ConnectionT]" = None,
517
- _config: "Optional[SQLConfig]" = None,
518
- **kwargs: Any,
519
- ) -> "Union[RowT, ModelDTOT]":
520
- """Execute a select statement and return exactly one row.
521
-
522
- Raises an exception if no rows or more than one row is returned.
523
- """
524
- result = await self.execute( # type: ignore[attr-defined]
525
- statement, *parameters, schema_type=schema_type, _connection=_connection, _config=_config, **kwargs
526
- )
527
- data = result.get_data()
528
- if not data:
529
- msg = "No rows found"
530
- raise NotFoundError(msg)
531
- if len(data) > 1:
532
- msg = f"Expected exactly one row, found {len(data)}"
533
- raise ValueError(msg)
534
- return data[0] # type: ignore[no-any-return]
535
-
536
- @overload
537
- async def select_one_or_none(
538
- self,
539
- statement: "Union[Statement, Select]",
540
- /,
541
- *parameters: "Union[StatementParameters, StatementFilter]",
542
- schema_type: "type[ModelDTOT]",
543
- _connection: "Optional[ConnectionT]" = None,
544
- _config: "Optional[SQLConfig]" = None,
545
- **kwargs: Any,
546
- ) -> "Optional[ModelDTOT]": ...
547
-
548
- @overload
549
- async def select_one_or_none(
550
- self,
551
- statement: "Union[Statement, Select]",
552
- /,
553
- *parameters: "Union[StatementParameters, StatementFilter]",
554
- schema_type: None = None,
555
- _connection: "Optional[ConnectionT]" = None,
556
- _config: "Optional[SQLConfig]" = None,
557
- **kwargs: Any,
558
- ) -> "Optional[RowT]": ...
559
-
560
- async def select_one_or_none(
561
- self,
562
- statement: "Union[Statement, Select]",
563
- /,
564
- *parameters: "Union[StatementParameters, StatementFilter]",
565
- schema_type: "Optional[type[ModelDTOT]]" = None,
566
- _connection: "Optional[ConnectionT]" = None,
567
- _config: "Optional[SQLConfig]" = None,
568
- **kwargs: Any,
569
- ) -> "Optional[Union[RowT, ModelDTOT]]":
570
- """Execute a select statement and return at most one row.
571
-
572
- Returns None if no rows are found.
573
- Raises an exception if more than one row is returned.
574
- """
575
- result = await self.execute( # type: ignore[attr-defined]
576
- statement, *parameters, schema_type=schema_type, _connection=_connection, _config=_config, **kwargs
577
- )
578
- data = result.get_data()
579
- if not data:
580
- return None
581
- if len(data) > 1:
582
- msg = f"Expected at most one row, found {len(data)}"
583
- raise ValueError(msg)
584
- return data[0] # type: ignore[no-any-return]
585
-
586
- @overload
587
- async def select(
588
- self,
589
- statement: "Union[Statement, Select]",
590
- /,
591
- *parameters: "Union[StatementParameters, StatementFilter]",
592
- schema_type: "type[ModelDTOT]",
593
- _connection: "Optional[ConnectionT]" = None,
594
- _config: "Optional[SQLConfig]" = None,
595
- **kwargs: Any,
596
- ) -> "list[ModelDTOT]": ...
597
-
598
- @overload
599
- async def select(
600
- self,
601
- statement: "Union[Statement, Select]",
602
- /,
603
- *parameters: "Union[StatementParameters, StatementFilter]",
604
- schema_type: None = None,
605
- _connection: "Optional[ConnectionT]" = None,
606
- _config: "Optional[SQLConfig]" = None,
607
- **kwargs: Any,
608
- ) -> "list[RowT]": ...
609
-
610
- async def select(
611
- self,
612
- statement: "Union[Statement, Select]",
613
- /,
614
- *parameters: "Union[StatementParameters, StatementFilter]",
615
- schema_type: "Optional[type[ModelDTOT]]" = None,
616
- _connection: "Optional[ConnectionT]" = None,
617
- _config: "Optional[SQLConfig]" = None,
618
- **kwargs: Any,
619
- ) -> "Union[list[RowT], list[ModelDTOT]]":
620
- """Execute a select statement and return all rows."""
621
- result = await self.execute( # type: ignore[attr-defined]
622
- statement, *parameters, schema_type=schema_type, _connection=_connection, _config=_config, **kwargs
623
- )
624
- return result.get_data() # type: ignore[no-any-return]
625
-
626
- async def select_value(
627
- self,
628
- statement: "Union[Statement, Select]",
629
- /,
630
- *parameters: "Union[StatementParameters, StatementFilter]",
631
- _connection: "Optional[ConnectionT]" = None,
632
- _config: "Optional[SQLConfig]" = None,
633
- **kwargs: Any,
634
- ) -> Any:
635
- """Execute a select statement and return a single scalar value.
636
-
637
- Expects exactly one row with one column.
638
- Raises an exception if no rows or more than one row/column is returned.
639
- """
640
- result = await self.execute(statement, *parameters, _connection=_connection, _config=_config, **kwargs) # type: ignore[attr-defined]
641
- row = result.one()
642
- if not row:
643
- msg = "No rows found"
644
- raise NotFoundError(msg)
645
- if is_dict_row(row):
646
- if not row:
647
- msg = "Row has no columns"
648
- raise ValueError(msg)
649
- return next(iter(row.values()))
650
- if is_indexable_row(row):
651
- # Tuple or list-like row
652
- if not row:
653
- msg = "Row has no columns"
654
- raise ValueError(msg)
655
- return row[0]
656
- msg = f"Unexpected row type: {type(row)}"
657
- raise ValueError(msg)
658
-
659
- async def select_value_or_none(
660
- self,
661
- statement: "Union[Statement, Select]",
662
- /,
663
- *parameters: "Union[StatementParameters, StatementFilter]",
664
- _connection: "Optional[ConnectionT]" = None,
665
- _config: "Optional[SQLConfig]" = None,
666
- **kwargs: Any,
667
- ) -> Any:
668
- """Execute a select statement and return a single scalar value or None.
669
-
670
- Returns None if no rows are found.
671
- Expects at most one row with one column.
672
- Raises an exception if more than one row is returned.
673
- """
674
- result = await self.execute( # type: ignore[attr-defined]
675
- statement, *parameters, _connection=_connection, _config=_config, **kwargs
676
- )
677
- data = result.get_data()
678
- # For select operations, data should be a list
679
- if not isinstance(data, list):
680
- msg = "Expected list result from select operation"
681
- raise TypeError(msg)
682
- if not data:
683
- return None
684
- if len(data) > 1:
685
- msg = f"Expected at most one row, found {len(data)}"
686
- raise ValueError(msg)
687
- row = data[0]
688
- if isinstance(row, dict):
689
- if not row:
690
- return None
691
- return next(iter(row.values()))
692
- if isinstance(row, (tuple, list)):
693
- # Tuple or list-like row
694
- return row[0]
695
- # Try indexing - if it fails, we'll get a proper error
696
- try:
697
- return row[0]
698
- except (TypeError, IndexError) as e:
699
- msg = f"Cannot extract value from row type {type(row).__name__}: {e}"
700
- raise TypeError(msg) from e
701
-
702
- @overload
703
- async def paginate(
704
- self,
705
- statement: "Union[Statement, Select]",
706
- /,
707
- *parameters: "Union[StatementParameters, StatementFilter]",
708
- schema_type: "type[ModelDTOT]",
709
- _connection: "Optional[ConnectionT]" = None,
710
- _config: "Optional[SQLConfig]" = None,
711
- **kwargs: Any,
712
- ) -> "OffsetPagination[ModelDTOT]": ...
713
-
714
- @overload
715
- async def paginate(
716
- self,
717
- statement: "Union[Statement, Select]",
718
- /,
719
- *parameters: "Union[StatementParameters, StatementFilter]",
720
- schema_type: None = None,
721
- _connection: "Optional[ConnectionT]" = None,
722
- _config: "Optional[SQLConfig]" = None,
723
- **kwargs: Any,
724
- ) -> "OffsetPagination[RowT]": ...
725
-
726
- async def paginate(
727
- self,
728
- statement: "Union[Statement, Select]",
729
- /,
730
- *parameters: "Union[StatementParameters, StatementFilter]",
731
- schema_type: "Optional[type[ModelDTOT]]" = None,
732
- _connection: "Optional[ConnectionT]" = None,
733
- _config: "Optional[SQLConfig]" = None,
734
- **kwargs: Any,
735
- ) -> "Union[OffsetPagination[RowT], OffsetPagination[ModelDTOT]]":
736
- # Separate filters from parameters
737
- filters: list[StatementFilter] = []
738
- params: list[Any] = []
739
-
740
- for p in parameters:
741
- # Use type guard to check if it implements the StatementFilter protocol
742
- if is_statement_filter(p):
743
- filters.append(p)
744
- else:
745
- params.append(p)
746
-
747
- # Check for LimitOffsetFilter in filters
748
- limit_offset_filter = None
749
- other_filters = []
750
- for f in filters:
751
- if is_limit_offset_filter(f):
752
- limit_offset_filter = f
753
- else:
754
- other_filters.append(f)
755
-
756
- if limit_offset_filter is not None:
757
- limit = limit_offset_filter.limit
758
- offset = limit_offset_filter.offset
759
- elif "limit" in kwargs and "offset" in kwargs:
760
- limit = kwargs.pop("limit")
761
- offset = kwargs.pop("offset")
762
- else:
763
- msg = "Pagination requires either a LimitOffsetFilter in parameters or 'limit' and 'offset' in kwargs."
764
- raise ValueError(msg)
765
-
766
- base_stmt = self._transform_to_sql(statement, params, _config) # type: ignore[arg-type]
767
-
768
- filtered_stmt = base_stmt
769
- for filter_obj in other_filters:
770
- filtered_stmt = filter_obj.append_to_statement(filtered_stmt)
771
- parsed = parse_one(filtered_stmt.to_sql())
772
-
773
- # Using exp.Subquery to properly wrap the parsed expression
774
- subquery = exp.Subquery(this=parsed, alias="_count_subquery")
775
- count_ast = exp.Select().select(exp.func("COUNT", exp.Star()).as_("total")).from_(subquery)
776
-
777
- # Preserve parameters from the original statement
778
- count_stmt = SQL(count_ast, *filtered_stmt.parameters, _config=_config)
779
-
780
- # Execute count query
781
- total = await self.select_value(count_stmt, _connection=_connection, _config=_config, **kwargs)
782
-
783
- data_stmt = self._transform_to_sql(statement, params, _config) # type: ignore[arg-type]
784
-
785
- for filter_obj in other_filters:
786
- data_stmt = filter_obj.append_to_statement(data_stmt)
787
-
788
- limit_offset = LimitOffsetFilter(limit=limit, offset=offset)
789
- data_stmt = limit_offset.append_to_statement(data_stmt)
790
-
791
- # Execute data query
792
- items = await self.select(
793
- data_stmt, *params, schema_type=schema_type, _connection=_connection, _config=_config, **kwargs
794
- )
795
-
796
- return OffsetPagination(items=items, limit=limit, offset=offset, total=total) # pyright: ignore