sqlspec 0.14.1__py3-none-any.whl → 0.16.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 (159) hide show
  1. sqlspec/__init__.py +50 -25
  2. sqlspec/__main__.py +1 -1
  3. sqlspec/__metadata__.py +1 -3
  4. sqlspec/_serialization.py +1 -2
  5. sqlspec/_sql.py +480 -121
  6. sqlspec/_typing.py +278 -142
  7. sqlspec/adapters/adbc/__init__.py +4 -3
  8. sqlspec/adapters/adbc/_types.py +12 -0
  9. sqlspec/adapters/adbc/config.py +115 -260
  10. sqlspec/adapters/adbc/driver.py +462 -367
  11. sqlspec/adapters/aiosqlite/__init__.py +18 -3
  12. sqlspec/adapters/aiosqlite/_types.py +13 -0
  13. sqlspec/adapters/aiosqlite/config.py +199 -129
  14. sqlspec/adapters/aiosqlite/driver.py +230 -269
  15. sqlspec/adapters/asyncmy/__init__.py +18 -3
  16. sqlspec/adapters/asyncmy/_types.py +12 -0
  17. sqlspec/adapters/asyncmy/config.py +80 -168
  18. sqlspec/adapters/asyncmy/driver.py +260 -225
  19. sqlspec/adapters/asyncpg/__init__.py +19 -4
  20. sqlspec/adapters/asyncpg/_types.py +17 -0
  21. sqlspec/adapters/asyncpg/config.py +82 -181
  22. sqlspec/adapters/asyncpg/driver.py +285 -383
  23. sqlspec/adapters/bigquery/__init__.py +17 -3
  24. sqlspec/adapters/bigquery/_types.py +12 -0
  25. sqlspec/adapters/bigquery/config.py +191 -258
  26. sqlspec/adapters/bigquery/driver.py +474 -646
  27. sqlspec/adapters/duckdb/__init__.py +14 -3
  28. sqlspec/adapters/duckdb/_types.py +12 -0
  29. sqlspec/adapters/duckdb/config.py +415 -351
  30. sqlspec/adapters/duckdb/driver.py +343 -413
  31. sqlspec/adapters/oracledb/__init__.py +19 -5
  32. sqlspec/adapters/oracledb/_types.py +14 -0
  33. sqlspec/adapters/oracledb/config.py +123 -379
  34. sqlspec/adapters/oracledb/driver.py +507 -560
  35. sqlspec/adapters/psqlpy/__init__.py +13 -3
  36. sqlspec/adapters/psqlpy/_types.py +11 -0
  37. sqlspec/adapters/psqlpy/config.py +93 -254
  38. sqlspec/adapters/psqlpy/driver.py +505 -234
  39. sqlspec/adapters/psycopg/__init__.py +19 -5
  40. sqlspec/adapters/psycopg/_types.py +17 -0
  41. sqlspec/adapters/psycopg/config.py +143 -403
  42. sqlspec/adapters/psycopg/driver.py +706 -872
  43. sqlspec/adapters/sqlite/__init__.py +14 -3
  44. sqlspec/adapters/sqlite/_types.py +11 -0
  45. sqlspec/adapters/sqlite/config.py +202 -118
  46. sqlspec/adapters/sqlite/driver.py +264 -303
  47. sqlspec/base.py +105 -9
  48. sqlspec/{statement/builder → builder}/__init__.py +12 -14
  49. sqlspec/{statement/builder → builder}/_base.py +120 -55
  50. sqlspec/{statement/builder → builder}/_column.py +17 -6
  51. sqlspec/{statement/builder → builder}/_ddl.py +46 -79
  52. sqlspec/{statement/builder → builder}/_ddl_utils.py +5 -10
  53. sqlspec/{statement/builder → builder}/_delete.py +6 -25
  54. sqlspec/{statement/builder → builder}/_insert.py +18 -65
  55. sqlspec/builder/_merge.py +56 -0
  56. sqlspec/{statement/builder → builder}/_parsing_utils.py +8 -11
  57. sqlspec/{statement/builder → builder}/_select.py +11 -56
  58. sqlspec/{statement/builder → builder}/_update.py +12 -18
  59. sqlspec/{statement/builder → builder}/mixins/__init__.py +10 -14
  60. sqlspec/{statement/builder → builder}/mixins/_cte_and_set_ops.py +48 -59
  61. sqlspec/{statement/builder → builder}/mixins/_insert_operations.py +34 -18
  62. sqlspec/{statement/builder → builder}/mixins/_join_operations.py +1 -3
  63. sqlspec/{statement/builder → builder}/mixins/_merge_operations.py +19 -9
  64. sqlspec/{statement/builder → builder}/mixins/_order_limit_operations.py +3 -3
  65. sqlspec/{statement/builder → builder}/mixins/_pivot_operations.py +4 -8
  66. sqlspec/{statement/builder → builder}/mixins/_select_operations.py +25 -38
  67. sqlspec/{statement/builder → builder}/mixins/_update_operations.py +15 -16
  68. sqlspec/{statement/builder → builder}/mixins/_where_clause.py +210 -137
  69. sqlspec/cli.py +4 -5
  70. sqlspec/config.py +180 -133
  71. sqlspec/core/__init__.py +63 -0
  72. sqlspec/core/cache.py +873 -0
  73. sqlspec/core/compiler.py +396 -0
  74. sqlspec/core/filters.py +830 -0
  75. sqlspec/core/hashing.py +310 -0
  76. sqlspec/core/parameters.py +1209 -0
  77. sqlspec/core/result.py +664 -0
  78. sqlspec/{statement → core}/splitter.py +321 -191
  79. sqlspec/core/statement.py +666 -0
  80. sqlspec/driver/__init__.py +7 -10
  81. sqlspec/driver/_async.py +387 -176
  82. sqlspec/driver/_common.py +527 -289
  83. sqlspec/driver/_sync.py +390 -172
  84. sqlspec/driver/mixins/__init__.py +2 -19
  85. sqlspec/driver/mixins/_result_tools.py +164 -0
  86. sqlspec/driver/mixins/_sql_translator.py +6 -3
  87. sqlspec/exceptions.py +5 -252
  88. sqlspec/extensions/aiosql/adapter.py +93 -96
  89. sqlspec/extensions/litestar/cli.py +1 -1
  90. sqlspec/extensions/litestar/config.py +0 -1
  91. sqlspec/extensions/litestar/handlers.py +15 -26
  92. sqlspec/extensions/litestar/plugin.py +18 -16
  93. sqlspec/extensions/litestar/providers.py +17 -52
  94. sqlspec/loader.py +424 -105
  95. sqlspec/migrations/__init__.py +12 -0
  96. sqlspec/migrations/base.py +92 -68
  97. sqlspec/migrations/commands.py +24 -106
  98. sqlspec/migrations/loaders.py +402 -0
  99. sqlspec/migrations/runner.py +49 -51
  100. sqlspec/migrations/tracker.py +31 -44
  101. sqlspec/migrations/utils.py +64 -24
  102. sqlspec/protocols.py +7 -183
  103. sqlspec/storage/__init__.py +1 -1
  104. sqlspec/storage/backends/base.py +37 -40
  105. sqlspec/storage/backends/fsspec.py +136 -112
  106. sqlspec/storage/backends/obstore.py +138 -160
  107. sqlspec/storage/capabilities.py +5 -4
  108. sqlspec/storage/registry.py +57 -106
  109. sqlspec/typing.py +136 -115
  110. sqlspec/utils/__init__.py +2 -3
  111. sqlspec/utils/correlation.py +0 -3
  112. sqlspec/utils/deprecation.py +6 -6
  113. sqlspec/utils/fixtures.py +6 -6
  114. sqlspec/utils/logging.py +0 -2
  115. sqlspec/utils/module_loader.py +7 -12
  116. sqlspec/utils/singleton.py +0 -1
  117. sqlspec/utils/sync_tools.py +17 -38
  118. sqlspec/utils/text.py +12 -51
  119. sqlspec/utils/type_guards.py +443 -232
  120. {sqlspec-0.14.1.dist-info → sqlspec-0.16.0.dist-info}/METADATA +7 -2
  121. sqlspec-0.16.0.dist-info/RECORD +134 -0
  122. sqlspec/adapters/adbc/transformers.py +0 -108
  123. sqlspec/driver/connection.py +0 -207
  124. sqlspec/driver/mixins/_cache.py +0 -114
  125. sqlspec/driver/mixins/_csv_writer.py +0 -91
  126. sqlspec/driver/mixins/_pipeline.py +0 -508
  127. sqlspec/driver/mixins/_query_tools.py +0 -796
  128. sqlspec/driver/mixins/_result_utils.py +0 -138
  129. sqlspec/driver/mixins/_storage.py +0 -912
  130. sqlspec/driver/mixins/_type_coercion.py +0 -128
  131. sqlspec/driver/parameters.py +0 -138
  132. sqlspec/statement/__init__.py +0 -21
  133. sqlspec/statement/builder/_merge.py +0 -95
  134. sqlspec/statement/cache.py +0 -50
  135. sqlspec/statement/filters.py +0 -625
  136. sqlspec/statement/parameters.py +0 -956
  137. sqlspec/statement/pipelines/__init__.py +0 -210
  138. sqlspec/statement/pipelines/analyzers/__init__.py +0 -9
  139. sqlspec/statement/pipelines/analyzers/_analyzer.py +0 -646
  140. sqlspec/statement/pipelines/context.py +0 -109
  141. sqlspec/statement/pipelines/transformers/__init__.py +0 -7
  142. sqlspec/statement/pipelines/transformers/_expression_simplifier.py +0 -88
  143. sqlspec/statement/pipelines/transformers/_literal_parameterizer.py +0 -1247
  144. sqlspec/statement/pipelines/transformers/_remove_comments_and_hints.py +0 -76
  145. sqlspec/statement/pipelines/validators/__init__.py +0 -23
  146. sqlspec/statement/pipelines/validators/_dml_safety.py +0 -290
  147. sqlspec/statement/pipelines/validators/_parameter_style.py +0 -370
  148. sqlspec/statement/pipelines/validators/_performance.py +0 -714
  149. sqlspec/statement/pipelines/validators/_security.py +0 -967
  150. sqlspec/statement/result.py +0 -435
  151. sqlspec/statement/sql.py +0 -1774
  152. sqlspec/utils/cached_property.py +0 -25
  153. sqlspec/utils/statement_hashing.py +0 -203
  154. sqlspec-0.14.1.dist-info/RECORD +0 -145
  155. /sqlspec/{statement/builder → builder}/mixins/_delete_operations.py +0 -0
  156. {sqlspec-0.14.1.dist-info → sqlspec-0.16.0.dist-info}/WHEEL +0 -0
  157. {sqlspec-0.14.1.dist-info → sqlspec-0.16.0.dist-info}/entry_points.txt +0 -0
  158. {sqlspec-0.14.1.dist-info → sqlspec-0.16.0.dist-info}/licenses/LICENSE +0 -0
  159. {sqlspec-0.14.1.dist-info → sqlspec-0.16.0.dist-info}/licenses/NOTICE +0 -0
@@ -1,74 +1,84 @@
1
1
  # ruff: noqa: PLR2004
2
2
  """Consolidated WHERE and HAVING clause mixins."""
3
3
 
4
- from typing import TYPE_CHECKING, Any, Callable, Optional, Union, cast
4
+ from typing import TYPE_CHECKING, Any, Optional, Union, cast
5
5
 
6
6
  from sqlglot import exp
7
7
  from typing_extensions import Self
8
8
 
9
+ from sqlspec.builder._parsing_utils import parse_column_expression, parse_condition_expression
9
10
  from sqlspec.exceptions import SQLBuilderError
10
- from sqlspec.statement.builder._parsing_utils import parse_column_expression, parse_condition_expression
11
11
  from sqlspec.utils.type_guards import has_query_builder_parameters, has_sqlglot_expression, is_iterable_parameters
12
12
 
13
- if TYPE_CHECKING:
14
- from sqlspec.protocols import SQLBuilderProtocol
15
- from sqlspec.statement.builder._column import ColumnExpression
16
-
17
- __all__ = ("HavingClauseMixin", "WhereClauseMixin")
18
13
 
14
+ def _extract_column_name(column: Union[str, exp.Column]) -> str:
15
+ """Extract column name from column expression for parameter naming.
19
16
 
20
- class WhereClauseMixin:
21
- """Mixin providing WHERE clause methods for SELECT, UPDATE, and DELETE builders."""
17
+ Args:
18
+ column: Column expression (string or SQLGlot Column)
22
19
 
23
- def _create_operator_handler(self, operator_class: type[exp.Expression]) -> Callable:
24
- """Create a handler that properly parameterizes values."""
20
+ Returns:
21
+ Column name as string for use as parameter name
22
+ """
23
+ if isinstance(column, str):
24
+ # Handle simple column names and table.column references
25
+ if "." in column:
26
+ return column.split(".")[-1] # Return just the column part
27
+ return column
28
+ if isinstance(column, exp.Column):
29
+ # Extract the column name from SQLGlot Column expression
30
+ if column.this and hasattr(column.this, "this"):
31
+ return str(column.this.this)
32
+ return str(column.this) if column.this else "column"
33
+ return "column"
25
34
 
26
- def handler(self: "SQLBuilderProtocol", column_exp: exp.Expression, value: Any) -> exp.Expression:
27
- _, param_name = self.add_parameter(value)
28
- return operator_class(this=column_exp, expression=exp.Placeholder(this=param_name))
29
35
 
30
- return handler
31
-
32
- def _create_like_handler(self) -> Callable:
33
- """Create LIKE handler."""
34
-
35
- def handler(self: "SQLBuilderProtocol", column_exp: exp.Expression, value: Any) -> exp.Expression:
36
- _, param_name = self.add_parameter(value)
37
- return exp.Like(this=column_exp, expression=exp.Placeholder(this=param_name))
38
-
39
- return handler
36
+ if TYPE_CHECKING:
37
+ from sqlspec.builder._column import ColumnExpression
38
+ from sqlspec.protocols import SQLBuilderProtocol
40
39
 
41
- def _create_not_like_handler(self) -> Callable:
42
- """Create NOT LIKE handler."""
40
+ __all__ = ("HavingClauseMixin", "WhereClauseMixin")
43
41
 
44
- def handler(self: "SQLBuilderProtocol", column_exp: exp.Expression, value: Any) -> exp.Expression:
45
- _, param_name = self.add_parameter(value)
46
- return exp.Not(this=exp.Like(this=column_exp, expression=exp.Placeholder(this=param_name)))
47
42
 
48
- return handler
43
+ class WhereClauseMixin:
44
+ """Mixin providing WHERE clause methods for SELECT, UPDATE, and DELETE builders."""
49
45
 
50
- def _handle_in_operator(self, column_exp: exp.Expression, value: Any) -> exp.Expression:
46
+ def _handle_in_operator(
47
+ self, column_exp: exp.Expression, value: Any, column_name: str = "column"
48
+ ) -> exp.Expression:
51
49
  """Handle IN operator."""
52
50
  builder = cast("SQLBuilderProtocol", self)
53
51
  if is_iterable_parameters(value):
54
52
  placeholders = []
55
- for v in value:
56
- _, param_name = builder.add_parameter(v)
53
+ for i, v in enumerate(value):
54
+ if len(value) == 1:
55
+ param_name = builder._generate_unique_parameter_name(column_name) # type: ignore[attr-defined]
56
+ else:
57
+ param_name = builder._generate_unique_parameter_name(f"{column_name}_{i + 1}") # type: ignore[attr-defined]
58
+ _, param_name = builder.add_parameter(v, name=param_name)
57
59
  placeholders.append(exp.Placeholder(this=param_name))
58
60
  return exp.In(this=column_exp, expressions=placeholders)
59
- _, param_name = builder.add_parameter(value)
61
+ param_name = builder._generate_unique_parameter_name(column_name) # type: ignore[attr-defined]
62
+ _, param_name = builder.add_parameter(value, name=param_name)
60
63
  return exp.In(this=column_exp, expressions=[exp.Placeholder(this=param_name)])
61
64
 
62
- def _handle_not_in_operator(self, column_exp: exp.Expression, value: Any) -> exp.Expression:
65
+ def _handle_not_in_operator(
66
+ self, column_exp: exp.Expression, value: Any, column_name: str = "column"
67
+ ) -> exp.Expression:
63
68
  """Handle NOT IN operator."""
64
69
  builder = cast("SQLBuilderProtocol", self)
65
70
  if is_iterable_parameters(value):
66
71
  placeholders = []
67
- for v in value:
68
- _, param_name = builder.add_parameter(v)
72
+ for i, v in enumerate(value):
73
+ if len(value) == 1:
74
+ param_name = builder._generate_unique_parameter_name(column_name) # type: ignore[attr-defined]
75
+ else:
76
+ param_name = builder._generate_unique_parameter_name(f"{column_name}_{i + 1}") # type: ignore[attr-defined]
77
+ _, param_name = builder.add_parameter(v, name=param_name)
69
78
  placeholders.append(exp.Placeholder(this=param_name))
70
79
  return exp.Not(this=exp.In(this=column_exp, expressions=placeholders))
71
- _, param_name = builder.add_parameter(value)
80
+ param_name = builder._generate_unique_parameter_name(column_name) # type: ignore[attr-defined]
81
+ _, param_name = builder.add_parameter(value, name=param_name)
72
82
  return exp.Not(this=exp.In(this=column_exp, expressions=[exp.Placeholder(this=param_name)]))
73
83
 
74
84
  def _handle_is_operator(self, column_exp: exp.Expression, value: Any) -> exp.Expression:
@@ -81,26 +91,34 @@ class WhereClauseMixin:
81
91
  value_expr = exp.Null() if value is None else exp.convert(value)
82
92
  return exp.Not(this=exp.Is(this=column_exp, expression=value_expr))
83
93
 
84
- def _handle_between_operator(self, column_exp: exp.Expression, value: Any) -> exp.Expression:
94
+ def _handle_between_operator(
95
+ self, column_exp: exp.Expression, value: Any, column_name: str = "column"
96
+ ) -> exp.Expression:
85
97
  """Handle BETWEEN operator."""
86
98
  if is_iterable_parameters(value) and len(value) == 2:
87
99
  builder = cast("SQLBuilderProtocol", self)
88
100
  low, high = value
89
- _, low_param = builder.add_parameter(low)
90
- _, high_param = builder.add_parameter(high)
101
+ low_param = builder._generate_unique_parameter_name(f"{column_name}_low") # type: ignore[attr-defined]
102
+ high_param = builder._generate_unique_parameter_name(f"{column_name}_high") # type: ignore[attr-defined]
103
+ _, low_param = builder.add_parameter(low, name=low_param)
104
+ _, high_param = builder.add_parameter(high, name=high_param)
91
105
  return exp.Between(
92
106
  this=column_exp, low=exp.Placeholder(this=low_param), high=exp.Placeholder(this=high_param)
93
107
  )
94
108
  msg = f"BETWEEN operator requires a tuple of two values, got {type(value).__name__}"
95
109
  raise SQLBuilderError(msg)
96
110
 
97
- def _handle_not_between_operator(self, column_exp: exp.Expression, value: Any) -> exp.Expression:
111
+ def _handle_not_between_operator(
112
+ self, column_exp: exp.Expression, value: Any, column_name: str = "column"
113
+ ) -> exp.Expression:
98
114
  """Handle NOT BETWEEN operator."""
99
115
  if is_iterable_parameters(value) and len(value) == 2:
100
116
  builder = cast("SQLBuilderProtocol", self)
101
117
  low, high = value
102
- _, low_param = builder.add_parameter(low)
103
- _, high_param = builder.add_parameter(high)
118
+ low_param = builder._generate_unique_parameter_name(f"{column_name}_low") # type: ignore[attr-defined]
119
+ high_param = builder._generate_unique_parameter_name(f"{column_name}_high") # type: ignore[attr-defined]
120
+ _, low_param = builder.add_parameter(low, name=low_param)
121
+ _, high_param = builder.add_parameter(high, name=high_param)
104
122
  return exp.Not(
105
123
  this=exp.Between(
106
124
  this=column_exp, low=exp.Placeholder(this=low_param), high=exp.Placeholder(this=high_param)
@@ -112,13 +130,15 @@ class WhereClauseMixin:
112
130
  def _process_tuple_condition(self, condition: tuple) -> exp.Expression:
113
131
  """Process tuple-based WHERE conditions."""
114
132
  builder = cast("SQLBuilderProtocol", self)
115
- column_name = str(condition[0])
116
- column_exp = parse_column_expression(column_name)
133
+ column_name_raw = str(condition[0])
134
+ column_exp = parse_column_expression(column_name_raw)
135
+ column_name = _extract_column_name(column_name_raw)
117
136
 
118
137
  if len(condition) == 2:
119
138
  # (column, value) tuple for equality
120
139
  value = condition[1]
121
- _, param_name = builder.add_parameter(value)
140
+ param_name = builder._generate_unique_parameter_name(column_name) # type: ignore[attr-defined]
141
+ _, param_name = builder.add_parameter(value, name=param_name)
122
142
  return exp.EQ(this=column_exp, expression=exp.Placeholder(this=param_name))
123
143
 
124
144
  if len(condition) == 3:
@@ -126,45 +146,51 @@ class WhereClauseMixin:
126
146
  operator = str(condition[1]).upper()
127
147
  value = condition[2]
128
148
 
129
- # Handle simple operators
130
149
  if operator == "=":
131
- _, param_name = builder.add_parameter(value)
150
+ param_name = builder._generate_unique_parameter_name(column_name) # type: ignore[attr-defined]
151
+ _, param_name = builder.add_parameter(value, name=param_name)
132
152
  return exp.EQ(this=column_exp, expression=exp.Placeholder(this=param_name))
133
153
  if operator in {"!=", "<>"}:
134
- _, param_name = builder.add_parameter(value)
154
+ param_name = builder._generate_unique_parameter_name(column_name) # type: ignore[attr-defined]
155
+ _, param_name = builder.add_parameter(value, name=param_name)
135
156
  return exp.NEQ(this=column_exp, expression=exp.Placeholder(this=param_name))
136
157
  if operator == ">":
137
- _, param_name = builder.add_parameter(value)
158
+ param_name = builder._generate_unique_parameter_name(column_name) # type: ignore[attr-defined]
159
+ _, param_name = builder.add_parameter(value, name=param_name)
138
160
  return exp.GT(this=column_exp, expression=exp.Placeholder(this=param_name))
139
161
  if operator == ">=":
140
- _, param_name = builder.add_parameter(value)
162
+ param_name = builder._generate_unique_parameter_name(column_name) # type: ignore[attr-defined]
163
+ _, param_name = builder.add_parameter(value, name=param_name)
141
164
  return exp.GTE(this=column_exp, expression=exp.Placeholder(this=param_name))
142
165
  if operator == "<":
143
- _, param_name = builder.add_parameter(value)
166
+ param_name = builder._generate_unique_parameter_name(column_name) # type: ignore[attr-defined]
167
+ _, param_name = builder.add_parameter(value, name=param_name)
144
168
  return exp.LT(this=column_exp, expression=exp.Placeholder(this=param_name))
145
169
  if operator == "<=":
146
- _, param_name = builder.add_parameter(value)
170
+ param_name = builder._generate_unique_parameter_name(column_name) # type: ignore[attr-defined]
171
+ _, param_name = builder.add_parameter(value, name=param_name)
147
172
  return exp.LTE(this=column_exp, expression=exp.Placeholder(this=param_name))
148
173
  if operator == "LIKE":
149
- _, param_name = builder.add_parameter(value)
174
+ param_name = builder._generate_unique_parameter_name(column_name) # type: ignore[attr-defined]
175
+ _, param_name = builder.add_parameter(value, name=param_name)
150
176
  return exp.Like(this=column_exp, expression=exp.Placeholder(this=param_name))
151
177
  if operator == "NOT LIKE":
152
- _, param_name = builder.add_parameter(value)
178
+ param_name = builder._generate_unique_parameter_name(column_name) # type: ignore[attr-defined]
179
+ _, param_name = builder.add_parameter(value, name=param_name)
153
180
  return exp.Not(this=exp.Like(this=column_exp, expression=exp.Placeholder(this=param_name)))
154
181
 
155
- # Handle complex operators
156
182
  if operator == "IN":
157
- return self._handle_in_operator(column_exp, value)
183
+ return self._handle_in_operator(column_exp, value, column_name)
158
184
  if operator == "NOT IN":
159
- return self._handle_not_in_operator(column_exp, value)
185
+ return self._handle_not_in_operator(column_exp, value, column_name)
160
186
  if operator == "IS":
161
187
  return self._handle_is_operator(column_exp, value)
162
188
  if operator == "IS NOT":
163
189
  return self._handle_is_not_operator(column_exp, value)
164
190
  if operator == "BETWEEN":
165
- return self._handle_between_operator(column_exp, value)
191
+ return self._handle_between_operator(column_exp, value, column_name)
166
192
  if operator == "NOT BETWEEN":
167
- return self._handle_not_between_operator(column_exp, value)
193
+ return self._handle_not_between_operator(column_exp, value, column_name)
168
194
 
169
195
  msg = f"Unsupported operator: {operator}"
170
196
  raise SQLBuilderError(msg)
@@ -175,15 +201,20 @@ class WhereClauseMixin:
175
201
  def where(
176
202
  self,
177
203
  condition: Union[str, exp.Expression, exp.Condition, tuple[str, Any], tuple[str, str, Any], "ColumnExpression"],
204
+ value: Optional[Any] = None,
205
+ operator: Optional[str] = None,
178
206
  ) -> Self:
179
207
  """Add a WHERE clause to the statement.
180
208
 
181
209
  Args:
182
210
  condition: The condition for the WHERE clause. Can be:
183
- - A string condition
211
+ - A string condition (when value is None)
212
+ - A string column name (when value is provided)
184
213
  - A sqlglot Expression or Condition
185
214
  - A 2-tuple (column, value) for equality comparison
186
215
  - A 3-tuple (column, operator, value) for custom comparison
216
+ value: Value for comparison (when condition is a column name)
217
+ operator: Operator for comparison (when both condition and value provided)
187
218
 
188
219
  Raises:
189
220
  SQLBuilderError: If the current expression is not a supported statement type.
@@ -191,10 +222,7 @@ class WhereClauseMixin:
191
222
  Returns:
192
223
  The current builder instance for method chaining.
193
224
  """
194
- # Special case: if this is an Update and _expression is not exp.Update, raise the expected error for test coverage
195
- if self.__class__.__name__ == "Update" and not (
196
- hasattr(self, "_expression") and isinstance(getattr(self, "_expression", None), exp.Update)
197
- ):
225
+ if self.__class__.__name__ == "Update" and not isinstance(self._expression, exp.Update): # type: ignore[attr-defined]
198
226
  msg = "Cannot add WHERE clause to non-UPDATE expression"
199
227
  raise SQLBuilderError(msg)
200
228
 
@@ -203,35 +231,38 @@ class WhereClauseMixin:
203
231
  msg = "Cannot add WHERE clause: expression is not initialized."
204
232
  raise SQLBuilderError(msg)
205
233
 
206
- # Check if DELETE has a table set
207
234
  if isinstance(builder._expression, exp.Delete) and not builder._expression.args.get("this"):
208
235
  msg = "WHERE clause requires a table to be set. Use from() to set the table first."
209
236
  raise SQLBuilderError(msg)
210
237
 
211
- # Process different condition types
212
- if isinstance(condition, str):
238
+ if value is not None:
239
+ if not isinstance(condition, str):
240
+ msg = "When value is provided, condition must be a column name (string)"
241
+ raise SQLBuilderError(msg)
242
+
243
+ if operator is not None:
244
+ where_expr = self._process_tuple_condition((condition, operator, value))
245
+ else:
246
+ where_expr = self._process_tuple_condition((condition, value))
247
+ elif isinstance(condition, str):
213
248
  where_expr = parse_condition_expression(condition)
214
249
  elif isinstance(condition, (exp.Expression, exp.Condition)):
215
250
  where_expr = condition
216
251
  elif isinstance(condition, tuple):
217
252
  where_expr = self._process_tuple_condition(condition)
218
253
  elif has_query_builder_parameters(condition):
219
- # Handle ColumnExpression objects
220
254
  column_expr_obj = cast("ColumnExpression", condition)
221
255
  where_expr = column_expr_obj._expression # pyright: ignore
222
256
  elif has_sqlglot_expression(condition):
223
- # This is a ColumnExpression from our new Column syntax
224
- raw_expr = getattr(condition, "sqlglot_expression", None)
257
+ raw_expr = condition.sqlglot_expression # pyright: ignore[attr-defined]
225
258
  if raw_expr is not None:
226
259
  where_expr = builder._parameterize_expression(raw_expr)
227
260
  else:
228
- # Fallback if attribute exists but is None
229
261
  where_expr = parse_condition_expression(str(condition))
230
262
  else:
231
263
  msg = f"Unsupported condition type: {type(condition).__name__}"
232
264
  raise SQLBuilderError(msg)
233
265
 
234
- # Apply WHERE clause based on statement type
235
266
  if isinstance(builder._expression, (exp.Select, exp.Update, exp.Delete)):
236
267
  builder._expression = builder._expression.where(where_expr, copy=False)
237
268
  else:
@@ -242,86 +273,107 @@ class WhereClauseMixin:
242
273
  def where_eq(self, column: Union[str, exp.Column], value: Any) -> Self:
243
274
  """Add WHERE column = value clause."""
244
275
  builder = cast("SQLBuilderProtocol", self)
245
- _, param_name = builder.add_parameter(value)
276
+ column_name = _extract_column_name(column)
277
+ param_name = builder._generate_unique_parameter_name(column_name) # type: ignore[attr-defined]
278
+ _, param_name = builder.add_parameter(value, name=param_name)
246
279
  col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
247
- condition: exp.Expression = col_expr.eq(exp.var(param_name))
280
+ condition: exp.Expression = col_expr.eq(exp.Placeholder(this=param_name))
248
281
  return self.where(condition)
249
282
 
250
283
  def where_neq(self, column: Union[str, exp.Column], value: Any) -> Self:
251
284
  """Add WHERE column != value clause."""
252
285
  builder = cast("SQLBuilderProtocol", self)
253
- _, param_name = builder.add_parameter(value)
286
+ column_name = _extract_column_name(column)
287
+ param_name = builder._generate_unique_parameter_name(column_name) # type: ignore[attr-defined]
288
+ _, param_name = builder.add_parameter(value, name=param_name)
254
289
  col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
255
- condition: exp.Expression = col_expr.neq(exp.var(param_name))
290
+ condition: exp.Expression = col_expr.neq(exp.Placeholder(this=param_name))
256
291
  return self.where(condition)
257
292
 
258
293
  def where_lt(self, column: Union[str, exp.Column], value: Any) -> Self:
259
294
  """Add WHERE column < value clause."""
260
295
  builder = cast("SQLBuilderProtocol", self)
261
- _, param_name = builder.add_parameter(value)
296
+ column_name = _extract_column_name(column)
297
+ param_name = builder._generate_unique_parameter_name(column_name) # type: ignore[attr-defined]
298
+ _, param_name = builder.add_parameter(value, name=param_name)
262
299
  col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
263
- condition: exp.Expression = exp.LT(this=col_expr, expression=exp.var(param_name))
300
+ condition: exp.Expression = exp.LT(this=col_expr, expression=exp.Placeholder(this=param_name))
264
301
  return self.where(condition)
265
302
 
266
303
  def where_lte(self, column: Union[str, exp.Column], value: Any) -> Self:
267
304
  """Add WHERE column <= value clause."""
268
305
  builder = cast("SQLBuilderProtocol", self)
269
- _, param_name = builder.add_parameter(value)
306
+ column_name = _extract_column_name(column)
307
+ param_name = builder._generate_unique_parameter_name(column_name) # type: ignore[attr-defined]
308
+ _, param_name = builder.add_parameter(value, name=param_name)
270
309
  col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
271
- condition: exp.Expression = exp.LTE(this=col_expr, expression=exp.var(param_name))
310
+ condition: exp.Expression = exp.LTE(this=col_expr, expression=exp.Placeholder(this=param_name))
272
311
  return self.where(condition)
273
312
 
274
313
  def where_gt(self, column: Union[str, exp.Column], value: Any) -> Self:
275
314
  """Add WHERE column > value clause."""
276
315
  builder = cast("SQLBuilderProtocol", self)
277
- _, param_name = builder.add_parameter(value)
316
+ column_name = _extract_column_name(column)
317
+ param_name = builder._generate_unique_parameter_name(column_name) # type: ignore[attr-defined]
318
+ _, param_name = builder.add_parameter(value, name=param_name)
278
319
  col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
279
- condition: exp.Expression = exp.GT(this=col_expr, expression=exp.var(param_name))
320
+ condition: exp.Expression = exp.GT(this=col_expr, expression=exp.Placeholder(this=param_name))
280
321
  return self.where(condition)
281
322
 
282
323
  def where_gte(self, column: Union[str, exp.Column], value: Any) -> Self:
283
324
  """Add WHERE column >= value clause."""
284
325
  builder = cast("SQLBuilderProtocol", self)
285
- _, param_name = builder.add_parameter(value)
326
+ column_name = _extract_column_name(column)
327
+ param_name = builder._generate_unique_parameter_name(column_name) # type: ignore[attr-defined]
328
+ _, param_name = builder.add_parameter(value, name=param_name)
286
329
  col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
287
- condition: exp.Expression = exp.GTE(this=col_expr, expression=exp.var(param_name))
330
+ condition: exp.Expression = exp.GTE(this=col_expr, expression=exp.Placeholder(this=param_name))
288
331
  return self.where(condition)
289
332
 
290
333
  def where_between(self, column: Union[str, exp.Column], low: Any, high: Any) -> Self:
291
334
  """Add WHERE column BETWEEN low AND high clause."""
292
335
  builder = cast("SQLBuilderProtocol", self)
293
- _, low_param = builder.add_parameter(low)
294
- _, high_param = builder.add_parameter(high)
336
+ column_name = _extract_column_name(column)
337
+ low_param = builder._generate_unique_parameter_name(f"{column_name}_low") # type: ignore[attr-defined]
338
+ high_param = builder._generate_unique_parameter_name(f"{column_name}_high") # type: ignore[attr-defined]
339
+ _, low_param = builder.add_parameter(low, name=low_param)
340
+ _, high_param = builder.add_parameter(high, name=high_param)
295
341
  col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
296
- condition: exp.Expression = col_expr.between(exp.var(low_param), exp.var(high_param))
342
+ condition: exp.Expression = col_expr.between(exp.Placeholder(this=low_param), exp.Placeholder(this=high_param))
297
343
  return self.where(condition)
298
344
 
299
345
  def where_like(self, column: Union[str, exp.Column], pattern: str, escape: Optional[str] = None) -> Self:
300
346
  """Add WHERE column LIKE pattern clause."""
301
347
  builder = cast("SQLBuilderProtocol", self)
302
- _, param_name = builder.add_parameter(pattern)
348
+ column_name = _extract_column_name(column)
349
+ param_name = builder._generate_unique_parameter_name(column_name) # type: ignore[attr-defined]
350
+ _, param_name = builder.add_parameter(pattern, name=param_name)
303
351
  col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
304
352
  if escape is not None:
305
- cond = exp.Like(this=col_expr, expression=exp.var(param_name), escape=exp.Literal.string(str(escape)))
353
+ cond = exp.Like(this=col_expr, expression=exp.Placeholder(this=param_name), escape=exp.convert(str(escape)))
306
354
  else:
307
- cond = col_expr.like(exp.var(param_name))
355
+ cond = col_expr.like(exp.Placeholder(this=param_name))
308
356
  condition: exp.Expression = cond
309
357
  return self.where(condition)
310
358
 
311
359
  def where_not_like(self, column: Union[str, exp.Column], pattern: str) -> Self:
312
360
  """Add WHERE column NOT LIKE pattern clause."""
313
361
  builder = cast("SQLBuilderProtocol", self)
314
- _, param_name = builder.add_parameter(pattern)
362
+ column_name = _extract_column_name(column)
363
+ param_name = builder._generate_unique_parameter_name(column_name) # type: ignore[attr-defined]
364
+ _, param_name = builder.add_parameter(pattern, name=param_name)
315
365
  col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
316
- condition: exp.Expression = col_expr.like(exp.var(param_name)).not_()
366
+ condition: exp.Expression = col_expr.like(exp.Placeholder(this=param_name)).not_()
317
367
  return self.where(condition)
318
368
 
319
369
  def where_ilike(self, column: Union[str, exp.Column], pattern: str) -> Self:
320
370
  """Add WHERE column ILIKE pattern clause."""
321
371
  builder = cast("SQLBuilderProtocol", self)
322
- _, param_name = builder.add_parameter(pattern)
372
+ column_name = _extract_column_name(column)
373
+ param_name = builder._generate_unique_parameter_name(column_name) # type: ignore[attr-defined]
374
+ _, param_name = builder.add_parameter(pattern, name=param_name)
323
375
  col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
324
- condition: exp.Expression = col_expr.ilike(exp.var(param_name))
376
+ condition: exp.Expression = col_expr.ilike(exp.Placeholder(this=param_name))
325
377
  return self.where(condition)
326
378
 
327
379
  def where_is_null(self, column: Union[str, exp.Column]) -> Self:
@@ -344,8 +396,12 @@ class WhereClauseMixin:
344
396
  subquery_exp: exp.Expression
345
397
  if has_query_builder_parameters(values):
346
398
  subquery = values.build() # pyright: ignore
347
- sql_str = getattr(subquery, "sql", str(subquery))
348
- subquery_exp = exp.paren(exp.maybe_parse(sql_str, dialect=getattr(builder, "dialect_name", None)))
399
+ sql_str = subquery.sql
400
+ subquery_exp = exp.paren(exp.maybe_parse(sql_str, dialect=builder.dialect_name)) # pyright: ignore
401
+ # Merge subquery parameters into parent builder
402
+ if hasattr(subquery, "parameters"):
403
+ for param_name, param_value in subquery.parameters.items(): # pyright: ignore[reportAttributeAccessIssue]
404
+ builder.add_parameter(param_value, name=param_name)
349
405
  else:
350
406
  subquery_exp = values # type: ignore[assignment]
351
407
  condition = col_expr.isin(subquery_exp)
@@ -353,11 +409,16 @@ class WhereClauseMixin:
353
409
  if not is_iterable_parameters(values) or isinstance(values, (str, bytes)):
354
410
  msg = "Unsupported type for 'values' in WHERE IN"
355
411
  raise SQLBuilderError(msg)
356
- params = []
357
- for v in values:
358
- _, param_name = builder.add_parameter(v)
359
- params.append(exp.var(param_name))
360
- condition = col_expr.isin(*params)
412
+ column_name = _extract_column_name(column)
413
+ parameters = []
414
+ for i, v in enumerate(values):
415
+ if len(values) == 1:
416
+ param_name = builder._generate_unique_parameter_name(column_name) # type: ignore[attr-defined]
417
+ else:
418
+ param_name = builder._generate_unique_parameter_name(f"{column_name}_{i + 1}") # type: ignore[attr-defined]
419
+ _, param_name = builder.add_parameter(v, name=param_name)
420
+ parameters.append(exp.Placeholder(this=param_name))
421
+ condition = col_expr.isin(*parameters)
361
422
  return self.where(condition)
362
423
 
363
424
  def where_not_in(self, column: Union[str, exp.Column], values: Any) -> Self:
@@ -368,8 +429,8 @@ class WhereClauseMixin:
368
429
  subquery_exp: exp.Expression
369
430
  if has_query_builder_parameters(values):
370
431
  subquery = values.build() # pyright: ignore
371
- sql_str = getattr(subquery, "sql", str(subquery))
372
- subquery_exp = exp.paren(exp.maybe_parse(sql_str, dialect=getattr(builder, "dialect_name", None)))
432
+
433
+ subquery_exp = exp.paren(exp.maybe_parse(subquery.sql, dialect=builder.dialect_name)) # pyright: ignore
373
434
  else:
374
435
  subquery_exp = values # type: ignore[assignment]
375
436
  condition = exp.Not(this=col_expr.isin(subquery_exp))
@@ -377,11 +438,16 @@ class WhereClauseMixin:
377
438
  if not is_iterable_parameters(values) or isinstance(values, (str, bytes)):
378
439
  msg = "Values for where_not_in must be a non-string iterable or subquery."
379
440
  raise SQLBuilderError(msg)
380
- params = []
381
- for v in values:
382
- _, param_name = builder.add_parameter(v)
383
- params.append(exp.var(param_name))
384
- condition = exp.Not(this=col_expr.isin(*params))
441
+ column_name = _extract_column_name(column)
442
+ parameters = []
443
+ for i, v in enumerate(values):
444
+ if len(values) == 1:
445
+ param_name = builder._generate_unique_parameter_name(column_name) # type: ignore[attr-defined]
446
+ else:
447
+ param_name = builder._generate_unique_parameter_name(f"{column_name}_{i + 1}") # type: ignore[attr-defined]
448
+ _, param_name = builder.add_parameter(v, name=param_name)
449
+ parameters.append(exp.Placeholder(this=param_name))
450
+ condition = exp.Not(this=col_expr.isin(*parameters))
385
451
  return self.where(condition)
386
452
 
387
453
  def where_null(self, column: Union[str, exp.Column]) -> Self:
@@ -397,15 +463,15 @@ class WhereClauseMixin:
397
463
  builder = cast("SQLBuilderProtocol", self)
398
464
  sub_expr: exp.Expression
399
465
  if has_query_builder_parameters(subquery):
400
- subquery_builder_params: dict[str, Any] = subquery.parameters
401
- if subquery_builder_params:
402
- for p_name, p_value in subquery_builder_params.items():
466
+ subquery_builder_parameters: dict[str, Any] = subquery.parameters
467
+ if subquery_builder_parameters:
468
+ for p_name, p_value in subquery_builder_parameters.items():
403
469
  builder.add_parameter(p_value, name=p_name)
404
470
  sub_sql_obj = subquery.build() # pyright: ignore
405
- sql_str = getattr(sub_sql_obj, "sql", str(sub_sql_obj))
406
- sub_expr = exp.maybe_parse(sql_str, dialect=getattr(builder, "dialect_name", None))
471
+
472
+ sub_expr = exp.maybe_parse(sub_sql_obj.sql, dialect=builder.dialect_name) # pyright: ignore
407
473
  else:
408
- sub_expr = exp.maybe_parse(str(subquery), dialect=getattr(builder, "dialect_name", None))
474
+ sub_expr = exp.maybe_parse(str(subquery), dialect=builder.dialect_name)
409
475
 
410
476
  if sub_expr is None:
411
477
  msg = "Could not parse subquery for EXISTS"
@@ -419,15 +485,14 @@ class WhereClauseMixin:
419
485
  builder = cast("SQLBuilderProtocol", self)
420
486
  sub_expr: exp.Expression
421
487
  if has_query_builder_parameters(subquery):
422
- subquery_builder_params: dict[str, Any] = subquery.parameters
423
- if subquery_builder_params:
424
- for p_name, p_value in subquery_builder_params.items():
488
+ subquery_builder_parameters: dict[str, Any] = subquery.parameters
489
+ if subquery_builder_parameters:
490
+ for p_name, p_value in subquery_builder_parameters.items():
425
491
  builder.add_parameter(p_value, name=p_name)
426
492
  sub_sql_obj = subquery.build() # pyright: ignore
427
- sql_str = getattr(sub_sql_obj, "sql", str(sub_sql_obj))
428
- sub_expr = exp.maybe_parse(sql_str, dialect=getattr(builder, "dialect_name", None))
493
+ sub_expr = exp.maybe_parse(sub_sql_obj.sql, dialect=builder.dialect_name) # pyright: ignore
429
494
  else:
430
- sub_expr = exp.maybe_parse(str(subquery), dialect=getattr(builder, "dialect_name", None))
495
+ sub_expr = exp.maybe_parse(str(subquery), dialect=builder.dialect_name)
431
496
 
432
497
  if sub_expr is None:
433
498
  msg = "Could not parse subquery for NOT EXISTS"
@@ -444,8 +509,7 @@ class WhereClauseMixin:
444
509
  subquery_exp: exp.Expression
445
510
  if has_query_builder_parameters(values):
446
511
  subquery = values.build() # pyright: ignore
447
- sql_str = getattr(subquery, "sql", str(subquery))
448
- subquery_exp = exp.paren(exp.maybe_parse(sql_str, dialect=getattr(builder, "dialect_name", None)))
512
+ subquery_exp = exp.paren(exp.maybe_parse(subquery.sql, dialect=builder.dialect_name)) # pyright: ignore
449
513
  else:
450
514
  subquery_exp = values # type: ignore[assignment]
451
515
  condition = exp.EQ(this=col_expr, expression=exp.Any(this=subquery_exp))
@@ -464,24 +528,28 @@ class WhereClauseMixin:
464
528
  if not is_iterable_parameters(values) or isinstance(values, bytes):
465
529
  msg = "Unsupported type for 'values' in WHERE ANY"
466
530
  raise SQLBuilderError(msg)
467
- params = []
468
- for v in values:
469
- _, param_name = builder.add_parameter(v)
470
- params.append(exp.var(param_name))
471
- tuple_expr = exp.Tuple(expressions=params)
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) # type: ignore[attr-defined]
536
+ else:
537
+ param_name = builder._generate_unique_parameter_name(f"{column_name}_any_{i + 1}") # type: ignore[attr-defined]
538
+ _, param_name = builder.add_parameter(v, name=param_name)
539
+ parameters.append(exp.Placeholder(this=param_name))
540
+ tuple_expr = exp.Tuple(expressions=parameters)
472
541
  condition = exp.EQ(this=col_expr, expression=exp.Any(this=tuple_expr))
473
542
  return self.where(condition)
474
543
 
475
544
  def where_not_any(self, column: Union[str, exp.Column], values: Any) -> Self:
476
- """Add WHERE column != ANY(values) clause."""
545
+ """Add WHERE column <> ANY(values) clause."""
477
546
  builder = cast("SQLBuilderProtocol", self)
478
547
  col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
479
548
  if has_query_builder_parameters(values) or isinstance(values, exp.Expression):
480
549
  subquery_exp: exp.Expression
481
550
  if has_query_builder_parameters(values):
482
551
  subquery = values.build() # pyright: ignore
483
- sql_str = getattr(subquery, "sql", str(subquery))
484
- subquery_exp = exp.paren(exp.maybe_parse(sql_str, dialect=getattr(builder, "dialect_name", None)))
552
+ subquery_exp = exp.paren(exp.maybe_parse(subquery.sql, dialect=builder.dialect_name)) # pyright: ignore
485
553
  else:
486
554
  subquery_exp = values # type: ignore[assignment]
487
555
  condition = exp.NEQ(this=col_expr, expression=exp.Any(this=subquery_exp))
@@ -500,11 +568,16 @@ class WhereClauseMixin:
500
568
  if not is_iterable_parameters(values) or isinstance(values, bytes):
501
569
  msg = "Unsupported type for 'values' in WHERE NOT ANY"
502
570
  raise SQLBuilderError(msg)
503
- params = []
504
- for v in values:
505
- _, param_name = builder.add_parameter(v)
506
- params.append(exp.var(param_name))
507
- tuple_expr = exp.Tuple(expressions=params)
571
+ column_name = _extract_column_name(column)
572
+ parameters = []
573
+ for i, v in enumerate(values):
574
+ if len(values) == 1:
575
+ param_name = builder._generate_unique_parameter_name(column_name) # type: ignore[attr-defined]
576
+ else:
577
+ param_name = builder._generate_unique_parameter_name(f"{column_name}_not_any_{i + 1}") # type: ignore[attr-defined]
578
+ _, param_name = builder.add_parameter(v, name=param_name)
579
+ parameters.append(exp.Placeholder(this=param_name))
580
+ tuple_expr = exp.Tuple(expressions=parameters)
508
581
  condition = exp.NEQ(this=col_expr, expression=exp.Any(this=tuple_expr))
509
582
  return self.where(condition)
510
583