sqlspec 0.14.1__py3-none-any.whl → 0.16.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 (159) hide show
  1. sqlspec/__init__.py +50 -25
  2. sqlspec/__main__.py +1 -1
  3. sqlspec/__metadata__.py +1 -3
  4. sqlspec/_serialization.py +1 -2
  5. sqlspec/_sql.py +480 -121
  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 -260
  10. sqlspec/adapters/adbc/driver.py +462 -367
  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 +18 -65
  55. sqlspec/builder/_merge.py +56 -0
  56. sqlspec/{statement/builder → builder}/_parsing_utils.py +8 -11
  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 +34 -18
  62. sqlspec/{statement/builder → builder}/mixins/_join_operations.py +1 -3
  63. sqlspec/{statement/builder → builder}/mixins/_merge_operations.py +19 -9
  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 +25 -38
  67. sqlspec/{statement/builder → builder}/mixins/_update_operations.py +15 -16
  68. sqlspec/{statement/builder → builder}/mixins/_where_clause.py +210 -137
  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 +830 -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 +666 -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 +164 -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/cli.py +1 -1
  90. sqlspec/extensions/litestar/config.py +0 -1
  91. sqlspec/extensions/litestar/handlers.py +15 -26
  92. sqlspec/extensions/litestar/plugin.py +18 -16
  93. sqlspec/extensions/litestar/providers.py +17 -52
  94. sqlspec/loader.py +424 -105
  95. sqlspec/migrations/__init__.py +12 -0
  96. sqlspec/migrations/base.py +92 -68
  97. sqlspec/migrations/commands.py +24 -106
  98. sqlspec/migrations/loaders.py +402 -0
  99. sqlspec/migrations/runner.py +49 -51
  100. sqlspec/migrations/tracker.py +31 -44
  101. sqlspec/migrations/utils.py +64 -24
  102. sqlspec/protocols.py +7 -183
  103. sqlspec/storage/__init__.py +1 -1
  104. sqlspec/storage/backends/base.py +37 -40
  105. sqlspec/storage/backends/fsspec.py +136 -112
  106. sqlspec/storage/backends/obstore.py +138 -160
  107. sqlspec/storage/capabilities.py +5 -4
  108. sqlspec/storage/registry.py +57 -106
  109. sqlspec/typing.py +136 -115
  110. sqlspec/utils/__init__.py +2 -3
  111. sqlspec/utils/correlation.py +0 -3
  112. sqlspec/utils/deprecation.py +6 -6
  113. sqlspec/utils/fixtures.py +6 -6
  114. sqlspec/utils/logging.py +0 -2
  115. sqlspec/utils/module_loader.py +7 -12
  116. sqlspec/utils/singleton.py +0 -1
  117. sqlspec/utils/sync_tools.py +17 -38
  118. sqlspec/utils/text.py +12 -51
  119. sqlspec/utils/type_guards.py +443 -232
  120. {sqlspec-0.14.1.dist-info → sqlspec-0.16.0.dist-info}/METADATA +7 -2
  121. sqlspec-0.16.0.dist-info/RECORD +134 -0
  122. sqlspec/adapters/adbc/transformers.py +0 -108
  123. sqlspec/driver/connection.py +0 -207
  124. sqlspec/driver/mixins/_cache.py +0 -114
  125. sqlspec/driver/mixins/_csv_writer.py +0 -91
  126. sqlspec/driver/mixins/_pipeline.py +0 -508
  127. sqlspec/driver/mixins/_query_tools.py +0 -796
  128. sqlspec/driver/mixins/_result_utils.py +0 -138
  129. sqlspec/driver/mixins/_storage.py +0 -912
  130. sqlspec/driver/mixins/_type_coercion.py +0 -128
  131. sqlspec/driver/parameters.py +0 -138
  132. sqlspec/statement/__init__.py +0 -21
  133. sqlspec/statement/builder/_merge.py +0 -95
  134. sqlspec/statement/cache.py +0 -50
  135. sqlspec/statement/filters.py +0 -625
  136. sqlspec/statement/parameters.py +0 -956
  137. sqlspec/statement/pipelines/__init__.py +0 -210
  138. sqlspec/statement/pipelines/analyzers/__init__.py +0 -9
  139. sqlspec/statement/pipelines/analyzers/_analyzer.py +0 -646
  140. sqlspec/statement/pipelines/context.py +0 -109
  141. sqlspec/statement/pipelines/transformers/__init__.py +0 -7
  142. sqlspec/statement/pipelines/transformers/_expression_simplifier.py +0 -88
  143. sqlspec/statement/pipelines/transformers/_literal_parameterizer.py +0 -1247
  144. sqlspec/statement/pipelines/transformers/_remove_comments_and_hints.py +0 -76
  145. sqlspec/statement/pipelines/validators/__init__.py +0 -23
  146. sqlspec/statement/pipelines/validators/_dml_safety.py +0 -290
  147. sqlspec/statement/pipelines/validators/_parameter_style.py +0 -370
  148. sqlspec/statement/pipelines/validators/_performance.py +0 -714
  149. sqlspec/statement/pipelines/validators/_security.py +0 -967
  150. sqlspec/statement/result.py +0 -435
  151. sqlspec/statement/sql.py +0 -1774
  152. sqlspec/utils/cached_property.py +0 -25
  153. sqlspec/utils/statement_hashing.py +0 -203
  154. sqlspec-0.14.1.dist-info/RECORD +0 -145
  155. /sqlspec/{statement/builder → builder}/mixins/_delete_operations.py +0 -0
  156. {sqlspec-0.14.1.dist-info → sqlspec-0.16.0.dist-info}/WHEEL +0 -0
  157. {sqlspec-0.14.1.dist-info → sqlspec-0.16.0.dist-info}/entry_points.txt +0 -0
  158. {sqlspec-0.14.1.dist-info → sqlspec-0.16.0.dist-info}/licenses/LICENSE +0 -0
  159. {sqlspec-0.14.1.dist-info → sqlspec-0.16.0.dist-info}/licenses/NOTICE +0 -0
@@ -1,91 +0,0 @@
1
- """Optimized CSV writing utilities."""
2
-
3
- import csv
4
- from typing import TYPE_CHECKING, Any
5
-
6
- from sqlspec.typing import PYARROW_INSTALLED
7
-
8
- if TYPE_CHECKING:
9
- from sqlspec.statement.result import SQLResult
10
-
11
- __all__ = ("write_csv", "write_csv_default", "write_csv_optimized")
12
-
13
-
14
- def _raise_no_column_names_error() -> None:
15
- """Raise error when no column names are available."""
16
- msg = "No column names available"
17
- raise ValueError(msg)
18
-
19
-
20
- def write_csv(result: "SQLResult", file: Any, **options: Any) -> None:
21
- """Write result to CSV file.
22
-
23
- Args:
24
- result: SQL result to write
25
- file: File-like object to write to
26
- **options: CSV writer options
27
- """
28
- if PYARROW_INSTALLED:
29
- try:
30
- write_csv_optimized(result, file, **options)
31
- except Exception:
32
- write_csv_default(result, file, **options)
33
- else:
34
- write_csv_default(result, file, **options)
35
-
36
-
37
- def write_csv_default(result: "SQLResult", file: Any, **options: Any) -> None:
38
- """Write result to CSV file using default method.
39
-
40
- Args:
41
- result: SQL result to write
42
- file: File-like object to write to
43
- **options: CSV writer options
44
- """
45
- csv_options = options.copy()
46
- csv_options.pop("compression", None)
47
- csv_options.pop("partition_by", None)
48
-
49
- writer = csv.writer(file, **csv_options)
50
- if result.column_names:
51
- writer.writerow(result.column_names)
52
- if result.data:
53
- if result.data and isinstance(result.data[0], dict):
54
- rows = []
55
- for row_dict in result.data:
56
- row_values = [row_dict.get(col) for col in result.column_names or []]
57
- rows.append(row_values)
58
- writer.writerows(rows)
59
- else:
60
- writer.writerows(result.data)
61
-
62
-
63
- def write_csv_optimized(result: "SQLResult", file: Any, **options: Any) -> None:
64
- """Write result to CSV using PyArrow if available for better performance.
65
-
66
- Args:
67
- result: SQL result to write
68
- file: File-like object to write to
69
- **options: CSV writer options
70
- """
71
- _ = options
72
- import pyarrow as pa
73
- import pyarrow.csv as pa_csv
74
-
75
- if not result.data:
76
- return
77
-
78
- if not hasattr(file, "name"):
79
- msg = "PyArrow CSV writer requires a file with a 'name' attribute"
80
- raise ValueError(msg)
81
-
82
- table: Any
83
- if isinstance(result.data[0], dict):
84
- table = pa.Table.from_pylist(result.data)
85
- elif result.column_names:
86
- data_dicts = [dict(zip(result.column_names, row)) for row in result.data]
87
- table = pa.Table.from_pylist(data_dicts)
88
- else:
89
- _raise_no_column_names_error()
90
-
91
- pa_csv.write_csv(table, file.name) # pyright: ignore
@@ -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()