sqlspec 0.25.0__py3-none-any.whl → 0.27.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of sqlspec might be problematic. Click here for more details.

Files changed (199) hide show
  1. sqlspec/__init__.py +7 -15
  2. sqlspec/_serialization.py +256 -24
  3. sqlspec/_typing.py +71 -52
  4. sqlspec/adapters/adbc/_types.py +1 -1
  5. sqlspec/adapters/adbc/adk/__init__.py +5 -0
  6. sqlspec/adapters/adbc/adk/store.py +870 -0
  7. sqlspec/adapters/adbc/config.py +69 -12
  8. sqlspec/adapters/adbc/data_dictionary.py +340 -0
  9. sqlspec/adapters/adbc/driver.py +266 -58
  10. sqlspec/adapters/adbc/litestar/__init__.py +5 -0
  11. sqlspec/adapters/adbc/litestar/store.py +504 -0
  12. sqlspec/adapters/adbc/type_converter.py +153 -0
  13. sqlspec/adapters/aiosqlite/_types.py +1 -1
  14. sqlspec/adapters/aiosqlite/adk/__init__.py +5 -0
  15. sqlspec/adapters/aiosqlite/adk/store.py +527 -0
  16. sqlspec/adapters/aiosqlite/config.py +88 -15
  17. sqlspec/adapters/aiosqlite/data_dictionary.py +149 -0
  18. sqlspec/adapters/aiosqlite/driver.py +143 -40
  19. sqlspec/adapters/aiosqlite/litestar/__init__.py +5 -0
  20. sqlspec/adapters/aiosqlite/litestar/store.py +281 -0
  21. sqlspec/adapters/aiosqlite/pool.py +7 -7
  22. sqlspec/adapters/asyncmy/__init__.py +7 -1
  23. sqlspec/adapters/asyncmy/_types.py +2 -2
  24. sqlspec/adapters/asyncmy/adk/__init__.py +5 -0
  25. sqlspec/adapters/asyncmy/adk/store.py +493 -0
  26. sqlspec/adapters/asyncmy/config.py +68 -23
  27. sqlspec/adapters/asyncmy/data_dictionary.py +161 -0
  28. sqlspec/adapters/asyncmy/driver.py +313 -58
  29. sqlspec/adapters/asyncmy/litestar/__init__.py +5 -0
  30. sqlspec/adapters/asyncmy/litestar/store.py +296 -0
  31. sqlspec/adapters/asyncpg/__init__.py +2 -1
  32. sqlspec/adapters/asyncpg/_type_handlers.py +71 -0
  33. sqlspec/adapters/asyncpg/_types.py +11 -7
  34. sqlspec/adapters/asyncpg/adk/__init__.py +5 -0
  35. sqlspec/adapters/asyncpg/adk/store.py +450 -0
  36. sqlspec/adapters/asyncpg/config.py +59 -35
  37. sqlspec/adapters/asyncpg/data_dictionary.py +173 -0
  38. sqlspec/adapters/asyncpg/driver.py +170 -25
  39. sqlspec/adapters/asyncpg/litestar/__init__.py +5 -0
  40. sqlspec/adapters/asyncpg/litestar/store.py +253 -0
  41. sqlspec/adapters/bigquery/_types.py +1 -1
  42. sqlspec/adapters/bigquery/adk/__init__.py +5 -0
  43. sqlspec/adapters/bigquery/adk/store.py +576 -0
  44. sqlspec/adapters/bigquery/config.py +27 -10
  45. sqlspec/adapters/bigquery/data_dictionary.py +149 -0
  46. sqlspec/adapters/bigquery/driver.py +368 -142
  47. sqlspec/adapters/bigquery/litestar/__init__.py +5 -0
  48. sqlspec/adapters/bigquery/litestar/store.py +327 -0
  49. sqlspec/adapters/bigquery/type_converter.py +125 -0
  50. sqlspec/adapters/duckdb/_types.py +1 -1
  51. sqlspec/adapters/duckdb/adk/__init__.py +14 -0
  52. sqlspec/adapters/duckdb/adk/store.py +553 -0
  53. sqlspec/adapters/duckdb/config.py +80 -20
  54. sqlspec/adapters/duckdb/data_dictionary.py +163 -0
  55. sqlspec/adapters/duckdb/driver.py +167 -45
  56. sqlspec/adapters/duckdb/litestar/__init__.py +5 -0
  57. sqlspec/adapters/duckdb/litestar/store.py +332 -0
  58. sqlspec/adapters/duckdb/pool.py +4 -4
  59. sqlspec/adapters/duckdb/type_converter.py +133 -0
  60. sqlspec/adapters/oracledb/_numpy_handlers.py +133 -0
  61. sqlspec/adapters/oracledb/_types.py +20 -2
  62. sqlspec/adapters/oracledb/adk/__init__.py +5 -0
  63. sqlspec/adapters/oracledb/adk/store.py +1745 -0
  64. sqlspec/adapters/oracledb/config.py +122 -32
  65. sqlspec/adapters/oracledb/data_dictionary.py +509 -0
  66. sqlspec/adapters/oracledb/driver.py +353 -91
  67. sqlspec/adapters/oracledb/litestar/__init__.py +5 -0
  68. sqlspec/adapters/oracledb/litestar/store.py +767 -0
  69. sqlspec/adapters/oracledb/migrations.py +348 -73
  70. sqlspec/adapters/oracledb/type_converter.py +207 -0
  71. sqlspec/adapters/psqlpy/_type_handlers.py +44 -0
  72. sqlspec/adapters/psqlpy/_types.py +2 -1
  73. sqlspec/adapters/psqlpy/adk/__init__.py +5 -0
  74. sqlspec/adapters/psqlpy/adk/store.py +482 -0
  75. sqlspec/adapters/psqlpy/config.py +46 -17
  76. sqlspec/adapters/psqlpy/data_dictionary.py +172 -0
  77. sqlspec/adapters/psqlpy/driver.py +123 -209
  78. sqlspec/adapters/psqlpy/litestar/__init__.py +5 -0
  79. sqlspec/adapters/psqlpy/litestar/store.py +272 -0
  80. sqlspec/adapters/psqlpy/type_converter.py +102 -0
  81. sqlspec/adapters/psycopg/_type_handlers.py +80 -0
  82. sqlspec/adapters/psycopg/_types.py +2 -1
  83. sqlspec/adapters/psycopg/adk/__init__.py +5 -0
  84. sqlspec/adapters/psycopg/adk/store.py +944 -0
  85. sqlspec/adapters/psycopg/config.py +69 -35
  86. sqlspec/adapters/psycopg/data_dictionary.py +331 -0
  87. sqlspec/adapters/psycopg/driver.py +238 -81
  88. sqlspec/adapters/psycopg/litestar/__init__.py +5 -0
  89. sqlspec/adapters/psycopg/litestar/store.py +554 -0
  90. sqlspec/adapters/sqlite/__init__.py +2 -1
  91. sqlspec/adapters/sqlite/_type_handlers.py +86 -0
  92. sqlspec/adapters/sqlite/_types.py +1 -1
  93. sqlspec/adapters/sqlite/adk/__init__.py +5 -0
  94. sqlspec/adapters/sqlite/adk/store.py +572 -0
  95. sqlspec/adapters/sqlite/config.py +87 -15
  96. sqlspec/adapters/sqlite/data_dictionary.py +149 -0
  97. sqlspec/adapters/sqlite/driver.py +137 -54
  98. sqlspec/adapters/sqlite/litestar/__init__.py +5 -0
  99. sqlspec/adapters/sqlite/litestar/store.py +318 -0
  100. sqlspec/adapters/sqlite/pool.py +18 -9
  101. sqlspec/base.py +45 -26
  102. sqlspec/builder/__init__.py +73 -4
  103. sqlspec/builder/_base.py +162 -89
  104. sqlspec/builder/_column.py +62 -29
  105. sqlspec/builder/_ddl.py +180 -121
  106. sqlspec/builder/_delete.py +5 -4
  107. sqlspec/builder/_dml.py +388 -0
  108. sqlspec/{_sql.py → builder/_factory.py} +53 -94
  109. sqlspec/builder/_insert.py +32 -131
  110. sqlspec/builder/_join.py +375 -0
  111. sqlspec/builder/_merge.py +446 -11
  112. sqlspec/builder/_parsing_utils.py +111 -17
  113. sqlspec/builder/_select.py +1457 -24
  114. sqlspec/builder/_update.py +11 -42
  115. sqlspec/cli.py +307 -194
  116. sqlspec/config.py +252 -67
  117. sqlspec/core/__init__.py +5 -4
  118. sqlspec/core/cache.py +17 -17
  119. sqlspec/core/compiler.py +62 -9
  120. sqlspec/core/filters.py +37 -37
  121. sqlspec/core/hashing.py +9 -9
  122. sqlspec/core/parameters.py +83 -48
  123. sqlspec/core/result.py +102 -46
  124. sqlspec/core/splitter.py +16 -17
  125. sqlspec/core/statement.py +36 -30
  126. sqlspec/core/type_conversion.py +235 -0
  127. sqlspec/driver/__init__.py +7 -6
  128. sqlspec/driver/_async.py +188 -151
  129. sqlspec/driver/_common.py +285 -80
  130. sqlspec/driver/_sync.py +188 -152
  131. sqlspec/driver/mixins/_result_tools.py +20 -236
  132. sqlspec/driver/mixins/_sql_translator.py +4 -4
  133. sqlspec/exceptions.py +75 -7
  134. sqlspec/extensions/adk/__init__.py +53 -0
  135. sqlspec/extensions/adk/_types.py +51 -0
  136. sqlspec/extensions/adk/converters.py +172 -0
  137. sqlspec/extensions/adk/migrations/0001_create_adk_tables.py +144 -0
  138. sqlspec/extensions/adk/migrations/__init__.py +0 -0
  139. sqlspec/extensions/adk/service.py +181 -0
  140. sqlspec/extensions/adk/store.py +536 -0
  141. sqlspec/extensions/aiosql/adapter.py +73 -53
  142. sqlspec/extensions/litestar/__init__.py +21 -4
  143. sqlspec/extensions/litestar/cli.py +54 -10
  144. sqlspec/extensions/litestar/config.py +59 -266
  145. sqlspec/extensions/litestar/handlers.py +46 -17
  146. sqlspec/extensions/litestar/migrations/0001_create_session_table.py +137 -0
  147. sqlspec/extensions/litestar/migrations/__init__.py +3 -0
  148. sqlspec/extensions/litestar/plugin.py +324 -223
  149. sqlspec/extensions/litestar/providers.py +25 -25
  150. sqlspec/extensions/litestar/store.py +265 -0
  151. sqlspec/loader.py +30 -49
  152. sqlspec/migrations/__init__.py +4 -3
  153. sqlspec/migrations/base.py +302 -39
  154. sqlspec/migrations/commands.py +611 -144
  155. sqlspec/migrations/context.py +142 -0
  156. sqlspec/migrations/fix.py +199 -0
  157. sqlspec/migrations/loaders.py +68 -23
  158. sqlspec/migrations/runner.py +543 -107
  159. sqlspec/migrations/tracker.py +237 -21
  160. sqlspec/migrations/utils.py +51 -3
  161. sqlspec/migrations/validation.py +177 -0
  162. sqlspec/protocols.py +66 -36
  163. sqlspec/storage/_utils.py +98 -0
  164. sqlspec/storage/backends/fsspec.py +134 -106
  165. sqlspec/storage/backends/local.py +78 -51
  166. sqlspec/storage/backends/obstore.py +278 -162
  167. sqlspec/storage/registry.py +75 -39
  168. sqlspec/typing.py +16 -84
  169. sqlspec/utils/config_resolver.py +153 -0
  170. sqlspec/utils/correlation.py +4 -5
  171. sqlspec/utils/data_transformation.py +3 -2
  172. sqlspec/utils/deprecation.py +9 -8
  173. sqlspec/utils/fixtures.py +4 -4
  174. sqlspec/utils/logging.py +46 -6
  175. sqlspec/utils/module_loader.py +2 -2
  176. sqlspec/utils/schema.py +288 -0
  177. sqlspec/utils/serializers.py +50 -2
  178. sqlspec/utils/sync_tools.py +21 -17
  179. sqlspec/utils/text.py +1 -2
  180. sqlspec/utils/type_guards.py +111 -20
  181. sqlspec/utils/version.py +433 -0
  182. {sqlspec-0.25.0.dist-info → sqlspec-0.27.0.dist-info}/METADATA +40 -21
  183. sqlspec-0.27.0.dist-info/RECORD +207 -0
  184. sqlspec/builder/mixins/__init__.py +0 -55
  185. sqlspec/builder/mixins/_cte_and_set_ops.py +0 -254
  186. sqlspec/builder/mixins/_delete_operations.py +0 -50
  187. sqlspec/builder/mixins/_insert_operations.py +0 -282
  188. sqlspec/builder/mixins/_join_operations.py +0 -389
  189. sqlspec/builder/mixins/_merge_operations.py +0 -592
  190. sqlspec/builder/mixins/_order_limit_operations.py +0 -152
  191. sqlspec/builder/mixins/_pivot_operations.py +0 -157
  192. sqlspec/builder/mixins/_select_operations.py +0 -936
  193. sqlspec/builder/mixins/_update_operations.py +0 -218
  194. sqlspec/builder/mixins/_where_clause.py +0 -1304
  195. sqlspec-0.25.0.dist-info/RECORD +0 -139
  196. sqlspec-0.25.0.dist-info/licenses/NOTICE +0 -29
  197. {sqlspec-0.25.0.dist-info → sqlspec-0.27.0.dist-info}/WHEEL +0 -0
  198. {sqlspec-0.25.0.dist-info → sqlspec-0.27.0.dist-info}/entry_points.txt +0 -0
  199. {sqlspec-0.25.0.dist-info → sqlspec-0.27.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,1304 +0,0 @@
1
- # ruff: noqa: PLR2004
2
- # pyright: reportPrivateUsage=false, reportPrivateImportUsage=false
3
- """WHERE and HAVING clause mixins.
4
-
5
- Provides mixins for WHERE and HAVING clause functionality with
6
- parameter binding and various condition operators.
7
- """
8
-
9
- from typing import TYPE_CHECKING, Any, Callable, Optional, Union, cast
10
-
11
- if TYPE_CHECKING:
12
- from sqlspec.core.statement import SQL
13
-
14
- from mypy_extensions import trait
15
- from sqlglot import exp
16
- from typing_extensions import Self
17
-
18
- from sqlspec.builder._parsing_utils import extract_column_name, parse_column_expression, parse_condition_expression
19
- from sqlspec.exceptions import SQLBuilderError
20
- from sqlspec.utils.type_guards import (
21
- has_expression_and_parameters,
22
- has_expression_and_sql,
23
- has_query_builder_parameters,
24
- has_sqlglot_expression,
25
- is_iterable_parameters,
26
- )
27
-
28
- if TYPE_CHECKING:
29
- from sqlspec.builder._column import ColumnExpression
30
- from sqlspec.protocols import SQLBuilderProtocol
31
-
32
- __all__ = ("HavingClauseMixin", "WhereClauseMixin")
33
-
34
-
35
- @trait
36
- class WhereClauseMixin:
37
- """Mixin providing WHERE clause methods for SELECT, UPDATE, and DELETE builders."""
38
-
39
- __slots__ = ()
40
-
41
- # Type annotations for PyRight - these will be provided by the base class
42
- def get_expression(self) -> Optional[exp.Expression]: ...
43
- def set_expression(self, expression: exp.Expression) -> None: ...
44
-
45
- def _create_parameterized_condition(
46
- self,
47
- column: Union[str, exp.Column],
48
- value: Any,
49
- condition_factory: "Callable[[exp.Expression, exp.Placeholder], exp.Expression]",
50
- ) -> exp.Expression:
51
- """Create a parameterized condition using the provided factory function.
52
-
53
- Args:
54
- column: Column expression
55
- value: Parameter value
56
- condition_factory: Function that creates the condition expression
57
-
58
- Returns:
59
- The created condition expression
60
- """
61
- builder = cast("SQLBuilderProtocol", self)
62
- column_name = extract_column_name(column)
63
- param_name = builder._generate_unique_parameter_name(column_name)
64
- _, param_name = builder.add_parameter(value, name=param_name)
65
- col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
66
- placeholder = exp.Placeholder(this=param_name)
67
- return condition_factory(col_expr, placeholder)
68
-
69
- def _merge_sql_object_parameters(self, sql_obj: Any) -> None:
70
- """Merge parameters from a SQL object into the builder.
71
-
72
- Args:
73
- sql_obj: Object with parameters attribute containing parameter mappings
74
- """
75
- if not has_expression_and_parameters(sql_obj):
76
- return
77
-
78
- builder = cast("SQLBuilderProtocol", self)
79
- sql_parameters = getattr(sql_obj, "parameters", {})
80
- for param_name, param_value in sql_parameters.items():
81
- unique_name = builder._generate_unique_parameter_name(param_name)
82
- builder.add_parameter(param_value, name=unique_name)
83
-
84
- def _apply_or_where(self, where_method: "Callable[..., Self]", *args: Any, **kwargs: Any) -> Self:
85
- """Apply a where method but use OR logic instead of AND.
86
-
87
- This allows reusing all where_* methods for or_where_* functionality.
88
-
89
- Args:
90
- where_method: The where method to apply (e.g., self.where_eq)
91
- *args: Arguments to pass to the where method
92
- **kwargs: Keyword arguments to pass to the where method
93
-
94
- Returns:
95
- Self with OR condition applied
96
- """
97
- # Create a temporary clone to capture the condition
98
- original_expr = self.get_expression()
99
-
100
- # Apply the where method to get the condition
101
- where_method(*args, **kwargs)
102
-
103
- # Get the last condition added by extracting it from the modified expression
104
- current_expr = self.get_expression()
105
- if isinstance(current_expr, (exp.Select, exp.Update, exp.Delete)) and original_expr != current_expr:
106
- last_where = current_expr.find(exp.Where)
107
- if last_where and last_where.this:
108
- condition = last_where.this
109
- # Restore original expression
110
- if original_expr is not None:
111
- self.set_expression(original_expr)
112
- # Apply as OR
113
- return self.or_where(condition)
114
-
115
- return self
116
-
117
- def _handle_in_operator(
118
- self, column_exp: exp.Expression, value: Any, column_name: str = "column"
119
- ) -> exp.Expression:
120
- """Handle IN operator."""
121
- builder = cast("SQLBuilderProtocol", self)
122
- if is_iterable_parameters(value):
123
- placeholders = []
124
- for i, v in enumerate(value):
125
- if len(value) == 1:
126
- param_name = builder._generate_unique_parameter_name(column_name)
127
- else:
128
- param_name = builder._generate_unique_parameter_name(f"{column_name}_{i + 1}")
129
- _, param_name = builder.add_parameter(v, name=param_name)
130
- placeholders.append(exp.Placeholder(this=param_name))
131
- return exp.In(this=column_exp, expressions=placeholders)
132
- param_name = builder._generate_unique_parameter_name(column_name)
133
- _, param_name = builder.add_parameter(value, name=param_name)
134
- return exp.In(this=column_exp, expressions=[exp.Placeholder(this=param_name)])
135
-
136
- def _handle_not_in_operator(
137
- self, column_exp: exp.Expression, value: Any, column_name: str = "column"
138
- ) -> exp.Expression:
139
- """Handle NOT IN operator."""
140
- builder = cast("SQLBuilderProtocol", self)
141
- if is_iterable_parameters(value):
142
- placeholders = []
143
- for i, v in enumerate(value):
144
- if len(value) == 1:
145
- param_name = builder._generate_unique_parameter_name(column_name)
146
- else:
147
- param_name = builder._generate_unique_parameter_name(f"{column_name}_{i + 1}")
148
- _, param_name = builder.add_parameter(v, name=param_name)
149
- placeholders.append(exp.Placeholder(this=param_name))
150
- return exp.Not(this=exp.In(this=column_exp, expressions=placeholders))
151
- param_name = builder._generate_unique_parameter_name(column_name)
152
- _, param_name = builder.add_parameter(value, name=param_name)
153
- return exp.Not(this=exp.In(this=column_exp, expressions=[exp.Placeholder(this=param_name)]))
154
-
155
- def _handle_is_operator(self, column_exp: exp.Expression, value: Any) -> exp.Expression:
156
- """Handle IS operator."""
157
- value_expr = exp.Null() if value is None else exp.convert(value)
158
- return exp.Is(this=column_exp, expression=value_expr)
159
-
160
- def _handle_is_not_operator(self, column_exp: exp.Expression, value: Any) -> exp.Expression:
161
- """Handle IS NOT operator."""
162
- value_expr = exp.Null() if value is None else exp.convert(value)
163
- return exp.Not(this=exp.Is(this=column_exp, expression=value_expr))
164
-
165
- def _handle_between_operator(
166
- self, column_exp: exp.Expression, value: Any, column_name: str = "column"
167
- ) -> exp.Expression:
168
- """Handle BETWEEN operator."""
169
- if is_iterable_parameters(value) and len(value) == 2:
170
- builder = cast("SQLBuilderProtocol", self)
171
- low, high = value
172
- low_param = builder._generate_unique_parameter_name(f"{column_name}_low")
173
- high_param = builder._generate_unique_parameter_name(f"{column_name}_high")
174
- _, low_param = builder.add_parameter(low, name=low_param)
175
- _, high_param = builder.add_parameter(high, name=high_param)
176
- return exp.Between(
177
- this=column_exp, low=exp.Placeholder(this=low_param), high=exp.Placeholder(this=high_param)
178
- )
179
- msg = f"BETWEEN operator requires a tuple of two values, got {type(value).__name__}"
180
- raise SQLBuilderError(msg)
181
-
182
- def _handle_not_between_operator(
183
- self, column_exp: exp.Expression, value: Any, column_name: str = "column"
184
- ) -> exp.Expression:
185
- """Handle NOT BETWEEN operator."""
186
- if is_iterable_parameters(value) and len(value) == 2:
187
- builder = cast("SQLBuilderProtocol", self)
188
- low, high = value
189
- low_param = builder._generate_unique_parameter_name(f"{column_name}_low")
190
- high_param = builder._generate_unique_parameter_name(f"{column_name}_high")
191
- _, low_param = builder.add_parameter(low, name=low_param)
192
- _, high_param = builder.add_parameter(high, name=high_param)
193
- return exp.Not(
194
- this=exp.Between(
195
- this=column_exp, low=exp.Placeholder(this=low_param), high=exp.Placeholder(this=high_param)
196
- )
197
- )
198
- msg = f"NOT BETWEEN operator requires a tuple of two values, got {type(value).__name__}"
199
- raise SQLBuilderError(msg)
200
-
201
- def _process_tuple_condition(self, condition: "tuple[Any, ...]") -> exp.Expression:
202
- """Process tuple-based WHERE conditions."""
203
- if len(condition) == 2:
204
- # (column, value) tuple for equality
205
- column, value = condition
206
- return self._create_parameterized_condition(
207
- column, value, lambda col, placeholder: exp.EQ(this=col, expression=placeholder)
208
- )
209
-
210
- if len(condition) != 3:
211
- msg = f"Condition tuple must have 2 or 3 elements, got {len(condition)}"
212
- raise SQLBuilderError(msg)
213
-
214
- # (column, operator, value) tuple
215
- column_name_raw, operator, value = condition
216
- operator = str(operator).upper()
217
- column_exp = parse_column_expression(column_name_raw)
218
- column_name = extract_column_name(column_name_raw)
219
-
220
- # Simple operators that use direct parameterization
221
- simple_operators = {
222
- "=": lambda col, placeholder: exp.EQ(this=col, expression=placeholder),
223
- "!=": lambda col, placeholder: exp.NEQ(this=col, expression=placeholder),
224
- "<>": lambda col, placeholder: exp.NEQ(this=col, expression=placeholder),
225
- ">": lambda col, placeholder: exp.GT(this=col, expression=placeholder),
226
- ">=": lambda col, placeholder: exp.GTE(this=col, expression=placeholder),
227
- "<": lambda col, placeholder: exp.LT(this=col, expression=placeholder),
228
- "<=": lambda col, placeholder: exp.LTE(this=col, expression=placeholder),
229
- "LIKE": lambda col, placeholder: exp.Like(this=col, expression=placeholder),
230
- "NOT LIKE": lambda col, placeholder: exp.Not(this=exp.Like(this=col, expression=placeholder)),
231
- }
232
-
233
- if operator in simple_operators:
234
- return self._create_parameterized_condition(column_name_raw, value, simple_operators[operator])
235
-
236
- # Complex operators that need special handling
237
- if operator == "IN":
238
- return self._handle_in_operator(column_exp, value, column_name)
239
- if operator == "NOT IN":
240
- return self._handle_not_in_operator(column_exp, value, column_name)
241
- if operator == "IS":
242
- return self._handle_is_operator(column_exp, value)
243
- if operator == "IS NOT":
244
- return self._handle_is_not_operator(column_exp, value)
245
- if operator == "BETWEEN":
246
- return self._handle_between_operator(column_exp, value, column_name)
247
- if operator == "NOT BETWEEN":
248
- return self._handle_not_between_operator(column_exp, value, column_name)
249
-
250
- msg = f"Unsupported operator: {operator}"
251
- raise SQLBuilderError(msg)
252
-
253
- def where(
254
- self,
255
- condition: Union[
256
- str, exp.Expression, exp.Condition, tuple[str, Any], tuple[str, str, Any], "ColumnExpression", "SQL"
257
- ],
258
- *values: Any,
259
- operator: Optional[str] = None,
260
- **kwargs: Any,
261
- ) -> Self:
262
- """Add a WHERE clause to the statement.
263
-
264
- Args:
265
- condition: The condition for the WHERE clause. Can be:
266
- - A string condition with or without parameter placeholders
267
- - A string column name (when values are provided)
268
- - A sqlglot Expression or Condition
269
- - A 2-tuple (column, value) for equality comparison
270
- - A 3-tuple (column, operator, value) for custom comparison
271
- *values: Positional values for parameter binding (when condition contains placeholders or is a column name)
272
- operator: Operator for comparison (when condition is a column name)
273
- **kwargs: Named parameters for parameter binding (when condition contains named placeholders)
274
-
275
- Raises:
276
- SQLBuilderError: If the current expression is not a supported statement type.
277
-
278
- Returns:
279
- The current builder instance for method chaining.
280
- """
281
- current_expr = self.get_expression()
282
- if self.__class__.__name__ == "Update" and not isinstance(current_expr, exp.Update):
283
- msg = "Cannot add WHERE clause to non-UPDATE expression"
284
- raise SQLBuilderError(msg)
285
-
286
- builder = cast("SQLBuilderProtocol", self)
287
- if current_expr is None:
288
- msg = "Cannot add WHERE clause: expression is not initialized."
289
- raise SQLBuilderError(msg)
290
-
291
- if isinstance(current_expr, exp.Delete) and not current_expr.args.get("this"):
292
- msg = "WHERE clause requires a table to be set. Use from() to set the table first."
293
- raise SQLBuilderError(msg)
294
-
295
- # Handle string conditions with external parameters
296
- if values or kwargs:
297
- if not isinstance(condition, str):
298
- msg = "When values are provided, condition must be a string"
299
- raise SQLBuilderError(msg)
300
-
301
- # Check if condition contains parameter placeholders
302
- from sqlspec.core.parameters import ParameterStyle, ParameterValidator
303
-
304
- validator = ParameterValidator()
305
- param_info = validator.extract_parameters(condition)
306
-
307
- if param_info:
308
- # String condition with placeholders - create SQL object with parameters
309
- from sqlspec import sql as sql_factory
310
-
311
- # Create parameter mapping based on the detected parameter info
312
- param_dict = dict(kwargs) # Start with named parameters
313
-
314
- # Handle positional parameters - these are ordinal-based ($1, $2, :1, :2, ?)
315
- positional_params = [
316
- param
317
- for param in param_info
318
- if param.style in {ParameterStyle.NUMERIC, ParameterStyle.POSITIONAL_COLON, ParameterStyle.QMARK}
319
- ]
320
-
321
- # Map positional values to positional parameters
322
- if len(values) != len(positional_params):
323
- msg = f"Parameter count mismatch: condition has {len(positional_params)} positional placeholders, got {len(values)} values"
324
- raise SQLBuilderError(msg)
325
-
326
- for i, value in enumerate(values):
327
- param_dict[f"param_{i}"] = value
328
-
329
- # Create SQL object with parameters that will be processed correctly
330
- condition = sql_factory.raw(condition, **param_dict)
331
- # Fall through to existing SQL object handling logic
332
-
333
- elif len(values) == 1 and not kwargs:
334
- # Single value - treat as column = value
335
- if operator is not None:
336
- where_expr = self._process_tuple_condition((condition, operator, values[0]))
337
- else:
338
- where_expr = self._process_tuple_condition((condition, values[0]))
339
- # Process this condition and skip the rest
340
- if isinstance(current_expr, (exp.Select, exp.Update, exp.Delete)):
341
- updated_expr = current_expr.where(where_expr, copy=False)
342
- self.set_expression(updated_expr)
343
- else:
344
- msg = f"WHERE clause not supported for {type(current_expr).__name__}"
345
- raise SQLBuilderError(msg)
346
- return self
347
- else:
348
- msg = f"Cannot bind parameters to condition without placeholders: {condition}"
349
- raise SQLBuilderError(msg)
350
-
351
- # Handle all condition types (including SQL objects created above)
352
- if isinstance(condition, str):
353
- where_expr = parse_condition_expression(condition)
354
- elif isinstance(condition, (exp.Expression, exp.Condition)):
355
- where_expr = condition
356
- elif isinstance(condition, tuple):
357
- where_expr = self._process_tuple_condition(condition)
358
- elif has_query_builder_parameters(condition):
359
- column_expr_obj = cast("ColumnExpression", condition)
360
- where_expr = column_expr_obj._expression # pyright: ignore
361
- elif has_sqlglot_expression(condition):
362
- raw_expr = condition.sqlglot_expression # pyright: ignore[attr-defined]
363
- if raw_expr is not None:
364
- where_expr = builder._parameterize_expression(raw_expr)
365
- else:
366
- where_expr = parse_condition_expression(str(condition))
367
- elif has_expression_and_sql(condition):
368
- # Handle SQL objects (from sql.raw with parameters)
369
- expression = getattr(condition, "expression", None)
370
- if expression is not None and isinstance(expression, exp.Expression):
371
- # Merge parameters from SQL object into builder
372
- self._merge_sql_object_parameters(condition)
373
- where_expr = expression
374
- else:
375
- # If expression is None, fall back to parsing the raw SQL
376
- sql_text = getattr(condition, "sql", "")
377
- # Merge parameters even when parsing raw SQL
378
- self._merge_sql_object_parameters(condition)
379
- where_expr = parse_condition_expression(sql_text)
380
- else:
381
- msg = f"Unsupported condition type: {type(condition).__name__}"
382
- raise SQLBuilderError(msg)
383
-
384
- if isinstance(current_expr, (exp.Select, exp.Update, exp.Delete)):
385
- updated_expr = current_expr.where(where_expr, copy=False)
386
- self.set_expression(updated_expr)
387
- else:
388
- msg = f"WHERE clause not supported for {type(current_expr).__name__}"
389
- raise SQLBuilderError(msg)
390
- return self
391
-
392
- def where_eq(self, column: Union[str, exp.Column], value: Any) -> Self:
393
- """Add WHERE column = value clause."""
394
- condition = self._create_parameterized_condition(column, value, lambda col, placeholder: col.eq(placeholder))
395
- return self.where(condition)
396
-
397
- def where_neq(self, column: Union[str, exp.Column], value: Any) -> Self:
398
- """Add WHERE column != value clause."""
399
- condition = self._create_parameterized_condition(column, value, lambda col, placeholder: col.neq(placeholder))
400
- return self.where(condition)
401
-
402
- def where_lt(self, column: Union[str, exp.Column], value: Any) -> Self:
403
- """Add WHERE column < value clause."""
404
- condition = self._create_parameterized_condition(
405
- column, value, lambda col, placeholder: exp.LT(this=col, expression=placeholder)
406
- )
407
- return self.where(condition)
408
-
409
- def where_lte(self, column: Union[str, exp.Column], value: Any) -> Self:
410
- """Add WHERE column <= value clause."""
411
- condition = self._create_parameterized_condition(
412
- column, value, lambda col, placeholder: exp.LTE(this=col, expression=placeholder)
413
- )
414
- return self.where(condition)
415
-
416
- def where_gt(self, column: Union[str, exp.Column], value: Any) -> Self:
417
- """Add WHERE column > value clause."""
418
- condition = self._create_parameterized_condition(
419
- column, value, lambda col, placeholder: exp.GT(this=col, expression=placeholder)
420
- )
421
- return self.where(condition)
422
-
423
- def where_gte(self, column: Union[str, exp.Column], value: Any) -> Self:
424
- """Add WHERE column >= value clause."""
425
- condition = self._create_parameterized_condition(
426
- column, value, lambda col, placeholder: exp.GTE(this=col, expression=placeholder)
427
- )
428
- return self.where(condition)
429
-
430
- def where_between(self, column: Union[str, exp.Column], low: Any, high: Any) -> Self:
431
- """Add WHERE column BETWEEN low AND high clause."""
432
- builder = cast("SQLBuilderProtocol", self)
433
- column_name = extract_column_name(column)
434
- low_param = builder._generate_unique_parameter_name(f"{column_name}_low")
435
- high_param = builder._generate_unique_parameter_name(f"{column_name}_high")
436
- _, low_param = builder.add_parameter(low, name=low_param)
437
- _, high_param = builder.add_parameter(high, name=high_param)
438
- col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
439
- condition: exp.Expression = col_expr.between(exp.Placeholder(this=low_param), exp.Placeholder(this=high_param))
440
- return self.where(condition)
441
-
442
- def where_like(self, column: Union[str, exp.Column], pattern: str, escape: Optional[str] = None) -> Self:
443
- """Add WHERE column LIKE pattern clause."""
444
- builder = cast("SQLBuilderProtocol", self)
445
- column_name = extract_column_name(column)
446
- param_name = builder._generate_unique_parameter_name(column_name)
447
- _, param_name = builder.add_parameter(pattern, name=param_name)
448
- col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
449
- if escape is not None:
450
- cond = exp.Like(this=col_expr, expression=exp.Placeholder(this=param_name), escape=exp.convert(str(escape)))
451
- else:
452
- cond = col_expr.like(exp.Placeholder(this=param_name))
453
- condition: exp.Expression = cond
454
- return self.where(condition)
455
-
456
- def where_not_like(self, column: Union[str, exp.Column], pattern: str) -> Self:
457
- """Add WHERE column NOT LIKE pattern clause."""
458
- condition = self._create_parameterized_condition(
459
- column, pattern, lambda col, placeholder: col.like(placeholder).not_()
460
- )
461
- return self.where(condition)
462
-
463
- def where_ilike(self, column: Union[str, exp.Column], pattern: str) -> Self:
464
- """Add WHERE column ILIKE pattern clause."""
465
- condition = self._create_parameterized_condition(
466
- column, pattern, lambda col, placeholder: col.ilike(placeholder)
467
- )
468
- return self.where(condition)
469
-
470
- def where_is_null(self, column: Union[str, exp.Column]) -> Self:
471
- """Add WHERE column IS NULL clause."""
472
- col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
473
- condition: exp.Expression = col_expr.is_(exp.null())
474
- return self.where(condition)
475
-
476
- def where_is_not_null(self, column: Union[str, exp.Column]) -> Self:
477
- """Add WHERE column IS NOT NULL clause."""
478
- col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
479
- condition: exp.Expression = col_expr.is_(exp.null()).not_()
480
- return self.where(condition)
481
-
482
- def where_in(self, column: Union[str, exp.Column], values: Any) -> Self:
483
- """Add WHERE column IN (values) clause."""
484
- builder = cast("SQLBuilderProtocol", self)
485
- col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
486
- if has_query_builder_parameters(values) or isinstance(values, exp.Expression):
487
- subquery_exp: exp.Expression
488
- if has_query_builder_parameters(values):
489
- subquery = values.build() # pyright: ignore
490
- sql_str = subquery.sql
491
- subquery_exp = exp.paren(exp.maybe_parse(sql_str, dialect=builder.dialect_name)) # pyright: ignore
492
- # Merge subquery parameters into parent builder with unique naming
493
- if hasattr(subquery, "parameters") and isinstance(subquery.parameters, dict): # pyright: ignore[reportAttributeAccessIssue]
494
- for param_name, param_value in subquery.parameters.items(): # pyright: ignore[reportAttributeAccessIssue]
495
- unique_name = builder._generate_unique_parameter_name(param_name)
496
- builder.add_parameter(param_value, name=unique_name)
497
- else:
498
- subquery_exp = values # type: ignore[assignment]
499
- condition = col_expr.isin(subquery_exp)
500
- return self.where(condition)
501
- if not is_iterable_parameters(values) or isinstance(values, (str, bytes)):
502
- msg = "Unsupported type for 'values' in WHERE IN"
503
- raise SQLBuilderError(msg)
504
- column_name = extract_column_name(column)
505
- parameters = []
506
- for i, v in enumerate(values):
507
- if len(values) == 1:
508
- param_name = builder._generate_unique_parameter_name(column_name)
509
- else:
510
- param_name = builder._generate_unique_parameter_name(f"{column_name}_{i + 1}")
511
- _, param_name = builder.add_parameter(v, name=param_name)
512
- parameters.append(exp.Placeholder(this=param_name))
513
- condition = col_expr.isin(*parameters)
514
- return self.where(condition)
515
-
516
- def where_not_in(self, column: Union[str, exp.Column], values: Any) -> Self:
517
- """Add WHERE column NOT IN (values) clause."""
518
- builder = cast("SQLBuilderProtocol", self)
519
- col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
520
- if has_query_builder_parameters(values) or isinstance(values, exp.Expression):
521
- subquery_exp: exp.Expression
522
- if has_query_builder_parameters(values):
523
- subquery = values.build() # pyright: ignore
524
-
525
- subquery_exp = exp.paren(exp.maybe_parse(subquery.sql, dialect=builder.dialect_name)) # pyright: ignore
526
- else:
527
- subquery_exp = values # type: ignore[assignment]
528
- condition = exp.Not(this=col_expr.isin(subquery_exp))
529
- return self.where(condition)
530
- if not is_iterable_parameters(values) or isinstance(values, (str, bytes)):
531
- msg = "Values for where_not_in must be a non-string iterable or subquery."
532
- raise SQLBuilderError(msg)
533
- column_name = extract_column_name(column)
534
- parameters = []
535
- for i, v in enumerate(values):
536
- if len(values) == 1:
537
- param_name = builder._generate_unique_parameter_name(column_name)
538
- else:
539
- param_name = builder._generate_unique_parameter_name(f"{column_name}_{i + 1}")
540
- _, param_name = builder.add_parameter(v, name=param_name)
541
- parameters.append(exp.Placeholder(this=param_name))
542
- condition = exp.Not(this=col_expr.isin(*parameters))
543
- return self.where(condition)
544
-
545
- def where_null(self, column: Union[str, exp.Column]) -> Self:
546
- """Add WHERE column IS NULL clause."""
547
- return self.where_is_null(column)
548
-
549
- def where_not_null(self, column: Union[str, exp.Column]) -> Self:
550
- """Add WHERE column IS NOT NULL clause."""
551
- return self.where_is_not_null(column)
552
-
553
- def where_exists(self, subquery: Union[str, Any]) -> Self:
554
- """Add WHERE EXISTS (subquery) clause."""
555
- builder = cast("SQLBuilderProtocol", self)
556
- sub_expr: exp.Expression
557
- if has_query_builder_parameters(subquery):
558
- subquery_builder_parameters: dict[str, Any] = subquery.parameters
559
- if subquery_builder_parameters:
560
- for p_name, p_value in subquery_builder_parameters.items():
561
- builder.add_parameter(p_value, name=p_name)
562
- sub_sql_obj = subquery.build() # pyright: ignore
563
-
564
- sub_expr = exp.maybe_parse(sub_sql_obj.sql, dialect=builder.dialect_name) # pyright: ignore
565
- else:
566
- sub_expr = exp.maybe_parse(str(subquery), dialect=builder.dialect_name)
567
-
568
- if sub_expr is None:
569
- msg = "Could not parse subquery for EXISTS"
570
- raise SQLBuilderError(msg)
571
-
572
- exists_expr = exp.Exists(this=sub_expr)
573
- return self.where(exists_expr)
574
-
575
- def where_not_exists(self, subquery: Union[str, Any]) -> Self:
576
- """Add WHERE NOT EXISTS (subquery) clause."""
577
- builder = cast("SQLBuilderProtocol", self)
578
- sub_expr: exp.Expression
579
- if has_query_builder_parameters(subquery):
580
- subquery_builder_parameters: dict[str, Any] = subquery.parameters
581
- if subquery_builder_parameters:
582
- for p_name, p_value in subquery_builder_parameters.items():
583
- builder.add_parameter(p_value, name=p_name)
584
- sub_sql_obj = subquery.build() # pyright: ignore
585
- sub_expr = exp.maybe_parse(sub_sql_obj.sql, dialect=builder.dialect_name) # pyright: ignore
586
- else:
587
- sub_expr = exp.maybe_parse(str(subquery), dialect=builder.dialect_name)
588
-
589
- if sub_expr is None:
590
- msg = "Could not parse subquery for NOT EXISTS"
591
- raise SQLBuilderError(msg)
592
-
593
- not_exists_expr = exp.Not(this=exp.Exists(this=sub_expr))
594
- return self.where(not_exists_expr)
595
-
596
- def where_any(self, column: Union[str, exp.Column], values: Any) -> Self:
597
- """Add WHERE column = ANY(values) clause."""
598
- builder = cast("SQLBuilderProtocol", self)
599
- col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
600
- if has_query_builder_parameters(values) or isinstance(values, exp.Expression):
601
- subquery_exp: exp.Expression
602
- if has_query_builder_parameters(values):
603
- subquery = values.build() # pyright: ignore
604
- subquery_exp = exp.paren(exp.maybe_parse(subquery.sql, dialect=builder.dialect_name)) # pyright: ignore
605
- else:
606
- subquery_exp = values # type: ignore[assignment]
607
- condition = exp.EQ(this=col_expr, expression=exp.Any(this=subquery_exp))
608
- return self.where(condition)
609
- if isinstance(values, str):
610
- try:
611
- parsed_expr: Optional[exp.Expression] = exp.maybe_parse(values)
612
- if isinstance(parsed_expr, (exp.Select, exp.Union, exp.Subquery)):
613
- subquery_exp = exp.paren(parsed_expr)
614
- condition = exp.EQ(this=col_expr, expression=exp.Any(this=subquery_exp))
615
- return self.where(condition)
616
- except Exception: # noqa: S110
617
- pass
618
- msg = "Unsupported type for 'values' in WHERE ANY"
619
- raise SQLBuilderError(msg)
620
- if not is_iterable_parameters(values) or isinstance(values, bytes):
621
- msg = "Unsupported type for 'values' in WHERE ANY"
622
- raise SQLBuilderError(msg)
623
- column_name = extract_column_name(column)
624
- parameters = []
625
- for i, v in enumerate(values):
626
- if len(values) == 1:
627
- param_name = builder._generate_unique_parameter_name(column_name)
628
- else:
629
- param_name = builder._generate_unique_parameter_name(f"{column_name}_any_{i + 1}")
630
- _, param_name = builder.add_parameter(v, name=param_name)
631
- parameters.append(exp.Placeholder(this=param_name))
632
- tuple_expr = exp.Tuple(expressions=parameters)
633
- condition = exp.EQ(this=col_expr, expression=exp.Any(this=tuple_expr))
634
- return self.where(condition)
635
-
636
- def where_not_any(self, column: Union[str, exp.Column], values: Any) -> Self:
637
- """Add WHERE column <> ANY(values) clause."""
638
- builder = cast("SQLBuilderProtocol", self)
639
- col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
640
- if has_query_builder_parameters(values) or isinstance(values, exp.Expression):
641
- subquery_exp: exp.Expression
642
- if has_query_builder_parameters(values):
643
- subquery = values.build() # pyright: ignore
644
- subquery_exp = exp.paren(exp.maybe_parse(subquery.sql, dialect=builder.dialect_name)) # pyright: ignore
645
- else:
646
- subquery_exp = values # type: ignore[assignment]
647
- condition = exp.NEQ(this=col_expr, expression=exp.Any(this=subquery_exp))
648
- return self.where(condition)
649
- if isinstance(values, str):
650
- try:
651
- parsed_expr: Optional[exp.Expression] = exp.maybe_parse(values)
652
- if isinstance(parsed_expr, (exp.Select, exp.Union, exp.Subquery)):
653
- subquery_exp = exp.paren(parsed_expr)
654
- condition = exp.NEQ(this=col_expr, expression=exp.Any(this=subquery_exp))
655
- return self.where(condition)
656
- except Exception: # noqa: S110
657
- pass
658
- msg = "Unsupported type for 'values' in WHERE NOT ANY"
659
- raise SQLBuilderError(msg)
660
- if not is_iterable_parameters(values) or isinstance(values, bytes):
661
- msg = "Unsupported type for 'values' in WHERE NOT ANY"
662
- raise SQLBuilderError(msg)
663
- column_name = extract_column_name(column)
664
- parameters = []
665
- for i, v in enumerate(values):
666
- if len(values) == 1:
667
- param_name = builder._generate_unique_parameter_name(column_name)
668
- else:
669
- param_name = builder._generate_unique_parameter_name(f"{column_name}_not_any_{i + 1}")
670
- _, param_name = builder.add_parameter(v, name=param_name)
671
- parameters.append(exp.Placeholder(this=param_name))
672
- tuple_expr = exp.Tuple(expressions=parameters)
673
- condition = exp.NEQ(this=col_expr, expression=exp.Any(this=tuple_expr))
674
- return self.where(condition)
675
-
676
- def or_where(
677
- self,
678
- condition: Union[
679
- str, exp.Expression, exp.Condition, tuple[str, Any], tuple[str, str, Any], "ColumnExpression", "SQL"
680
- ],
681
- *values: Any,
682
- operator: Optional[str] = None,
683
- **kwargs: Any,
684
- ) -> Self:
685
- """Add an OR condition to the existing WHERE clause.
686
-
687
- Args:
688
- condition: The condition for the OR WHERE clause. Can be:
689
- - A string condition with or without parameter placeholders
690
- - A string column name (when values are provided)
691
- - A sqlglot Expression or Condition
692
- - A 2-tuple (column, value) for equality comparison
693
- - A 3-tuple (column, operator, value) for custom comparison
694
- *values: Positional values for parameter binding (when condition contains placeholders or is a column name)
695
- operator: Operator for comparison (when condition is a column name)
696
- **kwargs: Named parameters for parameter binding (when condition contains named placeholders)
697
-
698
- Raises:
699
- SQLBuilderError: If the current expression is not a supported statement type or no existing WHERE clause.
700
-
701
- Returns:
702
- The current builder instance for method chaining.
703
- """
704
- builder = cast("SQLBuilderProtocol", self)
705
- if builder._expression is None:
706
- msg = "Cannot add OR WHERE clause: expression is not initialized."
707
- raise SQLBuilderError(msg)
708
-
709
- # Get the existing WHERE condition
710
- existing_where = None
711
- if isinstance(builder._expression, (exp.Select, exp.Update, exp.Delete)):
712
- existing_where = builder._expression.find(exp.Where)
713
- if existing_where:
714
- existing_where = existing_where.this
715
-
716
- if existing_where is None:
717
- msg = "Cannot add OR WHERE clause: no existing WHERE clause found. Use where() first."
718
- raise SQLBuilderError(msg)
719
-
720
- # Process the new condition (reuse existing logic from where method)
721
- new_condition = self._process_where_condition(condition, values, operator, kwargs)
722
-
723
- # Combine with existing WHERE using OR
724
- or_condition = exp.Or(this=existing_where, expression=new_condition)
725
-
726
- # Update the WHERE clause by modifying the existing WHERE node
727
- if isinstance(builder._expression, (exp.Select, exp.Update, exp.Delete)):
728
- where_node = builder._expression.find(exp.Where)
729
- if where_node:
730
- where_node.set("this", or_condition)
731
- else:
732
- # This shouldn't happen since we checked for existing_where above
733
- builder._expression = builder._expression.where(or_condition, copy=False)
734
- else:
735
- msg = f"OR WHERE clause not supported for {type(builder._expression).__name__}"
736
- raise SQLBuilderError(msg)
737
-
738
- return self
739
-
740
- def where_or(self, *conditions: Union[str, "tuple[Any, ...]", exp.Expression]) -> Self:
741
- """Combine multiple conditions with OR logic.
742
-
743
- Args:
744
- *conditions: Multiple conditions to combine with OR. Each condition can be:
745
- - A string condition
746
- - A 2-tuple (column, value) for equality comparison
747
- - A 3-tuple (column, operator, value) for custom comparison
748
- - A sqlglot Expression or Condition
749
-
750
- Raises:
751
- SQLBuilderError: If no conditions provided or current expression not supported.
752
-
753
- Returns:
754
- The current builder instance for method chaining.
755
-
756
- Examples:
757
- query.where_or(
758
- ("name", "John"),
759
- ("email", "john@email.com"),
760
- "age > 25"
761
- )
762
- # Produces: WHERE (name = :name OR email = :email OR age > 25)
763
- """
764
- if not conditions:
765
- msg = "where_or() requires at least one condition"
766
- raise SQLBuilderError(msg)
767
-
768
- builder = cast("SQLBuilderProtocol", self)
769
- if builder._expression is None:
770
- msg = "Cannot add WHERE OR clause: expression is not initialized."
771
- raise SQLBuilderError(msg)
772
-
773
- # Process all conditions
774
- processed_conditions = []
775
- for condition in conditions:
776
- processed_condition = self._process_where_condition(condition, (), None, {})
777
- processed_conditions.append(processed_condition)
778
-
779
- # Create OR expression from all conditions
780
- or_condition = self._create_or_expression(processed_conditions)
781
-
782
- # Apply the OR condition
783
- if isinstance(builder._expression, (exp.Select, exp.Update, exp.Delete)):
784
- builder._expression = builder._expression.where(or_condition, copy=False)
785
- else:
786
- msg = f"WHERE OR clause not supported for {type(builder._expression).__name__}"
787
- raise SQLBuilderError(msg)
788
-
789
- return self
790
-
791
- def _process_where_condition(
792
- self,
793
- condition: Union[
794
- str, exp.Expression, exp.Condition, tuple[str, Any], tuple[str, str, Any], "ColumnExpression", "SQL"
795
- ],
796
- values: tuple[Any, ...],
797
- operator: Optional[str],
798
- kwargs: dict[str, Any],
799
- ) -> exp.Expression:
800
- """Process a WHERE condition into a sqlglot expression.
801
-
802
- This is extracted from the where() method to be reusable by OR methods.
803
-
804
- Args:
805
- condition: The condition to process
806
- values: Positional values for parameter binding
807
- operator: Operator for comparison
808
- kwargs: Named parameters for parameter binding
809
-
810
- Returns:
811
- Processed sqlglot expression
812
- """
813
- builder = cast("SQLBuilderProtocol", self)
814
-
815
- # Handle string conditions with external parameters
816
- if values or kwargs:
817
- if not isinstance(condition, str):
818
- msg = "When values are provided, condition must be a string"
819
- raise SQLBuilderError(msg)
820
-
821
- # Check if condition contains parameter placeholders
822
- from sqlspec.core.parameters import ParameterStyle, ParameterValidator
823
-
824
- validator = ParameterValidator()
825
- param_info = validator.extract_parameters(condition)
826
-
827
- if param_info:
828
- # String condition with placeholders - create SQL object with parameters
829
- from sqlspec import sql as sql_factory
830
-
831
- # Create parameter mapping based on the detected parameter info
832
- param_dict = dict(kwargs) # Start with named parameters
833
-
834
- # Handle positional parameters - these are ordinal-based ($1, $2, :1, :2, ?)
835
- positional_params = [
836
- param
837
- for param in param_info
838
- if param.style in {ParameterStyle.NUMERIC, ParameterStyle.POSITIONAL_COLON, ParameterStyle.QMARK}
839
- ]
840
-
841
- # Map positional values to positional parameters
842
- if len(values) != len(positional_params):
843
- msg = f"Parameter count mismatch: condition has {len(positional_params)} positional placeholders, got {len(values)} values"
844
- raise SQLBuilderError(msg)
845
-
846
- for i, value in enumerate(values):
847
- param_dict[f"param_{i}"] = value
848
-
849
- # Create SQL object with parameters that will be processed correctly
850
- condition = sql_factory.raw(condition, **param_dict)
851
- # Fall through to existing SQL object handling logic
852
-
853
- elif len(values) == 1 and not kwargs:
854
- # Single value - treat as column = value
855
- if operator is not None:
856
- return self._process_tuple_condition((condition, operator, values[0]))
857
- return self._process_tuple_condition((condition, values[0]))
858
- else:
859
- msg = f"Cannot bind parameters to condition without placeholders: {condition}"
860
- raise SQLBuilderError(msg)
861
-
862
- # Handle all condition types (including SQL objects created above)
863
- if isinstance(condition, str):
864
- return parse_condition_expression(condition)
865
- if isinstance(condition, (exp.Expression, exp.Condition)):
866
- return condition
867
- if isinstance(condition, tuple):
868
- return self._process_tuple_condition(condition)
869
- if has_query_builder_parameters(condition):
870
- column_expr_obj = cast("ColumnExpression", condition)
871
- return column_expr_obj._expression # pyright: ignore
872
- if has_sqlglot_expression(condition):
873
- raw_expr = condition.sqlglot_expression # pyright: ignore[attr-defined]
874
- if raw_expr is not None:
875
- return builder._parameterize_expression(raw_expr)
876
- return parse_condition_expression(str(condition))
877
- if hasattr(condition, "expression") and hasattr(condition, "sql"):
878
- # Handle SQL objects (from sql.raw with parameters)
879
- expression = getattr(condition, "expression", None)
880
- if expression is not None and isinstance(expression, exp.Expression):
881
- # Merge parameters from SQL object into builder
882
- if hasattr(condition, "parameters") and hasattr(builder, "add_parameter"):
883
- sql_parameters = getattr(condition, "parameters", {})
884
- for param_name, param_value in sql_parameters.items():
885
- unique_name = builder._generate_unique_parameter_name(param_name)
886
- builder.add_parameter(param_value, name=unique_name)
887
- return cast("exp.Expression", expression)
888
- # If expression is None, fall back to parsing the raw SQL
889
- sql_text = getattr(condition, "sql", "")
890
- # Merge parameters even when parsing raw SQL
891
- if hasattr(condition, "parameters") and hasattr(builder, "add_parameter"):
892
- sql_parameters = getattr(condition, "parameters", {})
893
- for param_name, param_value in sql_parameters.items():
894
- unique_name = builder._generate_unique_parameter_name(param_name)
895
- builder.add_parameter(param_value, name=unique_name)
896
- return parse_condition_expression(sql_text)
897
- msg = f"Unsupported condition type: {type(condition).__name__}"
898
- raise SQLBuilderError(msg)
899
-
900
- def _create_or_expression(self, conditions: list[exp.Expression]) -> exp.Expression:
901
- """Create OR expression from multiple conditions.
902
-
903
- Args:
904
- conditions: List of sqlglot expressions to combine with OR
905
-
906
- Returns:
907
- Combined OR expression, or single condition if only one provided
908
- """
909
- if len(conditions) == 1:
910
- return conditions[0]
911
-
912
- result = conditions[0]
913
- for condition in conditions[1:]:
914
- result = exp.Or(this=result, expression=condition)
915
- return result
916
-
917
- # OR helper methods for consistency with existing where_* methods
918
- def or_where_eq(self, column: Union[str, exp.Column], value: Any) -> Self:
919
- """Add OR column = value clause."""
920
- condition = self._create_parameterized_condition(column, value, lambda col, placeholder: col.eq(placeholder))
921
- return self.or_where(condition)
922
-
923
- def or_where_neq(self, column: Union[str, exp.Column], value: Any) -> Self:
924
- """Add OR column != value clause."""
925
- condition = self._create_parameterized_condition(column, value, lambda col, placeholder: col.neq(placeholder))
926
- return self.or_where(condition)
927
-
928
- def or_where_in(self, column: Union[str, exp.Column], values: Any) -> Self:
929
- """Add OR column IN (values) clause."""
930
- builder = cast("SQLBuilderProtocol", self)
931
- col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
932
- if has_query_builder_parameters(values) or isinstance(values, exp.Expression):
933
- subquery_exp: exp.Expression
934
- if has_query_builder_parameters(values):
935
- subquery_builder_parameters: dict[str, Any] = values.parameters # pyright: ignore
936
- param_mapping = {}
937
- if subquery_builder_parameters:
938
- for p_name, p_value in subquery_builder_parameters.items():
939
- unique_name = builder._generate_unique_parameter_name(p_name)
940
- param_mapping[p_name] = unique_name
941
- builder.add_parameter(p_value, name=unique_name)
942
- subquery = values.build() # pyright: ignore
943
- subquery_parsed = exp.maybe_parse(subquery.sql, dialect=builder.dialect_name) # pyright: ignore
944
- if param_mapping and subquery_parsed:
945
- subquery_parsed = cast("Any", builder)._update_placeholders_in_expression(
946
- subquery_parsed, param_mapping
947
- )
948
- subquery_exp = (
949
- exp.paren(subquery_parsed)
950
- if subquery_parsed
951
- else exp.paren(exp.maybe_parse(subquery.sql, dialect=builder.dialect_name)) # pyright: ignore
952
- ) # pyright: ignore
953
- else:
954
- subquery_exp = values # type: ignore[assignment]
955
- condition = col_expr.isin(subquery_exp)
956
- return self.or_where(condition)
957
- if not is_iterable_parameters(values) or isinstance(values, (str, bytes)):
958
- msg = "Unsupported type for 'values' in OR WHERE IN"
959
- raise SQLBuilderError(msg)
960
- column_name = extract_column_name(column)
961
- parameters = []
962
- for i, v in enumerate(values):
963
- if len(values) == 1:
964
- param_name = builder._generate_unique_parameter_name(column_name)
965
- else:
966
- param_name = builder._generate_unique_parameter_name(f"{column_name}_{i + 1}")
967
- _, param_name = builder.add_parameter(v, name=param_name)
968
- parameters.append(exp.Placeholder(this=param_name))
969
- condition = col_expr.isin(*parameters)
970
- return self.or_where(condition)
971
-
972
- def or_where_like(self, column: Union[str, exp.Column], pattern: str, escape: Optional[str] = None) -> Self:
973
- """Add OR column LIKE pattern clause."""
974
- builder = cast("SQLBuilderProtocol", self)
975
- column_name = extract_column_name(column)
976
- param_name = builder._generate_unique_parameter_name(column_name)
977
- _, param_name = builder.add_parameter(pattern, name=param_name)
978
- col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
979
- if escape is not None:
980
- cond = exp.Like(this=col_expr, expression=exp.Placeholder(this=param_name), escape=exp.convert(str(escape)))
981
- else:
982
- cond = col_expr.like(exp.Placeholder(this=param_name))
983
- condition: exp.Expression = cond
984
- return self.or_where(condition)
985
-
986
- def or_where_is_null(self, column: Union[str, exp.Column]) -> Self:
987
- """Add OR column IS NULL clause."""
988
- col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
989
- condition: exp.Expression = col_expr.is_(exp.null())
990
- return self.or_where(condition)
991
-
992
- def or_where_is_not_null(self, column: Union[str, exp.Column]) -> Self:
993
- """Add OR column IS NOT NULL clause."""
994
- col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
995
- condition: exp.Expression = col_expr.is_(exp.null()).not_()
996
- return self.or_where(condition)
997
-
998
- def or_where_lt(self, column: Union[str, exp.Column], value: Any) -> Self:
999
- """Add OR column < value clause."""
1000
- condition = self._create_parameterized_condition(
1001
- column, value, lambda col, placeholder: exp.LT(this=col, expression=placeholder)
1002
- )
1003
- return self.or_where(condition)
1004
-
1005
- def or_where_lte(self, column: Union[str, exp.Column], value: Any) -> Self:
1006
- """Add OR column <= value clause."""
1007
- condition = self._create_parameterized_condition(
1008
- column, value, lambda col, placeholder: exp.LTE(this=col, expression=placeholder)
1009
- )
1010
- return self.or_where(condition)
1011
-
1012
- def or_where_gt(self, column: Union[str, exp.Column], value: Any) -> Self:
1013
- """Add OR column > value clause."""
1014
- condition = self._create_parameterized_condition(
1015
- column, value, lambda col, placeholder: exp.GT(this=col, expression=placeholder)
1016
- )
1017
- return self.or_where(condition)
1018
-
1019
- def or_where_gte(self, column: Union[str, exp.Column], value: Any) -> Self:
1020
- """Add OR column >= value clause."""
1021
- condition = self._create_parameterized_condition(
1022
- column, value, lambda col, placeholder: exp.GTE(this=col, expression=placeholder)
1023
- )
1024
- return self.or_where(condition)
1025
-
1026
- def or_where_between(self, column: Union[str, exp.Column], low: Any, high: Any) -> Self:
1027
- """Add OR column BETWEEN low AND high clause."""
1028
- builder = cast("SQLBuilderProtocol", self)
1029
- column_name = extract_column_name(column)
1030
- low_param = builder._generate_unique_parameter_name(f"{column_name}_low")
1031
- high_param = builder._generate_unique_parameter_name(f"{column_name}_high")
1032
- _, low_param = builder.add_parameter(low, name=low_param)
1033
- _, high_param = builder.add_parameter(high, name=high_param)
1034
- col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
1035
- condition: exp.Expression = col_expr.between(exp.Placeholder(this=low_param), exp.Placeholder(this=high_param))
1036
- return self.or_where(condition)
1037
-
1038
- def or_where_not_like(self, column: Union[str, exp.Column], pattern: str) -> Self:
1039
- """Add OR column NOT LIKE pattern clause."""
1040
- condition = self._create_parameterized_condition(
1041
- column, pattern, lambda col, placeholder: col.like(placeholder).not_()
1042
- )
1043
- return self.or_where(condition)
1044
-
1045
- def or_where_not_in(self, column: Union[str, exp.Column], values: Any) -> Self:
1046
- """Add OR column NOT IN (values) clause."""
1047
- builder = cast("SQLBuilderProtocol", self)
1048
- col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
1049
- if has_query_builder_parameters(values) or isinstance(values, exp.Expression):
1050
- subquery_exp: exp.Expression
1051
- if has_query_builder_parameters(values):
1052
- subquery_builder_parameters: dict[str, Any] = values.parameters # pyright: ignore
1053
- param_mapping = {}
1054
- if subquery_builder_parameters:
1055
- for p_name, p_value in subquery_builder_parameters.items():
1056
- unique_name = builder._generate_unique_parameter_name(p_name)
1057
- param_mapping[p_name] = unique_name
1058
- builder.add_parameter(p_value, name=unique_name)
1059
- subquery = values.build() # pyright: ignore
1060
- subquery_parsed = exp.maybe_parse(subquery.sql, dialect=builder.dialect_name) # pyright: ignore
1061
- if param_mapping and subquery_parsed:
1062
- subquery_parsed = cast("Any", builder)._update_placeholders_in_expression(
1063
- subquery_parsed, param_mapping
1064
- )
1065
- subquery_exp = (
1066
- exp.paren(subquery_parsed)
1067
- if subquery_parsed
1068
- else exp.paren(exp.maybe_parse(subquery.sql, dialect=builder.dialect_name)) # pyright: ignore
1069
- ) # pyright: ignore
1070
- else:
1071
- subquery_exp = values # type: ignore[assignment]
1072
- condition = exp.Not(this=col_expr.isin(subquery_exp))
1073
- return self.or_where(condition)
1074
- if not is_iterable_parameters(values) or isinstance(values, (str, bytes)):
1075
- msg = "Values for or_where_not_in must be a non-string iterable or subquery."
1076
- raise SQLBuilderError(msg)
1077
- column_name = extract_column_name(column)
1078
- parameters = []
1079
- for i, v in enumerate(values):
1080
- if len(values) == 1:
1081
- param_name = builder._generate_unique_parameter_name(column_name)
1082
- else:
1083
- param_name = builder._generate_unique_parameter_name(f"{column_name}_{i + 1}")
1084
- _, param_name = builder.add_parameter(v, name=param_name)
1085
- parameters.append(exp.Placeholder(this=param_name))
1086
- condition = exp.Not(this=col_expr.isin(*parameters))
1087
- return self.or_where(condition)
1088
-
1089
- def or_where_ilike(self, column: Union[str, exp.Column], pattern: str) -> Self:
1090
- """Add OR column ILIKE pattern clause."""
1091
- condition = self._create_parameterized_condition(
1092
- column, pattern, lambda col, placeholder: col.ilike(placeholder)
1093
- )
1094
- return self.or_where(condition)
1095
-
1096
- def or_where_null(self, column: Union[str, exp.Column]) -> Self:
1097
- """Add OR column IS NULL clause."""
1098
- return self.or_where_is_null(column)
1099
-
1100
- def or_where_not_null(self, column: Union[str, exp.Column]) -> Self:
1101
- """Add OR column IS NOT NULL clause."""
1102
- return self.or_where_is_not_null(column)
1103
-
1104
- def or_where_exists(self, subquery: Union[str, Any]) -> Self:
1105
- """Add OR EXISTS (subquery) clause."""
1106
- builder = cast("SQLBuilderProtocol", self)
1107
- sub_expr: exp.Expression
1108
- if has_query_builder_parameters(subquery):
1109
- subquery_builder_parameters: dict[str, Any] = subquery.parameters
1110
- param_mapping = {}
1111
- if subquery_builder_parameters:
1112
- for p_name, p_value in subquery_builder_parameters.items():
1113
- unique_name = builder._generate_unique_parameter_name(p_name)
1114
- param_mapping[p_name] = unique_name
1115
- builder.add_parameter(p_value, name=unique_name)
1116
- sub_sql_obj = subquery.build() # pyright: ignore
1117
- sub_expr = exp.maybe_parse(sub_sql_obj.sql, dialect=builder.dialect_name) # pyright: ignore
1118
- # Update placeholders to use unique parameter names
1119
- if param_mapping and sub_expr:
1120
- sub_expr = cast("Any", builder)._update_placeholders_in_expression(sub_expr, param_mapping)
1121
- else:
1122
- sub_expr = exp.maybe_parse(str(subquery), dialect=builder.dialect_name)
1123
-
1124
- if sub_expr is None:
1125
- msg = "Could not parse subquery for OR EXISTS"
1126
- raise SQLBuilderError(msg)
1127
-
1128
- exists_expr = exp.Exists(this=sub_expr)
1129
- return self.or_where(exists_expr)
1130
-
1131
- def or_where_not_exists(self, subquery: Union[str, Any]) -> Self:
1132
- """Add OR NOT EXISTS (subquery) clause."""
1133
- builder = cast("SQLBuilderProtocol", self)
1134
- sub_expr: exp.Expression
1135
- if has_query_builder_parameters(subquery):
1136
- subquery_builder_parameters: dict[str, Any] = subquery.parameters
1137
- param_mapping = {}
1138
- if subquery_builder_parameters:
1139
- for p_name, p_value in subquery_builder_parameters.items():
1140
- unique_name = builder._generate_unique_parameter_name(p_name)
1141
- param_mapping[p_name] = unique_name
1142
- builder.add_parameter(p_value, name=unique_name)
1143
- sub_sql_obj = subquery.build() # pyright: ignore
1144
- sub_expr = exp.maybe_parse(sub_sql_obj.sql, dialect=builder.dialect_name) # pyright: ignore
1145
- # Update placeholders to use unique parameter names
1146
- if param_mapping and sub_expr:
1147
- sub_expr = cast("Any", builder)._update_placeholders_in_expression(sub_expr, param_mapping)
1148
- else:
1149
- sub_expr = exp.maybe_parse(str(subquery), dialect=builder.dialect_name)
1150
-
1151
- if sub_expr is None:
1152
- msg = "Could not parse subquery for OR NOT EXISTS"
1153
- raise SQLBuilderError(msg)
1154
-
1155
- not_exists_expr = exp.Not(this=exp.Exists(this=sub_expr))
1156
- return self.or_where(not_exists_expr)
1157
-
1158
- def or_where_any(self, column: Union[str, exp.Column], values: Any) -> Self:
1159
- """Add OR column = ANY(values) clause."""
1160
- builder = cast("SQLBuilderProtocol", self)
1161
- col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
1162
- if has_query_builder_parameters(values) or isinstance(values, exp.Expression):
1163
- subquery_exp: exp.Expression
1164
- if has_query_builder_parameters(values):
1165
- subquery_builder_parameters: dict[str, Any] = values.parameters # pyright: ignore
1166
- param_mapping = {}
1167
- if subquery_builder_parameters:
1168
- for p_name, p_value in subquery_builder_parameters.items():
1169
- unique_name = builder._generate_unique_parameter_name(p_name)
1170
- param_mapping[p_name] = unique_name
1171
- builder.add_parameter(p_value, name=unique_name)
1172
- subquery = values.build() # pyright: ignore
1173
- subquery_parsed = exp.maybe_parse(subquery.sql, dialect=builder.dialect_name) # pyright: ignore
1174
- if param_mapping and subquery_parsed:
1175
- subquery_parsed = cast("Any", builder)._update_placeholders_in_expression(
1176
- subquery_parsed, param_mapping
1177
- )
1178
- subquery_exp = (
1179
- exp.paren(subquery_parsed)
1180
- if subquery_parsed
1181
- else exp.paren(exp.maybe_parse(subquery.sql, dialect=builder.dialect_name)) # pyright: ignore
1182
- ) # pyright: ignore
1183
- else:
1184
- subquery_exp = values # type: ignore[assignment]
1185
- condition = exp.EQ(this=col_expr, expression=exp.Any(this=subquery_exp))
1186
- return self.or_where(condition)
1187
- if isinstance(values, str):
1188
- try:
1189
- parsed_expr: Optional[exp.Expression] = exp.maybe_parse(values)
1190
- if isinstance(parsed_expr, (exp.Select, exp.Union, exp.Subquery)):
1191
- subquery_exp = exp.paren(parsed_expr)
1192
- condition = exp.EQ(this=col_expr, expression=exp.Any(this=subquery_exp))
1193
- return self.or_where(condition)
1194
- except Exception: # noqa: S110
1195
- pass
1196
- msg = "Unsupported type for 'values' in OR WHERE ANY"
1197
- raise SQLBuilderError(msg)
1198
- if not is_iterable_parameters(values) or isinstance(values, bytes):
1199
- msg = "Unsupported type for 'values' in OR WHERE ANY"
1200
- raise SQLBuilderError(msg)
1201
- column_name = extract_column_name(column)
1202
- parameters = []
1203
- for i, v in enumerate(values):
1204
- if len(values) == 1:
1205
- param_name = builder._generate_unique_parameter_name(column_name)
1206
- else:
1207
- param_name = builder._generate_unique_parameter_name(f"{column_name}_any_{i + 1}")
1208
- _, param_name = builder.add_parameter(v, name=param_name)
1209
- parameters.append(exp.Placeholder(this=param_name))
1210
- tuple_expr = exp.Tuple(expressions=parameters)
1211
- condition = exp.EQ(this=col_expr, expression=exp.Any(this=tuple_expr))
1212
- return self.or_where(condition)
1213
-
1214
- def or_where_not_any(self, column: Union[str, exp.Column], values: Any) -> Self:
1215
- """Add OR column <> ANY(values) clause."""
1216
- builder = cast("SQLBuilderProtocol", self)
1217
- col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
1218
- if has_query_builder_parameters(values) or isinstance(values, exp.Expression):
1219
- subquery_exp: exp.Expression
1220
- if has_query_builder_parameters(values):
1221
- subquery_builder_parameters: dict[str, Any] = values.parameters # pyright: ignore
1222
- param_mapping = {}
1223
- if subquery_builder_parameters:
1224
- for p_name, p_value in subquery_builder_parameters.items():
1225
- unique_name = builder._generate_unique_parameter_name(p_name)
1226
- param_mapping[p_name] = unique_name
1227
- builder.add_parameter(p_value, name=unique_name)
1228
- subquery = values.build() # pyright: ignore
1229
- subquery_parsed = exp.maybe_parse(subquery.sql, dialect=builder.dialect_name) # pyright: ignore
1230
- if param_mapping and subquery_parsed:
1231
- subquery_parsed = cast("Any", builder)._update_placeholders_in_expression(
1232
- subquery_parsed, param_mapping
1233
- )
1234
- subquery_exp = (
1235
- exp.paren(subquery_parsed)
1236
- if subquery_parsed
1237
- else exp.paren(exp.maybe_parse(subquery.sql, dialect=builder.dialect_name)) # pyright: ignore
1238
- ) # pyright: ignore
1239
- else:
1240
- subquery_exp = values # type: ignore[assignment]
1241
- condition = exp.NEQ(this=col_expr, expression=exp.Any(this=subquery_exp))
1242
- return self.or_where(condition)
1243
- if isinstance(values, str):
1244
- try:
1245
- parsed_expr: Optional[exp.Expression] = exp.maybe_parse(values)
1246
- if isinstance(parsed_expr, (exp.Select, exp.Union, exp.Subquery)):
1247
- subquery_exp = exp.paren(parsed_expr)
1248
- condition = exp.NEQ(this=col_expr, expression=exp.Any(this=subquery_exp))
1249
- return self.or_where(condition)
1250
- except Exception: # noqa: S110
1251
- pass
1252
- msg = "Unsupported type for 'values' in OR WHERE NOT ANY"
1253
- raise SQLBuilderError(msg)
1254
- if not is_iterable_parameters(values) or isinstance(values, bytes):
1255
- msg = "Unsupported type for 'values' in OR WHERE NOT ANY"
1256
- raise SQLBuilderError(msg)
1257
- column_name = extract_column_name(column)
1258
- parameters = []
1259
- for i, v in enumerate(values):
1260
- if len(values) == 1:
1261
- param_name = builder._generate_unique_parameter_name(column_name)
1262
- else:
1263
- param_name = builder._generate_unique_parameter_name(f"{column_name}_not_any_{i + 1}")
1264
- _, param_name = builder.add_parameter(v, name=param_name)
1265
- parameters.append(exp.Placeholder(this=param_name))
1266
- tuple_expr = exp.Tuple(expressions=parameters)
1267
- condition = exp.NEQ(this=col_expr, expression=exp.Any(this=tuple_expr))
1268
- return self.or_where(condition)
1269
-
1270
-
1271
- @trait
1272
- class HavingClauseMixin:
1273
- """Mixin providing HAVING clause for SELECT builders."""
1274
-
1275
- __slots__ = ()
1276
-
1277
- # Type annotations for PyRight - these will be provided by the base class
1278
- def get_expression(self) -> Optional[exp.Expression]: ...
1279
- def set_expression(self, expression: exp.Expression) -> None: ...
1280
-
1281
- def having(self, condition: Union[str, exp.Expression]) -> Self:
1282
- """Add HAVING clause.
1283
-
1284
- Args:
1285
- condition: The condition for the HAVING clause.
1286
-
1287
- Raises:
1288
- SQLBuilderError: If the current expression is not a SELECT statement.
1289
-
1290
- Returns:
1291
- The current builder instance for method chaining.
1292
- """
1293
- current_expr = self.get_expression()
1294
- if current_expr is None:
1295
- self.set_expression(exp.Select())
1296
- current_expr = self.get_expression()
1297
-
1298
- if not isinstance(current_expr, exp.Select):
1299
- msg = "Cannot add HAVING to a non-SELECT expression."
1300
- raise SQLBuilderError(msg)
1301
- having_expr = exp.condition(condition) if isinstance(condition, str) else condition
1302
- updated_expr = current_expr.having(having_expr, copy=False)
1303
- self.set_expression(updated_expr)
1304
- return self