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