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.

Files changed (185) hide show
  1. sqlspec/__init__.py +71 -8
  2. sqlspec/__main__.py +12 -0
  3. sqlspec/__metadata__.py +1 -3
  4. sqlspec/_serialization.py +1 -2
  5. sqlspec/_sql.py +930 -136
  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 +116 -285
  10. sqlspec/adapters/adbc/driver.py +462 -340
  11. sqlspec/adapters/aiosqlite/__init__.py +18 -3
  12. sqlspec/adapters/aiosqlite/_types.py +13 -0
  13. sqlspec/adapters/aiosqlite/config.py +202 -150
  14. sqlspec/adapters/aiosqlite/driver.py +226 -247
  15. sqlspec/adapters/asyncmy/__init__.py +18 -3
  16. sqlspec/adapters/asyncmy/_types.py +12 -0
  17. sqlspec/adapters/asyncmy/config.py +80 -199
  18. sqlspec/adapters/asyncmy/driver.py +257 -215
  19. sqlspec/adapters/asyncpg/__init__.py +19 -4
  20. sqlspec/adapters/asyncpg/_types.py +17 -0
  21. sqlspec/adapters/asyncpg/config.py +81 -214
  22. sqlspec/adapters/asyncpg/driver.py +284 -359
  23. sqlspec/adapters/bigquery/__init__.py +17 -3
  24. sqlspec/adapters/bigquery/_types.py +12 -0
  25. sqlspec/adapters/bigquery/config.py +191 -299
  26. sqlspec/adapters/bigquery/driver.py +474 -634
  27. sqlspec/adapters/duckdb/__init__.py +14 -3
  28. sqlspec/adapters/duckdb/_types.py +12 -0
  29. sqlspec/adapters/duckdb/config.py +414 -397
  30. sqlspec/adapters/duckdb/driver.py +342 -393
  31. sqlspec/adapters/oracledb/__init__.py +19 -5
  32. sqlspec/adapters/oracledb/_types.py +14 -0
  33. sqlspec/adapters/oracledb/config.py +123 -458
  34. sqlspec/adapters/oracledb/driver.py +505 -531
  35. sqlspec/adapters/psqlpy/__init__.py +13 -3
  36. sqlspec/adapters/psqlpy/_types.py +11 -0
  37. sqlspec/adapters/psqlpy/config.py +93 -307
  38. sqlspec/adapters/psqlpy/driver.py +504 -213
  39. sqlspec/adapters/psycopg/__init__.py +19 -5
  40. sqlspec/adapters/psycopg/_types.py +17 -0
  41. sqlspec/adapters/psycopg/config.py +143 -472
  42. sqlspec/adapters/psycopg/driver.py +704 -825
  43. sqlspec/adapters/sqlite/__init__.py +14 -3
  44. sqlspec/adapters/sqlite/_types.py +11 -0
  45. sqlspec/adapters/sqlite/config.py +208 -142
  46. sqlspec/adapters/sqlite/driver.py +263 -278
  47. sqlspec/base.py +105 -9
  48. sqlspec/{statement/builder → builder}/__init__.py +12 -14
  49. sqlspec/{statement/builder/base.py → builder/_base.py} +184 -86
  50. sqlspec/{statement/builder/column.py → builder/_column.py} +97 -60
  51. sqlspec/{statement/builder/ddl.py → builder/_ddl.py} +61 -131
  52. sqlspec/{statement/builder → builder}/_ddl_utils.py +4 -10
  53. sqlspec/{statement/builder/delete.py → builder/_delete.py} +10 -30
  54. sqlspec/builder/_insert.py +421 -0
  55. sqlspec/builder/_merge.py +71 -0
  56. sqlspec/{statement/builder → builder}/_parsing_utils.py +49 -26
  57. sqlspec/builder/_select.py +170 -0
  58. sqlspec/{statement/builder/update.py → builder/_update.py} +16 -20
  59. sqlspec/builder/mixins/__init__.py +55 -0
  60. sqlspec/builder/mixins/_cte_and_set_ops.py +222 -0
  61. sqlspec/{statement/builder/mixins/_delete_from.py → builder/mixins/_delete_operations.py} +8 -1
  62. sqlspec/builder/mixins/_insert_operations.py +244 -0
  63. sqlspec/{statement/builder/mixins/_join.py → builder/mixins/_join_operations.py} +45 -13
  64. sqlspec/{statement/builder/mixins/_merge_clauses.py → builder/mixins/_merge_operations.py} +188 -30
  65. sqlspec/builder/mixins/_order_limit_operations.py +135 -0
  66. sqlspec/builder/mixins/_pivot_operations.py +153 -0
  67. sqlspec/builder/mixins/_select_operations.py +604 -0
  68. sqlspec/builder/mixins/_update_operations.py +202 -0
  69. sqlspec/builder/mixins/_where_clause.py +644 -0
  70. sqlspec/cli.py +247 -0
  71. sqlspec/config.py +183 -138
  72. sqlspec/core/__init__.py +63 -0
  73. sqlspec/core/cache.py +871 -0
  74. sqlspec/core/compiler.py +417 -0
  75. sqlspec/core/filters.py +830 -0
  76. sqlspec/core/hashing.py +310 -0
  77. sqlspec/core/parameters.py +1237 -0
  78. sqlspec/core/result.py +677 -0
  79. sqlspec/{statement → core}/splitter.py +321 -191
  80. sqlspec/core/statement.py +676 -0
  81. sqlspec/driver/__init__.py +7 -10
  82. sqlspec/driver/_async.py +422 -163
  83. sqlspec/driver/_common.py +545 -287
  84. sqlspec/driver/_sync.py +426 -160
  85. sqlspec/driver/mixins/__init__.py +2 -13
  86. sqlspec/driver/mixins/_result_tools.py +193 -0
  87. sqlspec/driver/mixins/_sql_translator.py +65 -14
  88. sqlspec/exceptions.py +5 -252
  89. sqlspec/extensions/aiosql/adapter.py +93 -96
  90. sqlspec/extensions/litestar/__init__.py +2 -1
  91. sqlspec/extensions/litestar/cli.py +48 -0
  92. sqlspec/extensions/litestar/config.py +0 -1
  93. sqlspec/extensions/litestar/handlers.py +15 -26
  94. sqlspec/extensions/litestar/plugin.py +21 -16
  95. sqlspec/extensions/litestar/providers.py +17 -52
  96. sqlspec/loader.py +423 -104
  97. sqlspec/migrations/__init__.py +35 -0
  98. sqlspec/migrations/base.py +414 -0
  99. sqlspec/migrations/commands.py +443 -0
  100. sqlspec/migrations/loaders.py +402 -0
  101. sqlspec/migrations/runner.py +213 -0
  102. sqlspec/migrations/tracker.py +140 -0
  103. sqlspec/migrations/utils.py +129 -0
  104. sqlspec/protocols.py +51 -186
  105. sqlspec/storage/__init__.py +1 -1
  106. sqlspec/storage/backends/base.py +37 -40
  107. sqlspec/storage/backends/fsspec.py +136 -112
  108. sqlspec/storage/backends/obstore.py +138 -160
  109. sqlspec/storage/capabilities.py +5 -4
  110. sqlspec/storage/registry.py +57 -106
  111. sqlspec/typing.py +136 -115
  112. sqlspec/utils/__init__.py +2 -2
  113. sqlspec/utils/correlation.py +0 -3
  114. sqlspec/utils/deprecation.py +6 -6
  115. sqlspec/utils/fixtures.py +6 -6
  116. sqlspec/utils/logging.py +0 -2
  117. sqlspec/utils/module_loader.py +7 -12
  118. sqlspec/utils/singleton.py +0 -1
  119. sqlspec/utils/sync_tools.py +17 -38
  120. sqlspec/utils/text.py +12 -51
  121. sqlspec/utils/type_guards.py +482 -235
  122. {sqlspec-0.13.1.dist-info → sqlspec-0.16.2.dist-info}/METADATA +7 -2
  123. sqlspec-0.16.2.dist-info/RECORD +134 -0
  124. sqlspec-0.16.2.dist-info/entry_points.txt +2 -0
  125. sqlspec/driver/connection.py +0 -207
  126. sqlspec/driver/mixins/_csv_writer.py +0 -91
  127. sqlspec/driver/mixins/_pipeline.py +0 -512
  128. sqlspec/driver/mixins/_result_utils.py +0 -140
  129. sqlspec/driver/mixins/_storage.py +0 -926
  130. sqlspec/driver/mixins/_type_coercion.py +0 -130
  131. sqlspec/driver/parameters.py +0 -138
  132. sqlspec/service/__init__.py +0 -4
  133. sqlspec/service/_util.py +0 -147
  134. sqlspec/service/base.py +0 -1131
  135. sqlspec/service/pagination.py +0 -26
  136. sqlspec/statement/__init__.py +0 -21
  137. sqlspec/statement/builder/insert.py +0 -288
  138. sqlspec/statement/builder/merge.py +0 -95
  139. sqlspec/statement/builder/mixins/__init__.py +0 -65
  140. sqlspec/statement/builder/mixins/_aggregate_functions.py +0 -250
  141. sqlspec/statement/builder/mixins/_case_builder.py +0 -91
  142. sqlspec/statement/builder/mixins/_common_table_expr.py +0 -90
  143. sqlspec/statement/builder/mixins/_from.py +0 -63
  144. sqlspec/statement/builder/mixins/_group_by.py +0 -118
  145. sqlspec/statement/builder/mixins/_having.py +0 -35
  146. sqlspec/statement/builder/mixins/_insert_from_select.py +0 -47
  147. sqlspec/statement/builder/mixins/_insert_into.py +0 -36
  148. sqlspec/statement/builder/mixins/_insert_values.py +0 -67
  149. sqlspec/statement/builder/mixins/_limit_offset.py +0 -53
  150. sqlspec/statement/builder/mixins/_order_by.py +0 -46
  151. sqlspec/statement/builder/mixins/_pivot.py +0 -79
  152. sqlspec/statement/builder/mixins/_returning.py +0 -37
  153. sqlspec/statement/builder/mixins/_select_columns.py +0 -61
  154. sqlspec/statement/builder/mixins/_set_ops.py +0 -122
  155. sqlspec/statement/builder/mixins/_unpivot.py +0 -77
  156. sqlspec/statement/builder/mixins/_update_from.py +0 -55
  157. sqlspec/statement/builder/mixins/_update_set.py +0 -94
  158. sqlspec/statement/builder/mixins/_update_table.py +0 -29
  159. sqlspec/statement/builder/mixins/_where.py +0 -401
  160. sqlspec/statement/builder/mixins/_window_functions.py +0 -86
  161. sqlspec/statement/builder/select.py +0 -221
  162. sqlspec/statement/filters.py +0 -596
  163. sqlspec/statement/parameter_manager.py +0 -220
  164. sqlspec/statement/parameters.py +0 -867
  165. sqlspec/statement/pipelines/__init__.py +0 -210
  166. sqlspec/statement/pipelines/analyzers/__init__.py +0 -9
  167. sqlspec/statement/pipelines/analyzers/_analyzer.py +0 -646
  168. sqlspec/statement/pipelines/context.py +0 -115
  169. sqlspec/statement/pipelines/transformers/__init__.py +0 -7
  170. sqlspec/statement/pipelines/transformers/_expression_simplifier.py +0 -88
  171. sqlspec/statement/pipelines/transformers/_literal_parameterizer.py +0 -1247
  172. sqlspec/statement/pipelines/transformers/_remove_comments_and_hints.py +0 -76
  173. sqlspec/statement/pipelines/validators/__init__.py +0 -23
  174. sqlspec/statement/pipelines/validators/_dml_safety.py +0 -290
  175. sqlspec/statement/pipelines/validators/_parameter_style.py +0 -370
  176. sqlspec/statement/pipelines/validators/_performance.py +0 -718
  177. sqlspec/statement/pipelines/validators/_security.py +0 -967
  178. sqlspec/statement/result.py +0 -435
  179. sqlspec/statement/sql.py +0 -1704
  180. sqlspec/statement/sql_compiler.py +0 -140
  181. sqlspec/utils/cached_property.py +0 -25
  182. sqlspec-0.13.1.dist-info/RECORD +0 -150
  183. {sqlspec-0.13.1.dist-info → sqlspec-0.16.2.dist-info}/WHEEL +0 -0
  184. {sqlspec-0.13.1.dist-info → sqlspec-0.16.2.dist-info}/licenses/LICENSE +0 -0
  185. {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._pipeline import AsyncPipelinedExecutionMixin, SyncPipelinedExecutionMixin
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(self, statement: "Statement", to_dialect: DialectType = None, pretty: bool = True) -> str:
16
- parsed_expression: exp.Expression
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
- msg = "Statement could not be parsed"
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
- try:
26
- parsed_expression = parse_one(statement, dialect=self.dialect) # type: ignore[attr-defined]
27
- except Exception as e:
28
- error_msg = f"Failed to parse SQL statement: {e!s}"
29
- raise SQLConversionError(error_msg) from e
30
- target_dialect = to_dialect if to_dialect is not None else self.dialect # type: ignore[attr-defined]
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 parsed_expression.sql(dialect=target_dialect, pretty=pretty)
69
+ return expression.sql(dialect=dialect, pretty=pretty)
33
70
  except Exception as e:
34
- error_msg = f"Failed to convert SQL expression to {target_dialect}: {e!s}"
35
- raise SQLConversionError(error_msg) from e
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 enum import Enum
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 from which all Advanced Alchemy exceptions inherit."""
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 ``AdvancedAlchemyException``.
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
- """Missing optional dependency.
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
- """Improper Configuration error.
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