sqlspec 0.13.1__py3-none-any.whl → 0.16.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of sqlspec might be problematic. Click here for more details.
- sqlspec/__init__.py +71 -8
- sqlspec/__main__.py +12 -0
- sqlspec/__metadata__.py +1 -3
- sqlspec/_serialization.py +1 -2
- sqlspec/_sql.py +930 -136
- sqlspec/_typing.py +278 -142
- sqlspec/adapters/adbc/__init__.py +4 -3
- sqlspec/adapters/adbc/_types.py +12 -0
- sqlspec/adapters/adbc/config.py +116 -285
- sqlspec/adapters/adbc/driver.py +462 -340
- sqlspec/adapters/aiosqlite/__init__.py +18 -3
- sqlspec/adapters/aiosqlite/_types.py +13 -0
- sqlspec/adapters/aiosqlite/config.py +202 -150
- sqlspec/adapters/aiosqlite/driver.py +226 -247
- sqlspec/adapters/asyncmy/__init__.py +18 -3
- sqlspec/adapters/asyncmy/_types.py +12 -0
- sqlspec/adapters/asyncmy/config.py +80 -199
- sqlspec/adapters/asyncmy/driver.py +257 -215
- sqlspec/adapters/asyncpg/__init__.py +19 -4
- sqlspec/adapters/asyncpg/_types.py +17 -0
- sqlspec/adapters/asyncpg/config.py +81 -214
- sqlspec/adapters/asyncpg/driver.py +284 -359
- sqlspec/adapters/bigquery/__init__.py +17 -3
- sqlspec/adapters/bigquery/_types.py +12 -0
- sqlspec/adapters/bigquery/config.py +191 -299
- sqlspec/adapters/bigquery/driver.py +474 -634
- sqlspec/adapters/duckdb/__init__.py +14 -3
- sqlspec/adapters/duckdb/_types.py +12 -0
- sqlspec/adapters/duckdb/config.py +414 -397
- sqlspec/adapters/duckdb/driver.py +342 -393
- sqlspec/adapters/oracledb/__init__.py +19 -5
- sqlspec/adapters/oracledb/_types.py +14 -0
- sqlspec/adapters/oracledb/config.py +123 -458
- sqlspec/adapters/oracledb/driver.py +505 -531
- sqlspec/adapters/psqlpy/__init__.py +13 -3
- sqlspec/adapters/psqlpy/_types.py +11 -0
- sqlspec/adapters/psqlpy/config.py +93 -307
- sqlspec/adapters/psqlpy/driver.py +504 -213
- sqlspec/adapters/psycopg/__init__.py +19 -5
- sqlspec/adapters/psycopg/_types.py +17 -0
- sqlspec/adapters/psycopg/config.py +143 -472
- sqlspec/adapters/psycopg/driver.py +704 -825
- sqlspec/adapters/sqlite/__init__.py +14 -3
- sqlspec/adapters/sqlite/_types.py +11 -0
- sqlspec/adapters/sqlite/config.py +208 -142
- sqlspec/adapters/sqlite/driver.py +263 -278
- sqlspec/base.py +105 -9
- sqlspec/{statement/builder → builder}/__init__.py +12 -14
- sqlspec/{statement/builder/base.py → builder/_base.py} +184 -86
- sqlspec/{statement/builder/column.py → builder/_column.py} +97 -60
- sqlspec/{statement/builder/ddl.py → builder/_ddl.py} +61 -131
- sqlspec/{statement/builder → builder}/_ddl_utils.py +4 -10
- sqlspec/{statement/builder/delete.py → builder/_delete.py} +10 -30
- sqlspec/builder/_insert.py +421 -0
- sqlspec/builder/_merge.py +71 -0
- sqlspec/{statement/builder → builder}/_parsing_utils.py +49 -26
- sqlspec/builder/_select.py +170 -0
- sqlspec/{statement/builder/update.py → builder/_update.py} +16 -20
- sqlspec/builder/mixins/__init__.py +55 -0
- sqlspec/builder/mixins/_cte_and_set_ops.py +222 -0
- sqlspec/{statement/builder/mixins/_delete_from.py → builder/mixins/_delete_operations.py} +8 -1
- sqlspec/builder/mixins/_insert_operations.py +244 -0
- sqlspec/{statement/builder/mixins/_join.py → builder/mixins/_join_operations.py} +45 -13
- sqlspec/{statement/builder/mixins/_merge_clauses.py → builder/mixins/_merge_operations.py} +188 -30
- sqlspec/builder/mixins/_order_limit_operations.py +135 -0
- sqlspec/builder/mixins/_pivot_operations.py +153 -0
- sqlspec/builder/mixins/_select_operations.py +604 -0
- sqlspec/builder/mixins/_update_operations.py +202 -0
- sqlspec/builder/mixins/_where_clause.py +644 -0
- sqlspec/cli.py +247 -0
- sqlspec/config.py +183 -138
- sqlspec/core/__init__.py +63 -0
- sqlspec/core/cache.py +871 -0
- sqlspec/core/compiler.py +417 -0
- sqlspec/core/filters.py +830 -0
- sqlspec/core/hashing.py +310 -0
- sqlspec/core/parameters.py +1237 -0
- sqlspec/core/result.py +677 -0
- sqlspec/{statement → core}/splitter.py +321 -191
- sqlspec/core/statement.py +676 -0
- sqlspec/driver/__init__.py +7 -10
- sqlspec/driver/_async.py +422 -163
- sqlspec/driver/_common.py +545 -287
- sqlspec/driver/_sync.py +426 -160
- sqlspec/driver/mixins/__init__.py +2 -13
- sqlspec/driver/mixins/_result_tools.py +193 -0
- sqlspec/driver/mixins/_sql_translator.py +65 -14
- sqlspec/exceptions.py +5 -252
- sqlspec/extensions/aiosql/adapter.py +93 -96
- sqlspec/extensions/litestar/__init__.py +2 -1
- sqlspec/extensions/litestar/cli.py +48 -0
- sqlspec/extensions/litestar/config.py +0 -1
- sqlspec/extensions/litestar/handlers.py +15 -26
- sqlspec/extensions/litestar/plugin.py +21 -16
- sqlspec/extensions/litestar/providers.py +17 -52
- sqlspec/loader.py +423 -104
- sqlspec/migrations/__init__.py +35 -0
- sqlspec/migrations/base.py +414 -0
- sqlspec/migrations/commands.py +443 -0
- sqlspec/migrations/loaders.py +402 -0
- sqlspec/migrations/runner.py +213 -0
- sqlspec/migrations/tracker.py +140 -0
- sqlspec/migrations/utils.py +129 -0
- sqlspec/protocols.py +51 -186
- sqlspec/storage/__init__.py +1 -1
- sqlspec/storage/backends/base.py +37 -40
- sqlspec/storage/backends/fsspec.py +136 -112
- sqlspec/storage/backends/obstore.py +138 -160
- sqlspec/storage/capabilities.py +5 -4
- sqlspec/storage/registry.py +57 -106
- sqlspec/typing.py +136 -115
- sqlspec/utils/__init__.py +2 -2
- sqlspec/utils/correlation.py +0 -3
- sqlspec/utils/deprecation.py +6 -6
- sqlspec/utils/fixtures.py +6 -6
- sqlspec/utils/logging.py +0 -2
- sqlspec/utils/module_loader.py +7 -12
- sqlspec/utils/singleton.py +0 -1
- sqlspec/utils/sync_tools.py +17 -38
- sqlspec/utils/text.py +12 -51
- sqlspec/utils/type_guards.py +482 -235
- {sqlspec-0.13.1.dist-info → sqlspec-0.16.2.dist-info}/METADATA +7 -2
- sqlspec-0.16.2.dist-info/RECORD +134 -0
- sqlspec-0.16.2.dist-info/entry_points.txt +2 -0
- sqlspec/driver/connection.py +0 -207
- sqlspec/driver/mixins/_csv_writer.py +0 -91
- sqlspec/driver/mixins/_pipeline.py +0 -512
- sqlspec/driver/mixins/_result_utils.py +0 -140
- sqlspec/driver/mixins/_storage.py +0 -926
- sqlspec/driver/mixins/_type_coercion.py +0 -130
- sqlspec/driver/parameters.py +0 -138
- sqlspec/service/__init__.py +0 -4
- sqlspec/service/_util.py +0 -147
- sqlspec/service/base.py +0 -1131
- sqlspec/service/pagination.py +0 -26
- sqlspec/statement/__init__.py +0 -21
- sqlspec/statement/builder/insert.py +0 -288
- sqlspec/statement/builder/merge.py +0 -95
- sqlspec/statement/builder/mixins/__init__.py +0 -65
- sqlspec/statement/builder/mixins/_aggregate_functions.py +0 -250
- sqlspec/statement/builder/mixins/_case_builder.py +0 -91
- sqlspec/statement/builder/mixins/_common_table_expr.py +0 -90
- sqlspec/statement/builder/mixins/_from.py +0 -63
- sqlspec/statement/builder/mixins/_group_by.py +0 -118
- sqlspec/statement/builder/mixins/_having.py +0 -35
- sqlspec/statement/builder/mixins/_insert_from_select.py +0 -47
- sqlspec/statement/builder/mixins/_insert_into.py +0 -36
- sqlspec/statement/builder/mixins/_insert_values.py +0 -67
- sqlspec/statement/builder/mixins/_limit_offset.py +0 -53
- sqlspec/statement/builder/mixins/_order_by.py +0 -46
- sqlspec/statement/builder/mixins/_pivot.py +0 -79
- sqlspec/statement/builder/mixins/_returning.py +0 -37
- sqlspec/statement/builder/mixins/_select_columns.py +0 -61
- sqlspec/statement/builder/mixins/_set_ops.py +0 -122
- sqlspec/statement/builder/mixins/_unpivot.py +0 -77
- sqlspec/statement/builder/mixins/_update_from.py +0 -55
- sqlspec/statement/builder/mixins/_update_set.py +0 -94
- sqlspec/statement/builder/mixins/_update_table.py +0 -29
- sqlspec/statement/builder/mixins/_where.py +0 -401
- sqlspec/statement/builder/mixins/_window_functions.py +0 -86
- sqlspec/statement/builder/select.py +0 -221
- sqlspec/statement/filters.py +0 -596
- sqlspec/statement/parameter_manager.py +0 -220
- sqlspec/statement/parameters.py +0 -867
- sqlspec/statement/pipelines/__init__.py +0 -210
- sqlspec/statement/pipelines/analyzers/__init__.py +0 -9
- sqlspec/statement/pipelines/analyzers/_analyzer.py +0 -646
- sqlspec/statement/pipelines/context.py +0 -115
- sqlspec/statement/pipelines/transformers/__init__.py +0 -7
- sqlspec/statement/pipelines/transformers/_expression_simplifier.py +0 -88
- sqlspec/statement/pipelines/transformers/_literal_parameterizer.py +0 -1247
- sqlspec/statement/pipelines/transformers/_remove_comments_and_hints.py +0 -76
- sqlspec/statement/pipelines/validators/__init__.py +0 -23
- sqlspec/statement/pipelines/validators/_dml_safety.py +0 -290
- sqlspec/statement/pipelines/validators/_parameter_style.py +0 -370
- sqlspec/statement/pipelines/validators/_performance.py +0 -718
- sqlspec/statement/pipelines/validators/_security.py +0 -967
- sqlspec/statement/result.py +0 -435
- sqlspec/statement/sql.py +0 -1704
- sqlspec/statement/sql_compiler.py +0 -140
- sqlspec/utils/cached_property.py +0 -25
- sqlspec-0.13.1.dist-info/RECORD +0 -150
- {sqlspec-0.13.1.dist-info → sqlspec-0.16.2.dist-info}/WHEEL +0 -0
- {sqlspec-0.13.1.dist-info → sqlspec-0.16.2.dist-info}/licenses/LICENSE +0 -0
- {sqlspec-0.13.1.dist-info → sqlspec-0.16.2.dist-info}/licenses/NOTICE +0 -0
|
@@ -1,17 +1,6 @@
|
|
|
1
1
|
"""Driver mixins for instrumentation, storage, and utilities."""
|
|
2
2
|
|
|
3
|
-
from sqlspec.driver.mixins.
|
|
4
|
-
from sqlspec.driver.mixins._result_utils import ToSchemaMixin
|
|
3
|
+
from sqlspec.driver.mixins._result_tools import ToSchemaMixin
|
|
5
4
|
from sqlspec.driver.mixins._sql_translator import SQLTranslatorMixin
|
|
6
|
-
from sqlspec.driver.mixins._storage import AsyncStorageMixin, SyncStorageMixin
|
|
7
|
-
from sqlspec.driver.mixins._type_coercion import TypeCoercionMixin
|
|
8
5
|
|
|
9
|
-
__all__ = (
|
|
10
|
-
"AsyncPipelinedExecutionMixin",
|
|
11
|
-
"AsyncStorageMixin",
|
|
12
|
-
"SQLTranslatorMixin",
|
|
13
|
-
"SyncPipelinedExecutionMixin",
|
|
14
|
-
"SyncStorageMixin",
|
|
15
|
-
"ToSchemaMixin",
|
|
16
|
-
"TypeCoercionMixin",
|
|
17
|
-
)
|
|
6
|
+
__all__ = ("SQLTranslatorMixin", "ToSchemaMixin")
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# pyright: reportCallIssue=false, reportAttributeAccessIssue=false, reportArgumentType=false
|
|
2
|
+
import datetime
|
|
3
|
+
import logging
|
|
4
|
+
from collections.abc import Sequence
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from functools import partial
|
|
7
|
+
from pathlib import Path, PurePath
|
|
8
|
+
from typing import Any, Callable, Final, Optional, overload
|
|
9
|
+
from uuid import UUID
|
|
10
|
+
|
|
11
|
+
from mypy_extensions import trait
|
|
12
|
+
|
|
13
|
+
from sqlspec.exceptions import SQLSpecError
|
|
14
|
+
from sqlspec.typing import (
|
|
15
|
+
CATTRS_INSTALLED,
|
|
16
|
+
ModelDTOT,
|
|
17
|
+
ModelT,
|
|
18
|
+
attrs_asdict,
|
|
19
|
+
cattrs_structure,
|
|
20
|
+
cattrs_unstructure,
|
|
21
|
+
convert,
|
|
22
|
+
get_type_adapter,
|
|
23
|
+
)
|
|
24
|
+
from sqlspec.utils.type_guards import is_attrs_schema, is_dataclass, is_msgspec_struct, is_pydantic_model
|
|
25
|
+
|
|
26
|
+
__all__ = ("_DEFAULT_TYPE_DECODERS", "_default_msgspec_deserializer")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
# Constants for performance optimization
|
|
32
|
+
_DATETIME_TYPES: Final[set[type]] = {datetime.datetime, datetime.date, datetime.time}
|
|
33
|
+
_PATH_TYPES: Final[tuple[type, ...]] = (Path, PurePath, UUID)
|
|
34
|
+
|
|
35
|
+
_DEFAULT_TYPE_DECODERS: Final[list[tuple[Callable[[Any], bool], Callable[[Any, Any], Any]]]] = [
|
|
36
|
+
(lambda x: x is UUID, lambda t, v: t(v.hex)),
|
|
37
|
+
(lambda x: x is datetime.datetime, lambda t, v: t(v.isoformat())),
|
|
38
|
+
(lambda x: x is datetime.date, lambda t, v: t(v.isoformat())),
|
|
39
|
+
(lambda x: x is datetime.time, lambda t, v: t(v.isoformat())),
|
|
40
|
+
(lambda x: x is Enum, lambda t, v: t(v.value)),
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _default_msgspec_deserializer(
|
|
45
|
+
target_type: Any, value: Any, type_decoders: "Optional[Sequence[tuple[Any, Any]]]" = None
|
|
46
|
+
) -> Any:
|
|
47
|
+
"""Default msgspec deserializer with type conversion support.
|
|
48
|
+
|
|
49
|
+
Converts values to appropriate types for msgspec deserialization, including
|
|
50
|
+
UUID, datetime, date, time, Enum, Path, and PurePath types.
|
|
51
|
+
"""
|
|
52
|
+
if type_decoders:
|
|
53
|
+
for predicate, decoder in type_decoders:
|
|
54
|
+
if predicate(target_type):
|
|
55
|
+
return decoder(target_type, value)
|
|
56
|
+
|
|
57
|
+
# Fast path checks using type identity and isinstance
|
|
58
|
+
if target_type is UUID and isinstance(value, UUID):
|
|
59
|
+
return value.hex
|
|
60
|
+
|
|
61
|
+
# Use pre-computed set for faster lookup
|
|
62
|
+
if target_type in _DATETIME_TYPES:
|
|
63
|
+
try:
|
|
64
|
+
return value.isoformat()
|
|
65
|
+
except AttributeError:
|
|
66
|
+
pass
|
|
67
|
+
|
|
68
|
+
if isinstance(target_type, type) and issubclass(target_type, Enum) and isinstance(value, Enum):
|
|
69
|
+
return value.value
|
|
70
|
+
|
|
71
|
+
if isinstance(value, target_type):
|
|
72
|
+
return value
|
|
73
|
+
|
|
74
|
+
# Check for path types using pre-computed tuple
|
|
75
|
+
if isinstance(target_type, type):
|
|
76
|
+
try:
|
|
77
|
+
if issubclass(target_type, (Path, PurePath)) or issubclass(target_type, UUID):
|
|
78
|
+
return target_type(str(value))
|
|
79
|
+
except (TypeError, ValueError):
|
|
80
|
+
pass
|
|
81
|
+
|
|
82
|
+
return value
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@trait
|
|
86
|
+
class ToSchemaMixin:
|
|
87
|
+
__slots__ = ()
|
|
88
|
+
|
|
89
|
+
# Schema conversion overloads - handle common cases first
|
|
90
|
+
@overload
|
|
91
|
+
@staticmethod
|
|
92
|
+
def to_schema(data: "list[dict[str, Any]]") -> "list[dict[str, Any]]": ...
|
|
93
|
+
@overload
|
|
94
|
+
@staticmethod
|
|
95
|
+
def to_schema(data: "list[dict[str, Any]]", *, schema_type: "type[ModelDTOT]") -> "list[ModelDTOT]": ...
|
|
96
|
+
@overload
|
|
97
|
+
@staticmethod
|
|
98
|
+
def to_schema(data: "list[dict[str, Any]]", *, schema_type: None = None) -> "list[dict[str, Any]]": ...
|
|
99
|
+
@overload
|
|
100
|
+
@staticmethod
|
|
101
|
+
def to_schema(data: "dict[str, Any]") -> "dict[str, Any]": ...
|
|
102
|
+
@overload
|
|
103
|
+
@staticmethod
|
|
104
|
+
def to_schema(data: "dict[str, Any]", *, schema_type: "type[ModelDTOT]") -> "ModelDTOT": ...
|
|
105
|
+
@overload
|
|
106
|
+
@staticmethod
|
|
107
|
+
def to_schema(data: "dict[str, Any]", *, schema_type: None = None) -> "dict[str, Any]": ...
|
|
108
|
+
@overload
|
|
109
|
+
@staticmethod
|
|
110
|
+
def to_schema(data: "list[ModelT]") -> "list[ModelT]": ...
|
|
111
|
+
@overload
|
|
112
|
+
@staticmethod
|
|
113
|
+
def to_schema(data: "list[ModelT]", *, schema_type: "type[ModelDTOT]") -> "list[ModelDTOT]": ...
|
|
114
|
+
@overload
|
|
115
|
+
@staticmethod
|
|
116
|
+
def to_schema(data: "list[ModelT]", *, schema_type: None = None) -> "list[ModelT]": ...
|
|
117
|
+
@overload
|
|
118
|
+
@staticmethod
|
|
119
|
+
def to_schema(data: "ModelT") -> "ModelT": ...
|
|
120
|
+
@overload
|
|
121
|
+
@staticmethod
|
|
122
|
+
def to_schema(data: Any, *, schema_type: None = None) -> Any: ...
|
|
123
|
+
|
|
124
|
+
@staticmethod
|
|
125
|
+
def to_schema(data: Any, *, schema_type: "Optional[type[ModelDTOT]]" = None) -> Any:
|
|
126
|
+
"""Convert data to a specified schema type.
|
|
127
|
+
|
|
128
|
+
Supports conversion to dataclasses, msgspec structs, Pydantic models, and attrs classes.
|
|
129
|
+
Handles both single objects and sequences.
|
|
130
|
+
|
|
131
|
+
Raises:
|
|
132
|
+
SQLSpecError if `schema_type` is not a valid type.
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
Converted data in the specified schema type.
|
|
136
|
+
|
|
137
|
+
"""
|
|
138
|
+
if schema_type is None:
|
|
139
|
+
return data
|
|
140
|
+
if is_dataclass(schema_type):
|
|
141
|
+
if isinstance(data, list):
|
|
142
|
+
result: list[Any] = []
|
|
143
|
+
for item in data:
|
|
144
|
+
if hasattr(item, "keys"):
|
|
145
|
+
result.append(schema_type(**dict(item))) # type: ignore[operator]
|
|
146
|
+
else:
|
|
147
|
+
result.append(item)
|
|
148
|
+
return result
|
|
149
|
+
if hasattr(data, "keys"):
|
|
150
|
+
return schema_type(**dict(data)) # type: ignore[operator]
|
|
151
|
+
if isinstance(data, dict):
|
|
152
|
+
return schema_type(**data) # type: ignore[operator]
|
|
153
|
+
return data
|
|
154
|
+
if is_msgspec_struct(schema_type):
|
|
155
|
+
# Cache the deserializer to avoid repeated partial() calls
|
|
156
|
+
deserializer = partial(_default_msgspec_deserializer, type_decoders=_DEFAULT_TYPE_DECODERS)
|
|
157
|
+
if not isinstance(data, Sequence):
|
|
158
|
+
return convert(obj=data, type=schema_type, from_attributes=True, dec_hook=deserializer)
|
|
159
|
+
return convert(
|
|
160
|
+
obj=data,
|
|
161
|
+
type=list[schema_type], # type: ignore[valid-type] # pyright: ignore
|
|
162
|
+
from_attributes=True,
|
|
163
|
+
dec_hook=deserializer,
|
|
164
|
+
)
|
|
165
|
+
if is_pydantic_model(schema_type):
|
|
166
|
+
if not isinstance(data, Sequence):
|
|
167
|
+
adapter = get_type_adapter(schema_type)
|
|
168
|
+
return adapter.validate_python(data, from_attributes=True) # pyright: ignore
|
|
169
|
+
list_adapter = get_type_adapter(list[schema_type]) # type: ignore[valid-type] # pyright: ignore
|
|
170
|
+
return list_adapter.validate_python(data, from_attributes=True)
|
|
171
|
+
if is_attrs_schema(schema_type):
|
|
172
|
+
if CATTRS_INSTALLED:
|
|
173
|
+
if isinstance(data, Sequence):
|
|
174
|
+
return cattrs_structure(data, list[schema_type]) # type: ignore[valid-type] # pyright: ignore
|
|
175
|
+
if hasattr(data, "__attrs_attrs__"):
|
|
176
|
+
unstructured_data = cattrs_unstructure(data)
|
|
177
|
+
return cattrs_structure(unstructured_data, schema_type) # pyright: ignore
|
|
178
|
+
return cattrs_structure(data, schema_type) # pyright: ignore
|
|
179
|
+
if isinstance(data, list):
|
|
180
|
+
attrs_result: list[Any] = []
|
|
181
|
+
for item in data:
|
|
182
|
+
if hasattr(item, "keys"):
|
|
183
|
+
attrs_result.append(schema_type(**dict(item)))
|
|
184
|
+
else:
|
|
185
|
+
attrs_result.append(schema_type(**attrs_asdict(item)))
|
|
186
|
+
return attrs_result
|
|
187
|
+
if hasattr(data, "keys"):
|
|
188
|
+
return schema_type(**dict(data))
|
|
189
|
+
if isinstance(data, dict):
|
|
190
|
+
return schema_type(**data)
|
|
191
|
+
return data
|
|
192
|
+
msg = "`schema_type` should be a valid Dataclass, Pydantic model, Msgspec struct, or Attrs class"
|
|
193
|
+
raise SQLSpecError(msg)
|
|
@@ -1,35 +1,86 @@
|
|
|
1
|
+
from typing import Final, NoReturn, Optional
|
|
2
|
+
|
|
3
|
+
from mypy_extensions import trait
|
|
1
4
|
from sqlglot import exp, parse_one
|
|
2
5
|
from sqlglot.dialects.dialect import DialectType
|
|
3
6
|
|
|
7
|
+
from sqlspec.core.statement import SQL, Statement
|
|
4
8
|
from sqlspec.exceptions import SQLConversionError
|
|
5
|
-
from sqlspec.statement.sql import SQL, Statement
|
|
6
9
|
|
|
7
10
|
__all__ = ("SQLTranslatorMixin",)
|
|
8
11
|
|
|
12
|
+
# Constants for better performance
|
|
13
|
+
_DEFAULT_PRETTY: Final[bool] = True
|
|
14
|
+
|
|
9
15
|
|
|
16
|
+
@trait
|
|
10
17
|
class SQLTranslatorMixin:
|
|
11
18
|
"""Mixin for drivers supporting SQL translation."""
|
|
12
19
|
|
|
13
20
|
__slots__ = ()
|
|
14
21
|
|
|
15
|
-
def convert_to_dialect(
|
|
16
|
-
|
|
22
|
+
def convert_to_dialect(
|
|
23
|
+
self, statement: "Statement", to_dialect: "Optional[DialectType]" = None, pretty: bool = _DEFAULT_PRETTY
|
|
24
|
+
) -> str:
|
|
25
|
+
"""Convert a statement to a target SQL dialect.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
statement: SQL statement to convert
|
|
29
|
+
to_dialect: Target dialect (defaults to current dialect)
|
|
30
|
+
pretty: Whether to format the output SQL
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
SQL string in target dialect
|
|
34
|
+
|
|
35
|
+
Raises:
|
|
36
|
+
SQLConversionError: If parsing or conversion fails
|
|
37
|
+
"""
|
|
38
|
+
# Fast path: get the parsed expression with minimal allocations
|
|
39
|
+
parsed_expression: Optional[exp.Expression] = None
|
|
40
|
+
|
|
17
41
|
if statement is not None and isinstance(statement, SQL):
|
|
18
42
|
if statement.expression is None:
|
|
19
|
-
|
|
20
|
-
raise SQLConversionError(msg)
|
|
43
|
+
self._raise_statement_parse_error()
|
|
21
44
|
parsed_expression = statement.expression
|
|
22
45
|
elif isinstance(statement, exp.Expression):
|
|
23
46
|
parsed_expression = statement
|
|
24
47
|
else:
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
48
|
+
parsed_expression = self._parse_statement_safely(statement)
|
|
49
|
+
|
|
50
|
+
# Get target dialect with fallback
|
|
51
|
+
target_dialect = to_dialect or self.dialect # type: ignore[attr-defined]
|
|
52
|
+
|
|
53
|
+
# Generate SQL with error handling
|
|
54
|
+
return self._generate_sql_safely(parsed_expression, target_dialect, pretty)
|
|
55
|
+
|
|
56
|
+
def _parse_statement_safely(self, statement: "Statement") -> "exp.Expression":
|
|
57
|
+
"""Parse statement with copy=False optimization and proper error handling."""
|
|
58
|
+
try:
|
|
59
|
+
# Convert statement to string if needed
|
|
60
|
+
sql_string = str(statement)
|
|
61
|
+
# Use copy=False for better performance
|
|
62
|
+
return parse_one(sql_string, dialect=self.dialect, copy=False) # type: ignore[attr-defined]
|
|
63
|
+
except Exception as e:
|
|
64
|
+
self._raise_parse_error(e)
|
|
65
|
+
|
|
66
|
+
def _generate_sql_safely(self, expression: "exp.Expression", dialect: DialectType, pretty: bool) -> str:
|
|
67
|
+
"""Generate SQL with proper error handling."""
|
|
31
68
|
try:
|
|
32
|
-
return
|
|
69
|
+
return expression.sql(dialect=dialect, pretty=pretty)
|
|
33
70
|
except Exception as e:
|
|
34
|
-
|
|
35
|
-
|
|
71
|
+
self._raise_conversion_error(dialect, e)
|
|
72
|
+
|
|
73
|
+
def _raise_statement_parse_error(self) -> NoReturn:
|
|
74
|
+
"""Raise error for unparsable statements."""
|
|
75
|
+
msg = "Statement could not be parsed"
|
|
76
|
+
raise SQLConversionError(msg)
|
|
77
|
+
|
|
78
|
+
def _raise_parse_error(self, e: Exception) -> NoReturn:
|
|
79
|
+
"""Raise error for parsing failures."""
|
|
80
|
+
error_msg = f"Failed to parse SQL statement: {e!s}"
|
|
81
|
+
raise SQLConversionError(error_msg) from e
|
|
82
|
+
|
|
83
|
+
def _raise_conversion_error(self, dialect: DialectType, e: Exception) -> NoReturn:
|
|
84
|
+
"""Raise error for conversion failures."""
|
|
85
|
+
error_msg = f"Failed to convert SQL expression to {dialect}: {e!s}"
|
|
86
|
+
raise SQLConversionError(error_msg) from e
|
sqlspec/exceptions.py
CHANGED
|
@@ -1,48 +1,33 @@
|
|
|
1
1
|
from collections.abc import Generator
|
|
2
2
|
from contextlib import contextmanager
|
|
3
|
-
from
|
|
4
|
-
from typing import Any, Optional, Union, cast
|
|
3
|
+
from typing import Any, Optional, Union
|
|
5
4
|
|
|
6
5
|
__all__ = (
|
|
7
|
-
"ExtraParameterError",
|
|
8
6
|
"FileNotFoundInStorageError",
|
|
9
7
|
"ImproperConfigurationError",
|
|
10
8
|
"IntegrityError",
|
|
11
9
|
"MissingDependencyError",
|
|
12
|
-
"MissingParameterError",
|
|
13
10
|
"MultipleResultsFoundError",
|
|
14
11
|
"NotFoundError",
|
|
15
|
-
"ParameterError",
|
|
16
|
-
"ParameterStyleMismatchError",
|
|
17
|
-
"PipelineExecutionError",
|
|
18
|
-
"QueryError",
|
|
19
12
|
"RepositoryError",
|
|
20
|
-
"RiskLevel",
|
|
21
13
|
"SQLBuilderError",
|
|
22
|
-
"SQLCompilationError",
|
|
23
14
|
"SQLConversionError",
|
|
24
15
|
"SQLFileNotFoundError",
|
|
25
16
|
"SQLFileParseError",
|
|
26
|
-
"SQLFileParsingError",
|
|
27
|
-
"SQLInjectionError",
|
|
28
17
|
"SQLParsingError",
|
|
29
18
|
"SQLSpecError",
|
|
30
|
-
"SQLTransformationError",
|
|
31
|
-
"SQLValidationError",
|
|
32
19
|
"SerializationError",
|
|
33
20
|
"StorageOperationFailedError",
|
|
34
|
-
"UnknownParameterError",
|
|
35
|
-
"UnsafeSQLError",
|
|
36
21
|
)
|
|
37
22
|
|
|
38
23
|
|
|
39
24
|
class SQLSpecError(Exception):
|
|
40
|
-
"""Base exception class
|
|
25
|
+
"""Base exception class for SQLSpec exceptions."""
|
|
41
26
|
|
|
42
27
|
detail: str
|
|
43
28
|
|
|
44
29
|
def __init__(self, *args: Any, detail: str = "") -> None:
|
|
45
|
-
"""Initialize
|
|
30
|
+
"""Initialize SQLSpecError.
|
|
46
31
|
|
|
47
32
|
Args:
|
|
48
33
|
*args: args are converted to :class:`str` before passing to :class:`Exception`
|
|
@@ -67,10 +52,7 @@ class SQLSpecError(Exception):
|
|
|
67
52
|
|
|
68
53
|
|
|
69
54
|
class MissingDependencyError(SQLSpecError, ImportError):
|
|
70
|
-
"""
|
|
71
|
-
|
|
72
|
-
This exception is raised only when a module depends on a dependency that has not been installed.
|
|
73
|
-
"""
|
|
55
|
+
"""Raised when a required dependency is not installed."""
|
|
74
56
|
|
|
75
57
|
def __init__(self, package: str, install_package: Optional[str] = None) -> None:
|
|
76
58
|
super().__init__(
|
|
@@ -87,15 +69,6 @@ class BackendNotRegisteredError(SQLSpecError):
|
|
|
87
69
|
super().__init__(f"Storage backend '{backend_key}' is not registered. Please register it before use.")
|
|
88
70
|
|
|
89
71
|
|
|
90
|
-
class SQLLoadingError(SQLSpecError):
|
|
91
|
-
"""Issues loading referenced SQL file."""
|
|
92
|
-
|
|
93
|
-
def __init__(self, message: Optional[str] = None) -> None:
|
|
94
|
-
if message is None:
|
|
95
|
-
message = "Issues loading referenced SQL file."
|
|
96
|
-
super().__init__(message)
|
|
97
|
-
|
|
98
|
-
|
|
99
72
|
class SQLParsingError(SQLSpecError):
|
|
100
73
|
"""Issues parsing SQL statements."""
|
|
101
74
|
|
|
@@ -105,15 +78,6 @@ class SQLParsingError(SQLSpecError):
|
|
|
105
78
|
super().__init__(message)
|
|
106
79
|
|
|
107
80
|
|
|
108
|
-
class SQLFileParsingError(SQLSpecError):
|
|
109
|
-
"""Issues parsing SQL files."""
|
|
110
|
-
|
|
111
|
-
def __init__(self, message: Optional[str] = None) -> None:
|
|
112
|
-
if message is None:
|
|
113
|
-
message = "Issues parsing SQL files."
|
|
114
|
-
super().__init__(message)
|
|
115
|
-
|
|
116
|
-
|
|
117
81
|
class SQLBuilderError(SQLSpecError):
|
|
118
82
|
"""Issues Building or Generating SQL statements."""
|
|
119
83
|
|
|
@@ -123,15 +87,6 @@ class SQLBuilderError(SQLSpecError):
|
|
|
123
87
|
super().__init__(message)
|
|
124
88
|
|
|
125
89
|
|
|
126
|
-
class SQLCompilationError(SQLSpecError):
|
|
127
|
-
"""Issues Compiling SQL statements."""
|
|
128
|
-
|
|
129
|
-
def __init__(self, message: Optional[str] = None) -> None:
|
|
130
|
-
if message is None:
|
|
131
|
-
message = "Issues compiling SQL statement."
|
|
132
|
-
super().__init__(message)
|
|
133
|
-
|
|
134
|
-
|
|
135
90
|
class SQLConversionError(SQLSpecError):
|
|
136
91
|
"""Issues converting SQL statements."""
|
|
137
92
|
|
|
@@ -141,170 +96,8 @@ class SQLConversionError(SQLSpecError):
|
|
|
141
96
|
super().__init__(message)
|
|
142
97
|
|
|
143
98
|
|
|
144
|
-
# -- SQL Validation Errors --
|
|
145
|
-
class RiskLevel(Enum):
|
|
146
|
-
"""SQL risk assessment levels."""
|
|
147
|
-
|
|
148
|
-
SKIP = 1
|
|
149
|
-
SAFE = 2
|
|
150
|
-
LOW = 3
|
|
151
|
-
MEDIUM = 4
|
|
152
|
-
HIGH = 5
|
|
153
|
-
CRITICAL = 6
|
|
154
|
-
|
|
155
|
-
def __str__(self) -> str:
|
|
156
|
-
"""String representation.
|
|
157
|
-
|
|
158
|
-
Returns:
|
|
159
|
-
Lowercase name of the style.
|
|
160
|
-
"""
|
|
161
|
-
return self.name.lower()
|
|
162
|
-
|
|
163
|
-
def __lt__(self, other: "RiskLevel") -> bool: # pragma: no cover
|
|
164
|
-
"""Less than comparison for ordering."""
|
|
165
|
-
if not isinstance(other, RiskLevel):
|
|
166
|
-
return NotImplemented
|
|
167
|
-
return self.value < other.value
|
|
168
|
-
|
|
169
|
-
def __le__(self, other: "RiskLevel") -> bool: # pragma: no cover
|
|
170
|
-
"""Less than or equal comparison for ordering."""
|
|
171
|
-
if not isinstance(other, RiskLevel):
|
|
172
|
-
return NotImplemented
|
|
173
|
-
return self.value <= other.value
|
|
174
|
-
|
|
175
|
-
def __gt__(self, other: "RiskLevel") -> bool: # pragma: no cover
|
|
176
|
-
"""Greater than comparison for ordering."""
|
|
177
|
-
if not isinstance(other, RiskLevel):
|
|
178
|
-
return NotImplemented
|
|
179
|
-
return self.value > other.value
|
|
180
|
-
|
|
181
|
-
def __ge__(self, other: "RiskLevel") -> bool: # pragma: no cover
|
|
182
|
-
"""Greater than or equal comparison for ordering."""
|
|
183
|
-
if not isinstance(other, RiskLevel):
|
|
184
|
-
return NotImplemented
|
|
185
|
-
return self.value >= other.value
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
class SQLValidationError(SQLSpecError):
|
|
189
|
-
"""Base class for SQL validation errors."""
|
|
190
|
-
|
|
191
|
-
sql: Optional[str]
|
|
192
|
-
risk_level: RiskLevel
|
|
193
|
-
|
|
194
|
-
def __init__(self, message: str, sql: Optional[str] = None, risk_level: RiskLevel = RiskLevel.MEDIUM) -> None:
|
|
195
|
-
"""Initialize with SQL context and risk level."""
|
|
196
|
-
detail_message = message
|
|
197
|
-
if sql is not None:
|
|
198
|
-
detail_message = f"{message}\nSQL: {sql}"
|
|
199
|
-
super().__init__(detail=detail_message)
|
|
200
|
-
self.sql = sql
|
|
201
|
-
self.risk_level = risk_level
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
class SQLTransformationError(SQLSpecError):
|
|
205
|
-
"""Base class for SQL transformation errors."""
|
|
206
|
-
|
|
207
|
-
sql: Optional[str]
|
|
208
|
-
|
|
209
|
-
def __init__(self, message: str, sql: Optional[str] = None) -> None:
|
|
210
|
-
"""Initialize with SQL context and risk level."""
|
|
211
|
-
detail_message = message
|
|
212
|
-
if sql is not None:
|
|
213
|
-
detail_message = f"{message}\nSQL: {sql}"
|
|
214
|
-
super().__init__(detail=detail_message)
|
|
215
|
-
self.sql = sql
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
class SQLInjectionError(SQLValidationError):
|
|
219
|
-
"""Raised when potential SQL injection is detected."""
|
|
220
|
-
|
|
221
|
-
pattern: Optional[str]
|
|
222
|
-
|
|
223
|
-
def __init__(self, message: str, sql: Optional[str] = None, pattern: Optional[str] = None) -> None:
|
|
224
|
-
"""Initialize with injection pattern context."""
|
|
225
|
-
detail_message = message
|
|
226
|
-
if pattern:
|
|
227
|
-
detail_message = f"{message} (Pattern: {pattern})"
|
|
228
|
-
super().__init__(detail_message, sql, RiskLevel.CRITICAL)
|
|
229
|
-
self.pattern = pattern
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
class UnsafeSQLError(SQLValidationError):
|
|
233
|
-
"""Raised when unsafe SQL constructs are detected."""
|
|
234
|
-
|
|
235
|
-
construct: Optional[str]
|
|
236
|
-
|
|
237
|
-
def __init__(self, message: str, sql: Optional[str] = None, construct: Optional[str] = None) -> None:
|
|
238
|
-
"""Initialize with unsafe construct context."""
|
|
239
|
-
detail_message = message
|
|
240
|
-
if construct:
|
|
241
|
-
detail_message = f"{message} (Construct: {construct})"
|
|
242
|
-
super().__init__(detail_message, sql, RiskLevel.HIGH)
|
|
243
|
-
self.construct = construct
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
# -- SQL Query Errors --
|
|
247
|
-
class QueryError(SQLSpecError):
|
|
248
|
-
"""Base class for Query errors."""
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
# -- SQL Parameter Errors --
|
|
252
|
-
class ParameterError(SQLSpecError):
|
|
253
|
-
"""Base class for parameter-related errors."""
|
|
254
|
-
|
|
255
|
-
sql: Optional[str]
|
|
256
|
-
|
|
257
|
-
def __init__(self, message: str, sql: Optional[str] = None) -> None:
|
|
258
|
-
"""Initialize with optional SQL context."""
|
|
259
|
-
detail_message = message
|
|
260
|
-
if sql is not None:
|
|
261
|
-
detail_message = f"{message}\nSQL: {sql}"
|
|
262
|
-
super().__init__(detail=detail_message)
|
|
263
|
-
self.sql = sql
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
class UnknownParameterError(ParameterError):
|
|
267
|
-
"""Raised when encountering unknown parameter syntax."""
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
class MissingParameterError(ParameterError):
|
|
271
|
-
"""Raised when required parameters are missing."""
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
class ExtraParameterError(ParameterError):
|
|
275
|
-
"""Raised when extra parameters are provided."""
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
class ParameterStyleMismatchError(SQLSpecError):
|
|
279
|
-
"""Error when parameter style doesn't match SQL placeholder style.
|
|
280
|
-
|
|
281
|
-
This exception is raised when there's a mismatch between the parameter type
|
|
282
|
-
(dictionary, tuple, etc.) and the placeholder style in the SQL query
|
|
283
|
-
(named, positional, etc.).
|
|
284
|
-
"""
|
|
285
|
-
|
|
286
|
-
sql: Optional[str]
|
|
287
|
-
|
|
288
|
-
def __init__(self, message: Optional[str] = None, sql: Optional[str] = None) -> None:
|
|
289
|
-
final_message = message
|
|
290
|
-
if final_message is None:
|
|
291
|
-
final_message = (
|
|
292
|
-
"Parameter style mismatch: dictionary parameters provided but no named placeholders found in SQL."
|
|
293
|
-
)
|
|
294
|
-
|
|
295
|
-
detail_message = final_message
|
|
296
|
-
if sql:
|
|
297
|
-
detail_message = f"{final_message}\nSQL: {sql}"
|
|
298
|
-
|
|
299
|
-
super().__init__(detail=detail_message)
|
|
300
|
-
self.sql = sql
|
|
301
|
-
|
|
302
|
-
|
|
303
99
|
class ImproperConfigurationError(SQLSpecError):
|
|
304
|
-
"""
|
|
305
|
-
|
|
306
|
-
This exception is raised only when a module depends on a dependency that has not been installed.
|
|
307
|
-
"""
|
|
100
|
+
"""Raised when configuration is invalid or incomplete."""
|
|
308
101
|
|
|
309
102
|
|
|
310
103
|
class SerializationError(SQLSpecError):
|
|
@@ -398,43 +191,3 @@ def wrap_exceptions(
|
|
|
398
191
|
raise
|
|
399
192
|
msg = "An error occurred during the operation."
|
|
400
193
|
raise RepositoryError(detail=msg) from exc
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
class PipelineExecutionError(SQLSpecError):
|
|
404
|
-
"""Rich error information for pipeline execution failures."""
|
|
405
|
-
|
|
406
|
-
def __init__(
|
|
407
|
-
self,
|
|
408
|
-
message: str,
|
|
409
|
-
*,
|
|
410
|
-
operation_index: "Optional[int]" = None,
|
|
411
|
-
failed_operation: "Optional[Any]" = None,
|
|
412
|
-
partial_results: "Optional[list[Any]]" = None,
|
|
413
|
-
driver_error: "Optional[Exception]" = None,
|
|
414
|
-
) -> None:
|
|
415
|
-
"""Initialize the pipeline execution error.
|
|
416
|
-
|
|
417
|
-
Args:
|
|
418
|
-
message: Error message describing the failure
|
|
419
|
-
operation_index: Index of the operation that failed
|
|
420
|
-
failed_operation: The PipelineOperation that failed
|
|
421
|
-
partial_results: Results from operations that succeeded before the failure
|
|
422
|
-
driver_error: Original exception from the database driver
|
|
423
|
-
"""
|
|
424
|
-
super().__init__(message)
|
|
425
|
-
self.operation_index = operation_index
|
|
426
|
-
self.failed_operation = failed_operation
|
|
427
|
-
self.partial_results = partial_results or []
|
|
428
|
-
self.driver_error = driver_error
|
|
429
|
-
|
|
430
|
-
def get_failed_sql(self) -> "Optional[str]":
|
|
431
|
-
"""Get the SQL that failed for debugging."""
|
|
432
|
-
if self.failed_operation and hasattr(self.failed_operation, "sql"):
|
|
433
|
-
return cast("str", self.failed_operation.sql.to_sql())
|
|
434
|
-
return None
|
|
435
|
-
|
|
436
|
-
def get_failed_parameters(self) -> "Optional[Any]":
|
|
437
|
-
"""Get the parameters that failed."""
|
|
438
|
-
if self.failed_operation and hasattr(self.failed_operation, "original_params"):
|
|
439
|
-
return self.failed_operation.original_params
|
|
440
|
-
return None
|