sqlspec 0.13.1__py3-none-any.whl → 0.16.2__py3-none-any.whl

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

Potentially problematic release.


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

Files changed (185) hide show
  1. sqlspec/__init__.py +71 -8
  2. sqlspec/__main__.py +12 -0
  3. sqlspec/__metadata__.py +1 -3
  4. sqlspec/_serialization.py +1 -2
  5. sqlspec/_sql.py +930 -136
  6. sqlspec/_typing.py +278 -142
  7. sqlspec/adapters/adbc/__init__.py +4 -3
  8. sqlspec/adapters/adbc/_types.py +12 -0
  9. sqlspec/adapters/adbc/config.py +116 -285
  10. sqlspec/adapters/adbc/driver.py +462 -340
  11. sqlspec/adapters/aiosqlite/__init__.py +18 -3
  12. sqlspec/adapters/aiosqlite/_types.py +13 -0
  13. sqlspec/adapters/aiosqlite/config.py +202 -150
  14. sqlspec/adapters/aiosqlite/driver.py +226 -247
  15. sqlspec/adapters/asyncmy/__init__.py +18 -3
  16. sqlspec/adapters/asyncmy/_types.py +12 -0
  17. sqlspec/adapters/asyncmy/config.py +80 -199
  18. sqlspec/adapters/asyncmy/driver.py +257 -215
  19. sqlspec/adapters/asyncpg/__init__.py +19 -4
  20. sqlspec/adapters/asyncpg/_types.py +17 -0
  21. sqlspec/adapters/asyncpg/config.py +81 -214
  22. sqlspec/adapters/asyncpg/driver.py +284 -359
  23. sqlspec/adapters/bigquery/__init__.py +17 -3
  24. sqlspec/adapters/bigquery/_types.py +12 -0
  25. sqlspec/adapters/bigquery/config.py +191 -299
  26. sqlspec/adapters/bigquery/driver.py +474 -634
  27. sqlspec/adapters/duckdb/__init__.py +14 -3
  28. sqlspec/adapters/duckdb/_types.py +12 -0
  29. sqlspec/adapters/duckdb/config.py +414 -397
  30. sqlspec/adapters/duckdb/driver.py +342 -393
  31. sqlspec/adapters/oracledb/__init__.py +19 -5
  32. sqlspec/adapters/oracledb/_types.py +14 -0
  33. sqlspec/adapters/oracledb/config.py +123 -458
  34. sqlspec/adapters/oracledb/driver.py +505 -531
  35. sqlspec/adapters/psqlpy/__init__.py +13 -3
  36. sqlspec/adapters/psqlpy/_types.py +11 -0
  37. sqlspec/adapters/psqlpy/config.py +93 -307
  38. sqlspec/adapters/psqlpy/driver.py +504 -213
  39. sqlspec/adapters/psycopg/__init__.py +19 -5
  40. sqlspec/adapters/psycopg/_types.py +17 -0
  41. sqlspec/adapters/psycopg/config.py +143 -472
  42. sqlspec/adapters/psycopg/driver.py +704 -825
  43. sqlspec/adapters/sqlite/__init__.py +14 -3
  44. sqlspec/adapters/sqlite/_types.py +11 -0
  45. sqlspec/adapters/sqlite/config.py +208 -142
  46. sqlspec/adapters/sqlite/driver.py +263 -278
  47. sqlspec/base.py +105 -9
  48. sqlspec/{statement/builder → builder}/__init__.py +12 -14
  49. sqlspec/{statement/builder/base.py → builder/_base.py} +184 -86
  50. sqlspec/{statement/builder/column.py → builder/_column.py} +97 -60
  51. sqlspec/{statement/builder/ddl.py → builder/_ddl.py} +61 -131
  52. sqlspec/{statement/builder → builder}/_ddl_utils.py +4 -10
  53. sqlspec/{statement/builder/delete.py → builder/_delete.py} +10 -30
  54. sqlspec/builder/_insert.py +421 -0
  55. sqlspec/builder/_merge.py +71 -0
  56. sqlspec/{statement/builder → builder}/_parsing_utils.py +49 -26
  57. sqlspec/builder/_select.py +170 -0
  58. sqlspec/{statement/builder/update.py → builder/_update.py} +16 -20
  59. sqlspec/builder/mixins/__init__.py +55 -0
  60. sqlspec/builder/mixins/_cte_and_set_ops.py +222 -0
  61. sqlspec/{statement/builder/mixins/_delete_from.py → builder/mixins/_delete_operations.py} +8 -1
  62. sqlspec/builder/mixins/_insert_operations.py +244 -0
  63. sqlspec/{statement/builder/mixins/_join.py → builder/mixins/_join_operations.py} +45 -13
  64. sqlspec/{statement/builder/mixins/_merge_clauses.py → builder/mixins/_merge_operations.py} +188 -30
  65. sqlspec/builder/mixins/_order_limit_operations.py +135 -0
  66. sqlspec/builder/mixins/_pivot_operations.py +153 -0
  67. sqlspec/builder/mixins/_select_operations.py +604 -0
  68. sqlspec/builder/mixins/_update_operations.py +202 -0
  69. sqlspec/builder/mixins/_where_clause.py +644 -0
  70. sqlspec/cli.py +247 -0
  71. sqlspec/config.py +183 -138
  72. sqlspec/core/__init__.py +63 -0
  73. sqlspec/core/cache.py +871 -0
  74. sqlspec/core/compiler.py +417 -0
  75. sqlspec/core/filters.py +830 -0
  76. sqlspec/core/hashing.py +310 -0
  77. sqlspec/core/parameters.py +1237 -0
  78. sqlspec/core/result.py +677 -0
  79. sqlspec/{statement → core}/splitter.py +321 -191
  80. sqlspec/core/statement.py +676 -0
  81. sqlspec/driver/__init__.py +7 -10
  82. sqlspec/driver/_async.py +422 -163
  83. sqlspec/driver/_common.py +545 -287
  84. sqlspec/driver/_sync.py +426 -160
  85. sqlspec/driver/mixins/__init__.py +2 -13
  86. sqlspec/driver/mixins/_result_tools.py +193 -0
  87. sqlspec/driver/mixins/_sql_translator.py +65 -14
  88. sqlspec/exceptions.py +5 -252
  89. sqlspec/extensions/aiosql/adapter.py +93 -96
  90. sqlspec/extensions/litestar/__init__.py +2 -1
  91. sqlspec/extensions/litestar/cli.py +48 -0
  92. sqlspec/extensions/litestar/config.py +0 -1
  93. sqlspec/extensions/litestar/handlers.py +15 -26
  94. sqlspec/extensions/litestar/plugin.py +21 -16
  95. sqlspec/extensions/litestar/providers.py +17 -52
  96. sqlspec/loader.py +423 -104
  97. sqlspec/migrations/__init__.py +35 -0
  98. sqlspec/migrations/base.py +414 -0
  99. sqlspec/migrations/commands.py +443 -0
  100. sqlspec/migrations/loaders.py +402 -0
  101. sqlspec/migrations/runner.py +213 -0
  102. sqlspec/migrations/tracker.py +140 -0
  103. sqlspec/migrations/utils.py +129 -0
  104. sqlspec/protocols.py +51 -186
  105. sqlspec/storage/__init__.py +1 -1
  106. sqlspec/storage/backends/base.py +37 -40
  107. sqlspec/storage/backends/fsspec.py +136 -112
  108. sqlspec/storage/backends/obstore.py +138 -160
  109. sqlspec/storage/capabilities.py +5 -4
  110. sqlspec/storage/registry.py +57 -106
  111. sqlspec/typing.py +136 -115
  112. sqlspec/utils/__init__.py +2 -2
  113. sqlspec/utils/correlation.py +0 -3
  114. sqlspec/utils/deprecation.py +6 -6
  115. sqlspec/utils/fixtures.py +6 -6
  116. sqlspec/utils/logging.py +0 -2
  117. sqlspec/utils/module_loader.py +7 -12
  118. sqlspec/utils/singleton.py +0 -1
  119. sqlspec/utils/sync_tools.py +17 -38
  120. sqlspec/utils/text.py +12 -51
  121. sqlspec/utils/type_guards.py +482 -235
  122. {sqlspec-0.13.1.dist-info → sqlspec-0.16.2.dist-info}/METADATA +7 -2
  123. sqlspec-0.16.2.dist-info/RECORD +134 -0
  124. sqlspec-0.16.2.dist-info/entry_points.txt +2 -0
  125. sqlspec/driver/connection.py +0 -207
  126. sqlspec/driver/mixins/_csv_writer.py +0 -91
  127. sqlspec/driver/mixins/_pipeline.py +0 -512
  128. sqlspec/driver/mixins/_result_utils.py +0 -140
  129. sqlspec/driver/mixins/_storage.py +0 -926
  130. sqlspec/driver/mixins/_type_coercion.py +0 -130
  131. sqlspec/driver/parameters.py +0 -138
  132. sqlspec/service/__init__.py +0 -4
  133. sqlspec/service/_util.py +0 -147
  134. sqlspec/service/base.py +0 -1131
  135. sqlspec/service/pagination.py +0 -26
  136. sqlspec/statement/__init__.py +0 -21
  137. sqlspec/statement/builder/insert.py +0 -288
  138. sqlspec/statement/builder/merge.py +0 -95
  139. sqlspec/statement/builder/mixins/__init__.py +0 -65
  140. sqlspec/statement/builder/mixins/_aggregate_functions.py +0 -250
  141. sqlspec/statement/builder/mixins/_case_builder.py +0 -91
  142. sqlspec/statement/builder/mixins/_common_table_expr.py +0 -90
  143. sqlspec/statement/builder/mixins/_from.py +0 -63
  144. sqlspec/statement/builder/mixins/_group_by.py +0 -118
  145. sqlspec/statement/builder/mixins/_having.py +0 -35
  146. sqlspec/statement/builder/mixins/_insert_from_select.py +0 -47
  147. sqlspec/statement/builder/mixins/_insert_into.py +0 -36
  148. sqlspec/statement/builder/mixins/_insert_values.py +0 -67
  149. sqlspec/statement/builder/mixins/_limit_offset.py +0 -53
  150. sqlspec/statement/builder/mixins/_order_by.py +0 -46
  151. sqlspec/statement/builder/mixins/_pivot.py +0 -79
  152. sqlspec/statement/builder/mixins/_returning.py +0 -37
  153. sqlspec/statement/builder/mixins/_select_columns.py +0 -61
  154. sqlspec/statement/builder/mixins/_set_ops.py +0 -122
  155. sqlspec/statement/builder/mixins/_unpivot.py +0 -77
  156. sqlspec/statement/builder/mixins/_update_from.py +0 -55
  157. sqlspec/statement/builder/mixins/_update_set.py +0 -94
  158. sqlspec/statement/builder/mixins/_update_table.py +0 -29
  159. sqlspec/statement/builder/mixins/_where.py +0 -401
  160. sqlspec/statement/builder/mixins/_window_functions.py +0 -86
  161. sqlspec/statement/builder/select.py +0 -221
  162. sqlspec/statement/filters.py +0 -596
  163. sqlspec/statement/parameter_manager.py +0 -220
  164. sqlspec/statement/parameters.py +0 -867
  165. sqlspec/statement/pipelines/__init__.py +0 -210
  166. sqlspec/statement/pipelines/analyzers/__init__.py +0 -9
  167. sqlspec/statement/pipelines/analyzers/_analyzer.py +0 -646
  168. sqlspec/statement/pipelines/context.py +0 -115
  169. sqlspec/statement/pipelines/transformers/__init__.py +0 -7
  170. sqlspec/statement/pipelines/transformers/_expression_simplifier.py +0 -88
  171. sqlspec/statement/pipelines/transformers/_literal_parameterizer.py +0 -1247
  172. sqlspec/statement/pipelines/transformers/_remove_comments_and_hints.py +0 -76
  173. sqlspec/statement/pipelines/validators/__init__.py +0 -23
  174. sqlspec/statement/pipelines/validators/_dml_safety.py +0 -290
  175. sqlspec/statement/pipelines/validators/_parameter_style.py +0 -370
  176. sqlspec/statement/pipelines/validators/_performance.py +0 -718
  177. sqlspec/statement/pipelines/validators/_security.py +0 -967
  178. sqlspec/statement/result.py +0 -435
  179. sqlspec/statement/sql.py +0 -1704
  180. sqlspec/statement/sql_compiler.py +0 -140
  181. sqlspec/utils/cached_property.py +0 -25
  182. sqlspec-0.13.1.dist-info/RECORD +0 -150
  183. {sqlspec-0.13.1.dist-info → sqlspec-0.16.2.dist-info}/WHEEL +0 -0
  184. {sqlspec-0.13.1.dist-info → sqlspec-0.16.2.dist-info}/licenses/LICENSE +0 -0
  185. {sqlspec-0.13.1.dist-info → sqlspec-0.16.2.dist-info}/licenses/NOTICE +0 -0
@@ -1,512 +0,0 @@
1
- """Pipeline execution mixin for batch database operations.
2
-
3
- This module provides mixins that enable pipelined execution of SQL statements,
4
- allowing multiple operations to be sent to the database in a single network
5
- round-trip for improved performance.
6
-
7
- The implementation leverages native driver support where available (psycopg, asyncpg, oracledb)
8
- and provides high-quality simulated behavior for others.
9
- """
10
-
11
- from dataclasses import dataclass
12
- from typing import TYPE_CHECKING, Any, Optional, Union, cast
13
-
14
- from sqlspec.exceptions import PipelineExecutionError
15
- from sqlspec.statement.filters import StatementFilter
16
- from sqlspec.statement.result import SQLResult
17
- from sqlspec.statement.sql import SQL
18
- from sqlspec.utils.logging import get_logger
19
- from sqlspec.utils.type_guards import (
20
- is_async_pipeline_capable_driver,
21
- is_async_transaction_state_capable,
22
- is_sync_pipeline_capable_driver,
23
- is_sync_transaction_state_capable,
24
- )
25
-
26
- if TYPE_CHECKING:
27
- from typing import Literal
28
-
29
- from sqlspec.config import DriverT
30
- from sqlspec.driver import AsyncDriverAdapterProtocol, SyncDriverAdapterProtocol
31
- from sqlspec.typing import StatementParameters
32
-
33
- __all__ = (
34
- "AsyncPipeline",
35
- "AsyncPipelinedExecutionMixin",
36
- "Pipeline",
37
- "PipelineOperation",
38
- "SyncPipelinedExecutionMixin",
39
- )
40
-
41
- logger = get_logger(__name__)
42
-
43
-
44
- @dataclass
45
- class PipelineOperation:
46
- """Container for a queued pipeline operation."""
47
-
48
- sql: SQL
49
- operation_type: "Literal['execute', 'execute_many', 'execute_script', 'select']"
50
- filters: "Optional[list[StatementFilter]]" = None
51
- original_params: "Optional[Any]" = None
52
-
53
-
54
- class SyncPipelinedExecutionMixin:
55
- """Mixin providing pipeline execution for sync drivers."""
56
-
57
- __slots__ = ()
58
-
59
- def pipeline(
60
- self,
61
- *,
62
- isolation_level: "Optional[str]" = None,
63
- continue_on_error: bool = False,
64
- max_operations: int = 1000,
65
- **options: Any,
66
- ) -> "Pipeline":
67
- """Create a new pipeline for batch operations.
68
-
69
- Args:
70
- isolation_level: Transaction isolation level
71
- continue_on_error: Continue processing after errors
72
- max_operations: Maximum operations before auto-flush
73
- **options: Driver-specific pipeline options
74
-
75
- Returns:
76
- A new Pipeline instance for queuing operations
77
- """
78
- return Pipeline(
79
- driver=cast("SyncDriverAdapterProtocol[Any, Any]", self),
80
- isolation_level=isolation_level,
81
- continue_on_error=continue_on_error,
82
- max_operations=max_operations,
83
- options=options,
84
- )
85
-
86
-
87
- class AsyncPipelinedExecutionMixin:
88
- """Async version of pipeline execution mixin."""
89
-
90
- __slots__ = ()
91
-
92
- def pipeline(
93
- self,
94
- *,
95
- isolation_level: "Optional[str]" = None,
96
- continue_on_error: bool = False,
97
- max_operations: int = 1000,
98
- **options: Any,
99
- ) -> "AsyncPipeline":
100
- """Create a new async pipeline for batch operations."""
101
- return AsyncPipeline(
102
- driver=cast("AsyncDriverAdapterProtocol[Any, Any]", self),
103
- isolation_level=isolation_level,
104
- continue_on_error=continue_on_error,
105
- max_operations=max_operations,
106
- options=options,
107
- )
108
-
109
-
110
- class Pipeline:
111
- """Synchronous pipeline with enhanced parameter handling."""
112
-
113
- def __init__(
114
- self,
115
- driver: "DriverT", # pyright: ignore
116
- isolation_level: "Optional[str]" = None,
117
- continue_on_error: bool = False,
118
- max_operations: int = 1000,
119
- options: "Optional[dict[str, Any]]" = None,
120
- ) -> None:
121
- self.driver = driver
122
- self.isolation_level = isolation_level
123
- self.continue_on_error = continue_on_error
124
- self.max_operations = max_operations
125
- self.options = options or {}
126
- self._operations: list[PipelineOperation] = []
127
- self._results: Optional[list[SQLResult[Any]]] = None
128
- self._simulation_logged = False
129
-
130
- def add_execute(
131
- self, statement: "Union[str, SQL]", /, *parameters: "Union[StatementParameters, StatementFilter]", **kwargs: Any
132
- ) -> "Pipeline":
133
- """Add an execute operation to the pipeline.
134
-
135
- Args:
136
- statement: SQL statement to execute
137
- *parameters: Mixed positional args containing parameters and filters
138
- **kwargs: Named parameters
139
-
140
- Returns:
141
- Self for fluent API
142
- """
143
- self._operations.append(
144
- PipelineOperation(
145
- sql=SQL(statement, parameters=parameters or None, config=self.driver.config, **kwargs),
146
- operation_type="execute",
147
- )
148
- )
149
-
150
- if len(self._operations) >= self.max_operations:
151
- logger.warning("Pipeline auto-flushing at %s operations", len(self._operations))
152
- self.process()
153
-
154
- return self
155
-
156
- def add_select(
157
- self, statement: "Union[str, SQL]", /, *parameters: "Union[StatementParameters, StatementFilter]", **kwargs: Any
158
- ) -> "Pipeline":
159
- """Add a select operation to the pipeline."""
160
- self._operations.append(
161
- PipelineOperation(
162
- sql=SQL(statement, parameters=parameters or None, config=self.driver.config, **kwargs),
163
- operation_type="select",
164
- )
165
- )
166
- return self
167
-
168
- def add_execute_many(
169
- self, statement: "Union[str, SQL]", /, *parameters: "Union[StatementParameters, StatementFilter]", **kwargs: Any
170
- ) -> "Pipeline":
171
- """Add batch execution preserving parameter types.
172
-
173
- Args:
174
- statement: SQL statement to execute multiple times
175
- *parameters: First arg should be batch data (list of param sets),
176
- followed by optional StatementFilter instances
177
- **kwargs: Not typically used for execute_many
178
- """
179
- # First parameter should be the batch data
180
- if not parameters or not isinstance(parameters[0], (list, tuple)):
181
- msg = "execute_many requires a sequence of parameter sets as first parameter"
182
- raise ValueError(msg)
183
-
184
- batch_params = parameters[0]
185
- if isinstance(batch_params, tuple):
186
- batch_params = list(batch_params)
187
- sql_obj = SQL(
188
- statement, parameters=parameters[1:] if len(parameters) > 1 else None, config=self.driver.config, **kwargs
189
- ).as_many(batch_params)
190
-
191
- self._operations.append(PipelineOperation(sql=sql_obj, operation_type="execute_many"))
192
- return self
193
-
194
- def add_execute_script(self, script: "Union[str, SQL]", *filters: StatementFilter, **kwargs: Any) -> "Pipeline":
195
- """Add a multi-statement script to the pipeline."""
196
- if isinstance(script, SQL):
197
- sql_obj = script.as_script()
198
- else:
199
- sql_obj = SQL(script, parameters=filters or None, config=self.driver.config, **kwargs).as_script()
200
-
201
- self._operations.append(PipelineOperation(sql=sql_obj, operation_type="execute_script"))
202
- return self
203
-
204
- def process(self, filters: "Optional[list[StatementFilter]]" = None) -> "list[SQLResult]":
205
- """Execute all queued operations.
206
-
207
- Args:
208
- filters: Global filters to apply to all operations
209
-
210
- Returns:
211
- List of results from all operations
212
- """
213
- if not self._operations:
214
- return []
215
-
216
- if filters:
217
- self._apply_global_filters(filters)
218
-
219
- if is_sync_pipeline_capable_driver(self.driver):
220
- results = self.driver._execute_pipeline_native(self._operations, **self.options)
221
- else:
222
- results = self._execute_pipeline_simulated()
223
-
224
- self._results = results
225
- self._operations.clear()
226
- return cast("list[SQLResult]", results)
227
-
228
- def _execute_pipeline_simulated(self) -> "list[SQLResult]":
229
- """Enhanced simulation with transaction support and error handling."""
230
- results: list[SQLResult[Any]] = []
231
- connection = None
232
- auto_transaction = False
233
-
234
- if not self._simulation_logged:
235
- logger.info(
236
- "%s using simulated pipeline. Native support: %s",
237
- self.driver.__class__.__name__,
238
- self._has_native_support(),
239
- )
240
- self._simulation_logged = True
241
-
242
- try:
243
- connection = self.driver._connection()
244
-
245
- if is_sync_transaction_state_capable(connection) and not connection.in_transaction():
246
- connection.begin()
247
- auto_transaction = True
248
-
249
- for i, op in enumerate(self._operations):
250
- self._execute_single_operation(i, op, results, connection, auto_transaction)
251
-
252
- # Commit if we started the transaction
253
- if auto_transaction and is_sync_transaction_state_capable(connection):
254
- connection.commit()
255
-
256
- except Exception as e:
257
- if connection and auto_transaction and is_sync_transaction_state_capable(connection):
258
- connection.rollback()
259
- if not isinstance(e, PipelineExecutionError):
260
- msg = f"Pipeline execution failed: {e}"
261
- raise PipelineExecutionError(msg) from e
262
- raise
263
-
264
- return results
265
-
266
- def _execute_single_operation(
267
- self, i: int, op: PipelineOperation, results: "list[SQLResult[Any]]", connection: Any, auto_transaction: bool
268
- ) -> None:
269
- """Execute a single pipeline operation with error handling."""
270
- try:
271
- # Execute based on operation type
272
- result: SQLResult[Any]
273
- if op.operation_type == "execute_script":
274
- result = cast("SQLResult[Any]", self.driver.execute_script(op.sql, _connection=connection))
275
- elif op.operation_type == "execute_many":
276
- result = cast("SQLResult[Any]", self.driver.execute_many(op.sql, _connection=connection))
277
- else:
278
- result = cast("SQLResult[Any]", self.driver.execute(op.sql, _connection=connection))
279
-
280
- result.operation_index = i
281
- result.pipeline_sql = op.sql
282
- results.append(result)
283
-
284
- except Exception as e:
285
- if self.continue_on_error:
286
- error_result = SQLResult(
287
- statement=op.sql, data=[], error=e, operation_index=i, parameters=op.sql.parameters
288
- )
289
- results.append(error_result)
290
- else:
291
- if auto_transaction and is_sync_transaction_state_capable(connection):
292
- connection.rollback()
293
- msg = f"Pipeline failed at operation {i}: {e}"
294
- raise PipelineExecutionError(
295
- msg, operation_index=i, partial_results=results, failed_operation=op
296
- ) from e
297
-
298
- def _apply_global_filters(self, filters: "list[StatementFilter]") -> None:
299
- """Apply global filters to all operations."""
300
- for operation in self._operations:
301
- if operation.filters is None:
302
- operation.filters = []
303
- operation.filters.extend(filters)
304
-
305
- def _apply_operation_filters(self, sql: SQL, filters: "list[StatementFilter]") -> SQL:
306
- """Apply filters to a SQL object."""
307
- result = sql
308
- for filter_obj in filters:
309
- result = filter_obj.append_to_statement(result)
310
- return result
311
-
312
- def _has_native_support(self) -> bool:
313
- """Check if driver has native pipeline support."""
314
- return is_sync_pipeline_capable_driver(self.driver)
315
-
316
- def _process_parameters(self, params: tuple[Any, ...]) -> tuple["list[StatementFilter]", "Optional[Any]"]:
317
- """Extract filters and parameters from mixed args.
318
-
319
- Returns:
320
- Tuple of (filters, parameters)
321
- """
322
- filters: list[StatementFilter] = []
323
- parameters: list[Any] = []
324
-
325
- for param in params:
326
- if isinstance(param, StatementFilter):
327
- filters.append(param)
328
- else:
329
- parameters.append(param)
330
-
331
- if not parameters:
332
- return filters, None
333
- if len(parameters) == 1:
334
- return filters, parameters[0]
335
- return filters, parameters
336
-
337
- @property
338
- def operations(self) -> "list[PipelineOperation]":
339
- """Get the current list of queued operations."""
340
- return self._operations.copy()
341
-
342
-
343
- class AsyncPipeline:
344
- """Asynchronous pipeline with identical structure to Pipeline."""
345
-
346
- def __init__(
347
- self,
348
- driver: "AsyncDriverAdapterProtocol[Any, Any]",
349
- isolation_level: "Optional[str]" = None,
350
- continue_on_error: bool = False,
351
- max_operations: int = 1000,
352
- options: "Optional[dict[str, Any]]" = None,
353
- ) -> None:
354
- self.driver = driver
355
- self.isolation_level = isolation_level
356
- self.continue_on_error = continue_on_error
357
- self.max_operations = max_operations
358
- self.options = options or {}
359
- self._operations: list[PipelineOperation] = []
360
- self._results: Optional[list[SQLResult[Any]]] = None
361
- self._simulation_logged = False
362
-
363
- async def add_execute(
364
- self, statement: "Union[str, SQL]", /, *parameters: "Union[StatementParameters, StatementFilter]", **kwargs: Any
365
- ) -> "AsyncPipeline":
366
- """Add an execute operation to the async pipeline."""
367
- self._operations.append(
368
- PipelineOperation(
369
- sql=SQL(statement, parameters=parameters or None, config=self.driver.config, **kwargs),
370
- operation_type="execute",
371
- )
372
- )
373
-
374
- if len(self._operations) >= self.max_operations:
375
- logger.warning("Async pipeline auto-flushing at %s operations", len(self._operations))
376
- await self.process()
377
-
378
- return self
379
-
380
- async def add_select(
381
- self, statement: "Union[str, SQL]", /, *parameters: "Union[StatementParameters, StatementFilter]", **kwargs: Any
382
- ) -> "AsyncPipeline":
383
- """Add a select operation to the async pipeline."""
384
- self._operations.append(
385
- PipelineOperation(
386
- sql=SQL(statement, parameters=parameters or None, config=self.driver.config, **kwargs),
387
- operation_type="select",
388
- )
389
- )
390
- return self
391
-
392
- async def add_execute_many(
393
- self, statement: "Union[str, SQL]", /, *parameters: "Union[StatementParameters, StatementFilter]", **kwargs: Any
394
- ) -> "AsyncPipeline":
395
- """Add batch execution to the async pipeline."""
396
- # First parameter should be the batch data
397
- if not parameters or not isinstance(parameters[0], (list, tuple)):
398
- msg = "execute_many requires a sequence of parameter sets as first parameter"
399
- raise ValueError(msg)
400
-
401
- batch_params = parameters[0]
402
- if isinstance(batch_params, tuple):
403
- batch_params = list(batch_params)
404
- sql_obj = SQL(
405
- statement, parameters=parameters[1:] if len(parameters) > 1 else None, config=self.driver.config, **kwargs
406
- ).as_many(batch_params)
407
-
408
- self._operations.append(PipelineOperation(sql=sql_obj, operation_type="execute_many"))
409
- return self
410
-
411
- async def add_execute_script(
412
- self, script: "Union[str, SQL]", *filters: StatementFilter, **kwargs: Any
413
- ) -> "AsyncPipeline":
414
- """Add a script to the async pipeline."""
415
- if isinstance(script, SQL):
416
- sql_obj = script.as_script()
417
- else:
418
- sql_obj = SQL(script, parameters=filters or None, config=self.driver.config, **kwargs).as_script()
419
-
420
- self._operations.append(PipelineOperation(sql=sql_obj, operation_type="execute_script"))
421
- return self
422
-
423
- async def process(self, filters: "Optional[list[StatementFilter]]" = None) -> "list[SQLResult]":
424
- """Execute all queued operations asynchronously."""
425
- if not self._operations:
426
- return []
427
-
428
- if is_async_pipeline_capable_driver(self.driver):
429
- results = await cast("Any", self.driver)._execute_pipeline_native(self._operations, **self.options)
430
- else:
431
- results = await self._execute_pipeline_simulated()
432
-
433
- self._results = results
434
- self._operations.clear()
435
- return cast("list[SQLResult]", results)
436
-
437
- async def _execute_pipeline_simulated(self) -> "list[SQLResult]":
438
- """Async version of simulated pipeline execution."""
439
- results: list[SQLResult[Any]] = []
440
- connection = None
441
- auto_transaction = False
442
-
443
- if not self._simulation_logged:
444
- logger.info(
445
- "%s using simulated async pipeline. Native support: %s",
446
- self.driver.__class__.__name__,
447
- self._has_native_support(),
448
- )
449
- self._simulation_logged = True
450
-
451
- try:
452
- connection = self.driver._connection()
453
-
454
- if is_async_transaction_state_capable(connection) and not connection.in_transaction():
455
- await connection.begin()
456
- auto_transaction = True
457
-
458
- for i, op in enumerate(self._operations):
459
- await self._execute_single_operation_async(i, op, results, connection, auto_transaction)
460
-
461
- if auto_transaction and is_async_transaction_state_capable(connection):
462
- await connection.commit()
463
-
464
- except Exception as e:
465
- if connection and auto_transaction and is_async_transaction_state_capable(connection):
466
- await connection.rollback()
467
- if not isinstance(e, PipelineExecutionError):
468
- msg = f"Async pipeline execution failed: {e}"
469
- raise PipelineExecutionError(msg) from e
470
- raise
471
-
472
- return results
473
-
474
- async def _execute_single_operation_async(
475
- self, i: int, op: PipelineOperation, results: "list[SQLResult[Any]]", connection: Any, auto_transaction: bool
476
- ) -> None:
477
- """Execute a single async pipeline operation with error handling."""
478
- try:
479
- result: SQLResult[Any]
480
- if op.operation_type == "execute_script":
481
- result = await self.driver.execute_script(op.sql, _connection=connection)
482
- elif op.operation_type == "execute_many":
483
- result = await self.driver.execute_many(op.sql, _connection=connection)
484
- else:
485
- result = await self.driver.execute(op.sql, _connection=connection)
486
-
487
- result.operation_index = i
488
- result.pipeline_sql = op.sql
489
- results.append(result)
490
-
491
- except Exception as e:
492
- if self.continue_on_error:
493
- error_result = SQLResult(
494
- statement=op.sql, data=[], error=e, operation_index=i, parameters=op.sql.parameters
495
- )
496
- results.append(error_result)
497
- else:
498
- if auto_transaction and is_async_transaction_state_capable(connection):
499
- await connection.rollback()
500
- msg = f"Async pipeline failed at operation {i}: {e}"
501
- raise PipelineExecutionError(
502
- msg, operation_index=i, partial_results=results, failed_operation=op
503
- ) from e
504
-
505
- def _has_native_support(self) -> bool:
506
- """Check if driver has native pipeline support."""
507
- return is_async_pipeline_capable_driver(self.driver)
508
-
509
- @property
510
- def operations(self) -> "list[PipelineOperation]":
511
- """Get the current list of queued operations."""
512
- return self._operations.copy()
@@ -1,140 +0,0 @@
1
- """Result conversion utilities for unified storage architecture.
2
-
3
- This module contains the result conversion functionality integrated with the unified
4
- storage architecture.
5
- """
6
-
7
- import datetime
8
- from collections.abc import Sequence
9
- from enum import Enum
10
- from functools import partial
11
- from pathlib import Path, PurePath
12
- from typing import Any, Callable, Optional, Union, cast, overload
13
- from uuid import UUID
14
-
15
- from sqlspec.exceptions import SQLSpecError, wrap_exceptions
16
- from sqlspec.statement.result import OperationType
17
- from sqlspec.typing import ModelDTOT, ModelT, convert, get_type_adapter
18
- from sqlspec.utils.type_guards import is_dataclass, is_msgspec_struct, is_pydantic_model
19
-
20
- __all__ = ("_DEFAULT_TYPE_DECODERS", "ToSchemaMixin", "_default_msgspec_deserializer")
21
-
22
-
23
- _DEFAULT_TYPE_DECODERS: list[tuple[Callable[[Any], bool], Callable[[Any, Any], Any]]] = [
24
- (lambda x: x is UUID, lambda t, v: t(v.hex)),
25
- (lambda x: x is datetime.datetime, lambda t, v: t(v.isoformat())),
26
- (lambda x: x is datetime.date, lambda t, v: t(v.isoformat())),
27
- (lambda x: x is datetime.time, lambda t, v: t(v.isoformat())),
28
- (lambda x: x is Enum, lambda t, v: t(v.value)),
29
- ]
30
-
31
-
32
- def _default_msgspec_deserializer(
33
- target_type: Any, value: Any, type_decoders: "Optional[Sequence[tuple[Any, Any]]]" = None
34
- ) -> Any:
35
- """Default msgspec deserializer with type conversion support."""
36
- if type_decoders:
37
- for predicate, decoder in type_decoders:
38
- if predicate(target_type):
39
- return decoder(target_type, value)
40
- if target_type is UUID and isinstance(value, UUID):
41
- return value.hex
42
- if target_type in {datetime.datetime, datetime.date, datetime.time}:
43
- with wrap_exceptions(suppress=AttributeError):
44
- return value.isoformat()
45
- if isinstance(target_type, type) and issubclass(target_type, Enum) and isinstance(value, Enum):
46
- return value.value
47
- if isinstance(value, target_type):
48
- return value
49
- if issubclass(target_type, (Path, PurePath, UUID)):
50
- return target_type(value)
51
- return value
52
-
53
-
54
- class ToSchemaMixin:
55
- __slots__ = ()
56
-
57
- @staticmethod
58
- def _determine_operation_type(statement: "Any") -> OperationType:
59
- """Determine operation type from SQL statement expression.
60
-
61
- Args:
62
- statement: SQL statement object with expression attribute
63
-
64
- Returns:
65
- OperationType literal value
66
- """
67
- if not hasattr(statement, "expression") or not statement.expression:
68
- return "EXECUTE"
69
-
70
- expr_type = type(statement.expression).__name__.upper()
71
- if "INSERT" in expr_type:
72
- return "INSERT"
73
- if "UPDATE" in expr_type:
74
- return "UPDATE"
75
- if "DELETE" in expr_type:
76
- return "DELETE"
77
- if "SELECT" in expr_type:
78
- return "SELECT"
79
- return "EXECUTE"
80
-
81
- @overload
82
- @staticmethod
83
- def to_schema(data: "ModelT", *, schema_type: None = None) -> "ModelT": ...
84
- @overload
85
- @staticmethod
86
- def to_schema(data: "dict[str, Any]", *, schema_type: "type[ModelDTOT]") -> "ModelDTOT": ...
87
- @overload
88
- @staticmethod
89
- def to_schema(data: "Sequence[ModelT]", *, schema_type: None = None) -> "Sequence[ModelT]": ...
90
- @overload
91
- @staticmethod
92
- def to_schema(data: "Sequence[dict[str, Any]]", *, schema_type: "type[ModelDTOT]") -> "Sequence[ModelDTOT]": ...
93
-
94
- @staticmethod
95
- def to_schema(
96
- data: "Union[ModelT, dict[str, Any], Sequence[ModelT], Sequence[dict[str, Any]]]",
97
- *,
98
- schema_type: "Optional[type[ModelDTOT]]" = None,
99
- ) -> "Union[ModelT, ModelDTOT, Sequence[ModelT], Sequence[ModelDTOT]]":
100
- """Convert data to a specified schema type."""
101
- if schema_type is None:
102
- if not isinstance(data, Sequence):
103
- return cast("ModelT", data)
104
- return cast("Sequence[ModelT]", data)
105
- if is_dataclass(schema_type):
106
- if not isinstance(data, Sequence):
107
- return cast("ModelDTOT", schema_type(**data)) # type: ignore[operator]
108
- return cast("Sequence[ModelDTOT]", [schema_type(**item) for item in data]) # type: ignore[operator]
109
- if is_msgspec_struct(schema_type):
110
- if not isinstance(data, Sequence):
111
- return cast(
112
- "ModelDTOT",
113
- convert(
114
- obj=data,
115
- type=schema_type,
116
- from_attributes=True,
117
- dec_hook=partial(_default_msgspec_deserializer, type_decoders=_DEFAULT_TYPE_DECODERS),
118
- ),
119
- )
120
- return cast(
121
- "Sequence[ModelDTOT]",
122
- convert(
123
- obj=data,
124
- type=list[schema_type], # type: ignore[valid-type] # pyright: ignore
125
- from_attributes=True,
126
- dec_hook=partial(_default_msgspec_deserializer, type_decoders=_DEFAULT_TYPE_DECODERS),
127
- ),
128
- )
129
- if schema_type is not None and is_pydantic_model(schema_type):
130
- if not isinstance(data, Sequence):
131
- return cast(
132
- "ModelDTOT",
133
- get_type_adapter(schema_type).validate_python(data, from_attributes=True), # pyright: ignore
134
- )
135
- return cast(
136
- "Sequence[ModelDTOT]",
137
- get_type_adapter(list[schema_type]).validate_python(data, from_attributes=True), # type: ignore[valid-type] # pyright: ignore
138
- )
139
- msg = "`schema_type` should be a valid Dataclass, Pydantic model or Msgspec struct"
140
- raise SQLSpecError(msg)