sqlspec 0.13.0__py3-none-any.whl → 0.14.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 (110) hide show
  1. sqlspec/__init__.py +39 -1
  2. sqlspec/adapters/adbc/config.py +4 -40
  3. sqlspec/adapters/adbc/driver.py +29 -16
  4. sqlspec/adapters/aiosqlite/config.py +15 -20
  5. sqlspec/adapters/aiosqlite/driver.py +36 -18
  6. sqlspec/adapters/asyncmy/config.py +16 -33
  7. sqlspec/adapters/asyncmy/driver.py +23 -16
  8. sqlspec/adapters/asyncpg/config.py +19 -61
  9. sqlspec/adapters/asyncpg/driver.py +41 -18
  10. sqlspec/adapters/bigquery/config.py +2 -43
  11. sqlspec/adapters/bigquery/driver.py +26 -14
  12. sqlspec/adapters/duckdb/config.py +2 -49
  13. sqlspec/adapters/duckdb/driver.py +35 -16
  14. sqlspec/adapters/oracledb/config.py +30 -83
  15. sqlspec/adapters/oracledb/driver.py +54 -27
  16. sqlspec/adapters/psqlpy/config.py +17 -57
  17. sqlspec/adapters/psqlpy/driver.py +28 -8
  18. sqlspec/adapters/psycopg/config.py +30 -73
  19. sqlspec/adapters/psycopg/driver.py +69 -24
  20. sqlspec/adapters/sqlite/config.py +3 -21
  21. sqlspec/adapters/sqlite/driver.py +50 -26
  22. sqlspec/cli.py +248 -0
  23. sqlspec/config.py +18 -20
  24. sqlspec/driver/_async.py +28 -10
  25. sqlspec/driver/_common.py +5 -4
  26. sqlspec/driver/_sync.py +28 -10
  27. sqlspec/driver/mixins/__init__.py +6 -0
  28. sqlspec/driver/mixins/_cache.py +114 -0
  29. sqlspec/driver/mixins/_pipeline.py +0 -4
  30. sqlspec/{service/base.py → driver/mixins/_query_tools.py} +86 -421
  31. sqlspec/driver/mixins/_result_utils.py +0 -2
  32. sqlspec/driver/mixins/_sql_translator.py +0 -2
  33. sqlspec/driver/mixins/_storage.py +4 -18
  34. sqlspec/driver/mixins/_type_coercion.py +0 -2
  35. sqlspec/driver/parameters.py +4 -4
  36. sqlspec/extensions/aiosql/adapter.py +4 -4
  37. sqlspec/extensions/litestar/__init__.py +2 -1
  38. sqlspec/extensions/litestar/cli.py +48 -0
  39. sqlspec/extensions/litestar/plugin.py +3 -0
  40. sqlspec/loader.py +1 -1
  41. sqlspec/migrations/__init__.py +23 -0
  42. sqlspec/migrations/base.py +390 -0
  43. sqlspec/migrations/commands.py +525 -0
  44. sqlspec/migrations/runner.py +215 -0
  45. sqlspec/migrations/tracker.py +153 -0
  46. sqlspec/migrations/utils.py +89 -0
  47. sqlspec/protocols.py +37 -3
  48. sqlspec/statement/builder/__init__.py +8 -8
  49. sqlspec/statement/builder/{column.py → _column.py} +82 -52
  50. sqlspec/statement/builder/{ddl.py → _ddl.py} +5 -5
  51. sqlspec/statement/builder/_ddl_utils.py +1 -1
  52. sqlspec/statement/builder/{delete.py → _delete.py} +1 -1
  53. sqlspec/statement/builder/{insert.py → _insert.py} +1 -1
  54. sqlspec/statement/builder/{merge.py → _merge.py} +1 -1
  55. sqlspec/statement/builder/_parsing_utils.py +5 -3
  56. sqlspec/statement/builder/{select.py → _select.py} +59 -61
  57. sqlspec/statement/builder/{update.py → _update.py} +2 -2
  58. sqlspec/statement/builder/mixins/__init__.py +24 -30
  59. sqlspec/statement/builder/mixins/{_set_ops.py → _cte_and_set_ops.py} +86 -2
  60. sqlspec/statement/builder/mixins/{_delete_from.py → _delete_operations.py} +2 -0
  61. sqlspec/statement/builder/mixins/{_insert_values.py → _insert_operations.py} +70 -1
  62. sqlspec/statement/builder/mixins/{_merge_clauses.py → _merge_operations.py} +2 -0
  63. sqlspec/statement/builder/mixins/_order_limit_operations.py +123 -0
  64. sqlspec/statement/builder/mixins/{_pivot.py → _pivot_operations.py} +71 -2
  65. sqlspec/statement/builder/mixins/_select_operations.py +612 -0
  66. sqlspec/statement/builder/mixins/{_update_set.py → _update_operations.py} +73 -2
  67. sqlspec/statement/builder/mixins/_where_clause.py +536 -0
  68. sqlspec/statement/cache.py +50 -0
  69. sqlspec/statement/filters.py +37 -8
  70. sqlspec/statement/parameters.py +154 -25
  71. sqlspec/statement/pipelines/__init__.py +1 -1
  72. sqlspec/statement/pipelines/context.py +4 -4
  73. sqlspec/statement/pipelines/transformers/_expression_simplifier.py +3 -3
  74. sqlspec/statement/pipelines/validators/_parameter_style.py +22 -22
  75. sqlspec/statement/pipelines/validators/_performance.py +1 -5
  76. sqlspec/statement/sql.py +246 -176
  77. sqlspec/utils/__init__.py +2 -1
  78. sqlspec/utils/statement_hashing.py +203 -0
  79. sqlspec/utils/type_guards.py +32 -0
  80. {sqlspec-0.13.0.dist-info → sqlspec-0.14.0.dist-info}/METADATA +1 -1
  81. sqlspec-0.14.0.dist-info/RECORD +143 -0
  82. sqlspec-0.14.0.dist-info/entry_points.txt +2 -0
  83. sqlspec/service/__init__.py +0 -4
  84. sqlspec/service/_util.py +0 -147
  85. sqlspec/service/pagination.py +0 -26
  86. sqlspec/statement/builder/mixins/_aggregate_functions.py +0 -250
  87. sqlspec/statement/builder/mixins/_case_builder.py +0 -91
  88. sqlspec/statement/builder/mixins/_common_table_expr.py +0 -90
  89. sqlspec/statement/builder/mixins/_from.py +0 -63
  90. sqlspec/statement/builder/mixins/_group_by.py +0 -118
  91. sqlspec/statement/builder/mixins/_having.py +0 -35
  92. sqlspec/statement/builder/mixins/_insert_from_select.py +0 -47
  93. sqlspec/statement/builder/mixins/_insert_into.py +0 -36
  94. sqlspec/statement/builder/mixins/_limit_offset.py +0 -53
  95. sqlspec/statement/builder/mixins/_order_by.py +0 -46
  96. sqlspec/statement/builder/mixins/_returning.py +0 -37
  97. sqlspec/statement/builder/mixins/_select_columns.py +0 -61
  98. sqlspec/statement/builder/mixins/_unpivot.py +0 -77
  99. sqlspec/statement/builder/mixins/_update_from.py +0 -55
  100. sqlspec/statement/builder/mixins/_update_table.py +0 -29
  101. sqlspec/statement/builder/mixins/_where.py +0 -401
  102. sqlspec/statement/builder/mixins/_window_functions.py +0 -86
  103. sqlspec/statement/parameter_manager.py +0 -220
  104. sqlspec/statement/sql_compiler.py +0 -140
  105. sqlspec-0.13.0.dist-info/RECORD +0 -150
  106. /sqlspec/statement/builder/{base.py → _base.py} +0 -0
  107. /sqlspec/statement/builder/mixins/{_join.py → _join_operations.py} +0 -0
  108. {sqlspec-0.13.0.dist-info → sqlspec-0.14.0.dist-info}/WHEEL +0 -0
  109. {sqlspec-0.13.0.dist-info → sqlspec-0.14.0.dist-info}/licenses/LICENSE +0 -0
  110. {sqlspec-0.13.0.dist-info → sqlspec-0.14.0.dist-info}/licenses/NOTICE +0 -0
@@ -1,10 +1,15 @@
1
- # mypy: disable-error-code="arg-type,misc,type-var"
2
- # pyright: reportCallIssue=false, reportArgumentType=false
3
- from typing import TYPE_CHECKING, Any, Generic, Optional, TypeVar, Union, overload
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
4
5
 
5
6
  from sqlglot import exp, parse_one
7
+ from typing_extensions import Self
6
8
 
7
- from sqlspec.typing import ConnectionT
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
8
13
  from sqlspec.utils.type_guards import (
9
14
  is_dict_row,
10
15
  is_indexable_row,
@@ -14,43 +19,37 @@ from sqlspec.utils.type_guards import (
14
19
  )
15
20
 
16
21
  if TYPE_CHECKING:
17
- from sqlspec.driver import AsyncDriverAdapterProtocol, SyncDriverAdapterProtocol
18
- from sqlspec.service.pagination import OffsetPagination
19
- from sqlspec.statement import SQLConfig, Statement, StatementFilter
20
- from sqlspec.statement.builder import Delete, Insert, QueryBuilder, Select, Update
21
- from sqlspec.statement.sql import SQL
22
- from sqlspec.typing import ModelDTOT, RowT, StatementParameters
22
+ from sqlglot.dialects.dialect import DialectType
23
23
 
24
- __all__ = ("SQLSpecAsyncService", "SQLSpecSyncService")
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
25
28
 
29
+ __all__ = ("AsyncQueryMixin", "SyncQueryMixin")
26
30
 
27
- T = TypeVar("T")
28
- SyncDriverT = TypeVar("SyncDriverT", bound="SyncDriverAdapterProtocol[Any]")
29
- AsyncDriverT = TypeVar("AsyncDriverT", bound="AsyncDriverAdapterProtocol[Any]")
31
+ logger = logging.getLogger(__name__)
30
32
 
33
+ WINDOWS_PATH_MIN_LENGTH = 3
31
34
 
32
- class SQLSpecSyncService(Generic[SyncDriverT, ConnectionT]):
33
- """Sync Service for database operations."""
34
35
 
35
- def __init__(self, driver: "SyncDriverT", connection: "ConnectionT") -> None:
36
- self._driver = driver
37
- self._connection = connection
36
+ class QueryBase(ABC, Generic[ConnectionT]):
37
+ """Base class with common query functionality."""
38
38
 
39
- @classmethod
40
- def new(cls, driver: "SyncDriverT", connection: "ConnectionT") -> "SQLSpecSyncService[SyncDriverT, ConnectionT]":
41
- return cls(driver=driver, connection=connection)
42
-
43
- @property
44
- def driver(self) -> "SyncDriverT":
45
- """Get the driver instance."""
46
- return self._driver
39
+ config: Any
40
+ _connection: Any
41
+ dialect: "DialectType"
47
42
 
48
43
  @property
49
44
  def connection(self) -> "ConnectionT":
50
45
  """Get the connection instance."""
51
- return self._connection
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]
52
51
 
53
- def _normalize_statement(
52
+ def _transform_to_sql(
54
53
  self,
55
54
  statement: "Union[Statement, Select]",
56
55
  params: "Optional[dict[str, Any]]" = None,
@@ -64,9 +63,8 @@ class SQLSpecSyncService(Generic[SyncDriverT, ConnectionT]):
64
63
  config: Optional SQL configuration
65
64
 
66
65
  Returns:
67
- A normalized SQL object
66
+ A converted SQL object
68
67
  """
69
- from sqlspec.statement.sql import SQL
70
68
 
71
69
  if is_select_builder(statement):
72
70
  # Select has its own parameters via build(), ignore external params
@@ -84,106 +82,9 @@ class SQLSpecSyncService(Generic[SyncDriverT, ConnectionT]):
84
82
  msg = f"Unsupported statement type: {type(statement).__name__}"
85
83
  raise TypeError(msg)
86
84
 
87
- @overload
88
- def execute(
89
- self,
90
- statement: "Select",
91
- /,
92
- *parameters: "Union[StatementParameters, StatementFilter]",
93
- schema_type: "type[ModelDTOT]",
94
- _connection: "Optional[ConnectionT]" = None,
95
- _config: "Optional[SQLConfig]" = None,
96
- **kwargs: Any,
97
- ) -> "list[ModelDTOT]": ...
98
-
99
- @overload
100
- def execute(
101
- self,
102
- statement: "Select",
103
- /,
104
- *parameters: "Union[StatementParameters, StatementFilter]",
105
- schema_type: None = None,
106
- _connection: "Optional[ConnectionT]" = None,
107
- _config: "Optional[SQLConfig]" = None,
108
- **kwargs: Any,
109
- ) -> "list[RowT]": ...
110
-
111
- @overload
112
- def execute(
113
- self,
114
- statement: "Union[Insert, Update, Delete]",
115
- /,
116
- *parameters: "Union[StatementParameters, StatementFilter]",
117
- _connection: "Optional[ConnectionT]" = None,
118
- _config: "Optional[SQLConfig]" = None,
119
- **kwargs: Any,
120
- ) -> "list[RowT]": ...
121
-
122
- @overload
123
- def execute(
124
- self,
125
- statement: "Union[str, SQL]", # exp.Expression
126
- /,
127
- *parameters: "Union[StatementParameters, StatementFilter]",
128
- schema_type: "type[ModelDTOT]",
129
- _connection: "Optional[ConnectionT]" = None,
130
- _config: "Optional[SQLConfig]" = None,
131
- **kwargs: Any,
132
- ) -> "list[ModelDTOT]": ...
133
-
134
- @overload
135
- def execute(
136
- self,
137
- statement: "Union[str, SQL]",
138
- /,
139
- *parameters: "Union[StatementParameters, StatementFilter]",
140
- schema_type: None = None,
141
- _connection: "Optional[ConnectionT]" = None,
142
- _config: "Optional[SQLConfig]" = None,
143
- **kwargs: Any,
144
- ) -> "list[RowT]": ...
145
-
146
- def execute(
147
- self,
148
- statement: "Union[Statement, QueryBuilder[Any]]",
149
- /,
150
- *parameters: "Union[StatementParameters, StatementFilter]",
151
- schema_type: "Optional[type[ModelDTOT]]" = None,
152
- _connection: "Optional[ConnectionT]" = None,
153
- _config: "Optional[SQLConfig]" = None,
154
- **kwargs: Any,
155
- ) -> Any:
156
- """Execute a statement and return the result."""
157
- result = self.driver.execute(
158
- statement, *parameters, schema_type=schema_type, _connection=_connection, _config=_config, **kwargs
159
- )
160
- return result.get_data()
161
-
162
- def execute_many(
163
- self,
164
- statement: "Union[Statement, QueryBuilder[Any]]",
165
- /,
166
- *parameters: "Union[StatementParameters, StatementFilter]",
167
- _connection: "Optional[ConnectionT]" = None,
168
- _config: "Optional[SQLConfig]" = None,
169
- **kwargs: Any,
170
- ) -> Any:
171
- """Execute a statement multiple times and return the result."""
172
- result = self.driver.execute_many(statement, *parameters, _connection=_connection, _config=_config, **kwargs)
173
- return result.get_data()
174
85
 
175
- def execute_script(
176
- self,
177
- statement: "Statement",
178
- /,
179
- *parameters: "Union[StatementParameters, StatementFilter]",
180
- _connection: "Optional[ConnectionT]" = None,
181
- _config: "Optional[SQLConfig]" = None,
182
- **kwargs: Any,
183
- ) -> Any:
184
- """Execute a script statement."""
185
- result = self.driver.execute_script(statement, *parameters, _connection=_connection, _config=_config, **kwargs)
186
- return result.get_data()
86
+ class SyncQueryMixin(QueryBase[ConnectionT]):
87
+ """Unified storage operations for synchronous drivers."""
187
88
 
188
89
  @overload
189
90
  def select_one(
@@ -207,7 +108,7 @@ class SQLSpecSyncService(Generic[SyncDriverT, ConnectionT]):
207
108
  _connection: "Optional[ConnectionT]" = None,
208
109
  _config: "Optional[SQLConfig]" = None,
209
110
  **kwargs: Any,
210
- ) -> "RowT": ...
111
+ ) -> "RowT": ... # type: ignore[type-var]
211
112
 
212
113
  def select_one(
213
114
  self,
@@ -218,26 +119,25 @@ class SQLSpecSyncService(Generic[SyncDriverT, ConnectionT]):
218
119
  _connection: "Optional[ConnectionT]" = None,
219
120
  _config: "Optional[SQLConfig]" = None,
220
121
  **kwargs: Any,
221
- ) -> Any:
122
+ ) -> "Union[RowT, ModelDTOT]":
222
123
  """Execute a select statement and return exactly one row.
223
124
 
224
125
  Raises an exception if no rows or more than one row is returned.
225
126
  """
226
- result = self.driver.execute(
127
+ result = self.execute( # type: ignore[attr-defined]
227
128
  statement, *parameters, schema_type=schema_type, _connection=_connection, _config=_config, **kwargs
228
129
  )
229
130
  data = result.get_data()
230
- # For select operations, data should be a list
231
131
  if not isinstance(data, list):
232
132
  msg = "Expected list result from select operation"
233
133
  raise TypeError(msg)
234
134
  if not data:
235
135
  msg = "No rows found"
236
- raise ValueError(msg)
136
+ raise NotFoundError(msg)
237
137
  if len(data) > 1:
238
138
  msg = f"Expected exactly one row, found {len(data)}"
239
139
  raise ValueError(msg)
240
- return data[0]
140
+ return data[0] # type: ignore[no-any-return]
241
141
 
242
142
  @overload
243
143
  def select_one_or_none(
@@ -271,14 +171,14 @@ class SQLSpecSyncService(Generic[SyncDriverT, ConnectionT]):
271
171
  schema_type: "Optional[type[ModelDTOT]]" = None,
272
172
  _connection: "Optional[ConnectionT]" = None,
273
173
  _config: "Optional[SQLConfig]" = None,
274
- **kwargs: Any,
174
+ **kwargs: "Optional[Union[RowT, ModelDTOT]]",
275
175
  ) -> Any:
276
176
  """Execute a select statement and return at most one row.
277
177
 
278
178
  Returns None if no rows are found.
279
179
  Raises an exception if more than one row is returned.
280
180
  """
281
- result = self.driver.execute(
181
+ result = self.execute( # type: ignore[attr-defined]
282
182
  statement, *parameters, schema_type=schema_type, _connection=_connection, _config=_config, **kwargs
283
183
  )
284
184
  data = result.get_data()
@@ -326,9 +226,9 @@ class SQLSpecSyncService(Generic[SyncDriverT, ConnectionT]):
326
226
  _connection: "Optional[ConnectionT]" = None,
327
227
  _config: "Optional[SQLConfig]" = None,
328
228
  **kwargs: Any,
329
- ) -> Any:
229
+ ) -> Union[list[RowT], list[ModelDTOT]]:
330
230
  """Execute a select statement and return all rows."""
331
- result = self.driver.execute(
231
+ result = self.execute( # type: ignore[attr-defined]
332
232
  statement, *parameters, schema_type=schema_type, _connection=_connection, _config=_config, **kwargs
333
233
  )
334
234
  data = result.get_data()
@@ -352,7 +252,7 @@ class SQLSpecSyncService(Generic[SyncDriverT, ConnectionT]):
352
252
  Expects exactly one row with one column.
353
253
  Raises an exception if no rows or more than one row/column is returned.
354
254
  """
355
- result = self.driver.execute(statement, *parameters, _connection=_connection, _config=_config, **kwargs)
255
+ result = self.execute(statement, *parameters, _connection=_connection, _config=_config, **kwargs) # type: ignore[attr-defined]
356
256
  data = result.get_data()
357
257
  # For select operations, data should be a list
358
258
  if not isinstance(data, list):
@@ -360,7 +260,7 @@ class SQLSpecSyncService(Generic[SyncDriverT, ConnectionT]):
360
260
  raise TypeError(msg)
361
261
  if not data:
362
262
  msg = "No rows found"
363
- raise ValueError(msg)
263
+ raise NotFoundError(msg)
364
264
  if len(data) > 1:
365
265
  msg = f"Expected exactly one row, found {len(data)}"
366
266
  raise ValueError(msg)
@@ -371,7 +271,6 @@ class SQLSpecSyncService(Generic[SyncDriverT, ConnectionT]):
371
271
  raise ValueError(msg)
372
272
  return next(iter(row.values()))
373
273
  if is_indexable_row(row):
374
- # Tuple or list-like row
375
274
  if not row:
376
275
  msg = "Row has no columns"
377
276
  raise ValueError(msg)
@@ -394,7 +293,7 @@ class SQLSpecSyncService(Generic[SyncDriverT, ConnectionT]):
394
293
  Expects at most one row with one column.
395
294
  Raises an exception if more than one row is returned.
396
295
  """
397
- result = self.driver.execute(statement, *parameters, _connection=_connection, _config=_config, **kwargs)
296
+ result = self.execute(statement, *parameters, _connection=_connection, _config=_config, **kwargs) # type: ignore[attr-defined]
398
297
  data = result.get_data()
399
298
  # For select operations, data should be a list
400
299
  if not isinstance(data, list):
@@ -452,7 +351,7 @@ class SQLSpecSyncService(Generic[SyncDriverT, ConnectionT]):
452
351
  _connection: "Optional[ConnectionT]" = None,
453
352
  _config: "Optional[SQLConfig]" = None,
454
353
  **kwargs: Any,
455
- ) -> Any:
354
+ ) -> "Union[OffsetPagination[RowT], OffsetPagination[ModelDTOT]]":
456
355
  """Execute a paginated query with automatic counting.
457
356
 
458
357
  This method performs two queries:
@@ -513,15 +412,12 @@ class SQLSpecSyncService(Generic[SyncDriverT, ConnectionT]):
513
412
  ... schema_type=User,
514
413
  ... )
515
414
  """
516
- from sqlspec.service.pagination import OffsetPagination
517
- from sqlspec.statement.sql import SQL
518
415
 
519
416
  # Separate filters from parameters
520
417
  filters: list[StatementFilter] = []
521
418
  params: list[Any] = []
522
419
 
523
420
  for p in parameters:
524
- # Use type guard to check if it implements the StatementFilter protocol
525
421
  if is_statement_filter(p):
526
422
  filters.append(p)
527
423
  else:
@@ -546,198 +442,46 @@ class SQLSpecSyncService(Generic[SyncDriverT, ConnectionT]):
546
442
  msg = "Pagination requires either a LimitOffsetFilter in parameters or 'limit' and 'offset' in kwargs."
547
443
  raise ValueError(msg)
548
444
 
549
- base_stmt = self._normalize_statement(statement, params, _config)
445
+ base_stmt = self._transform_to_sql(statement, params, _config) # type: ignore[arg-type]
550
446
 
551
447
  filtered_stmt = base_stmt
552
448
  for filter_obj in other_filters:
553
449
  filtered_stmt = filter_obj.append_to_statement(filtered_stmt)
554
450
 
555
451
  sql_str = filtered_stmt.to_sql()
556
-
557
- # Parse and transform the AST to create a count query
558
452
  parsed = parse_one(sql_str)
559
453
 
560
454
  # Using exp.Subquery to properly wrap the parsed expression
561
455
  subquery = exp.Subquery(this=parsed, alias="_count_subquery")
562
456
  count_ast = exp.Select().select(exp.func("COUNT", exp.Star()).as_("total")).from_(subquery)
563
457
 
564
- count_stmt = SQL(count_ast.sql(), _config=_config)
458
+ # Preserve parameters from the original statement
459
+ count_stmt = SQL(count_ast, parameters=filtered_stmt.parameters, _config=_config)
565
460
 
566
461
  # Execute count query
567
462
  total = self.select_value(count_stmt, _connection=_connection, _config=_config, **kwargs)
568
463
 
569
- data_stmt = self._normalize_statement(statement, params, _config)
464
+ data_stmt = self._transform_to_sql(statement, params, _config) # type: ignore[arg-type]
570
465
 
571
466
  for filter_obj in other_filters:
572
467
  data_stmt = filter_obj.append_to_statement(data_stmt)
573
468
 
574
- data_stmt = data_stmt.limit(limit).offset(offset)
575
-
576
- # Execute data query
577
- items = self.select(data_stmt, schema_type=schema_type, _connection=_connection, _config=_config, **kwargs)
578
-
579
- return OffsetPagination(items=items, limit=limit, offset=offset, total=total)
580
-
581
-
582
- class SQLSpecAsyncService(Generic[AsyncDriverT, ConnectionT]):
583
- """Async Service for database operations."""
584
-
585
- def __init__(self, driver: "AsyncDriverT", connection: "ConnectionT") -> None:
586
- self._driver = driver
587
- self._connection = connection
588
-
589
- @classmethod
590
- def new(cls, driver: "AsyncDriverT", connection: "ConnectionT") -> "SQLSpecAsyncService[AsyncDriverT, ConnectionT]":
591
- return cls(driver=driver, connection=connection)
592
-
593
- @property
594
- def driver(self) -> "AsyncDriverT":
595
- """Get the driver instance."""
596
- return self._driver
597
-
598
- @property
599
- def connection(self) -> "ConnectionT":
600
- """Get the connection instance."""
601
- return self._connection
602
-
603
- def _normalize_statement(
604
- self,
605
- statement: "Union[Statement, Select]",
606
- params: "Optional[dict[str, Any]]" = None,
607
- config: "Optional[SQLConfig]" = None,
608
- ) -> "SQL":
609
- """Normalize a statement of any supported type into a SQL object.
610
-
611
- Args:
612
- statement: The statement to normalize (str, Expression, SQL, or Select)
613
- params: Optional parameters (ignored for Select and SQL objects)
614
- config: Optional SQL configuration
615
-
616
- Returns:
617
- A normalized SQL object
618
- """
619
- from sqlspec.statement.sql import SQL
620
-
621
- if is_select_builder(statement):
622
- # Select has its own parameters via build(), ignore external params
623
- safe_query = statement.build()
624
- return SQL(safe_query.sql, parameters=safe_query.parameters, config=config)
625
-
626
- if isinstance(statement, SQL):
627
- # SQL object is already complete, ignore external params
628
- return statement
629
-
630
- if isinstance(statement, (str, exp.Expression)):
631
- return SQL(statement, parameters=params, config=config)
632
-
633
- # Fallback for type safety
634
- msg = f"Unsupported statement type: {type(statement).__name__}"
635
- raise TypeError(msg)
469
+ # Apply limit and offset using LimitOffsetFilter
470
+ from sqlspec.statement.filters import LimitOffsetFilter
636
471
 
637
- @overload
638
- async def execute(
639
- self,
640
- statement: "Select",
641
- /,
642
- *parameters: "Union[StatementParameters, StatementFilter]",
643
- schema_type: "type[ModelDTOT]",
644
- _connection: "Optional[ConnectionT]" = None,
645
- _config: "Optional[SQLConfig]" = None,
646
- **kwargs: Any,
647
- ) -> "list[ModelDTOT]": ...
472
+ limit_offset = LimitOffsetFilter(limit=limit, offset=offset)
473
+ data_stmt = limit_offset.append_to_statement(data_stmt)
648
474
 
649
- @overload
650
- async def execute(
651
- self,
652
- statement: "Select",
653
- /,
654
- *parameters: "Union[StatementParameters, StatementFilter]",
655
- schema_type: None = None,
656
- _connection: "Optional[ConnectionT]" = None,
657
- _config: "Optional[SQLConfig]" = None,
658
- **kwargs: Any,
659
- ) -> "list[RowT]": ...
660
-
661
- @overload
662
- async def execute(
663
- self,
664
- statement: "Union[Insert, Update, Delete]",
665
- /,
666
- *parameters: "Union[StatementParameters, StatementFilter]",
667
- _connection: "Optional[ConnectionT]" = None,
668
- _config: "Optional[SQLConfig]" = None,
669
- **kwargs: Any,
670
- ) -> "list[RowT]": ...
671
-
672
- @overload
673
- async def execute(
674
- self,
675
- statement: "Union[str, SQL]", # exp.Expression
676
- /,
677
- *parameters: "Union[StatementParameters, StatementFilter]",
678
- schema_type: "type[ModelDTOT]",
679
- _connection: "Optional[ConnectionT]" = None,
680
- _config: "Optional[SQLConfig]" = None,
681
- **kwargs: Any,
682
- ) -> "list[ModelDTOT]": ...
683
-
684
- @overload
685
- async def execute(
686
- self,
687
- statement: "Union[str, SQL]",
688
- /,
689
- *parameters: "Union[StatementParameters, StatementFilter]",
690
- schema_type: None = None,
691
- _connection: "Optional[ConnectionT]" = None,
692
- _config: "Optional[SQLConfig]" = None,
693
- **kwargs: Any,
694
- ) -> "list[RowT]": ...
695
-
696
- async def execute(
697
- self,
698
- statement: "Union[Statement, QueryBuilder[Any]]",
699
- /,
700
- *parameters: "Union[StatementParameters, StatementFilter]",
701
- schema_type: "Optional[type[ModelDTOT]]" = None,
702
- _connection: "Optional[ConnectionT]" = None,
703
- _config: "Optional[SQLConfig]" = None,
704
- **kwargs: Any,
705
- ) -> Any:
706
- """Execute a statement and return the result."""
707
- result = await self.driver.execute(
708
- statement, *parameters, schema_type=schema_type, _connection=_connection, _config=_config, **kwargs
475
+ # Execute data query
476
+ items = self.select(
477
+ data_stmt, params, schema_type=schema_type, _connection=_connection, _config=_config, **kwargs
709
478
  )
710
- return result.get_data()
711
479
 
712
- async def execute_many(
713
- self,
714
- statement: "Union[Statement, QueryBuilder[Any]]",
715
- /,
716
- *parameters: "Union[StatementParameters, StatementFilter]",
717
- _connection: "Optional[ConnectionT]" = None,
718
- _config: "Optional[SQLConfig]" = None,
719
- **kwargs: Any,
720
- ) -> Any:
721
- """Execute a statement multiple times and return the result."""
722
- result = await self.driver.execute_many(
723
- statement, *parameters, _connection=_connection, _config=_config, **kwargs
724
- )
725
- return result.get_data()
480
+ return OffsetPagination(items=items, limit=limit, offset=offset, total=total) # pyright: ignore
726
481
 
727
- async def execute_script(
728
- self,
729
- statement: "Statement",
730
- /,
731
- *parameters: "Union[StatementParameters, StatementFilter]",
732
- _connection: "Optional[ConnectionT]" = None,
733
- _config: "Optional[SQLConfig]" = None,
734
- **kwargs: Any,
735
- ) -> Any:
736
- """Execute a script statement."""
737
- result = await self.driver.execute_script(
738
- statement, *parameters, _connection=_connection, _config=_config, **kwargs
739
- )
740
- return result.get_data()
482
+
483
+ class AsyncQueryMixin(QueryBase[ConnectionT]):
484
+ """Unified query operations for asynchronous drivers."""
741
485
 
742
486
  @overload
743
487
  async def select_one(
@@ -772,26 +516,22 @@ class SQLSpecAsyncService(Generic[AsyncDriverT, ConnectionT]):
772
516
  _connection: "Optional[ConnectionT]" = None,
773
517
  _config: "Optional[SQLConfig]" = None,
774
518
  **kwargs: Any,
775
- ) -> Any:
519
+ ) -> "Union[RowT, ModelDTOT]":
776
520
  """Execute a select statement and return exactly one row.
777
521
 
778
522
  Raises an exception if no rows or more than one row is returned.
779
523
  """
780
- result = await self.driver.execute(
524
+ result = await self.execute( # type: ignore[attr-defined]
781
525
  statement, *parameters, schema_type=schema_type, _connection=_connection, _config=_config, **kwargs
782
526
  )
783
527
  data = result.get_data()
784
- # For select operations, data should be a list
785
- if not isinstance(data, list):
786
- msg = "Expected list result from select operation"
787
- raise TypeError(msg)
788
528
  if not data:
789
529
  msg = "No rows found"
790
- raise ValueError(msg)
530
+ raise NotFoundError(msg)
791
531
  if len(data) > 1:
792
532
  msg = f"Expected exactly one row, found {len(data)}"
793
533
  raise ValueError(msg)
794
- return data[0]
534
+ return data[0] # type: ignore[no-any-return]
795
535
 
796
536
  @overload
797
537
  async def select_one_or_none(
@@ -826,26 +566,22 @@ class SQLSpecAsyncService(Generic[AsyncDriverT, ConnectionT]):
826
566
  _connection: "Optional[ConnectionT]" = None,
827
567
  _config: "Optional[SQLConfig]" = None,
828
568
  **kwargs: Any,
829
- ) -> Any:
569
+ ) -> "Optional[Union[RowT, ModelDTOT]]":
830
570
  """Execute a select statement and return at most one row.
831
571
 
832
572
  Returns None if no rows are found.
833
573
  Raises an exception if more than one row is returned.
834
574
  """
835
- result = await self.driver.execute(
575
+ result = await self.execute( # type: ignore[attr-defined]
836
576
  statement, *parameters, schema_type=schema_type, _connection=_connection, _config=_config, **kwargs
837
577
  )
838
578
  data = result.get_data()
839
- # For select operations, data should be a list
840
- if not isinstance(data, list):
841
- msg = "Expected list result from select operation"
842
- raise TypeError(msg)
843
579
  if not data:
844
580
  return None
845
581
  if len(data) > 1:
846
582
  msg = f"Expected at most one row, found {len(data)}"
847
583
  raise ValueError(msg)
848
- return data[0]
584
+ return data[0] # type: ignore[no-any-return]
849
585
 
850
586
  @overload
851
587
  async def select(
@@ -880,17 +616,12 @@ class SQLSpecAsyncService(Generic[AsyncDriverT, ConnectionT]):
880
616
  _connection: "Optional[ConnectionT]" = None,
881
617
  _config: "Optional[SQLConfig]" = None,
882
618
  **kwargs: Any,
883
- ) -> Any:
619
+ ) -> "Union[list[RowT], list[ModelDTOT]]":
884
620
  """Execute a select statement and return all rows."""
885
- result = await self.driver.execute(
621
+ result = await self.execute( # type: ignore[attr-defined]
886
622
  statement, *parameters, schema_type=schema_type, _connection=_connection, _config=_config, **kwargs
887
623
  )
888
- data = result.get_data()
889
- # For select operations, data should be a list
890
- if not isinstance(data, list):
891
- msg = "Expected list result from select operation"
892
- raise TypeError(msg)
893
- return data
624
+ return result.get_data() # type: ignore[no-any-return]
894
625
 
895
626
  async def select_value(
896
627
  self,
@@ -906,19 +637,11 @@ class SQLSpecAsyncService(Generic[AsyncDriverT, ConnectionT]):
906
637
  Expects exactly one row with one column.
907
638
  Raises an exception if no rows or more than one row/column is returned.
908
639
  """
909
- result = await self.driver.execute(statement, *parameters, _connection=_connection, _config=_config, **kwargs)
910
- data = result.get_data()
911
- # For select operations, data should be a list
912
- if not isinstance(data, list):
913
- msg = "Expected list result from select operation"
914
- raise TypeError(msg)
915
- if not data:
640
+ result = await self.execute(statement, *parameters, _connection=_connection, _config=_config, **kwargs) # type: ignore[attr-defined]
641
+ row = result.one()
642
+ if not row:
916
643
  msg = "No rows found"
917
- raise ValueError(msg)
918
- if len(data) > 1:
919
- msg = f"Expected exactly one row, found {len(data)}"
920
- raise ValueError(msg)
921
- row = data[0]
644
+ raise NotFoundError(msg)
922
645
  if is_dict_row(row):
923
646
  if not row:
924
647
  msg = "Row has no columns"
@@ -948,7 +671,9 @@ class SQLSpecAsyncService(Generic[AsyncDriverT, ConnectionT]):
948
671
  Expects at most one row with one column.
949
672
  Raises an exception if more than one row is returned.
950
673
  """
951
- result = await self.driver.execute(statement, *parameters, _connection=_connection, _config=_config, **kwargs)
674
+ result = await self.execute( # type: ignore[attr-defined]
675
+ statement, *parameters, _connection=_connection, _config=_config, **kwargs
676
+ )
952
677
  data = result.get_data()
953
678
  # For select operations, data should be a list
954
679
  if not isinstance(data, list):
@@ -1007,65 +732,7 @@ class SQLSpecAsyncService(Generic[AsyncDriverT, ConnectionT]):
1007
732
  _connection: "Optional[ConnectionT]" = None,
1008
733
  _config: "Optional[SQLConfig]" = None,
1009
734
  **kwargs: Any,
1010
- ) -> Any:
1011
- """Execute a paginated query with automatic counting.
1012
-
1013
- This method performs two queries:
1014
- 1. A count query to get the total number of results
1015
- 2. A data query with limit/offset applied
1016
-
1017
- Pagination can be specified either via LimitOffsetFilter in parameters
1018
- or via 'limit' and 'offset' in kwargs.
1019
-
1020
- Args:
1021
- statement: The SELECT statement to paginate
1022
- *parameters: Statement parameters and filters (can include LimitOffsetFilter)
1023
- schema_type: Optional model type for automatic schema conversion
1024
- _connection: Optional connection to use
1025
- _config: Optional SQL configuration
1026
- **kwargs: Additional driver-specific arguments. Can include 'limit' and 'offset'
1027
- if LimitOffsetFilter is not provided
1028
-
1029
- Returns:
1030
- OffsetPagination object containing items, limit, offset, and total count
1031
-
1032
- Raises:
1033
- ValueError: If neither LimitOffsetFilter nor limit/offset kwargs are provided
1034
-
1035
- Example:
1036
- >>> # Basic pagination
1037
- >>> from sqlspec.statement.filters import LimitOffsetFilter
1038
- >>> result = await service.paginate(
1039
- ... sql.select("*").from_("users"),
1040
- ... LimitOffsetFilter(limit=10, offset=20),
1041
- ... )
1042
- >>> print(
1043
- ... f"Showing {len(result.items)} of {result.total} users"
1044
- ... )
1045
-
1046
- >>> # With schema conversion
1047
- >>> result = await service.paginate(
1048
- ... sql.select("*").from_("users"),
1049
- ... LimitOffsetFilter(limit=10, offset=0),
1050
- ... schema_type=User,
1051
- ... )
1052
- >>> # result.items is list[User] with proper type inference
1053
-
1054
- >>> # With multiple filters
1055
- >>> from sqlspec.statement.filters import (
1056
- ... LimitOffsetFilter,
1057
- ... OrderByFilter,
1058
- ... )
1059
- >>> result = await service.paginate(
1060
- ... sql.select("*").from_("users"),
1061
- ... OrderByFilter("created_at", "desc"),
1062
- ... LimitOffsetFilter(limit=20, offset=40),
1063
- ... schema_type=User,
1064
- ... )
1065
- """
1066
- from sqlspec.service.pagination import OffsetPagination
1067
- from sqlspec.statement.sql import SQL
1068
-
735
+ ) -> "Union[OffsetPagination[RowT], OffsetPagination[ModelDTOT]]":
1069
736
  # Separate filters from parameters
1070
737
  filters: list[StatementFilter] = []
1071
738
  params: list[Any] = []
@@ -1096,36 +763,34 @@ class SQLSpecAsyncService(Generic[AsyncDriverT, ConnectionT]):
1096
763
  msg = "Pagination requires either a LimitOffsetFilter in parameters or 'limit' and 'offset' in kwargs."
1097
764
  raise ValueError(msg)
1098
765
 
1099
- base_stmt = self._normalize_statement(statement, params, _config)
766
+ base_stmt = self._transform_to_sql(statement, params, _config) # type: ignore[arg-type]
1100
767
 
1101
768
  filtered_stmt = base_stmt
1102
769
  for filter_obj in other_filters:
1103
770
  filtered_stmt = filter_obj.append_to_statement(filtered_stmt)
1104
-
1105
- sql_str = filtered_stmt.to_sql()
1106
-
1107
- # Parse and transform the AST to create a count query
1108
- parsed = parse_one(sql_str)
771
+ parsed = parse_one(filtered_stmt.to_sql())
1109
772
 
1110
773
  # Using exp.Subquery to properly wrap the parsed expression
1111
774
  subquery = exp.Subquery(this=parsed, alias="_count_subquery")
1112
775
  count_ast = exp.Select().select(exp.func("COUNT", exp.Star()).as_("total")).from_(subquery)
1113
776
 
1114
- count_stmt = SQL(count_ast.sql(), _config=_config)
777
+ # Preserve parameters from the original statement
778
+ count_stmt = SQL(count_ast, *filtered_stmt.parameters, _config=_config)
1115
779
 
1116
780
  # Execute count query
1117
781
  total = await self.select_value(count_stmt, _connection=_connection, _config=_config, **kwargs)
1118
782
 
1119
- data_stmt = self._normalize_statement(statement, params, _config)
783
+ data_stmt = self._transform_to_sql(statement, params, _config) # type: ignore[arg-type]
1120
784
 
1121
785
  for filter_obj in other_filters:
1122
786
  data_stmt = filter_obj.append_to_statement(data_stmt)
1123
787
 
1124
- data_stmt = data_stmt.limit(limit).offset(offset)
788
+ limit_offset = LimitOffsetFilter(limit=limit, offset=offset)
789
+ data_stmt = limit_offset.append_to_statement(data_stmt)
1125
790
 
1126
791
  # Execute data query
1127
792
  items = await self.select(
1128
- data_stmt, schema_type=schema_type, _connection=_connection, _config=_config, **kwargs
793
+ data_stmt, *params, schema_type=schema_type, _connection=_connection, _config=_config, **kwargs
1129
794
  )
1130
795
 
1131
- return OffsetPagination(items=items, limit=limit, offset=offset, total=total)
796
+ return OffsetPagination(items=items, limit=limit, offset=offset, total=total) # pyright: ignore