sqlspec 0.11.1__py3-none-any.whl → 0.12.1__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 (155) hide show
  1. sqlspec/__init__.py +16 -3
  2. sqlspec/_serialization.py +3 -10
  3. sqlspec/_sql.py +1147 -0
  4. sqlspec/_typing.py +343 -41
  5. sqlspec/adapters/adbc/__init__.py +2 -6
  6. sqlspec/adapters/adbc/config.py +474 -149
  7. sqlspec/adapters/adbc/driver.py +330 -621
  8. sqlspec/adapters/aiosqlite/__init__.py +2 -6
  9. sqlspec/adapters/aiosqlite/config.py +143 -57
  10. sqlspec/adapters/aiosqlite/driver.py +269 -431
  11. sqlspec/adapters/asyncmy/__init__.py +3 -8
  12. sqlspec/adapters/asyncmy/config.py +247 -202
  13. sqlspec/adapters/asyncmy/driver.py +218 -436
  14. sqlspec/adapters/asyncpg/__init__.py +4 -7
  15. sqlspec/adapters/asyncpg/config.py +329 -176
  16. sqlspec/adapters/asyncpg/driver.py +417 -487
  17. sqlspec/adapters/bigquery/__init__.py +2 -2
  18. sqlspec/adapters/bigquery/config.py +407 -0
  19. sqlspec/adapters/bigquery/driver.py +600 -553
  20. sqlspec/adapters/duckdb/__init__.py +4 -1
  21. sqlspec/adapters/duckdb/config.py +432 -321
  22. sqlspec/adapters/duckdb/driver.py +392 -406
  23. sqlspec/adapters/oracledb/__init__.py +3 -8
  24. sqlspec/adapters/oracledb/config.py +625 -0
  25. sqlspec/adapters/oracledb/driver.py +548 -921
  26. sqlspec/adapters/psqlpy/__init__.py +4 -7
  27. sqlspec/adapters/psqlpy/config.py +372 -203
  28. sqlspec/adapters/psqlpy/driver.py +197 -533
  29. sqlspec/adapters/psycopg/__init__.py +3 -8
  30. sqlspec/adapters/psycopg/config.py +725 -0
  31. sqlspec/adapters/psycopg/driver.py +734 -694
  32. sqlspec/adapters/sqlite/__init__.py +2 -6
  33. sqlspec/adapters/sqlite/config.py +146 -81
  34. sqlspec/adapters/sqlite/driver.py +242 -405
  35. sqlspec/base.py +220 -784
  36. sqlspec/config.py +354 -0
  37. sqlspec/driver/__init__.py +22 -0
  38. sqlspec/driver/_async.py +252 -0
  39. sqlspec/driver/_common.py +338 -0
  40. sqlspec/driver/_sync.py +261 -0
  41. sqlspec/driver/mixins/__init__.py +17 -0
  42. sqlspec/driver/mixins/_pipeline.py +523 -0
  43. sqlspec/driver/mixins/_result_utils.py +122 -0
  44. sqlspec/driver/mixins/_sql_translator.py +35 -0
  45. sqlspec/driver/mixins/_storage.py +993 -0
  46. sqlspec/driver/mixins/_type_coercion.py +131 -0
  47. sqlspec/exceptions.py +299 -7
  48. sqlspec/extensions/aiosql/__init__.py +10 -0
  49. sqlspec/extensions/aiosql/adapter.py +474 -0
  50. sqlspec/extensions/litestar/__init__.py +1 -6
  51. sqlspec/extensions/litestar/_utils.py +1 -5
  52. sqlspec/extensions/litestar/config.py +5 -6
  53. sqlspec/extensions/litestar/handlers.py +13 -12
  54. sqlspec/extensions/litestar/plugin.py +22 -24
  55. sqlspec/extensions/litestar/providers.py +37 -55
  56. sqlspec/loader.py +528 -0
  57. sqlspec/service/__init__.py +3 -0
  58. sqlspec/service/base.py +24 -0
  59. sqlspec/service/pagination.py +26 -0
  60. sqlspec/statement/__init__.py +21 -0
  61. sqlspec/statement/builder/__init__.py +54 -0
  62. sqlspec/statement/builder/_ddl_utils.py +119 -0
  63. sqlspec/statement/builder/_parsing_utils.py +135 -0
  64. sqlspec/statement/builder/base.py +328 -0
  65. sqlspec/statement/builder/ddl.py +1379 -0
  66. sqlspec/statement/builder/delete.py +80 -0
  67. sqlspec/statement/builder/insert.py +274 -0
  68. sqlspec/statement/builder/merge.py +95 -0
  69. sqlspec/statement/builder/mixins/__init__.py +65 -0
  70. sqlspec/statement/builder/mixins/_aggregate_functions.py +151 -0
  71. sqlspec/statement/builder/mixins/_case_builder.py +91 -0
  72. sqlspec/statement/builder/mixins/_common_table_expr.py +91 -0
  73. sqlspec/statement/builder/mixins/_delete_from.py +34 -0
  74. sqlspec/statement/builder/mixins/_from.py +61 -0
  75. sqlspec/statement/builder/mixins/_group_by.py +119 -0
  76. sqlspec/statement/builder/mixins/_having.py +35 -0
  77. sqlspec/statement/builder/mixins/_insert_from_select.py +48 -0
  78. sqlspec/statement/builder/mixins/_insert_into.py +36 -0
  79. sqlspec/statement/builder/mixins/_insert_values.py +69 -0
  80. sqlspec/statement/builder/mixins/_join.py +110 -0
  81. sqlspec/statement/builder/mixins/_limit_offset.py +53 -0
  82. sqlspec/statement/builder/mixins/_merge_clauses.py +405 -0
  83. sqlspec/statement/builder/mixins/_order_by.py +46 -0
  84. sqlspec/statement/builder/mixins/_pivot.py +82 -0
  85. sqlspec/statement/builder/mixins/_returning.py +37 -0
  86. sqlspec/statement/builder/mixins/_select_columns.py +60 -0
  87. sqlspec/statement/builder/mixins/_set_ops.py +122 -0
  88. sqlspec/statement/builder/mixins/_unpivot.py +80 -0
  89. sqlspec/statement/builder/mixins/_update_from.py +54 -0
  90. sqlspec/statement/builder/mixins/_update_set.py +91 -0
  91. sqlspec/statement/builder/mixins/_update_table.py +29 -0
  92. sqlspec/statement/builder/mixins/_where.py +374 -0
  93. sqlspec/statement/builder/mixins/_window_functions.py +86 -0
  94. sqlspec/statement/builder/protocols.py +20 -0
  95. sqlspec/statement/builder/select.py +206 -0
  96. sqlspec/statement/builder/update.py +178 -0
  97. sqlspec/statement/filters.py +571 -0
  98. sqlspec/statement/parameters.py +736 -0
  99. sqlspec/statement/pipelines/__init__.py +67 -0
  100. sqlspec/statement/pipelines/analyzers/__init__.py +9 -0
  101. sqlspec/statement/pipelines/analyzers/_analyzer.py +649 -0
  102. sqlspec/statement/pipelines/base.py +315 -0
  103. sqlspec/statement/pipelines/context.py +119 -0
  104. sqlspec/statement/pipelines/result_types.py +41 -0
  105. sqlspec/statement/pipelines/transformers/__init__.py +8 -0
  106. sqlspec/statement/pipelines/transformers/_expression_simplifier.py +256 -0
  107. sqlspec/statement/pipelines/transformers/_literal_parameterizer.py +623 -0
  108. sqlspec/statement/pipelines/transformers/_remove_comments.py +66 -0
  109. sqlspec/statement/pipelines/transformers/_remove_hints.py +81 -0
  110. sqlspec/statement/pipelines/validators/__init__.py +23 -0
  111. sqlspec/statement/pipelines/validators/_dml_safety.py +275 -0
  112. sqlspec/statement/pipelines/validators/_parameter_style.py +297 -0
  113. sqlspec/statement/pipelines/validators/_performance.py +703 -0
  114. sqlspec/statement/pipelines/validators/_security.py +990 -0
  115. sqlspec/statement/pipelines/validators/base.py +67 -0
  116. sqlspec/statement/result.py +527 -0
  117. sqlspec/statement/splitter.py +701 -0
  118. sqlspec/statement/sql.py +1198 -0
  119. sqlspec/storage/__init__.py +15 -0
  120. sqlspec/storage/backends/__init__.py +0 -0
  121. sqlspec/storage/backends/base.py +166 -0
  122. sqlspec/storage/backends/fsspec.py +315 -0
  123. sqlspec/storage/backends/obstore.py +464 -0
  124. sqlspec/storage/protocol.py +170 -0
  125. sqlspec/storage/registry.py +315 -0
  126. sqlspec/typing.py +157 -36
  127. sqlspec/utils/correlation.py +155 -0
  128. sqlspec/utils/deprecation.py +3 -6
  129. sqlspec/utils/fixtures.py +6 -11
  130. sqlspec/utils/logging.py +135 -0
  131. sqlspec/utils/module_loader.py +45 -43
  132. sqlspec/utils/serializers.py +4 -0
  133. sqlspec/utils/singleton.py +6 -8
  134. sqlspec/utils/sync_tools.py +15 -27
  135. sqlspec/utils/text.py +58 -26
  136. {sqlspec-0.11.1.dist-info → sqlspec-0.12.1.dist-info}/METADATA +97 -26
  137. sqlspec-0.12.1.dist-info/RECORD +145 -0
  138. sqlspec/adapters/bigquery/config/__init__.py +0 -3
  139. sqlspec/adapters/bigquery/config/_common.py +0 -40
  140. sqlspec/adapters/bigquery/config/_sync.py +0 -87
  141. sqlspec/adapters/oracledb/config/__init__.py +0 -9
  142. sqlspec/adapters/oracledb/config/_asyncio.py +0 -186
  143. sqlspec/adapters/oracledb/config/_common.py +0 -131
  144. sqlspec/adapters/oracledb/config/_sync.py +0 -186
  145. sqlspec/adapters/psycopg/config/__init__.py +0 -19
  146. sqlspec/adapters/psycopg/config/_async.py +0 -169
  147. sqlspec/adapters/psycopg/config/_common.py +0 -56
  148. sqlspec/adapters/psycopg/config/_sync.py +0 -168
  149. sqlspec/filters.py +0 -331
  150. sqlspec/mixins.py +0 -305
  151. sqlspec/statement.py +0 -378
  152. sqlspec-0.11.1.dist-info/RECORD +0 -69
  153. {sqlspec-0.11.1.dist-info → sqlspec-0.12.1.dist-info}/WHEEL +0 -0
  154. {sqlspec-0.11.1.dist-info → sqlspec-0.12.1.dist-info}/licenses/LICENSE +0 -0
  155. {sqlspec-0.11.1.dist-info → sqlspec-0.12.1.dist-info}/licenses/NOTICE +0 -0
@@ -0,0 +1,131 @@
1
+ """Type coercion mixin for database drivers.
2
+
3
+ This module provides a mixin that all database drivers use to handle
4
+ TypedParameter objects and perform appropriate type conversions.
5
+ """
6
+
7
+ from decimal import Decimal
8
+ from typing import TYPE_CHECKING, Any, Optional, Union
9
+
10
+ if TYPE_CHECKING:
11
+ from sqlspec.typing import SQLParameterType
12
+
13
+ __all__ = ("TypeCoercionMixin",)
14
+
15
+
16
+ class TypeCoercionMixin:
17
+ """Mixin providing type coercion for database drivers.
18
+
19
+ This mixin is used by all database drivers to handle TypedParameter objects
20
+ and convert values to database-specific types.
21
+ """
22
+
23
+ __slots__ = ()
24
+
25
+ def _process_parameters(self, parameters: "SQLParameterType") -> "SQLParameterType":
26
+ """Process parameters, extracting values from TypedParameter objects.
27
+
28
+ This method is called by drivers before executing SQL to handle
29
+ TypedParameter objects and perform necessary type conversions.
30
+
31
+ Args:
32
+ parameters: Raw parameters that may contain TypedParameter objects
33
+
34
+ Returns:
35
+ Processed parameters with TypedParameter values extracted and converted
36
+ """
37
+ if parameters is None:
38
+ return None
39
+
40
+ if isinstance(parameters, dict):
41
+ return self._process_dict_parameters(parameters)
42
+ if isinstance(parameters, (list, tuple)):
43
+ return self._process_sequence_parameters(parameters)
44
+ # Single scalar parameter
45
+ return self._coerce_parameter_type(parameters)
46
+
47
+ def _process_dict_parameters(self, params: dict[str, Any]) -> dict[str, Any]:
48
+ """Process dictionary parameters."""
49
+ result = {}
50
+ for key, value in params.items():
51
+ result[key] = self._coerce_parameter_type(value)
52
+ return result
53
+
54
+ def _process_sequence_parameters(self, params: Union[list, tuple]) -> Union[list, tuple]:
55
+ """Process list/tuple parameters."""
56
+ result = [self._coerce_parameter_type(p) for p in params]
57
+ return tuple(result) if isinstance(params, tuple) else result
58
+
59
+ def _coerce_parameter_type(self, param: Any) -> Any:
60
+ """Coerce a single parameter to the appropriate database type.
61
+
62
+ This method checks if the parameter is a TypedParameter and extracts
63
+ its value, then applies driver-specific type conversions.
64
+
65
+ Args:
66
+ param: Parameter value or TypedParameter object
67
+
68
+ Returns:
69
+ Coerced parameter value suitable for the database
70
+ """
71
+ # Check if it's a TypedParameter
72
+ if hasattr(param, "__class__") and param.__class__.__name__ == "TypedParameter":
73
+ # Extract value and type hint
74
+ value = param.value
75
+ type_hint = param.type_hint
76
+
77
+ # Apply driver-specific coercion based on type hint
78
+ return self._apply_type_coercion(value, type_hint)
79
+ # Regular parameter - apply default coercion
80
+ return self._apply_type_coercion(param, None)
81
+
82
+ def _apply_type_coercion(self, value: Any, type_hint: Optional[str]) -> Any:
83
+ """Apply driver-specific type coercion.
84
+
85
+ This method should be overridden by each driver to implement
86
+ database-specific type conversions.
87
+
88
+ Args:
89
+ value: The value to coerce
90
+ type_hint: Optional type hint from TypedParameter
91
+
92
+ Returns:
93
+ Coerced value
94
+ """
95
+ # Default implementation - override in specific drivers
96
+ # This base implementation handles common cases
97
+
98
+ if value is None:
99
+ return None
100
+
101
+ # Use type hint if available
102
+ if type_hint:
103
+ if type_hint == "boolean":
104
+ return self._coerce_boolean(value)
105
+ if type_hint == "decimal":
106
+ return self._coerce_decimal(value)
107
+ if type_hint == "json":
108
+ return self._coerce_json(value)
109
+ if type_hint.startswith("array"):
110
+ return self._coerce_array(value)
111
+
112
+ # Default: return value as-is
113
+ return value
114
+
115
+ def _coerce_boolean(self, value: Any) -> Any:
116
+ """Coerce boolean values. Override in drivers without native boolean support."""
117
+ return value
118
+
119
+ def _coerce_decimal(self, value: Any) -> Any:
120
+ """Coerce decimal values. Override for specific decimal handling."""
121
+ if isinstance(value, str):
122
+ return Decimal(value)
123
+ return value
124
+
125
+ def _coerce_json(self, value: Any) -> Any:
126
+ """Coerce JSON values. Override for databases needing JSON strings."""
127
+ return value
128
+
129
+ def _coerce_array(self, value: Any) -> Any:
130
+ """Coerce array values. Override for databases without native array support."""
131
+ return value
sqlspec/exceptions.py CHANGED
@@ -1,18 +1,37 @@
1
1
  from collections.abc import Generator
2
2
  from contextlib import contextmanager
3
- from typing import Any, Optional
3
+ from enum import Enum
4
+ from typing import Any, Optional, Union, cast
4
5
 
5
6
  __all__ = (
7
+ "ExtraParameterError",
8
+ "FileNotFoundInStorageError",
6
9
  "ImproperConfigurationError",
7
10
  "IntegrityError",
8
11
  "MissingDependencyError",
12
+ "MissingParameterError",
9
13
  "MultipleResultsFoundError",
10
14
  "NotFoundError",
15
+ "ParameterError",
11
16
  "ParameterStyleMismatchError",
17
+ "PipelineExecutionError",
18
+ "QueryError",
12
19
  "RepositoryError",
20
+ "RiskLevel",
21
+ "SQLBuilderError",
22
+ "SQLConversionError",
23
+ "SQLFileNotFoundError",
24
+ "SQLFileParseError",
25
+ "SQLFileParsingError",
26
+ "SQLInjectionError",
13
27
  "SQLParsingError",
14
28
  "SQLSpecError",
29
+ "SQLTransformationError",
30
+ "SQLValidationError",
15
31
  "SerializationError",
32
+ "StorageOperationFailedError",
33
+ "UnknownParameterError",
34
+ "UnsafeSQLError",
16
35
  )
17
36
 
18
37
 
@@ -56,10 +75,17 @@ class MissingDependencyError(SQLSpecError, ImportError):
56
75
  super().__init__(
57
76
  f"Package {package!r} is not installed but required. You can install it by running "
58
77
  f"'pip install sqlspec[{install_package or package}]' to install sqlspec with the required extra "
59
- f"or 'pip install {install_package or package}' to install the package separately",
78
+ f"or 'pip install {install_package or package}' to install the package separately"
60
79
  )
61
80
 
62
81
 
82
+ class BackendNotRegisteredError(SQLSpecError):
83
+ """Raised when a requested storage backend key is not registered."""
84
+
85
+ def __init__(self, backend_key: str) -> None:
86
+ super().__init__(f"Storage backend '{backend_key}' is not registered. Please register it before use.")
87
+
88
+
63
89
  class SQLLoadingError(SQLSpecError):
64
90
  """Issues loading referenced SQL file."""
65
91
 
@@ -78,6 +104,24 @@ class SQLParsingError(SQLSpecError):
78
104
  super().__init__(message)
79
105
 
80
106
 
107
+ class SQLFileParsingError(SQLSpecError):
108
+ """Issues parsing SQL files."""
109
+
110
+ def __init__(self, message: Optional[str] = None) -> None:
111
+ if message is None:
112
+ message = "Issues parsing SQL files."
113
+ super().__init__(message)
114
+
115
+
116
+ class SQLBuilderError(SQLSpecError):
117
+ """Issues Building or Generating SQL statements."""
118
+
119
+ def __init__(self, message: Optional[str] = None) -> None:
120
+ if message is None:
121
+ message = "Issues building SQL statement."
122
+ super().__init__(message)
123
+
124
+
81
125
  class SQLConversionError(SQLSpecError):
82
126
  """Issues converting SQL statements."""
83
127
 
@@ -87,6 +131,140 @@ class SQLConversionError(SQLSpecError):
87
131
  super().__init__(message)
88
132
 
89
133
 
134
+ # -- SQL Validation Errors --
135
+ class RiskLevel(Enum):
136
+ """SQL risk assessment levels."""
137
+
138
+ SKIP = 1
139
+ SAFE = 2
140
+ LOW = 3
141
+ MEDIUM = 4
142
+ HIGH = 5
143
+ CRITICAL = 6
144
+
145
+ def __str__(self) -> str:
146
+ """String representation.
147
+
148
+ Returns:
149
+ Lowercase name of the style.
150
+ """
151
+ return self.name.lower()
152
+
153
+ def __lt__(self, other: "RiskLevel") -> bool: # pragma: no cover
154
+ """Less than comparison for ordering."""
155
+ if not isinstance(other, RiskLevel):
156
+ return NotImplemented
157
+ return self.value < other.value
158
+
159
+ def __le__(self, other: "RiskLevel") -> bool: # pragma: no cover
160
+ """Less than or equal comparison for ordering."""
161
+ if not isinstance(other, RiskLevel):
162
+ return NotImplemented
163
+ return self.value <= other.value
164
+
165
+ def __gt__(self, other: "RiskLevel") -> bool: # pragma: no cover
166
+ """Greater than comparison for ordering."""
167
+ if not isinstance(other, RiskLevel):
168
+ return NotImplemented
169
+ return self.value > other.value
170
+
171
+ def __ge__(self, other: "RiskLevel") -> bool: # pragma: no cover
172
+ """Greater than or equal comparison for ordering."""
173
+ if not isinstance(other, RiskLevel):
174
+ return NotImplemented
175
+ return self.value >= other.value
176
+
177
+
178
+ class SQLValidationError(SQLSpecError):
179
+ """Base class for SQL validation errors."""
180
+
181
+ sql: Optional[str]
182
+ risk_level: RiskLevel
183
+
184
+ def __init__(self, message: str, sql: Optional[str] = None, risk_level: RiskLevel = RiskLevel.MEDIUM) -> None:
185
+ """Initialize with SQL context and risk level."""
186
+ detail_message = message
187
+ if sql is not None:
188
+ detail_message = f"{message}\nSQL: {sql}"
189
+ super().__init__(detail=detail_message)
190
+ self.sql = sql
191
+ self.risk_level = risk_level
192
+
193
+
194
+ class SQLTransformationError(SQLSpecError):
195
+ """Base class for SQL transformation errors."""
196
+
197
+ sql: Optional[str]
198
+
199
+ def __init__(self, message: str, sql: Optional[str] = None) -> None:
200
+ """Initialize with SQL context and risk level."""
201
+ detail_message = message
202
+ if sql is not None:
203
+ detail_message = f"{message}\nSQL: {sql}"
204
+ super().__init__(detail=detail_message)
205
+ self.sql = sql
206
+
207
+
208
+ class SQLInjectionError(SQLValidationError):
209
+ """Raised when potential SQL injection is detected."""
210
+
211
+ pattern: Optional[str]
212
+
213
+ def __init__(self, message: str, sql: Optional[str] = None, pattern: Optional[str] = None) -> None:
214
+ """Initialize with injection pattern context."""
215
+ detail_message = message
216
+ if pattern:
217
+ detail_message = f"{message} (Pattern: {pattern})"
218
+ super().__init__(detail_message, sql, RiskLevel.CRITICAL)
219
+ self.pattern = pattern
220
+
221
+
222
+ class UnsafeSQLError(SQLValidationError):
223
+ """Raised when unsafe SQL constructs are detected."""
224
+
225
+ construct: Optional[str]
226
+
227
+ def __init__(self, message: str, sql: Optional[str] = None, construct: Optional[str] = None) -> None:
228
+ """Initialize with unsafe construct context."""
229
+ detail_message = message
230
+ if construct:
231
+ detail_message = f"{message} (Construct: {construct})"
232
+ super().__init__(detail_message, sql, RiskLevel.HIGH)
233
+ self.construct = construct
234
+
235
+
236
+ # -- SQL Query Errors --
237
+ class QueryError(SQLSpecError):
238
+ """Base class for Query errors."""
239
+
240
+
241
+ # -- SQL Parameter Errors --
242
+ class ParameterError(SQLSpecError):
243
+ """Base class for parameter-related errors."""
244
+
245
+ sql: Optional[str]
246
+
247
+ def __init__(self, message: str, sql: Optional[str] = None) -> None:
248
+ """Initialize with optional SQL context."""
249
+ detail_message = message
250
+ if sql is not None:
251
+ detail_message = f"{message}\nSQL: {sql}"
252
+ super().__init__(detail=detail_message)
253
+ self.sql = sql
254
+
255
+
256
+ class UnknownParameterError(ParameterError):
257
+ """Raised when encountering unknown parameter syntax."""
258
+
259
+
260
+ class MissingParameterError(ParameterError):
261
+ """Raised when required parameters are missing."""
262
+
263
+
264
+ class ExtraParameterError(ParameterError):
265
+ """Raised when extra parameters are provided."""
266
+
267
+
90
268
  class ParameterStyleMismatchError(SQLSpecError):
91
269
  """Error when parameter style doesn't match SQL placeholder style.
92
270
 
@@ -95,10 +273,21 @@ class ParameterStyleMismatchError(SQLSpecError):
95
273
  (named, positional, etc.).
96
274
  """
97
275
 
98
- def __init__(self, message: Optional[str] = None) -> None:
99
- if message is None:
100
- message = "Parameter style mismatch: dictionary parameters provided but no named placeholders found in SQL."
101
- super().__init__(message)
276
+ sql: Optional[str]
277
+
278
+ def __init__(self, message: Optional[str] = None, sql: Optional[str] = None) -> None:
279
+ final_message = message
280
+ if final_message is None:
281
+ final_message = (
282
+ "Parameter style mismatch: dictionary parameters provided but no named placeholders found in SQL."
283
+ )
284
+
285
+ detail_message = final_message
286
+ if sql:
287
+ detail_message = f"{final_message}\nSQL: {sql}"
288
+
289
+ super().__init__(detail=detail_message)
290
+ self.sql = sql
102
291
 
103
292
 
104
293
  class ImproperConfigurationError(SQLSpecError):
@@ -128,13 +317,116 @@ class MultipleResultsFoundError(RepositoryError):
128
317
  """A single database result was required but more than one were found."""
129
318
 
130
319
 
320
+ class StorageOperationFailedError(SQLSpecError):
321
+ """Raised when a storage backend operation fails (e.g., network, permission, API error)."""
322
+
323
+
324
+ class FileNotFoundInStorageError(StorageOperationFailedError):
325
+ """Raised when a file or object is not found in the storage backend."""
326
+
327
+
328
+ class SQLFileNotFoundError(SQLSpecError):
329
+ """Raised when a SQL file cannot be found."""
330
+
331
+ def __init__(self, name: str, path: "Optional[str]" = None) -> None:
332
+ """Initialize the error.
333
+
334
+ Args:
335
+ name: Name of the SQL file.
336
+ path: Optional path where the file was expected.
337
+ """
338
+ message = f"SQL file '{name}' not found at path: {path}" if path else f"SQL file '{name}' not found"
339
+ super().__init__(message)
340
+ self.name = name
341
+ self.path = path
342
+
343
+
344
+ class SQLFileParseError(SQLSpecError):
345
+ """Raised when a SQL file cannot be parsed."""
346
+
347
+ def __init__(self, name: str, path: str, original_error: "Exception") -> None:
348
+ """Initialize the error.
349
+
350
+ Args:
351
+ name: Name of the SQL file.
352
+ path: Path to the SQL file.
353
+ original_error: The underlying parsing error.
354
+ """
355
+ message = f"Failed to parse SQL file '{name}' at {path}: {original_error}"
356
+ super().__init__(message)
357
+ self.name = name
358
+ self.path = path
359
+ self.original_error = original_error
360
+
361
+
131
362
  @contextmanager
132
- def wrap_exceptions(wrap_exceptions: bool = True) -> Generator[None, None, None]:
363
+ def wrap_exceptions(
364
+ wrap_exceptions: bool = True, suppress: "Optional[Union[type[Exception], tuple[type[Exception], ...]]]" = None
365
+ ) -> Generator[None, None, None]:
366
+ """Context manager for exception handling with optional suppression.
367
+
368
+ Args:
369
+ wrap_exceptions: If True, wrap exceptions in RepositoryError. If False, let them pass through.
370
+ suppress: Exception type(s) to suppress completely (like contextlib.suppress).
371
+ If provided, these exceptions are caught and ignored.
372
+ """
133
373
  try:
134
374
  yield
135
375
 
136
376
  except Exception as exc:
377
+ # Handle suppression first
378
+ if suppress is not None and (
379
+ (isinstance(suppress, type) and isinstance(exc, suppress))
380
+ or (isinstance(suppress, tuple) and isinstance(exc, suppress))
381
+ ):
382
+ return # Suppress this exception
383
+
384
+ # If it's already a SQLSpec exception, don't wrap it
385
+ if isinstance(exc, SQLSpecError):
386
+ raise
387
+
388
+ # Handle wrapping
137
389
  if wrap_exceptions is False:
138
390
  raise
139
391
  msg = "An error occurred during the operation."
140
392
  raise RepositoryError(detail=msg) from exc
393
+
394
+
395
+ class PipelineExecutionError(SQLSpecError):
396
+ """Rich error information for pipeline execution failures."""
397
+
398
+ def __init__(
399
+ self,
400
+ message: str,
401
+ *,
402
+ operation_index: "Optional[int]" = None,
403
+ failed_operation: "Optional[Any]" = None,
404
+ partial_results: "Optional[list[Any]]" = None,
405
+ driver_error: "Optional[Exception]" = None,
406
+ ) -> None:
407
+ """Initialize the pipeline execution error.
408
+
409
+ Args:
410
+ message: Error message describing the failure
411
+ operation_index: Index of the operation that failed
412
+ failed_operation: The PipelineOperation that failed
413
+ partial_results: Results from operations that succeeded before the failure
414
+ driver_error: Original exception from the database driver
415
+ """
416
+ super().__init__(message)
417
+ self.operation_index = operation_index
418
+ self.failed_operation = failed_operation
419
+ self.partial_results = partial_results or []
420
+ self.driver_error = driver_error
421
+
422
+ def get_failed_sql(self) -> "Optional[str]":
423
+ """Get the SQL that failed for debugging."""
424
+ if self.failed_operation and hasattr(self.failed_operation, "sql"):
425
+ return cast("str", self.failed_operation.sql.to_sql())
426
+ return None
427
+
428
+ def get_failed_parameters(self) -> "Optional[Any]":
429
+ """Get the parameters that failed."""
430
+ if self.failed_operation and hasattr(self.failed_operation, "original_params"):
431
+ return self.failed_operation.original_params
432
+ return None
@@ -0,0 +1,10 @@
1
+ """SQLSpec aiosql integration for loading SQL files.
2
+
3
+ This module provides a simple way to load aiosql-style SQL files and use them
4
+ with SQLSpec drivers. It focuses on just the file parsing functionality,
5
+ returning SQL objects that work with existing SQLSpec execution.
6
+ """
7
+
8
+ from sqlspec.extensions.aiosql.adapter import AiosqlAsyncAdapter, AiosqlSyncAdapter
9
+
10
+ __all__ = ("AiosqlAsyncAdapter", "AiosqlSyncAdapter")