sqlspec 0.13.1__py3-none-any.whl → 0.14.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 (110) hide show
  1. sqlspec/__init__.py +39 -1
  2. sqlspec/adapters/adbc/config.py +4 -40
  3. sqlspec/adapters/adbc/driver.py +29 -16
  4. sqlspec/adapters/aiosqlite/config.py +2 -20
  5. sqlspec/adapters/aiosqlite/driver.py +36 -18
  6. sqlspec/adapters/asyncmy/config.py +2 -33
  7. sqlspec/adapters/asyncmy/driver.py +23 -16
  8. sqlspec/adapters/asyncpg/config.py +5 -39
  9. sqlspec/adapters/asyncpg/driver.py +41 -18
  10. sqlspec/adapters/bigquery/config.py +2 -43
  11. sqlspec/adapters/bigquery/driver.py +26 -14
  12. sqlspec/adapters/duckdb/config.py +2 -49
  13. sqlspec/adapters/duckdb/driver.py +35 -16
  14. sqlspec/adapters/oracledb/config.py +4 -83
  15. sqlspec/adapters/oracledb/driver.py +54 -27
  16. sqlspec/adapters/psqlpy/config.py +2 -55
  17. sqlspec/adapters/psqlpy/driver.py +28 -8
  18. sqlspec/adapters/psycopg/config.py +4 -73
  19. sqlspec/adapters/psycopg/driver.py +69 -24
  20. sqlspec/adapters/sqlite/config.py +3 -21
  21. sqlspec/adapters/sqlite/driver.py +50 -26
  22. sqlspec/cli.py +248 -0
  23. sqlspec/config.py +18 -20
  24. sqlspec/driver/_async.py +28 -10
  25. sqlspec/driver/_common.py +5 -4
  26. sqlspec/driver/_sync.py +28 -10
  27. sqlspec/driver/mixins/__init__.py +6 -0
  28. sqlspec/driver/mixins/_cache.py +114 -0
  29. sqlspec/driver/mixins/_pipeline.py +0 -4
  30. sqlspec/{service/base.py → driver/mixins/_query_tools.py} +86 -421
  31. sqlspec/driver/mixins/_result_utils.py +0 -2
  32. sqlspec/driver/mixins/_sql_translator.py +0 -2
  33. sqlspec/driver/mixins/_storage.py +4 -18
  34. sqlspec/driver/mixins/_type_coercion.py +0 -2
  35. sqlspec/driver/parameters.py +4 -4
  36. sqlspec/extensions/aiosql/adapter.py +4 -4
  37. sqlspec/extensions/litestar/__init__.py +2 -1
  38. sqlspec/extensions/litestar/cli.py +48 -0
  39. sqlspec/extensions/litestar/plugin.py +3 -0
  40. sqlspec/loader.py +1 -1
  41. sqlspec/migrations/__init__.py +23 -0
  42. sqlspec/migrations/base.py +390 -0
  43. sqlspec/migrations/commands.py +525 -0
  44. sqlspec/migrations/runner.py +215 -0
  45. sqlspec/migrations/tracker.py +153 -0
  46. sqlspec/migrations/utils.py +89 -0
  47. sqlspec/protocols.py +37 -3
  48. sqlspec/statement/builder/__init__.py +8 -8
  49. sqlspec/statement/builder/{column.py → _column.py} +82 -52
  50. sqlspec/statement/builder/{ddl.py → _ddl.py} +5 -5
  51. sqlspec/statement/builder/_ddl_utils.py +1 -1
  52. sqlspec/statement/builder/{delete.py → _delete.py} +1 -1
  53. sqlspec/statement/builder/{insert.py → _insert.py} +1 -1
  54. sqlspec/statement/builder/{merge.py → _merge.py} +1 -1
  55. sqlspec/statement/builder/_parsing_utils.py +5 -3
  56. sqlspec/statement/builder/{select.py → _select.py} +59 -61
  57. sqlspec/statement/builder/{update.py → _update.py} +2 -2
  58. sqlspec/statement/builder/mixins/__init__.py +24 -30
  59. sqlspec/statement/builder/mixins/{_set_ops.py → _cte_and_set_ops.py} +86 -2
  60. sqlspec/statement/builder/mixins/{_delete_from.py → _delete_operations.py} +2 -0
  61. sqlspec/statement/builder/mixins/{_insert_values.py → _insert_operations.py} +70 -1
  62. sqlspec/statement/builder/mixins/{_merge_clauses.py → _merge_operations.py} +2 -0
  63. sqlspec/statement/builder/mixins/_order_limit_operations.py +123 -0
  64. sqlspec/statement/builder/mixins/{_pivot.py → _pivot_operations.py} +71 -2
  65. sqlspec/statement/builder/mixins/_select_operations.py +612 -0
  66. sqlspec/statement/builder/mixins/{_update_set.py → _update_operations.py} +73 -2
  67. sqlspec/statement/builder/mixins/_where_clause.py +536 -0
  68. sqlspec/statement/cache.py +50 -0
  69. sqlspec/statement/filters.py +37 -8
  70. sqlspec/statement/parameters.py +154 -25
  71. sqlspec/statement/pipelines/__init__.py +1 -1
  72. sqlspec/statement/pipelines/context.py +4 -4
  73. sqlspec/statement/pipelines/transformers/_expression_simplifier.py +3 -3
  74. sqlspec/statement/pipelines/validators/_parameter_style.py +22 -22
  75. sqlspec/statement/pipelines/validators/_performance.py +1 -5
  76. sqlspec/statement/sql.py +246 -176
  77. sqlspec/utils/__init__.py +2 -1
  78. sqlspec/utils/statement_hashing.py +203 -0
  79. sqlspec/utils/type_guards.py +32 -0
  80. {sqlspec-0.13.1.dist-info → sqlspec-0.14.0.dist-info}/METADATA +1 -1
  81. sqlspec-0.14.0.dist-info/RECORD +143 -0
  82. sqlspec-0.14.0.dist-info/entry_points.txt +2 -0
  83. sqlspec/service/__init__.py +0 -4
  84. sqlspec/service/_util.py +0 -147
  85. sqlspec/service/pagination.py +0 -26
  86. sqlspec/statement/builder/mixins/_aggregate_functions.py +0 -250
  87. sqlspec/statement/builder/mixins/_case_builder.py +0 -91
  88. sqlspec/statement/builder/mixins/_common_table_expr.py +0 -90
  89. sqlspec/statement/builder/mixins/_from.py +0 -63
  90. sqlspec/statement/builder/mixins/_group_by.py +0 -118
  91. sqlspec/statement/builder/mixins/_having.py +0 -35
  92. sqlspec/statement/builder/mixins/_insert_from_select.py +0 -47
  93. sqlspec/statement/builder/mixins/_insert_into.py +0 -36
  94. sqlspec/statement/builder/mixins/_limit_offset.py +0 -53
  95. sqlspec/statement/builder/mixins/_order_by.py +0 -46
  96. sqlspec/statement/builder/mixins/_returning.py +0 -37
  97. sqlspec/statement/builder/mixins/_select_columns.py +0 -61
  98. sqlspec/statement/builder/mixins/_unpivot.py +0 -77
  99. sqlspec/statement/builder/mixins/_update_from.py +0 -55
  100. sqlspec/statement/builder/mixins/_update_table.py +0 -29
  101. sqlspec/statement/builder/mixins/_where.py +0 -401
  102. sqlspec/statement/builder/mixins/_window_functions.py +0 -86
  103. sqlspec/statement/parameter_manager.py +0 -220
  104. sqlspec/statement/sql_compiler.py +0 -140
  105. sqlspec-0.13.1.dist-info/RECORD +0 -150
  106. /sqlspec/statement/builder/{base.py → _base.py} +0 -0
  107. /sqlspec/statement/builder/mixins/{_join.py → _join_operations.py} +0 -0
  108. {sqlspec-0.13.1.dist-info → sqlspec-0.14.0.dist-info}/WHEEL +0 -0
  109. {sqlspec-0.13.1.dist-info → sqlspec-0.14.0.dist-info}/licenses/LICENSE +0 -0
  110. {sqlspec-0.13.1.dist-info → sqlspec-0.14.0.dist-info}/licenses/NOTICE +0 -0
@@ -1,5 +1,7 @@
1
+ """Update operation mixins for SQL builders."""
2
+
1
3
  from collections.abc import Mapping
2
- from typing import Any, Optional
4
+ from typing import Any, Optional, Union
3
5
 
4
6
  from sqlglot import exp
5
7
  from typing_extensions import Self
@@ -7,11 +9,34 @@ from typing_extensions import Self
7
9
  from sqlspec.exceptions import SQLBuilderError
8
10
  from sqlspec.utils.type_guards import has_query_builder_parameters
9
11
 
10
- __all__ = ("UpdateSetClauseMixin",)
12
+ __all__ = ("UpdateFromClauseMixin", "UpdateSetClauseMixin", "UpdateTableClauseMixin")
11
13
 
12
14
  MIN_SET_ARGS = 2
13
15
 
14
16
 
17
+ class UpdateTableClauseMixin:
18
+ """Mixin providing TABLE clause for UPDATE builders."""
19
+
20
+ _expression: Optional[exp.Expression] = None
21
+
22
+ def table(self, table_name: str, alias: Optional[str] = None) -> Self:
23
+ """Set the table to update.
24
+
25
+ Args:
26
+ table_name: The name of the table.
27
+ alias: Optional alias for the table.
28
+
29
+ Returns:
30
+ The current builder instance for method chaining.
31
+ """
32
+ if self._expression is None or not isinstance(self._expression, exp.Update):
33
+ self._expression = exp.Update(this=None, expressions=[], joins=[])
34
+ table_expr: exp.Expression = exp.to_table(table_name, alias=alias)
35
+ self._expression.set("this", table_expr)
36
+ setattr(self, "_table", table_name)
37
+ return self
38
+
39
+
15
40
  class UpdateSetClauseMixin:
16
41
  """Mixin providing SET clause for UPDATE builders."""
17
42
 
@@ -92,3 +117,49 @@ class UpdateSetClauseMixin:
92
117
  existing = self._expression.args.get("expressions", [])
93
118
  self._expression.set("expressions", existing + assignments)
94
119
  return self
120
+
121
+
122
+ class UpdateFromClauseMixin:
123
+ """Mixin providing FROM clause for UPDATE builders (e.g., PostgreSQL style)."""
124
+
125
+ def from_(self, table: Union[str, exp.Expression, Any], alias: Optional[str] = None) -> Self:
126
+ """Add a FROM clause to the UPDATE statement.
127
+
128
+ Args:
129
+ table: The table name, expression, or subquery to add to the FROM clause.
130
+ alias: Optional alias for the table in the FROM clause.
131
+
132
+ Returns:
133
+ The current builder instance for method chaining.
134
+
135
+ Raises:
136
+ SQLBuilderError: If the current expression is not an UPDATE statement.
137
+ """
138
+ if self._expression is None or not isinstance(self._expression, exp.Update): # type: ignore[attr-defined]
139
+ msg = "Cannot add FROM clause to non-UPDATE expression. Set the main table first."
140
+ raise SQLBuilderError(msg)
141
+ table_expr: exp.Expression
142
+ if isinstance(table, str):
143
+ table_expr = exp.to_table(table, alias=alias)
144
+ elif has_query_builder_parameters(table):
145
+ subquery_builder_params = getattr(table, "_parameters", None)
146
+ if subquery_builder_params:
147
+ for p_name, p_value in subquery_builder_params.items():
148
+ self.add_parameter(p_value, name=p_name) # type: ignore[attr-defined]
149
+ subquery_exp = exp.paren(getattr(table, "_expression", exp.select()))
150
+ table_expr = exp.alias_(subquery_exp, alias) if alias else subquery_exp
151
+ elif isinstance(table, exp.Expression):
152
+ table_expr = exp.alias_(table, alias) if alias else table
153
+ else:
154
+ msg = f"Unsupported table type for FROM clause: {type(table)}"
155
+ raise SQLBuilderError(msg)
156
+ if self._expression.args.get("from") is None: # type: ignore[attr-defined]
157
+ self._expression.set("from", exp.From(expressions=[])) # type: ignore[attr-defined]
158
+ from_clause = self._expression.args["from"] # type: ignore[attr-defined]
159
+ if hasattr(from_clause, "append"):
160
+ from_clause.append("expressions", table_expr)
161
+ else:
162
+ if not from_clause.expressions:
163
+ from_clause.expressions = []
164
+ from_clause.expressions.append(table_expr)
165
+ return self
@@ -0,0 +1,536 @@
1
+ # ruff: noqa: PLR2004
2
+ """Consolidated WHERE and HAVING clause mixins."""
3
+
4
+ from typing import TYPE_CHECKING, Any, Callable, Optional, Union, cast
5
+
6
+ from sqlglot import exp
7
+ from typing_extensions import Self
8
+
9
+ from sqlspec.exceptions import SQLBuilderError
10
+ from sqlspec.statement.builder._parsing_utils import parse_column_expression, parse_condition_expression
11
+ from sqlspec.utils.type_guards import has_query_builder_parameters, has_sqlglot_expression, is_iterable_parameters
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
+
19
+
20
+ class WhereClauseMixin:
21
+ """Mixin providing WHERE clause methods for SELECT, UPDATE, and DELETE builders."""
22
+
23
+ def _create_operator_handler(self, operator_class: type[exp.Expression]) -> Callable:
24
+ """Create a handler that properly parameterizes values."""
25
+
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
+
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
40
+
41
+ def _create_not_like_handler(self) -> Callable:
42
+ """Create NOT LIKE handler."""
43
+
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
+
48
+ return handler
49
+
50
+ def _handle_in_operator(self, column_exp: exp.Expression, value: Any) -> exp.Expression:
51
+ """Handle IN operator."""
52
+ builder = cast("SQLBuilderProtocol", self)
53
+ if is_iterable_parameters(value):
54
+ placeholders = []
55
+ for v in value:
56
+ _, param_name = builder.add_parameter(v)
57
+ placeholders.append(exp.Placeholder(this=param_name))
58
+ return exp.In(this=column_exp, expressions=placeholders)
59
+ _, param_name = builder.add_parameter(value)
60
+ return exp.In(this=column_exp, expressions=[exp.Placeholder(this=param_name)])
61
+
62
+ def _handle_not_in_operator(self, column_exp: exp.Expression, value: Any) -> exp.Expression:
63
+ """Handle NOT IN operator."""
64
+ builder = cast("SQLBuilderProtocol", self)
65
+ if is_iterable_parameters(value):
66
+ placeholders = []
67
+ for v in value:
68
+ _, param_name = builder.add_parameter(v)
69
+ placeholders.append(exp.Placeholder(this=param_name))
70
+ return exp.Not(this=exp.In(this=column_exp, expressions=placeholders))
71
+ _, param_name = builder.add_parameter(value)
72
+ return exp.Not(this=exp.In(this=column_exp, expressions=[exp.Placeholder(this=param_name)]))
73
+
74
+ def _handle_is_operator(self, column_exp: exp.Expression, value: Any) -> exp.Expression:
75
+ """Handle IS operator."""
76
+ value_expr = exp.Null() if value is None else exp.convert(value)
77
+ return exp.Is(this=column_exp, expression=value_expr)
78
+
79
+ def _handle_is_not_operator(self, column_exp: exp.Expression, value: Any) -> exp.Expression:
80
+ """Handle IS NOT operator."""
81
+ value_expr = exp.Null() if value is None else exp.convert(value)
82
+ return exp.Not(this=exp.Is(this=column_exp, expression=value_expr))
83
+
84
+ def _handle_between_operator(self, column_exp: exp.Expression, value: Any) -> exp.Expression:
85
+ """Handle BETWEEN operator."""
86
+ if is_iterable_parameters(value) and len(value) == 2:
87
+ builder = cast("SQLBuilderProtocol", self)
88
+ low, high = value
89
+ _, low_param = builder.add_parameter(low)
90
+ _, high_param = builder.add_parameter(high)
91
+ return exp.Between(
92
+ this=column_exp, low=exp.Placeholder(this=low_param), high=exp.Placeholder(this=high_param)
93
+ )
94
+ msg = f"BETWEEN operator requires a tuple of two values, got {type(value).__name__}"
95
+ raise SQLBuilderError(msg)
96
+
97
+ def _handle_not_between_operator(self, column_exp: exp.Expression, value: Any) -> exp.Expression:
98
+ """Handle NOT BETWEEN operator."""
99
+ if is_iterable_parameters(value) and len(value) == 2:
100
+ builder = cast("SQLBuilderProtocol", self)
101
+ low, high = value
102
+ _, low_param = builder.add_parameter(low)
103
+ _, high_param = builder.add_parameter(high)
104
+ return exp.Not(
105
+ this=exp.Between(
106
+ this=column_exp, low=exp.Placeholder(this=low_param), high=exp.Placeholder(this=high_param)
107
+ )
108
+ )
109
+ msg = f"NOT BETWEEN operator requires a tuple of two values, got {type(value).__name__}"
110
+ raise SQLBuilderError(msg)
111
+
112
+ def _process_tuple_condition(self, condition: tuple) -> exp.Expression:
113
+ """Process tuple-based WHERE conditions."""
114
+ builder = cast("SQLBuilderProtocol", self)
115
+ column_name = str(condition[0])
116
+ column_exp = parse_column_expression(column_name)
117
+
118
+ if len(condition) == 2:
119
+ # (column, value) tuple for equality
120
+ value = condition[1]
121
+ _, param_name = builder.add_parameter(value)
122
+ return exp.EQ(this=column_exp, expression=exp.Placeholder(this=param_name))
123
+
124
+ if len(condition) == 3:
125
+ # (column, operator, value) tuple
126
+ operator = str(condition[1]).upper()
127
+ value = condition[2]
128
+
129
+ # Handle simple operators
130
+ if operator == "=":
131
+ _, param_name = builder.add_parameter(value)
132
+ return exp.EQ(this=column_exp, expression=exp.Placeholder(this=param_name))
133
+ if operator in {"!=", "<>"}:
134
+ _, param_name = builder.add_parameter(value)
135
+ return exp.NEQ(this=column_exp, expression=exp.Placeholder(this=param_name))
136
+ if operator == ">":
137
+ _, param_name = builder.add_parameter(value)
138
+ return exp.GT(this=column_exp, expression=exp.Placeholder(this=param_name))
139
+ if operator == ">=":
140
+ _, param_name = builder.add_parameter(value)
141
+ return exp.GTE(this=column_exp, expression=exp.Placeholder(this=param_name))
142
+ if operator == "<":
143
+ _, param_name = builder.add_parameter(value)
144
+ return exp.LT(this=column_exp, expression=exp.Placeholder(this=param_name))
145
+ if operator == "<=":
146
+ _, param_name = builder.add_parameter(value)
147
+ return exp.LTE(this=column_exp, expression=exp.Placeholder(this=param_name))
148
+ if operator == "LIKE":
149
+ _, param_name = builder.add_parameter(value)
150
+ return exp.Like(this=column_exp, expression=exp.Placeholder(this=param_name))
151
+ if operator == "NOT LIKE":
152
+ _, param_name = builder.add_parameter(value)
153
+ return exp.Not(this=exp.Like(this=column_exp, expression=exp.Placeholder(this=param_name)))
154
+
155
+ # Handle complex operators
156
+ if operator == "IN":
157
+ return self._handle_in_operator(column_exp, value)
158
+ if operator == "NOT IN":
159
+ return self._handle_not_in_operator(column_exp, value)
160
+ if operator == "IS":
161
+ return self._handle_is_operator(column_exp, value)
162
+ if operator == "IS NOT":
163
+ return self._handle_is_not_operator(column_exp, value)
164
+ if operator == "BETWEEN":
165
+ return self._handle_between_operator(column_exp, value)
166
+ if operator == "NOT BETWEEN":
167
+ return self._handle_not_between_operator(column_exp, value)
168
+
169
+ msg = f"Unsupported operator: {operator}"
170
+ raise SQLBuilderError(msg)
171
+
172
+ msg = f"Condition tuple must have 2 or 3 elements, got {len(condition)}"
173
+ raise SQLBuilderError(msg)
174
+
175
+ def where(
176
+ self,
177
+ condition: Union[str, exp.Expression, exp.Condition, tuple[str, Any], tuple[str, str, Any], "ColumnExpression"],
178
+ ) -> Self:
179
+ """Add a WHERE clause to the statement.
180
+
181
+ Args:
182
+ condition: The condition for the WHERE clause. Can be:
183
+ - A string condition
184
+ - A sqlglot Expression or Condition
185
+ - A 2-tuple (column, value) for equality comparison
186
+ - A 3-tuple (column, operator, value) for custom comparison
187
+
188
+ Raises:
189
+ SQLBuilderError: If the current expression is not a supported statement type.
190
+
191
+ Returns:
192
+ The current builder instance for method chaining.
193
+ """
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
+ ):
198
+ msg = "Cannot add WHERE clause to non-UPDATE expression"
199
+ raise SQLBuilderError(msg)
200
+
201
+ builder = cast("SQLBuilderProtocol", self)
202
+ if builder._expression is None:
203
+ msg = "Cannot add WHERE clause: expression is not initialized."
204
+ raise SQLBuilderError(msg)
205
+
206
+ # Check if DELETE has a table set
207
+ if isinstance(builder._expression, exp.Delete) and not builder._expression.args.get("this"):
208
+ msg = "WHERE clause requires a table to be set. Use from() to set the table first."
209
+ raise SQLBuilderError(msg)
210
+
211
+ # Process different condition types
212
+ if isinstance(condition, str):
213
+ where_expr = parse_condition_expression(condition)
214
+ elif isinstance(condition, (exp.Expression, exp.Condition)):
215
+ where_expr = condition
216
+ elif isinstance(condition, tuple):
217
+ where_expr = self._process_tuple_condition(condition)
218
+ elif has_query_builder_parameters(condition):
219
+ # Handle ColumnExpression objects
220
+ column_expr_obj = cast("ColumnExpression", condition)
221
+ where_expr = column_expr_obj._expression # pyright: ignore
222
+ elif has_sqlglot_expression(condition):
223
+ # This is a ColumnExpression from our new Column syntax
224
+ raw_expr = getattr(condition, "sqlglot_expression", None)
225
+ if raw_expr is not None:
226
+ where_expr = builder._parameterize_expression(raw_expr)
227
+ else:
228
+ # Fallback if attribute exists but is None
229
+ where_expr = parse_condition_expression(str(condition))
230
+ else:
231
+ msg = f"Unsupported condition type: {type(condition).__name__}"
232
+ raise SQLBuilderError(msg)
233
+
234
+ # Apply WHERE clause based on statement type
235
+ if isinstance(builder._expression, (exp.Select, exp.Update, exp.Delete)):
236
+ builder._expression = builder._expression.where(where_expr, copy=False)
237
+ else:
238
+ msg = f"WHERE clause not supported for {type(builder._expression).__name__}"
239
+ raise SQLBuilderError(msg)
240
+ return self
241
+
242
+ def where_eq(self, column: Union[str, exp.Column], value: Any) -> Self:
243
+ """Add WHERE column = value clause."""
244
+ builder = cast("SQLBuilderProtocol", self)
245
+ _, param_name = builder.add_parameter(value)
246
+ 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))
248
+ return self.where(condition)
249
+
250
+ def where_neq(self, column: Union[str, exp.Column], value: Any) -> Self:
251
+ """Add WHERE column != value clause."""
252
+ builder = cast("SQLBuilderProtocol", self)
253
+ _, param_name = builder.add_parameter(value)
254
+ 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))
256
+ return self.where(condition)
257
+
258
+ def where_lt(self, column: Union[str, exp.Column], value: Any) -> Self:
259
+ """Add WHERE column < value clause."""
260
+ builder = cast("SQLBuilderProtocol", self)
261
+ _, param_name = builder.add_parameter(value)
262
+ 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))
264
+ return self.where(condition)
265
+
266
+ def where_lte(self, column: Union[str, exp.Column], value: Any) -> Self:
267
+ """Add WHERE column <= value clause."""
268
+ builder = cast("SQLBuilderProtocol", self)
269
+ _, param_name = builder.add_parameter(value)
270
+ 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))
272
+ return self.where(condition)
273
+
274
+ def where_gt(self, column: Union[str, exp.Column], value: Any) -> Self:
275
+ """Add WHERE column > value clause."""
276
+ builder = cast("SQLBuilderProtocol", self)
277
+ _, param_name = builder.add_parameter(value)
278
+ 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))
280
+ return self.where(condition)
281
+
282
+ def where_gte(self, column: Union[str, exp.Column], value: Any) -> Self:
283
+ """Add WHERE column >= value clause."""
284
+ builder = cast("SQLBuilderProtocol", self)
285
+ _, param_name = builder.add_parameter(value)
286
+ 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))
288
+ return self.where(condition)
289
+
290
+ def where_between(self, column: Union[str, exp.Column], low: Any, high: Any) -> Self:
291
+ """Add WHERE column BETWEEN low AND high clause."""
292
+ builder = cast("SQLBuilderProtocol", self)
293
+ _, low_param = builder.add_parameter(low)
294
+ _, high_param = builder.add_parameter(high)
295
+ 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))
297
+ return self.where(condition)
298
+
299
+ def where_like(self, column: Union[str, exp.Column], pattern: str, escape: Optional[str] = None) -> Self:
300
+ """Add WHERE column LIKE pattern clause."""
301
+ builder = cast("SQLBuilderProtocol", self)
302
+ _, param_name = builder.add_parameter(pattern)
303
+ col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
304
+ if escape is not None:
305
+ cond = exp.Like(this=col_expr, expression=exp.var(param_name), escape=exp.Literal.string(str(escape)))
306
+ else:
307
+ cond = col_expr.like(exp.var(param_name))
308
+ condition: exp.Expression = cond
309
+ return self.where(condition)
310
+
311
+ def where_not_like(self, column: Union[str, exp.Column], pattern: str) -> Self:
312
+ """Add WHERE column NOT LIKE pattern clause."""
313
+ builder = cast("SQLBuilderProtocol", self)
314
+ _, param_name = builder.add_parameter(pattern)
315
+ 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_()
317
+ return self.where(condition)
318
+
319
+ def where_ilike(self, column: Union[str, exp.Column], pattern: str) -> Self:
320
+ """Add WHERE column ILIKE pattern clause."""
321
+ builder = cast("SQLBuilderProtocol", self)
322
+ _, param_name = builder.add_parameter(pattern)
323
+ 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))
325
+ return self.where(condition)
326
+
327
+ def where_is_null(self, column: Union[str, exp.Column]) -> Self:
328
+ """Add WHERE column IS NULL clause."""
329
+ col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
330
+ condition: exp.Expression = col_expr.is_(exp.null())
331
+ return self.where(condition)
332
+
333
+ def where_is_not_null(self, column: Union[str, exp.Column]) -> Self:
334
+ """Add WHERE column IS NOT NULL clause."""
335
+ col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
336
+ condition: exp.Expression = col_expr.is_(exp.null()).not_()
337
+ return self.where(condition)
338
+
339
+ def where_in(self, column: Union[str, exp.Column], values: Any) -> Self:
340
+ """Add WHERE column IN (values) clause."""
341
+ builder = cast("SQLBuilderProtocol", self)
342
+ col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
343
+ if has_query_builder_parameters(values) or isinstance(values, exp.Expression):
344
+ subquery_exp: exp.Expression
345
+ if has_query_builder_parameters(values):
346
+ 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)))
349
+ else:
350
+ subquery_exp = values # type: ignore[assignment]
351
+ condition = col_expr.isin(subquery_exp)
352
+ return self.where(condition)
353
+ if not is_iterable_parameters(values) or isinstance(values, (str, bytes)):
354
+ msg = "Unsupported type for 'values' in WHERE IN"
355
+ 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)
361
+ return self.where(condition)
362
+
363
+ def where_not_in(self, column: Union[str, exp.Column], values: Any) -> Self:
364
+ """Add WHERE column NOT IN (values) clause."""
365
+ builder = cast("SQLBuilderProtocol", self)
366
+ col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
367
+ if has_query_builder_parameters(values) or isinstance(values, exp.Expression):
368
+ subquery_exp: exp.Expression
369
+ if has_query_builder_parameters(values):
370
+ 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)))
373
+ else:
374
+ subquery_exp = values # type: ignore[assignment]
375
+ condition = exp.Not(this=col_expr.isin(subquery_exp))
376
+ return self.where(condition)
377
+ if not is_iterable_parameters(values) or isinstance(values, (str, bytes)):
378
+ msg = "Values for where_not_in must be a non-string iterable or subquery."
379
+ 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))
385
+ return self.where(condition)
386
+
387
+ def where_null(self, column: Union[str, exp.Column]) -> Self:
388
+ """Add WHERE column IS NULL clause."""
389
+ return self.where_is_null(column)
390
+
391
+ def where_not_null(self, column: Union[str, exp.Column]) -> Self:
392
+ """Add WHERE column IS NOT NULL clause."""
393
+ return self.where_is_not_null(column)
394
+
395
+ def where_exists(self, subquery: Union[str, Any]) -> Self:
396
+ """Add WHERE EXISTS (subquery) clause."""
397
+ builder = cast("SQLBuilderProtocol", self)
398
+ sub_expr: exp.Expression
399
+ 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():
403
+ builder.add_parameter(p_value, name=p_name)
404
+ 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))
407
+ else:
408
+ sub_expr = exp.maybe_parse(str(subquery), dialect=getattr(builder, "dialect_name", None))
409
+
410
+ if sub_expr is None:
411
+ msg = "Could not parse subquery for EXISTS"
412
+ raise SQLBuilderError(msg)
413
+
414
+ exists_expr = exp.Exists(this=sub_expr)
415
+ return self.where(exists_expr)
416
+
417
+ def where_not_exists(self, subquery: Union[str, Any]) -> Self:
418
+ """Add WHERE NOT EXISTS (subquery) clause."""
419
+ builder = cast("SQLBuilderProtocol", self)
420
+ sub_expr: exp.Expression
421
+ 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():
425
+ builder.add_parameter(p_value, name=p_name)
426
+ 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))
429
+ else:
430
+ sub_expr = exp.maybe_parse(str(subquery), dialect=getattr(builder, "dialect_name", None))
431
+
432
+ if sub_expr is None:
433
+ msg = "Could not parse subquery for NOT EXISTS"
434
+ raise SQLBuilderError(msg)
435
+
436
+ not_exists_expr = exp.Not(this=exp.Exists(this=sub_expr))
437
+ return self.where(not_exists_expr)
438
+
439
+ def where_any(self, column: Union[str, exp.Column], values: Any) -> Self:
440
+ """Add WHERE column = ANY(values) clause."""
441
+ builder = cast("SQLBuilderProtocol", self)
442
+ col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
443
+ if has_query_builder_parameters(values) or isinstance(values, exp.Expression):
444
+ subquery_exp: exp.Expression
445
+ if has_query_builder_parameters(values):
446
+ 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)))
449
+ else:
450
+ subquery_exp = values # type: ignore[assignment]
451
+ condition = exp.EQ(this=col_expr, expression=exp.Any(this=subquery_exp))
452
+ return self.where(condition)
453
+ if isinstance(values, str):
454
+ try:
455
+ parsed_expr: Optional[exp.Expression] = exp.maybe_parse(values)
456
+ if isinstance(parsed_expr, (exp.Select, exp.Union, exp.Subquery)):
457
+ subquery_exp = exp.paren(parsed_expr)
458
+ condition = exp.EQ(this=col_expr, expression=exp.Any(this=subquery_exp))
459
+ return self.where(condition)
460
+ except Exception: # noqa: S110
461
+ pass
462
+ msg = "Unsupported type for 'values' in WHERE ANY"
463
+ raise SQLBuilderError(msg)
464
+ if not is_iterable_parameters(values) or isinstance(values, bytes):
465
+ msg = "Unsupported type for 'values' in WHERE ANY"
466
+ 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)
472
+ condition = exp.EQ(this=col_expr, expression=exp.Any(this=tuple_expr))
473
+ return self.where(condition)
474
+
475
+ def where_not_any(self, column: Union[str, exp.Column], values: Any) -> Self:
476
+ """Add WHERE column != ANY(values) clause."""
477
+ builder = cast("SQLBuilderProtocol", self)
478
+ col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
479
+ if has_query_builder_parameters(values) or isinstance(values, exp.Expression):
480
+ subquery_exp: exp.Expression
481
+ if has_query_builder_parameters(values):
482
+ 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)))
485
+ else:
486
+ subquery_exp = values # type: ignore[assignment]
487
+ condition = exp.NEQ(this=col_expr, expression=exp.Any(this=subquery_exp))
488
+ return self.where(condition)
489
+ if isinstance(values, str):
490
+ try:
491
+ parsed_expr: Optional[exp.Expression] = exp.maybe_parse(values)
492
+ if isinstance(parsed_expr, (exp.Select, exp.Union, exp.Subquery)):
493
+ subquery_exp = exp.paren(parsed_expr)
494
+ condition = exp.NEQ(this=col_expr, expression=exp.Any(this=subquery_exp))
495
+ return self.where(condition)
496
+ except Exception: # noqa: S110
497
+ pass
498
+ msg = "Unsupported type for 'values' in WHERE NOT ANY"
499
+ raise SQLBuilderError(msg)
500
+ if not is_iterable_parameters(values) or isinstance(values, bytes):
501
+ msg = "Unsupported type for 'values' in WHERE NOT ANY"
502
+ 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)
508
+ condition = exp.NEQ(this=col_expr, expression=exp.Any(this=tuple_expr))
509
+ return self.where(condition)
510
+
511
+
512
+ class HavingClauseMixin:
513
+ """Mixin providing HAVING clause for SELECT builders."""
514
+
515
+ _expression: Optional[exp.Expression] = None
516
+
517
+ def having(self, condition: Union[str, exp.Expression]) -> Self:
518
+ """Add HAVING clause.
519
+
520
+ Args:
521
+ condition: The condition for the HAVING clause.
522
+
523
+ Raises:
524
+ SQLBuilderError: If the current expression is not a SELECT statement.
525
+
526
+ Returns:
527
+ The current builder instance for method chaining.
528
+ """
529
+ if self._expression is None:
530
+ self._expression = exp.Select()
531
+ if not isinstance(self._expression, exp.Select):
532
+ msg = "Cannot add HAVING to a non-SELECT expression."
533
+ raise SQLBuilderError(msg)
534
+ having_expr = exp.condition(condition) if isinstance(condition, str) else condition
535
+ self._expression = self._expression.having(having_expr, copy=False)
536
+ return self
@@ -0,0 +1,50 @@
1
+ """Cache implementation for SQL statement processing."""
2
+
3
+ import threading
4
+ from collections import OrderedDict
5
+ from typing import Any, Optional
6
+
7
+ __all__ = ("SQLCache",)
8
+
9
+
10
+ DEFAULT_CACHE_MAX_SIZE = 1000
11
+
12
+
13
+ class SQLCache:
14
+ """A thread-safe LRU cache for processed SQL states."""
15
+
16
+ def __init__(self, max_size: int = DEFAULT_CACHE_MAX_SIZE) -> None:
17
+ self.cache: OrderedDict[str, Any] = OrderedDict()
18
+ self.max_size = max_size
19
+ self.lock = threading.Lock()
20
+
21
+ @property
22
+ def size(self) -> int:
23
+ """Get current cache size."""
24
+ return len(self.cache)
25
+
26
+ def get(self, key: str) -> Optional[Any]:
27
+ """Get an item from the cache, marking it as recently used."""
28
+ with self.lock:
29
+ if key in self.cache:
30
+ self.cache.move_to_end(key)
31
+ return self.cache[key]
32
+ return None
33
+
34
+ def set(self, key: str, value: Any) -> None:
35
+ """Set an item in the cache with LRU eviction."""
36
+ with self.lock:
37
+ if key in self.cache:
38
+ self.cache.move_to_end(key)
39
+ # Add new entry
40
+ elif len(self.cache) >= self.max_size:
41
+ self.cache.popitem(last=False)
42
+ self.cache[key] = value
43
+
44
+ def clear(self) -> None:
45
+ """Clear the cache."""
46
+ with self.lock:
47
+ self.cache.clear()
48
+
49
+
50
+ sql_cache = SQLCache()