sqlspec 0.16.1__cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.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 (148) hide show
  1. 51ff5a9eadfdefd49f98__mypyc.cpython-311-aarch64-linux-gnu.so +0 -0
  2. sqlspec/__init__.py +92 -0
  3. sqlspec/__main__.py +12 -0
  4. sqlspec/__metadata__.py +14 -0
  5. sqlspec/_serialization.py +77 -0
  6. sqlspec/_sql.py +1780 -0
  7. sqlspec/_typing.py +680 -0
  8. sqlspec/adapters/__init__.py +0 -0
  9. sqlspec/adapters/adbc/__init__.py +5 -0
  10. sqlspec/adapters/adbc/_types.py +12 -0
  11. sqlspec/adapters/adbc/config.py +361 -0
  12. sqlspec/adapters/adbc/driver.py +512 -0
  13. sqlspec/adapters/aiosqlite/__init__.py +19 -0
  14. sqlspec/adapters/aiosqlite/_types.py +13 -0
  15. sqlspec/adapters/aiosqlite/config.py +253 -0
  16. sqlspec/adapters/aiosqlite/driver.py +248 -0
  17. sqlspec/adapters/asyncmy/__init__.py +19 -0
  18. sqlspec/adapters/asyncmy/_types.py +12 -0
  19. sqlspec/adapters/asyncmy/config.py +180 -0
  20. sqlspec/adapters/asyncmy/driver.py +274 -0
  21. sqlspec/adapters/asyncpg/__init__.py +21 -0
  22. sqlspec/adapters/asyncpg/_types.py +17 -0
  23. sqlspec/adapters/asyncpg/config.py +229 -0
  24. sqlspec/adapters/asyncpg/driver.py +344 -0
  25. sqlspec/adapters/bigquery/__init__.py +18 -0
  26. sqlspec/adapters/bigquery/_types.py +12 -0
  27. sqlspec/adapters/bigquery/config.py +298 -0
  28. sqlspec/adapters/bigquery/driver.py +558 -0
  29. sqlspec/adapters/duckdb/__init__.py +22 -0
  30. sqlspec/adapters/duckdb/_types.py +12 -0
  31. sqlspec/adapters/duckdb/config.py +504 -0
  32. sqlspec/adapters/duckdb/driver.py +368 -0
  33. sqlspec/adapters/oracledb/__init__.py +32 -0
  34. sqlspec/adapters/oracledb/_types.py +14 -0
  35. sqlspec/adapters/oracledb/config.py +317 -0
  36. sqlspec/adapters/oracledb/driver.py +538 -0
  37. sqlspec/adapters/psqlpy/__init__.py +16 -0
  38. sqlspec/adapters/psqlpy/_types.py +11 -0
  39. sqlspec/adapters/psqlpy/config.py +214 -0
  40. sqlspec/adapters/psqlpy/driver.py +530 -0
  41. sqlspec/adapters/psycopg/__init__.py +32 -0
  42. sqlspec/adapters/psycopg/_types.py +17 -0
  43. sqlspec/adapters/psycopg/config.py +426 -0
  44. sqlspec/adapters/psycopg/driver.py +796 -0
  45. sqlspec/adapters/sqlite/__init__.py +15 -0
  46. sqlspec/adapters/sqlite/_types.py +11 -0
  47. sqlspec/adapters/sqlite/config.py +240 -0
  48. sqlspec/adapters/sqlite/driver.py +294 -0
  49. sqlspec/base.py +571 -0
  50. sqlspec/builder/__init__.py +62 -0
  51. sqlspec/builder/_base.py +473 -0
  52. sqlspec/builder/_column.py +320 -0
  53. sqlspec/builder/_ddl.py +1346 -0
  54. sqlspec/builder/_ddl_utils.py +103 -0
  55. sqlspec/builder/_delete.py +76 -0
  56. sqlspec/builder/_insert.py +256 -0
  57. sqlspec/builder/_merge.py +71 -0
  58. sqlspec/builder/_parsing_utils.py +140 -0
  59. sqlspec/builder/_select.py +170 -0
  60. sqlspec/builder/_update.py +188 -0
  61. sqlspec/builder/mixins/__init__.py +55 -0
  62. sqlspec/builder/mixins/_cte_and_set_ops.py +222 -0
  63. sqlspec/builder/mixins/_delete_operations.py +41 -0
  64. sqlspec/builder/mixins/_insert_operations.py +244 -0
  65. sqlspec/builder/mixins/_join_operations.py +122 -0
  66. sqlspec/builder/mixins/_merge_operations.py +476 -0
  67. sqlspec/builder/mixins/_order_limit_operations.py +135 -0
  68. sqlspec/builder/mixins/_pivot_operations.py +153 -0
  69. sqlspec/builder/mixins/_select_operations.py +603 -0
  70. sqlspec/builder/mixins/_update_operations.py +187 -0
  71. sqlspec/builder/mixins/_where_clause.py +621 -0
  72. sqlspec/cli.py +247 -0
  73. sqlspec/config.py +395 -0
  74. sqlspec/core/__init__.py +63 -0
  75. sqlspec/core/cache.cpython-311-aarch64-linux-gnu.so +0 -0
  76. sqlspec/core/cache.py +871 -0
  77. sqlspec/core/compiler.cpython-311-aarch64-linux-gnu.so +0 -0
  78. sqlspec/core/compiler.py +417 -0
  79. sqlspec/core/filters.cpython-311-aarch64-linux-gnu.so +0 -0
  80. sqlspec/core/filters.py +830 -0
  81. sqlspec/core/hashing.cpython-311-aarch64-linux-gnu.so +0 -0
  82. sqlspec/core/hashing.py +310 -0
  83. sqlspec/core/parameters.cpython-311-aarch64-linux-gnu.so +0 -0
  84. sqlspec/core/parameters.py +1237 -0
  85. sqlspec/core/result.cpython-311-aarch64-linux-gnu.so +0 -0
  86. sqlspec/core/result.py +677 -0
  87. sqlspec/core/splitter.cpython-311-aarch64-linux-gnu.so +0 -0
  88. sqlspec/core/splitter.py +819 -0
  89. sqlspec/core/statement.cpython-311-aarch64-linux-gnu.so +0 -0
  90. sqlspec/core/statement.py +676 -0
  91. sqlspec/driver/__init__.py +19 -0
  92. sqlspec/driver/_async.py +502 -0
  93. sqlspec/driver/_common.py +631 -0
  94. sqlspec/driver/_sync.py +503 -0
  95. sqlspec/driver/mixins/__init__.py +6 -0
  96. sqlspec/driver/mixins/_result_tools.py +193 -0
  97. sqlspec/driver/mixins/_sql_translator.py +86 -0
  98. sqlspec/exceptions.py +193 -0
  99. sqlspec/extensions/__init__.py +0 -0
  100. sqlspec/extensions/aiosql/__init__.py +10 -0
  101. sqlspec/extensions/aiosql/adapter.py +461 -0
  102. sqlspec/extensions/litestar/__init__.py +6 -0
  103. sqlspec/extensions/litestar/_utils.py +52 -0
  104. sqlspec/extensions/litestar/cli.py +48 -0
  105. sqlspec/extensions/litestar/config.py +92 -0
  106. sqlspec/extensions/litestar/handlers.py +260 -0
  107. sqlspec/extensions/litestar/plugin.py +145 -0
  108. sqlspec/extensions/litestar/providers.py +454 -0
  109. sqlspec/loader.cpython-311-aarch64-linux-gnu.so +0 -0
  110. sqlspec/loader.py +760 -0
  111. sqlspec/migrations/__init__.py +35 -0
  112. sqlspec/migrations/base.py +414 -0
  113. sqlspec/migrations/commands.py +443 -0
  114. sqlspec/migrations/loaders.py +402 -0
  115. sqlspec/migrations/runner.py +213 -0
  116. sqlspec/migrations/tracker.py +140 -0
  117. sqlspec/migrations/utils.py +129 -0
  118. sqlspec/protocols.py +407 -0
  119. sqlspec/py.typed +0 -0
  120. sqlspec/storage/__init__.py +23 -0
  121. sqlspec/storage/backends/__init__.py +0 -0
  122. sqlspec/storage/backends/base.py +163 -0
  123. sqlspec/storage/backends/fsspec.py +386 -0
  124. sqlspec/storage/backends/obstore.py +459 -0
  125. sqlspec/storage/capabilities.py +102 -0
  126. sqlspec/storage/registry.py +239 -0
  127. sqlspec/typing.py +299 -0
  128. sqlspec/utils/__init__.py +3 -0
  129. sqlspec/utils/correlation.py +150 -0
  130. sqlspec/utils/deprecation.py +106 -0
  131. sqlspec/utils/fixtures.cpython-311-aarch64-linux-gnu.so +0 -0
  132. sqlspec/utils/fixtures.py +58 -0
  133. sqlspec/utils/logging.py +127 -0
  134. sqlspec/utils/module_loader.py +89 -0
  135. sqlspec/utils/serializers.py +4 -0
  136. sqlspec/utils/singleton.py +32 -0
  137. sqlspec/utils/sync_tools.cpython-311-aarch64-linux-gnu.so +0 -0
  138. sqlspec/utils/sync_tools.py +237 -0
  139. sqlspec/utils/text.cpython-311-aarch64-linux-gnu.so +0 -0
  140. sqlspec/utils/text.py +96 -0
  141. sqlspec/utils/type_guards.cpython-311-aarch64-linux-gnu.so +0 -0
  142. sqlspec/utils/type_guards.py +1139 -0
  143. sqlspec-0.16.1.dist-info/METADATA +365 -0
  144. sqlspec-0.16.1.dist-info/RECORD +148 -0
  145. sqlspec-0.16.1.dist-info/WHEEL +7 -0
  146. sqlspec-0.16.1.dist-info/entry_points.txt +2 -0
  147. sqlspec-0.16.1.dist-info/licenses/LICENSE +21 -0
  148. sqlspec-0.16.1.dist-info/licenses/NOTICE +29 -0
@@ -0,0 +1,473 @@
1
+ """Safe SQL query builder with validation and parameter binding.
2
+
3
+ This module provides a fluent interface for building SQL queries safely,
4
+ with automatic parameter binding and validation.
5
+ """
6
+
7
+ from abc import ABC, abstractmethod
8
+ from typing import TYPE_CHECKING, Any, NoReturn, Optional, Union, cast
9
+
10
+ import sqlglot
11
+ from sqlglot import Dialect, exp
12
+ from sqlglot.dialects.dialect import DialectType
13
+ from sqlglot.errors import ParseError as SQLGlotParseError
14
+ from sqlglot.optimizer import optimize
15
+ from typing_extensions import Self
16
+
17
+ from sqlspec.core.cache import CacheKey, get_cache_config, get_default_cache
18
+ from sqlspec.core.hashing import hash_optimized_expression
19
+ from sqlspec.core.parameters import ParameterStyle, ParameterStyleConfig
20
+ from sqlspec.core.statement import SQL, StatementConfig
21
+ from sqlspec.exceptions import SQLBuilderError
22
+ from sqlspec.utils.logging import get_logger
23
+ from sqlspec.utils.type_guards import has_sql_method, has_with_method
24
+
25
+ if TYPE_CHECKING:
26
+ from sqlspec.core.result import SQLResult
27
+
28
+ __all__ = ("QueryBuilder", "SafeQuery")
29
+
30
+ logger = get_logger(__name__)
31
+
32
+
33
+ class SafeQuery:
34
+ """A safely constructed SQL query with bound parameters."""
35
+
36
+ __slots__ = ("dialect", "parameters", "sql")
37
+
38
+ def __init__(
39
+ self, sql: str, parameters: Optional[dict[str, Any]] = None, dialect: Optional[DialectType] = None
40
+ ) -> None:
41
+ self.sql = sql
42
+ self.parameters = parameters if parameters is not None else {}
43
+ self.dialect = dialect
44
+
45
+
46
+ class QueryBuilder(ABC):
47
+ """Abstract base class for SQL query builders with SQLGlot optimization.
48
+
49
+ Provides common functionality for dialect handling, parameter management,
50
+ query construction, and query optimization using SQLGlot.
51
+ """
52
+
53
+ __slots__ = (
54
+ "_expression",
55
+ "_parameter_counter",
56
+ "_parameters",
57
+ "_with_ctes",
58
+ "dialect",
59
+ "enable_optimization",
60
+ "optimize_joins",
61
+ "optimize_predicates",
62
+ "schema",
63
+ "simplify_expressions",
64
+ )
65
+
66
+ def __init__(
67
+ self,
68
+ dialect: Optional[DialectType] = None,
69
+ schema: Optional[dict[str, dict[str, str]]] = None,
70
+ enable_optimization: bool = True,
71
+ optimize_joins: bool = True,
72
+ optimize_predicates: bool = True,
73
+ simplify_expressions: bool = True,
74
+ ) -> None:
75
+ self.dialect = dialect
76
+ self.schema = schema
77
+ self.enable_optimization = enable_optimization
78
+ self.optimize_joins = optimize_joins
79
+ self.optimize_predicates = optimize_predicates
80
+ self.simplify_expressions = simplify_expressions
81
+
82
+ # Initialize mutable attributes
83
+ self._expression: Optional[exp.Expression] = None
84
+ self._parameters: dict[str, Any] = {}
85
+ self._parameter_counter: int = 0
86
+ self._with_ctes: dict[str, exp.CTE] = {}
87
+
88
+ def _initialize_expression(self) -> None:
89
+ """Initialize the base expression. Called after __init__."""
90
+ self._expression = self._create_base_expression()
91
+ if not self._expression:
92
+ self._raise_sql_builder_error(
93
+ "QueryBuilder._create_base_expression must return a valid sqlglot expression."
94
+ )
95
+
96
+ @abstractmethod
97
+ def _create_base_expression(self) -> exp.Expression:
98
+ """Create the base sqlglot expression for the specific query type.
99
+
100
+ Returns:
101
+ A new sqlglot expression appropriate for the query type.
102
+ """
103
+
104
+ @property
105
+ @abstractmethod
106
+ def _expected_result_type(self) -> "type[SQLResult]":
107
+ """The expected result type for the query being built.
108
+
109
+ Returns:
110
+ type[ResultT]: The type of the result.
111
+ """
112
+
113
+ @staticmethod
114
+ def _raise_sql_builder_error(message: str, cause: Optional[BaseException] = None) -> NoReturn:
115
+ """Helper to raise SQLBuilderError, potentially with a cause.
116
+
117
+ Args:
118
+ message: The error message.
119
+ cause: The optional original exception to chain.
120
+
121
+ Raises:
122
+ SQLBuilderError: Always raises this exception.
123
+ """
124
+ raise SQLBuilderError(message) from cause
125
+
126
+ def _add_parameter(self, value: Any, context: Optional[str] = None) -> str:
127
+ """Adds a parameter to the query and returns its placeholder name.
128
+
129
+ Args:
130
+ value: The value of the parameter.
131
+ context: Optional context hint for parameter naming (e.g., "where", "join")
132
+
133
+ Returns:
134
+ str: The placeholder name for the parameter (e.g., :param_1 or :where_param_1).
135
+ """
136
+ self._parameter_counter += 1
137
+
138
+ param_name = f"{context}_param_{self._parameter_counter}" if context else f"param_{self._parameter_counter}"
139
+
140
+ self._parameters[param_name] = value
141
+ return param_name
142
+
143
+ def _parameterize_expression(self, expression: exp.Expression) -> exp.Expression:
144
+ """Replace literal values in an expression with bound parameters.
145
+
146
+ This method traverses a SQLGlot expression tree and replaces literal
147
+ values with parameter placeholders, adding the values to the builder's
148
+ parameter collection.
149
+
150
+ Args:
151
+ expression: The SQLGlot expression to parameterize
152
+
153
+ Returns:
154
+ A new expression with literals replaced by parameter placeholders
155
+ """
156
+
157
+ def replacer(node: exp.Expression) -> exp.Expression:
158
+ if isinstance(node, exp.Literal):
159
+ if node.this in {True, False, None}:
160
+ return node
161
+ param_name = self._add_parameter(node.this, context="where")
162
+ return exp.Placeholder(this=param_name)
163
+ return node
164
+
165
+ return expression.transform(replacer, copy=False)
166
+
167
+ def add_parameter(self: Self, value: Any, name: Optional[str] = None) -> tuple[Self, str]:
168
+ """Explicitly adds a parameter to the query.
169
+
170
+ This is useful for parameters that are not directly tied to a
171
+ builder method like `where` or `values`.
172
+
173
+ Args:
174
+ value: The value of the parameter.
175
+ name: Optional explicit name for the parameter. If None, a name
176
+ will be generated.
177
+
178
+ Returns:
179
+ tuple[Self, str]: The builder instance and the parameter name.
180
+ """
181
+ if name:
182
+ if name in self._parameters:
183
+ self._raise_sql_builder_error(f"Parameter name '{name}' already exists.")
184
+ self._parameters[name] = value
185
+ return self, name
186
+
187
+ self._parameter_counter += 1
188
+ param_name = f"param_{self._parameter_counter}"
189
+ self._parameters[param_name] = value
190
+ return self, param_name
191
+
192
+ def _generate_unique_parameter_name(self, base_name: str) -> str:
193
+ """Generate unique parameter name when collision occurs.
194
+
195
+ Args:
196
+ base_name: The desired base name for the parameter
197
+
198
+ Returns:
199
+ A unique parameter name that doesn't exist in current parameters
200
+ """
201
+ if base_name not in self._parameters:
202
+ return base_name
203
+
204
+ for i in range(1, 1000): # Reasonable upper bound to prevent infinite loops
205
+ name = f"{base_name}_{i}"
206
+ if name not in self._parameters:
207
+ return name
208
+
209
+ # Fallback for edge case
210
+ import uuid
211
+
212
+ return f"{base_name}_{uuid.uuid4().hex[:8]}"
213
+
214
+ def _generate_builder_cache_key(self, config: "Optional[StatementConfig]" = None) -> str:
215
+ """Generate cache key based on builder state and configuration.
216
+
217
+ Args:
218
+ config: Optional SQL configuration that affects the generated SQL
219
+
220
+ Returns:
221
+ A unique cache key representing the builder state and configuration
222
+ """
223
+ import hashlib
224
+
225
+ dialect_name: str = self.dialect_name or "default"
226
+ expr_sql: str = self._expression.sql() if self._expression else "None"
227
+
228
+ state_parts = [
229
+ f"expression:{expr_sql}",
230
+ f"parameters:{sorted(self._parameters.items())}",
231
+ f"ctes:{sorted(self._with_ctes.keys())}",
232
+ f"dialect:{dialect_name}",
233
+ f"schema:{self.schema}",
234
+ f"optimization:{self.enable_optimization}",
235
+ f"optimize_joins:{self.optimize_joins}",
236
+ f"optimize_predicates:{self.optimize_predicates}",
237
+ f"simplify_expressions:{self.simplify_expressions}",
238
+ ]
239
+
240
+ if config:
241
+ config_parts = [
242
+ f"config_dialect:{config.dialect or 'default'}",
243
+ f"enable_parsing:{config.enable_parsing}",
244
+ f"enable_validation:{config.enable_validation}",
245
+ f"enable_transformations:{config.enable_transformations}",
246
+ f"enable_analysis:{config.enable_analysis}",
247
+ f"enable_caching:{config.enable_caching}",
248
+ f"param_style:{config.parameter_config.default_parameter_style.value}",
249
+ ]
250
+ state_parts.extend(config_parts)
251
+
252
+ state_string = "|".join(state_parts)
253
+ return f"builder:{hashlib.sha256(state_string.encode()).hexdigest()[:16]}"
254
+
255
+ def with_cte(self: Self, alias: str, query: "Union[QueryBuilder, exp.Select, str]") -> Self:
256
+ """Adds a Common Table Expression (CTE) to the query.
257
+
258
+ Args:
259
+ alias: The alias for the CTE.
260
+ query: The CTE query, which can be another QueryBuilder instance,
261
+ a raw SQL string, or a sqlglot Select expression.
262
+
263
+ Returns:
264
+ Self: The current builder instance for method chaining.
265
+ """
266
+ if alias in self._with_ctes:
267
+ self._raise_sql_builder_error(f"CTE with alias '{alias}' already exists.")
268
+
269
+ cte_select_expression: exp.Select
270
+
271
+ if isinstance(query, QueryBuilder):
272
+ if query._expression is None:
273
+ self._raise_sql_builder_error("CTE query builder has no expression.")
274
+ if not isinstance(query._expression, exp.Select):
275
+ msg = f"CTE query builder expression must be a Select, got {type(query._expression).__name__}."
276
+ self._raise_sql_builder_error(msg)
277
+ cte_select_expression = query._expression
278
+ for p_name, p_value in query.parameters.items():
279
+ unique_name = self._generate_unique_parameter_name(p_name)
280
+ self.add_parameter(p_value, unique_name)
281
+
282
+ elif isinstance(query, str):
283
+ try:
284
+ parsed_expression = sqlglot.parse_one(query, read=self.dialect_name)
285
+ if not isinstance(parsed_expression, exp.Select):
286
+ msg = f"CTE query string must parse to a SELECT statement, got {type(parsed_expression).__name__}."
287
+ self._raise_sql_builder_error(msg)
288
+ cte_select_expression = parsed_expression
289
+ except SQLGlotParseError as e:
290
+ self._raise_sql_builder_error(f"Failed to parse CTE query string: {e!s}", e)
291
+ except Exception as e:
292
+ msg = f"An unexpected error occurred while parsing CTE query string: {e!s}"
293
+ self._raise_sql_builder_error(msg, e)
294
+ elif isinstance(query, exp.Select):
295
+ cte_select_expression = query
296
+ else:
297
+ msg = f"Invalid query type for CTE: {type(query).__name__}"
298
+ self._raise_sql_builder_error(msg)
299
+ return self
300
+
301
+ self._with_ctes[alias] = exp.CTE(this=cte_select_expression, alias=exp.to_table(alias))
302
+ return self
303
+
304
+ def build(self) -> "SafeQuery":
305
+ """Builds the SQL query string and parameters.
306
+
307
+ Returns:
308
+ SafeQuery: A dataclass containing the SQL string and parameters.
309
+ """
310
+ if self._expression is None:
311
+ self._raise_sql_builder_error("QueryBuilder expression not initialized.")
312
+
313
+ if self._with_ctes:
314
+ final_expression = self._expression
315
+ if has_with_method(final_expression):
316
+ for alias, cte_node in self._with_ctes.items():
317
+ final_expression = cast("Any", final_expression).with_(cte_node.args["this"], as_=alias, copy=False)
318
+ elif isinstance(final_expression, (exp.Select, exp.Insert, exp.Update, exp.Delete, exp.Union)):
319
+ final_expression = exp.With(expressions=list(self._with_ctes.values()), this=final_expression)
320
+ else:
321
+ final_expression = self._expression
322
+
323
+ if self.enable_optimization and isinstance(final_expression, exp.Expression):
324
+ final_expression = self._optimize_expression(final_expression)
325
+
326
+ try:
327
+ if has_sql_method(final_expression):
328
+ sql_string = final_expression.sql(dialect=self.dialect_name, pretty=True)
329
+ else:
330
+ sql_string = str(final_expression)
331
+ except Exception as e:
332
+ err_msg = f"Error generating SQL from expression: {e!s}"
333
+ logger.exception("SQL generation failed")
334
+ self._raise_sql_builder_error(err_msg, e)
335
+
336
+ return SafeQuery(sql=sql_string, parameters=self._parameters.copy(), dialect=self.dialect)
337
+
338
+ def _optimize_expression(self, expression: exp.Expression) -> exp.Expression:
339
+ """Apply SQLGlot optimizations to the expression with caching.
340
+
341
+ Args:
342
+ expression: The expression to optimize
343
+
344
+ Returns:
345
+ The optimized expression
346
+ """
347
+ if not self.enable_optimization:
348
+ return expression
349
+
350
+ optimizer_settings = {
351
+ "optimize_joins": self.optimize_joins,
352
+ "pushdown_predicates": self.optimize_predicates,
353
+ "simplify_expressions": self.simplify_expressions,
354
+ }
355
+
356
+ dialect_name = self.dialect_name or "default"
357
+ cache_key = hash_optimized_expression(
358
+ expression, dialect=dialect_name, schema=self.schema, optimizer_settings=optimizer_settings
359
+ )
360
+
361
+ cache_key_obj = CacheKey((cache_key,))
362
+ unified_cache = get_default_cache()
363
+ cached_optimized = unified_cache.get(cache_key_obj)
364
+ if cached_optimized:
365
+ return cast("exp.Expression", cached_optimized)
366
+
367
+ try:
368
+ optimized = optimize(
369
+ expression, schema=self.schema, dialect=self.dialect_name, optimizer_settings=optimizer_settings
370
+ )
371
+
372
+ unified_cache.put(cache_key_obj, optimized)
373
+
374
+ except Exception:
375
+ return expression
376
+ else:
377
+ return optimized
378
+
379
+ def to_statement(self, config: "Optional[StatementConfig]" = None) -> "SQL":
380
+ """Converts the built query into a SQL statement object with caching.
381
+
382
+ Args:
383
+ config: Optional SQL configuration.
384
+
385
+ Returns:
386
+ SQL: A SQL statement object.
387
+ """
388
+ cache_config = get_cache_config()
389
+ if not cache_config.compiled_cache_enabled:
390
+ return self._to_statement_without_cache(config)
391
+
392
+ cache_key_str = self._generate_builder_cache_key(config)
393
+ cache_key = CacheKey((cache_key_str,))
394
+
395
+ unified_cache = get_default_cache()
396
+ cached_sql = unified_cache.get(cache_key)
397
+ if cached_sql is not None:
398
+ return cast("SQL", cached_sql)
399
+
400
+ sql_statement = self._to_statement_without_cache(config)
401
+ unified_cache.put(cache_key, sql_statement)
402
+
403
+ return sql_statement
404
+
405
+ def _to_statement_without_cache(self, config: "Optional[StatementConfig]" = None) -> "SQL":
406
+ """Internal method to create SQL statement without caching.
407
+
408
+ Args:
409
+ config: Optional SQL configuration.
410
+
411
+ Returns:
412
+ SQL: A SQL statement object.
413
+ """
414
+ safe_query = self.build()
415
+
416
+ if isinstance(safe_query.parameters, dict):
417
+ kwargs = safe_query.parameters
418
+ parameters: Optional[tuple] = None
419
+ else:
420
+ kwargs = None
421
+ parameters = (
422
+ safe_query.parameters
423
+ if isinstance(safe_query.parameters, tuple)
424
+ else tuple(safe_query.parameters)
425
+ if safe_query.parameters
426
+ else None
427
+ )
428
+
429
+ if config is None:
430
+ from sqlspec.core.statement import StatementConfig
431
+
432
+ parameter_config = ParameterStyleConfig(
433
+ default_parameter_style=ParameterStyle.QMARK, supported_parameter_styles={ParameterStyle.QMARK}
434
+ )
435
+ config = StatementConfig(parameter_config=parameter_config, dialect=safe_query.dialect)
436
+
437
+ if kwargs:
438
+ return SQL(safe_query.sql, statement_config=config, **kwargs)
439
+ if parameters:
440
+ return SQL(safe_query.sql, *parameters, statement_config=config)
441
+ return SQL(safe_query.sql, statement_config=config)
442
+
443
+ def __str__(self) -> str:
444
+ """Return the SQL string representation of the query.
445
+
446
+ Returns:
447
+ str: The SQL string for this query.
448
+ """
449
+ try:
450
+ return self.build().sql
451
+ except Exception:
452
+ return super().__str__()
453
+
454
+ @property
455
+ def dialect_name(self) -> "Optional[str]":
456
+ """Returns the name of the dialect, if set."""
457
+ if isinstance(self.dialect, str):
458
+ return self.dialect
459
+ if self.dialect is not None:
460
+ if isinstance(self.dialect, type) and issubclass(self.dialect, Dialect):
461
+ return self.dialect.__name__.lower()
462
+ if isinstance(self.dialect, Dialect):
463
+ return type(self.dialect).__name__.lower()
464
+ try:
465
+ return self.dialect.__name__.lower()
466
+ except AttributeError:
467
+ pass
468
+ return None
469
+
470
+ @property
471
+ def parameters(self) -> dict[str, Any]:
472
+ """Public access to query parameters."""
473
+ return self._parameters