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