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,508 +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
- def pipeline(
58
- self,
59
- *,
60
- isolation_level: "Optional[str]" = None,
61
- continue_on_error: bool = False,
62
- max_operations: int = 1000,
63
- **options: Any,
64
- ) -> "Pipeline":
65
- """Create a new pipeline for batch operations.
66
-
67
- Args:
68
- isolation_level: Transaction isolation level
69
- continue_on_error: Continue processing after errors
70
- max_operations: Maximum operations before auto-flush
71
- **options: Driver-specific pipeline options
72
-
73
- Returns:
74
- A new Pipeline instance for queuing operations
75
- """
76
- return Pipeline(
77
- driver=cast("SyncDriverAdapterProtocol[Any, Any]", self),
78
- isolation_level=isolation_level,
79
- continue_on_error=continue_on_error,
80
- max_operations=max_operations,
81
- options=options,
82
- )
83
-
84
-
85
- class AsyncPipelinedExecutionMixin:
86
- """Async version of pipeline execution mixin."""
87
-
88
- def pipeline(
89
- self,
90
- *,
91
- isolation_level: "Optional[str]" = None,
92
- continue_on_error: bool = False,
93
- max_operations: int = 1000,
94
- **options: Any,
95
- ) -> "AsyncPipeline":
96
- """Create a new async pipeline for batch operations."""
97
- return AsyncPipeline(
98
- driver=cast("AsyncDriverAdapterProtocol[Any, Any]", self),
99
- isolation_level=isolation_level,
100
- continue_on_error=continue_on_error,
101
- max_operations=max_operations,
102
- options=options,
103
- )
104
-
105
-
106
- class Pipeline:
107
- """Synchronous pipeline with enhanced parameter handling."""
108
-
109
- def __init__(
110
- self,
111
- driver: "DriverT", # pyright: ignore
112
- isolation_level: "Optional[str]" = None,
113
- continue_on_error: bool = False,
114
- max_operations: int = 1000,
115
- options: "Optional[dict[str, Any]]" = None,
116
- ) -> None:
117
- self.driver = driver
118
- self.isolation_level = isolation_level
119
- self.continue_on_error = continue_on_error
120
- self.max_operations = max_operations
121
- self.options = options or {}
122
- self._operations: list[PipelineOperation] = []
123
- self._results: Optional[list[SQLResult[Any]]] = None
124
- self._simulation_logged = False
125
-
126
- def add_execute(
127
- self, statement: "Union[str, SQL]", /, *parameters: "Union[StatementParameters, StatementFilter]", **kwargs: Any
128
- ) -> "Pipeline":
129
- """Add an execute operation to the pipeline.
130
-
131
- Args:
132
- statement: SQL statement to execute
133
- *parameters: Mixed positional args containing parameters and filters
134
- **kwargs: Named parameters
135
-
136
- Returns:
137
- Self for fluent API
138
- """
139
- self._operations.append(
140
- PipelineOperation(
141
- sql=SQL(statement, parameters=parameters or None, config=self.driver.config, **kwargs),
142
- operation_type="execute",
143
- )
144
- )
145
-
146
- if len(self._operations) >= self.max_operations:
147
- logger.warning("Pipeline auto-flushing at %s operations", len(self._operations))
148
- self.process()
149
-
150
- return self
151
-
152
- def add_select(
153
- self, statement: "Union[str, SQL]", /, *parameters: "Union[StatementParameters, StatementFilter]", **kwargs: Any
154
- ) -> "Pipeline":
155
- """Add a select operation to the pipeline."""
156
- self._operations.append(
157
- PipelineOperation(
158
- sql=SQL(statement, parameters=parameters or None, config=self.driver.config, **kwargs),
159
- operation_type="select",
160
- )
161
- )
162
- return self
163
-
164
- def add_execute_many(
165
- self, statement: "Union[str, SQL]", /, *parameters: "Union[StatementParameters, StatementFilter]", **kwargs: Any
166
- ) -> "Pipeline":
167
- """Add batch execution preserving parameter types.
168
-
169
- Args:
170
- statement: SQL statement to execute multiple times
171
- *parameters: First arg should be batch data (list of param sets),
172
- followed by optional StatementFilter instances
173
- **kwargs: Not typically used for execute_many
174
- """
175
- # First parameter should be the batch data
176
- if not parameters or not isinstance(parameters[0], (list, tuple)):
177
- msg = "execute_many requires a sequence of parameter sets as first parameter"
178
- raise ValueError(msg)
179
-
180
- batch_params = parameters[0]
181
- if isinstance(batch_params, tuple):
182
- batch_params = list(batch_params)
183
- sql_obj = SQL(
184
- statement, parameters=parameters[1:] if len(parameters) > 1 else None, config=self.driver.config, **kwargs
185
- ).as_many(batch_params)
186
-
187
- self._operations.append(PipelineOperation(sql=sql_obj, operation_type="execute_many"))
188
- return self
189
-
190
- def add_execute_script(self, script: "Union[str, SQL]", *filters: StatementFilter, **kwargs: Any) -> "Pipeline":
191
- """Add a multi-statement script to the pipeline."""
192
- if isinstance(script, SQL):
193
- sql_obj = script.as_script()
194
- else:
195
- sql_obj = SQL(script, parameters=filters or None, config=self.driver.config, **kwargs).as_script()
196
-
197
- self._operations.append(PipelineOperation(sql=sql_obj, operation_type="execute_script"))
198
- return self
199
-
200
- def process(self, filters: "Optional[list[StatementFilter]]" = None) -> "list[SQLResult]":
201
- """Execute all queued operations.
202
-
203
- Args:
204
- filters: Global filters to apply to all operations
205
-
206
- Returns:
207
- List of results from all operations
208
- """
209
- if not self._operations:
210
- return []
211
-
212
- if filters:
213
- self._apply_global_filters(filters)
214
-
215
- if is_sync_pipeline_capable_driver(self.driver):
216
- results = self.driver._execute_pipeline_native(self._operations, **self.options)
217
- else:
218
- results = self._execute_pipeline_simulated()
219
-
220
- self._results = results
221
- self._operations.clear()
222
- return cast("list[SQLResult]", results)
223
-
224
- def _execute_pipeline_simulated(self) -> "list[SQLResult]":
225
- """Enhanced simulation with transaction support and error handling."""
226
- results: list[SQLResult[Any]] = []
227
- connection = None
228
- auto_transaction = False
229
-
230
- if not self._simulation_logged:
231
- logger.info(
232
- "%s using simulated pipeline. Native support: %s",
233
- self.driver.__class__.__name__,
234
- self._has_native_support(),
235
- )
236
- self._simulation_logged = True
237
-
238
- try:
239
- connection = self.driver._connection()
240
-
241
- if is_sync_transaction_state_capable(connection) and not connection.in_transaction():
242
- connection.begin()
243
- auto_transaction = True
244
-
245
- for i, op in enumerate(self._operations):
246
- self._execute_single_operation(i, op, results, connection, auto_transaction)
247
-
248
- # Commit if we started the transaction
249
- if auto_transaction and is_sync_transaction_state_capable(connection):
250
- connection.commit()
251
-
252
- except Exception as e:
253
- if connection and auto_transaction and is_sync_transaction_state_capable(connection):
254
- connection.rollback()
255
- if not isinstance(e, PipelineExecutionError):
256
- msg = f"Pipeline execution failed: {e}"
257
- raise PipelineExecutionError(msg) from e
258
- raise
259
-
260
- return results
261
-
262
- def _execute_single_operation(
263
- self, i: int, op: PipelineOperation, results: "list[SQLResult[Any]]", connection: Any, auto_transaction: bool
264
- ) -> None:
265
- """Execute a single pipeline operation with error handling."""
266
- try:
267
- # Execute based on operation type
268
- result: SQLResult[Any]
269
- if op.operation_type == "execute_script":
270
- result = cast("SQLResult[Any]", self.driver.execute_script(op.sql, _connection=connection))
271
- elif op.operation_type == "execute_many":
272
- result = cast("SQLResult[Any]", self.driver.execute_many(op.sql, _connection=connection))
273
- else:
274
- result = cast("SQLResult[Any]", self.driver.execute(op.sql, _connection=connection))
275
-
276
- result.operation_index = i
277
- result.pipeline_sql = op.sql
278
- results.append(result)
279
-
280
- except Exception as e:
281
- if self.continue_on_error:
282
- error_result = SQLResult(
283
- statement=op.sql, data=[], error=e, operation_index=i, parameters=op.sql.parameters
284
- )
285
- results.append(error_result)
286
- else:
287
- if auto_transaction and is_sync_transaction_state_capable(connection):
288
- connection.rollback()
289
- msg = f"Pipeline failed at operation {i}: {e}"
290
- raise PipelineExecutionError(
291
- msg, operation_index=i, partial_results=results, failed_operation=op
292
- ) from e
293
-
294
- def _apply_global_filters(self, filters: "list[StatementFilter]") -> None:
295
- """Apply global filters to all operations."""
296
- for operation in self._operations:
297
- if operation.filters is None:
298
- operation.filters = []
299
- operation.filters.extend(filters)
300
-
301
- def _apply_operation_filters(self, sql: SQL, filters: "list[StatementFilter]") -> SQL:
302
- """Apply filters to a SQL object."""
303
- result = sql
304
- for filter_obj in filters:
305
- result = filter_obj.append_to_statement(result)
306
- return result
307
-
308
- def _has_native_support(self) -> bool:
309
- """Check if driver has native pipeline support."""
310
- return is_sync_pipeline_capable_driver(self.driver)
311
-
312
- def _process_parameters(self, params: tuple[Any, ...]) -> tuple["list[StatementFilter]", "Optional[Any]"]:
313
- """Extract filters and parameters from mixed args.
314
-
315
- Returns:
316
- Tuple of (filters, parameters)
317
- """
318
- filters: list[StatementFilter] = []
319
- parameters: list[Any] = []
320
-
321
- for param in params:
322
- if isinstance(param, StatementFilter):
323
- filters.append(param)
324
- else:
325
- parameters.append(param)
326
-
327
- if not parameters:
328
- return filters, None
329
- if len(parameters) == 1:
330
- return filters, parameters[0]
331
- return filters, parameters
332
-
333
- @property
334
- def operations(self) -> "list[PipelineOperation]":
335
- """Get the current list of queued operations."""
336
- return self._operations.copy()
337
-
338
-
339
- class AsyncPipeline:
340
- """Asynchronous pipeline with identical structure to Pipeline."""
341
-
342
- def __init__(
343
- self,
344
- driver: "AsyncDriverAdapterProtocol[Any, Any]",
345
- isolation_level: "Optional[str]" = None,
346
- continue_on_error: bool = False,
347
- max_operations: int = 1000,
348
- options: "Optional[dict[str, Any]]" = None,
349
- ) -> None:
350
- self.driver = driver
351
- self.isolation_level = isolation_level
352
- self.continue_on_error = continue_on_error
353
- self.max_operations = max_operations
354
- self.options = options or {}
355
- self._operations: list[PipelineOperation] = []
356
- self._results: Optional[list[SQLResult[Any]]] = None
357
- self._simulation_logged = False
358
-
359
- async def add_execute(
360
- self, statement: "Union[str, SQL]", /, *parameters: "Union[StatementParameters, StatementFilter]", **kwargs: Any
361
- ) -> "AsyncPipeline":
362
- """Add an execute operation to the async pipeline."""
363
- self._operations.append(
364
- PipelineOperation(
365
- sql=SQL(statement, parameters=parameters or None, config=self.driver.config, **kwargs),
366
- operation_type="execute",
367
- )
368
- )
369
-
370
- if len(self._operations) >= self.max_operations:
371
- logger.warning("Async pipeline auto-flushing at %s operations", len(self._operations))
372
- await self.process()
373
-
374
- return self
375
-
376
- async def add_select(
377
- self, statement: "Union[str, SQL]", /, *parameters: "Union[StatementParameters, StatementFilter]", **kwargs: Any
378
- ) -> "AsyncPipeline":
379
- """Add a select operation to the async pipeline."""
380
- self._operations.append(
381
- PipelineOperation(
382
- sql=SQL(statement, parameters=parameters or None, config=self.driver.config, **kwargs),
383
- operation_type="select",
384
- )
385
- )
386
- return self
387
-
388
- async def add_execute_many(
389
- self, statement: "Union[str, SQL]", /, *parameters: "Union[StatementParameters, StatementFilter]", **kwargs: Any
390
- ) -> "AsyncPipeline":
391
- """Add batch execution to the async pipeline."""
392
- # First parameter should be the batch data
393
- if not parameters or not isinstance(parameters[0], (list, tuple)):
394
- msg = "execute_many requires a sequence of parameter sets as first parameter"
395
- raise ValueError(msg)
396
-
397
- batch_params = parameters[0]
398
- if isinstance(batch_params, tuple):
399
- batch_params = list(batch_params)
400
- sql_obj = SQL(
401
- statement, parameters=parameters[1:] if len(parameters) > 1 else None, config=self.driver.config, **kwargs
402
- ).as_many(batch_params)
403
-
404
- self._operations.append(PipelineOperation(sql=sql_obj, operation_type="execute_many"))
405
- return self
406
-
407
- async def add_execute_script(
408
- self, script: "Union[str, SQL]", *filters: StatementFilter, **kwargs: Any
409
- ) -> "AsyncPipeline":
410
- """Add a script to the async pipeline."""
411
- if isinstance(script, SQL):
412
- sql_obj = script.as_script()
413
- else:
414
- sql_obj = SQL(script, parameters=filters or None, config=self.driver.config, **kwargs).as_script()
415
-
416
- self._operations.append(PipelineOperation(sql=sql_obj, operation_type="execute_script"))
417
- return self
418
-
419
- async def process(self, filters: "Optional[list[StatementFilter]]" = None) -> "list[SQLResult]":
420
- """Execute all queued operations asynchronously."""
421
- if not self._operations:
422
- return []
423
-
424
- if is_async_pipeline_capable_driver(self.driver):
425
- results = await cast("Any", self.driver)._execute_pipeline_native(self._operations, **self.options)
426
- else:
427
- results = await self._execute_pipeline_simulated()
428
-
429
- self._results = results
430
- self._operations.clear()
431
- return cast("list[SQLResult]", results)
432
-
433
- async def _execute_pipeline_simulated(self) -> "list[SQLResult]":
434
- """Async version of simulated pipeline execution."""
435
- results: list[SQLResult[Any]] = []
436
- connection = None
437
- auto_transaction = False
438
-
439
- if not self._simulation_logged:
440
- logger.info(
441
- "%s using simulated async pipeline. Native support: %s",
442
- self.driver.__class__.__name__,
443
- self._has_native_support(),
444
- )
445
- self._simulation_logged = True
446
-
447
- try:
448
- connection = self.driver._connection()
449
-
450
- if is_async_transaction_state_capable(connection) and not connection.in_transaction():
451
- await connection.begin()
452
- auto_transaction = True
453
-
454
- for i, op in enumerate(self._operations):
455
- await self._execute_single_operation_async(i, op, results, connection, auto_transaction)
456
-
457
- if auto_transaction and is_async_transaction_state_capable(connection):
458
- await connection.commit()
459
-
460
- except Exception as e:
461
- if connection and auto_transaction and is_async_transaction_state_capable(connection):
462
- await connection.rollback()
463
- if not isinstance(e, PipelineExecutionError):
464
- msg = f"Async pipeline execution failed: {e}"
465
- raise PipelineExecutionError(msg) from e
466
- raise
467
-
468
- return results
469
-
470
- async def _execute_single_operation_async(
471
- self, i: int, op: PipelineOperation, results: "list[SQLResult[Any]]", connection: Any, auto_transaction: bool
472
- ) -> None:
473
- """Execute a single async pipeline operation with error handling."""
474
- try:
475
- result: SQLResult[Any]
476
- if op.operation_type == "execute_script":
477
- result = await self.driver.execute_script(op.sql, _connection=connection)
478
- elif op.operation_type == "execute_many":
479
- result = await self.driver.execute_many(op.sql, _connection=connection)
480
- else:
481
- result = await self.driver.execute(op.sql, _connection=connection)
482
-
483
- result.operation_index = i
484
- result.pipeline_sql = op.sql
485
- results.append(result)
486
-
487
- except Exception as e:
488
- if self.continue_on_error:
489
- error_result = SQLResult(
490
- statement=op.sql, data=[], error=e, operation_index=i, parameters=op.sql.parameters
491
- )
492
- results.append(error_result)
493
- else:
494
- if auto_transaction and is_async_transaction_state_capable(connection):
495
- await connection.rollback()
496
- msg = f"Async pipeline failed at operation {i}: {e}"
497
- raise PipelineExecutionError(
498
- msg, operation_index=i, partial_results=results, failed_operation=op
499
- ) from e
500
-
501
- def _has_native_support(self) -> bool:
502
- """Check if driver has native pipeline support."""
503
- return is_async_pipeline_capable_driver(self.driver)
504
-
505
- @property
506
- def operations(self) -> "list[PipelineOperation]":
507
- """Get the current list of queued operations."""
508
- return self._operations.copy()