sqlspec 0.11.1__py3-none-any.whl → 0.12.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.
- sqlspec/__init__.py +16 -3
- sqlspec/_serialization.py +3 -10
- sqlspec/_sql.py +1147 -0
- sqlspec/_typing.py +343 -41
- sqlspec/adapters/adbc/__init__.py +2 -6
- sqlspec/adapters/adbc/config.py +474 -149
- sqlspec/adapters/adbc/driver.py +330 -621
- sqlspec/adapters/aiosqlite/__init__.py +2 -6
- sqlspec/adapters/aiosqlite/config.py +143 -57
- sqlspec/adapters/aiosqlite/driver.py +269 -431
- sqlspec/adapters/asyncmy/__init__.py +3 -8
- sqlspec/adapters/asyncmy/config.py +247 -202
- sqlspec/adapters/asyncmy/driver.py +218 -436
- sqlspec/adapters/asyncpg/__init__.py +4 -7
- sqlspec/adapters/asyncpg/config.py +329 -176
- sqlspec/adapters/asyncpg/driver.py +417 -487
- sqlspec/adapters/bigquery/__init__.py +2 -2
- sqlspec/adapters/bigquery/config.py +407 -0
- sqlspec/adapters/bigquery/driver.py +600 -553
- sqlspec/adapters/duckdb/__init__.py +4 -1
- sqlspec/adapters/duckdb/config.py +432 -321
- sqlspec/adapters/duckdb/driver.py +392 -406
- sqlspec/adapters/oracledb/__init__.py +3 -8
- sqlspec/adapters/oracledb/config.py +625 -0
- sqlspec/adapters/oracledb/driver.py +548 -921
- sqlspec/adapters/psqlpy/__init__.py +4 -7
- sqlspec/adapters/psqlpy/config.py +372 -203
- sqlspec/adapters/psqlpy/driver.py +197 -533
- sqlspec/adapters/psycopg/__init__.py +3 -8
- sqlspec/adapters/psycopg/config.py +741 -0
- sqlspec/adapters/psycopg/driver.py +734 -694
- sqlspec/adapters/sqlite/__init__.py +2 -6
- sqlspec/adapters/sqlite/config.py +146 -81
- sqlspec/adapters/sqlite/driver.py +242 -405
- sqlspec/base.py +220 -784
- sqlspec/config.py +354 -0
- sqlspec/driver/__init__.py +22 -0
- sqlspec/driver/_async.py +252 -0
- sqlspec/driver/_common.py +338 -0
- sqlspec/driver/_sync.py +261 -0
- sqlspec/driver/mixins/__init__.py +17 -0
- sqlspec/driver/mixins/_pipeline.py +523 -0
- sqlspec/driver/mixins/_result_utils.py +122 -0
- sqlspec/driver/mixins/_sql_translator.py +35 -0
- sqlspec/driver/mixins/_storage.py +993 -0
- sqlspec/driver/mixins/_type_coercion.py +131 -0
- sqlspec/exceptions.py +299 -7
- sqlspec/extensions/aiosql/__init__.py +10 -0
- sqlspec/extensions/aiosql/adapter.py +474 -0
- sqlspec/extensions/litestar/__init__.py +1 -6
- sqlspec/extensions/litestar/_utils.py +1 -5
- sqlspec/extensions/litestar/config.py +5 -6
- sqlspec/extensions/litestar/handlers.py +13 -12
- sqlspec/extensions/litestar/plugin.py +22 -24
- sqlspec/extensions/litestar/providers.py +37 -55
- sqlspec/loader.py +528 -0
- sqlspec/service/__init__.py +3 -0
- sqlspec/service/base.py +24 -0
- sqlspec/service/pagination.py +26 -0
- sqlspec/statement/__init__.py +21 -0
- sqlspec/statement/builder/__init__.py +54 -0
- sqlspec/statement/builder/_ddl_utils.py +119 -0
- sqlspec/statement/builder/_parsing_utils.py +135 -0
- sqlspec/statement/builder/base.py +328 -0
- sqlspec/statement/builder/ddl.py +1379 -0
- sqlspec/statement/builder/delete.py +80 -0
- sqlspec/statement/builder/insert.py +274 -0
- sqlspec/statement/builder/merge.py +95 -0
- sqlspec/statement/builder/mixins/__init__.py +65 -0
- sqlspec/statement/builder/mixins/_aggregate_functions.py +151 -0
- sqlspec/statement/builder/mixins/_case_builder.py +91 -0
- sqlspec/statement/builder/mixins/_common_table_expr.py +91 -0
- sqlspec/statement/builder/mixins/_delete_from.py +34 -0
- sqlspec/statement/builder/mixins/_from.py +61 -0
- sqlspec/statement/builder/mixins/_group_by.py +119 -0
- sqlspec/statement/builder/mixins/_having.py +35 -0
- sqlspec/statement/builder/mixins/_insert_from_select.py +48 -0
- sqlspec/statement/builder/mixins/_insert_into.py +36 -0
- sqlspec/statement/builder/mixins/_insert_values.py +69 -0
- sqlspec/statement/builder/mixins/_join.py +110 -0
- sqlspec/statement/builder/mixins/_limit_offset.py +53 -0
- sqlspec/statement/builder/mixins/_merge_clauses.py +405 -0
- sqlspec/statement/builder/mixins/_order_by.py +46 -0
- sqlspec/statement/builder/mixins/_pivot.py +82 -0
- sqlspec/statement/builder/mixins/_returning.py +37 -0
- sqlspec/statement/builder/mixins/_select_columns.py +60 -0
- sqlspec/statement/builder/mixins/_set_ops.py +122 -0
- sqlspec/statement/builder/mixins/_unpivot.py +80 -0
- sqlspec/statement/builder/mixins/_update_from.py +54 -0
- sqlspec/statement/builder/mixins/_update_set.py +91 -0
- sqlspec/statement/builder/mixins/_update_table.py +29 -0
- sqlspec/statement/builder/mixins/_where.py +374 -0
- sqlspec/statement/builder/mixins/_window_functions.py +86 -0
- sqlspec/statement/builder/protocols.py +20 -0
- sqlspec/statement/builder/select.py +206 -0
- sqlspec/statement/builder/update.py +178 -0
- sqlspec/statement/filters.py +571 -0
- sqlspec/statement/parameters.py +736 -0
- sqlspec/statement/pipelines/__init__.py +67 -0
- sqlspec/statement/pipelines/analyzers/__init__.py +9 -0
- sqlspec/statement/pipelines/analyzers/_analyzer.py +649 -0
- sqlspec/statement/pipelines/base.py +315 -0
- sqlspec/statement/pipelines/context.py +119 -0
- sqlspec/statement/pipelines/result_types.py +41 -0
- sqlspec/statement/pipelines/transformers/__init__.py +8 -0
- sqlspec/statement/pipelines/transformers/_expression_simplifier.py +256 -0
- sqlspec/statement/pipelines/transformers/_literal_parameterizer.py +623 -0
- sqlspec/statement/pipelines/transformers/_remove_comments.py +66 -0
- sqlspec/statement/pipelines/transformers/_remove_hints.py +81 -0
- sqlspec/statement/pipelines/validators/__init__.py +23 -0
- sqlspec/statement/pipelines/validators/_dml_safety.py +275 -0
- sqlspec/statement/pipelines/validators/_parameter_style.py +297 -0
- sqlspec/statement/pipelines/validators/_performance.py +703 -0
- sqlspec/statement/pipelines/validators/_security.py +990 -0
- sqlspec/statement/pipelines/validators/base.py +67 -0
- sqlspec/statement/result.py +527 -0
- sqlspec/statement/splitter.py +701 -0
- sqlspec/statement/sql.py +1198 -0
- sqlspec/storage/__init__.py +15 -0
- sqlspec/storage/backends/__init__.py +0 -0
- sqlspec/storage/backends/base.py +166 -0
- sqlspec/storage/backends/fsspec.py +315 -0
- sqlspec/storage/backends/obstore.py +464 -0
- sqlspec/storage/protocol.py +170 -0
- sqlspec/storage/registry.py +315 -0
- sqlspec/typing.py +157 -36
- sqlspec/utils/correlation.py +155 -0
- sqlspec/utils/deprecation.py +3 -6
- sqlspec/utils/fixtures.py +6 -11
- sqlspec/utils/logging.py +135 -0
- sqlspec/utils/module_loader.py +45 -43
- sqlspec/utils/serializers.py +4 -0
- sqlspec/utils/singleton.py +6 -8
- sqlspec/utils/sync_tools.py +15 -27
- sqlspec/utils/text.py +58 -26
- {sqlspec-0.11.1.dist-info → sqlspec-0.12.0.dist-info}/METADATA +97 -26
- sqlspec-0.12.0.dist-info/RECORD +145 -0
- sqlspec/adapters/bigquery/config/__init__.py +0 -3
- sqlspec/adapters/bigquery/config/_common.py +0 -40
- sqlspec/adapters/bigquery/config/_sync.py +0 -87
- sqlspec/adapters/oracledb/config/__init__.py +0 -9
- sqlspec/adapters/oracledb/config/_asyncio.py +0 -186
- sqlspec/adapters/oracledb/config/_common.py +0 -131
- sqlspec/adapters/oracledb/config/_sync.py +0 -186
- sqlspec/adapters/psycopg/config/__init__.py +0 -19
- sqlspec/adapters/psycopg/config/_async.py +0 -169
- sqlspec/adapters/psycopg/config/_common.py +0 -56
- sqlspec/adapters/psycopg/config/_sync.py +0 -168
- sqlspec/filters.py +0 -331
- sqlspec/mixins.py +0 -305
- sqlspec/statement.py +0 -378
- sqlspec-0.11.1.dist-info/RECORD +0 -69
- {sqlspec-0.11.1.dist-info → sqlspec-0.12.0.dist-info}/WHEEL +0 -0
- {sqlspec-0.11.1.dist-info → sqlspec-0.12.0.dist-info}/licenses/LICENSE +0 -0
- {sqlspec-0.11.1.dist-info → sqlspec-0.12.0.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
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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(
|
|
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")
|