sqlspec 0.15.0__py3-none-any.whl → 0.16.1__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 (43) hide show
  1. sqlspec/_sql.py +699 -43
  2. sqlspec/builder/_base.py +77 -44
  3. sqlspec/builder/_column.py +0 -4
  4. sqlspec/builder/_ddl.py +15 -52
  5. sqlspec/builder/_ddl_utils.py +0 -1
  6. sqlspec/builder/_delete.py +4 -5
  7. sqlspec/builder/_insert.py +61 -35
  8. sqlspec/builder/_merge.py +17 -2
  9. sqlspec/builder/_parsing_utils.py +16 -12
  10. sqlspec/builder/_select.py +29 -33
  11. sqlspec/builder/_update.py +4 -2
  12. sqlspec/builder/mixins/_cte_and_set_ops.py +47 -20
  13. sqlspec/builder/mixins/_delete_operations.py +6 -1
  14. sqlspec/builder/mixins/_insert_operations.py +126 -24
  15. sqlspec/builder/mixins/_join_operations.py +11 -4
  16. sqlspec/builder/mixins/_merge_operations.py +91 -19
  17. sqlspec/builder/mixins/_order_limit_operations.py +15 -3
  18. sqlspec/builder/mixins/_pivot_operations.py +11 -2
  19. sqlspec/builder/mixins/_select_operations.py +16 -10
  20. sqlspec/builder/mixins/_update_operations.py +43 -10
  21. sqlspec/builder/mixins/_where_clause.py +177 -65
  22. sqlspec/core/cache.py +26 -28
  23. sqlspec/core/compiler.py +58 -37
  24. sqlspec/core/filters.py +12 -10
  25. sqlspec/core/parameters.py +80 -52
  26. sqlspec/core/result.py +30 -17
  27. sqlspec/core/statement.py +47 -22
  28. sqlspec/driver/_async.py +76 -46
  29. sqlspec/driver/_common.py +25 -6
  30. sqlspec/driver/_sync.py +73 -43
  31. sqlspec/driver/mixins/_result_tools.py +62 -37
  32. sqlspec/driver/mixins/_sql_translator.py +61 -11
  33. sqlspec/extensions/litestar/cli.py +1 -1
  34. sqlspec/extensions/litestar/plugin.py +2 -2
  35. sqlspec/protocols.py +7 -0
  36. sqlspec/utils/sync_tools.py +1 -1
  37. sqlspec/utils/type_guards.py +7 -3
  38. {sqlspec-0.15.0.dist-info → sqlspec-0.16.1.dist-info}/METADATA +1 -1
  39. {sqlspec-0.15.0.dist-info → sqlspec-0.16.1.dist-info}/RECORD +43 -43
  40. {sqlspec-0.15.0.dist-info → sqlspec-0.16.1.dist-info}/WHEEL +0 -0
  41. {sqlspec-0.15.0.dist-info → sqlspec-0.16.1.dist-info}/entry_points.txt +0 -0
  42. {sqlspec-0.15.0.dist-info → sqlspec-0.16.1.dist-info}/licenses/LICENSE +0 -0
  43. {sqlspec-0.15.0.dist-info → sqlspec-0.16.1.dist-info}/licenses/NOTICE +0 -0
@@ -1,20 +1,28 @@
1
1
  """Insert operation mixins for SQL builders."""
2
2
 
3
3
  from collections.abc import Sequence
4
- from typing import Any, Optional, Union
4
+ from typing import Any, Optional, TypeVar, Union
5
5
 
6
+ from mypy_extensions import trait
6
7
  from sqlglot import exp
7
8
  from typing_extensions import Self
8
9
 
9
10
  from sqlspec.exceptions import SQLBuilderError
11
+ from sqlspec.protocols import SQLBuilderProtocol
12
+
13
+ BuilderT = TypeVar("BuilderT", bound=SQLBuilderProtocol)
10
14
 
11
15
  __all__ = ("InsertFromSelectMixin", "InsertIntoClauseMixin", "InsertValuesMixin")
12
16
 
13
17
 
18
+ @trait
14
19
  class InsertIntoClauseMixin:
15
20
  """Mixin providing INTO clause for INSERT builders."""
16
21
 
17
- _expression: Optional[exp.Expression] = None
22
+ __slots__ = ()
23
+
24
+ # Type annotation for PyRight - this will be provided by the base class
25
+ _expression: Optional[exp.Expression]
18
26
 
19
27
  def into(self, table: str) -> Self:
20
28
  """Set the target table for the INSERT statement.
@@ -39,10 +47,26 @@ class InsertIntoClauseMixin:
39
47
  return self
40
48
 
41
49
 
50
+ @trait
42
51
  class InsertValuesMixin:
43
52
  """Mixin providing VALUES and columns methods for INSERT builders."""
44
53
 
45
- _expression: Optional[exp.Expression] = None
54
+ __slots__ = ()
55
+
56
+ # Type annotation for PyRight - this will be provided by the base class
57
+ _expression: Optional[exp.Expression]
58
+
59
+ _columns: Any # Provided by QueryBuilder
60
+
61
+ def add_parameter(self, value: Any, name: Optional[str] = None) -> tuple[Any, str]:
62
+ """Add parameter - provided by QueryBuilder."""
63
+ msg = "Method must be provided by QueryBuilder subclass"
64
+ raise NotImplementedError(msg)
65
+
66
+ def _generate_unique_parameter_name(self, base_name: str) -> str:
67
+ """Generate unique parameter name - provided by QueryBuilder."""
68
+ msg = "Method must be provided by QueryBuilder subclass"
69
+ raise NotImplementedError(msg)
46
70
 
47
71
  def columns(self, *columns: Union[str, exp.Expression]) -> Self:
48
72
  """Set the columns for the INSERT statement and synchronize the _columns attribute on the builder."""
@@ -54,7 +78,7 @@ class InsertValuesMixin:
54
78
  column_exprs = [exp.column(col) if isinstance(col, str) else col for col in columns]
55
79
  self._expression.set("columns", column_exprs)
56
80
  try:
57
- cols = self._columns # type: ignore[attr-defined]
81
+ cols = self._columns
58
82
  if not columns:
59
83
  cols.clear()
60
84
  else:
@@ -63,27 +87,94 @@ class InsertValuesMixin:
63
87
  pass
64
88
  return self
65
89
 
66
- def values(self, *values: Any) -> Self:
67
- """Add a row of values to the INSERT statement, validating against _columns if set."""
90
+ def values(self, *values: Any, **kwargs: Any) -> Self:
91
+ """Add a row of values to the INSERT statement.
92
+
93
+ Supports:
94
+ - values(val1, val2, val3)
95
+ - values(col1=val1, col2=val2)
96
+ - values(mapping)
97
+
98
+ Args:
99
+ *values: Either positional values or a single mapping.
100
+ **kwargs: Column-value pairs.
101
+
102
+ Returns:
103
+ The current builder instance for method chaining.
104
+ """
68
105
  if self._expression is None:
69
106
  self._expression = exp.Insert()
70
107
  if not isinstance(self._expression, exp.Insert):
71
108
  msg = "Cannot add values to a non-INSERT expression."
72
109
  raise SQLBuilderError(msg)
73
- try:
74
- _columns = self._columns # type: ignore[attr-defined]
75
- if _columns and len(values) != len(_columns):
76
- msg = f"Number of values ({len(values)}) does not match the number of specified columns ({len(_columns)})."
110
+
111
+ if kwargs:
112
+ if values:
113
+ msg = "Cannot mix positional values with keyword values."
77
114
  raise SQLBuilderError(msg)
78
- except AttributeError:
79
- pass
80
- row_exprs = []
81
- for v in values:
82
- if isinstance(v, exp.Expression):
83
- row_exprs.append(v)
84
- else:
85
- _, param_name = self.add_parameter(v) # type: ignore[attr-defined]
86
- row_exprs.append(exp.var(param_name))
115
+ try:
116
+ _columns = self._columns
117
+ if not _columns:
118
+ self.columns(*kwargs.keys())
119
+ except AttributeError:
120
+ pass
121
+ row_exprs = []
122
+ for col, val in kwargs.items():
123
+ if isinstance(val, exp.Expression):
124
+ row_exprs.append(val)
125
+ else:
126
+ column_name = col if isinstance(col, str) else str(col)
127
+ if "." in column_name:
128
+ column_name = column_name.split(".")[-1]
129
+ param_name = self._generate_unique_parameter_name(column_name)
130
+ _, param_name = self.add_parameter(val, name=param_name)
131
+ row_exprs.append(exp.var(param_name))
132
+ elif len(values) == 1 and hasattr(values[0], "items"):
133
+ mapping = values[0]
134
+ try:
135
+ _columns = self._columns
136
+ if not _columns:
137
+ self.columns(*mapping.keys())
138
+ except AttributeError:
139
+ pass
140
+ row_exprs = []
141
+ for col, val in mapping.items():
142
+ if isinstance(val, exp.Expression):
143
+ row_exprs.append(val)
144
+ else:
145
+ column_name = col if isinstance(col, str) else str(col)
146
+ if "." in column_name:
147
+ column_name = column_name.split(".")[-1]
148
+ param_name = self._generate_unique_parameter_name(column_name)
149
+ _, param_name = self.add_parameter(val, name=param_name)
150
+ row_exprs.append(exp.var(param_name))
151
+ else:
152
+ try:
153
+ _columns = self._columns
154
+ if _columns and len(values) != len(_columns):
155
+ msg = f"Number of values ({len(values)}) does not match the number of specified columns ({len(_columns)})."
156
+ raise SQLBuilderError(msg)
157
+ except AttributeError:
158
+ pass
159
+ row_exprs = []
160
+ for i, v in enumerate(values):
161
+ if isinstance(v, exp.Expression):
162
+ row_exprs.append(v)
163
+ else:
164
+ try:
165
+ _columns = self._columns
166
+ if _columns and i < len(_columns):
167
+ column_name = (
168
+ str(_columns[i]).split(".")[-1] if "." in str(_columns[i]) else str(_columns[i])
169
+ )
170
+ param_name = self._generate_unique_parameter_name(column_name)
171
+ else:
172
+ param_name = self._generate_unique_parameter_name(f"value_{i + 1}")
173
+ except AttributeError:
174
+ param_name = self._generate_unique_parameter_name(f"value_{i + 1}")
175
+ _, param_name = self.add_parameter(v, name=param_name)
176
+ row_exprs.append(exp.var(param_name))
177
+
87
178
  values_expr = exp.Values(expressions=[row_exprs])
88
179
  self._expression.set("expression", values_expr)
89
180
  return self
@@ -100,10 +191,21 @@ class InsertValuesMixin:
100
191
  return self.values(*values)
101
192
 
102
193
 
194
+ @trait
103
195
  class InsertFromSelectMixin:
104
196
  """Mixin providing INSERT ... SELECT support for INSERT builders."""
105
197
 
106
- _expression: Optional[exp.Expression] = None
198
+ __slots__ = ()
199
+
200
+ # Type annotation for PyRight - this will be provided by the base class
201
+ _expression: Optional[exp.Expression]
202
+
203
+ _table: Any # Provided by QueryBuilder
204
+
205
+ def add_parameter(self, value: Any, name: Optional[str] = None) -> tuple[Any, str]:
206
+ """Add parameter - provided by QueryBuilder."""
207
+ msg = "Method must be provided by QueryBuilder subclass"
208
+ raise NotImplementedError(msg)
107
209
 
108
210
  def from_select(self, select_builder: Any) -> Self:
109
211
  """Sets the INSERT source to a SELECT statement.
@@ -118,7 +220,7 @@ class InsertFromSelectMixin:
118
220
  SQLBuilderError: If the table is not set or the select_builder is invalid.
119
221
  """
120
222
  try:
121
- if not self._table: # type: ignore[attr-defined]
223
+ if not self._table:
122
224
  msg = "The target table must be set using .into() before adding values."
123
225
  raise SQLBuilderError(msg)
124
226
  except AttributeError:
@@ -129,11 +231,11 @@ class InsertFromSelectMixin:
129
231
  if not isinstance(self._expression, exp.Insert):
130
232
  msg = "Cannot set INSERT source on a non-INSERT expression."
131
233
  raise SQLBuilderError(msg)
132
- subquery_parameters = select_builder._parameters # pyright: ignore[attr-defined]
234
+ subquery_parameters = select_builder._parameters
133
235
  if subquery_parameters:
134
236
  for p_name, p_value in subquery_parameters.items():
135
- self.add_parameter(p_value, name=p_name) # type: ignore[attr-defined]
136
- select_expr = select_builder._expression # pyright: ignore[attr-defined]
237
+ self.add_parameter(p_value, name=p_name)
238
+ select_expr = select_builder._expression
137
239
  if select_expr and isinstance(select_expr, exp.Select):
138
240
  self._expression.set("expression", select_expr.copy())
139
241
  else:
@@ -1,5 +1,6 @@
1
1
  from typing import TYPE_CHECKING, Any, Optional, Union, cast
2
2
 
3
+ from mypy_extensions import trait
3
4
  from sqlglot import exp
4
5
  from typing_extensions import Self
5
6
 
@@ -13,9 +14,15 @@ if TYPE_CHECKING:
13
14
  __all__ = ("JoinClauseMixin",)
14
15
 
15
16
 
17
+ @trait
16
18
  class JoinClauseMixin:
17
19
  """Mixin providing JOIN clause methods for SELECT builders."""
18
20
 
21
+ __slots__ = ()
22
+
23
+ # Type annotation for PyRight - this will be provided by the base class
24
+ _expression: Optional[exp.Expression]
25
+
19
26
  def join(
20
27
  self,
21
28
  table: Union[str, exp.Expression, Any],
@@ -36,12 +43,12 @@ class JoinClauseMixin:
36
43
  if hasattr(table, "_expression") and getattr(table, "_expression", None) is not None:
37
44
  table_expr_value = getattr(table, "_expression", None)
38
45
  if table_expr_value is not None:
39
- subquery_exp = exp.paren(table_expr_value.copy()) # pyright: ignore
46
+ subquery_exp = exp.paren(table_expr_value)
40
47
  else:
41
48
  subquery_exp = exp.paren(exp.Anonymous(this=""))
42
49
  table_expr = exp.alias_(subquery_exp, alias) if alias else subquery_exp
43
50
  else:
44
- subquery = table.build() # pyright: ignore
51
+ subquery = table.build()
45
52
  sql_str = subquery.sql if hasattr(subquery, "sql") and not callable(subquery.sql) else str(subquery)
46
53
  subquery_exp = exp.paren(exp.maybe_parse(sql_str, dialect=getattr(builder, "dialect", None)))
47
54
  table_expr = exp.alias_(subquery_exp, alias) if alias else subquery_exp
@@ -99,12 +106,12 @@ class JoinClauseMixin:
99
106
  if hasattr(table, "_expression") and getattr(table, "_expression", None) is not None:
100
107
  table_expr_value = getattr(table, "_expression", None)
101
108
  if table_expr_value is not None:
102
- subquery_exp = exp.paren(table_expr_value.copy()) # pyright: ignore
109
+ subquery_exp = exp.paren(table_expr_value)
103
110
  else:
104
111
  subquery_exp = exp.paren(exp.Anonymous(this=""))
105
112
  table_expr = exp.alias_(subquery_exp, alias) if alias else subquery_exp
106
113
  else:
107
- subquery = table.build() # pyright: ignore
114
+ subquery = table.build()
108
115
  sql_str = subquery.sql if hasattr(subquery, "sql") and not callable(subquery.sql) else str(subquery)
109
116
  subquery_exp = exp.paren(exp.maybe_parse(sql_str, dialect=getattr(builder, "dialect", None)))
110
117
  table_expr = exp.alias_(subquery_exp, alias) if alias else subquery_exp
@@ -2,6 +2,7 @@
2
2
 
3
3
  from typing import Any, Optional, Union
4
4
 
5
+ from mypy_extensions import trait
5
6
  from sqlglot import exp
6
7
  from typing_extensions import Self
7
8
 
@@ -18,10 +19,12 @@ __all__ = (
18
19
  )
19
20
 
20
21
 
22
+ @trait
21
23
  class MergeIntoClauseMixin:
22
24
  """Mixin providing INTO clause for MERGE builders."""
23
25
 
24
- _expression: Optional[exp.Expression] = None
26
+ __slots__ = ()
27
+ _expression: Optional[exp.Expression]
25
28
 
26
29
  def into(self, table: Union[str, exp.Expression], alias: Optional[str] = None) -> Self:
27
30
  """Set the target table for the MERGE operation (INTO clause).
@@ -35,17 +38,24 @@ class MergeIntoClauseMixin:
35
38
  The current builder instance for method chaining.
36
39
  """
37
40
  if self._expression is None:
38
- self._expression = exp.Merge(this=None, using=None, on=None, whens=exp.Whens(expressions=[])) # pyright: ignore
39
- if not isinstance(self._expression, exp.Merge): # pyright: ignore
40
- self._expression = exp.Merge(this=None, using=None, on=None, whens=exp.Whens(expressions=[])) # pyright: ignore
41
+ self._expression = exp.Merge(this=None, using=None, on=None, whens=exp.Whens(expressions=[]))
42
+ if not isinstance(self._expression, exp.Merge):
43
+ self._expression = exp.Merge(this=None, using=None, on=None, whens=exp.Whens(expressions=[]))
41
44
  self._expression.set("this", exp.to_table(table, alias=alias) if isinstance(table, str) else table)
42
45
  return self
43
46
 
44
47
 
48
+ @trait
45
49
  class MergeUsingClauseMixin:
46
50
  """Mixin providing USING clause for MERGE builders."""
47
51
 
48
- _expression: Optional[exp.Expression] = None
52
+ __slots__ = ()
53
+ _expression: Optional[exp.Expression]
54
+
55
+ def add_parameter(self, value: Any, name: Optional[str] = None) -> tuple[Any, str]:
56
+ """Add parameter - provided by QueryBuilder."""
57
+ msg = "Method must be provided by QueryBuilder subclass"
58
+ raise NotImplementedError(msg)
49
59
 
50
60
  def using(self, source: Union[str, exp.Expression, Any], alias: Optional[str] = None) -> Self:
51
61
  """Set the source data for the MERGE operation (USING clause).
@@ -73,7 +83,7 @@ class MergeUsingClauseMixin:
73
83
  subquery_builder_parameters = source.parameters
74
84
  if subquery_builder_parameters:
75
85
  for p_name, p_value in subquery_builder_parameters.items():
76
- self.add_parameter(p_value, name=p_name) # type: ignore[attr-defined]
86
+ self.add_parameter(p_value, name=p_name)
77
87
 
78
88
  subquery_exp = exp.paren(getattr(source, "_expression", exp.select()))
79
89
  source_expr = exp.alias_(subquery_exp, alias) if alias else subquery_exp
@@ -89,10 +99,12 @@ class MergeUsingClauseMixin:
89
99
  return self
90
100
 
91
101
 
102
+ @trait
92
103
  class MergeOnClauseMixin:
93
104
  """Mixin providing ON clause for MERGE builders."""
94
105
 
95
- _expression: Optional[exp.Expression] = None
106
+ __slots__ = ()
107
+ _expression: Optional[exp.Expression]
96
108
 
97
109
  def on(self, condition: Union[str, exp.Expression]) -> Self:
98
110
  """Set the join condition for the MERGE operation (ON clause).
@@ -131,10 +143,22 @@ class MergeOnClauseMixin:
131
143
  return self
132
144
 
133
145
 
146
+ @trait
134
147
  class MergeMatchedClauseMixin:
135
148
  """Mixin providing WHEN MATCHED THEN ... clauses for MERGE builders."""
136
149
 
137
- _expression: Optional[exp.Expression] = None
150
+ __slots__ = ()
151
+ _expression: Optional[exp.Expression]
152
+
153
+ def add_parameter(self, value: Any, name: Optional[str] = None) -> tuple[Any, str]:
154
+ """Add parameter - provided by QueryBuilder."""
155
+ msg = "Method must be provided by QueryBuilder subclass"
156
+ raise NotImplementedError(msg)
157
+
158
+ def _generate_unique_parameter_name(self, base_name: str) -> str:
159
+ """Generate unique parameter name - provided by QueryBuilder."""
160
+ msg = "Method must be provided by QueryBuilder subclass"
161
+ raise NotImplementedError(msg)
138
162
 
139
163
  def _add_when_clause(self, when_clause: exp.When) -> None:
140
164
  """Helper to add a WHEN clause to the MERGE statement.
@@ -143,9 +167,9 @@ class MergeMatchedClauseMixin:
143
167
  when_clause: The WHEN clause to add.
144
168
  """
145
169
  if self._expression is None:
146
- self._expression = exp.Merge(this=None, using=None, on=None, whens=exp.Whens(expressions=[]))
170
+ self._expression = exp.Merge(this=None, using=None, on=None, whens=exp.Whens(expressions=[])) # type: ignore[misc]
147
171
  if not isinstance(self._expression, exp.Merge):
148
- self._expression = exp.Merge(this=None, using=None, on=None, whens=exp.Whens(expressions=[]))
172
+ self._expression = exp.Merge(this=None, using=None, on=None, whens=exp.Whens(expressions=[])) # type: ignore[misc]
149
173
 
150
174
  whens = self._expression.args.get("whens")
151
175
  if not whens:
@@ -172,7 +196,11 @@ class MergeMatchedClauseMixin:
172
196
  """
173
197
  update_expressions: list[exp.EQ] = []
174
198
  for col, val in set_values.items():
175
- param_name = self.add_parameter(val)[1] # type: ignore[attr-defined]
199
+ column_name = col if isinstance(col, str) else str(col)
200
+ if "." in column_name:
201
+ column_name = column_name.split(".")[-1]
202
+ param_name = self._generate_unique_parameter_name(column_name)
203
+ param_name = self.add_parameter(val, name=param_name)[1]
176
204
  update_expressions.append(exp.EQ(this=exp.column(col), expression=exp.var(param_name)))
177
205
 
178
206
  when_args: dict[str, Any] = {"matched": True, "then": exp.Update(expressions=update_expressions)}
@@ -234,10 +262,28 @@ class MergeMatchedClauseMixin:
234
262
  return self
235
263
 
236
264
 
265
+ @trait
237
266
  class MergeNotMatchedClauseMixin:
238
267
  """Mixin providing WHEN NOT MATCHED THEN ... clauses for MERGE builders."""
239
268
 
240
- _expression: Optional[exp.Expression] = None
269
+ __slots__ = ()
270
+
271
+ _expression: Optional[exp.Expression]
272
+
273
+ def add_parameter(self, value: Any, name: Optional[str] = None) -> tuple[Any, str]:
274
+ """Add parameter - provided by QueryBuilder."""
275
+ msg = "Method must be provided by QueryBuilder subclass"
276
+ raise NotImplementedError(msg)
277
+
278
+ def _generate_unique_parameter_name(self, base_name: str) -> str:
279
+ """Generate unique parameter name - provided by QueryBuilder."""
280
+ msg = "Method must be provided by QueryBuilder subclass"
281
+ raise NotImplementedError(msg)
282
+
283
+ def _add_when_clause(self, when_clause: exp.When) -> None:
284
+ """Helper to add a WHEN clause to the MERGE statement - provided by QueryBuilder."""
285
+ msg = "Method must be provided by QueryBuilder subclass"
286
+ raise NotImplementedError(msg)
241
287
 
242
288
  def when_not_matched_then_insert(
243
289
  self,
@@ -270,8 +316,12 @@ class MergeNotMatchedClauseMixin:
270
316
  raise SQLBuilderError(msg)
271
317
 
272
318
  parameterized_values: list[exp.Expression] = []
273
- for val in values:
274
- param_name = self.add_parameter(val)[1] # type: ignore[attr-defined]
319
+ for i, val in enumerate(values):
320
+ column_name = columns[i] if isinstance(columns[i], str) else str(columns[i])
321
+ if "." in column_name:
322
+ column_name = column_name.split(".")[-1]
323
+ param_name = self._generate_unique_parameter_name(column_name)
324
+ param_name = self.add_parameter(val, name=param_name)[1]
275
325
  parameterized_values.append(exp.var(param_name))
276
326
 
277
327
  insert_args["this"] = exp.Tuple(expressions=[exp.column(c) for c in columns])
@@ -308,14 +358,32 @@ class MergeNotMatchedClauseMixin:
308
358
  when_args["this"] = condition_expr
309
359
 
310
360
  when_clause = exp.When(**when_args)
311
- self._add_when_clause(when_clause) # type: ignore[attr-defined]
361
+ self._add_when_clause(when_clause)
312
362
  return self
313
363
 
314
364
 
365
+ @trait
315
366
  class MergeNotMatchedBySourceClauseMixin:
316
367
  """Mixin providing WHEN NOT MATCHED BY SOURCE THEN ... clauses for MERGE builders."""
317
368
 
318
- _expression: Optional[exp.Expression] = None
369
+ __slots__ = ()
370
+
371
+ _expression: Optional[exp.Expression]
372
+
373
+ def add_parameter(self, value: Any, name: Optional[str] = None) -> tuple[Any, str]:
374
+ """Add parameter - provided by QueryBuilder."""
375
+ msg = "Method must be provided by QueryBuilder subclass"
376
+ raise NotImplementedError(msg)
377
+
378
+ def _generate_unique_parameter_name(self, base_name: str) -> str:
379
+ """Generate unique parameter name - provided by QueryBuilder."""
380
+ msg = "Method must be provided by QueryBuilder subclass"
381
+ raise NotImplementedError(msg)
382
+
383
+ def _add_when_clause(self, when_clause: exp.When) -> None:
384
+ """Helper to add a WHEN clause to the MERGE statement - provided by QueryBuilder."""
385
+ msg = "Method must be provided by QueryBuilder subclass"
386
+ raise NotImplementedError(msg)
319
387
 
320
388
  def when_not_matched_by_source_then_update(
321
389
  self, set_values: dict[str, Any], condition: Optional[Union[str, exp.Expression]] = None
@@ -336,7 +404,11 @@ class MergeNotMatchedBySourceClauseMixin:
336
404
  """
337
405
  update_expressions: list[exp.EQ] = []
338
406
  for col, val in set_values.items():
339
- param_name = self.add_parameter(val)[1] # type: ignore[attr-defined]
407
+ column_name = col if isinstance(col, str) else str(col)
408
+ if "." in column_name:
409
+ column_name = column_name.split(".")[-1]
410
+ param_name = self._generate_unique_parameter_name(column_name)
411
+ param_name = self.add_parameter(val, name=param_name)[1]
340
412
  update_expressions.append(exp.EQ(this=exp.column(col), expression=exp.var(param_name)))
341
413
 
342
414
  when_args: dict[str, Any] = {
@@ -363,7 +435,7 @@ class MergeNotMatchedBySourceClauseMixin:
363
435
  when_args["this"] = condition_expr
364
436
 
365
437
  when_clause = exp.When(**when_args)
366
- self._add_when_clause(when_clause) # type: ignore[attr-defined]
438
+ self._add_when_clause(when_clause)
367
439
  return self
368
440
 
369
441
  def when_not_matched_by_source_then_delete(self, condition: Optional[Union[str, exp.Expression]] = None) -> Self:
@@ -400,5 +472,5 @@ class MergeNotMatchedBySourceClauseMixin:
400
472
  when_args["this"] = condition_expr
401
473
 
402
474
  when_clause = exp.When(**when_args)
403
- self._add_when_clause(when_clause) # type: ignore[attr-defined]
475
+ self._add_when_clause(when_clause)
404
476
  return self
@@ -2,6 +2,7 @@
2
2
 
3
3
  from typing import TYPE_CHECKING, Optional, Union, cast
4
4
 
5
+ from mypy_extensions import trait
5
6
  from sqlglot import exp
6
7
  from typing_extensions import Self
7
8
 
@@ -14,10 +15,14 @@ if TYPE_CHECKING:
14
15
  __all__ = ("LimitOffsetClauseMixin", "OrderByClauseMixin", "ReturningClauseMixin")
15
16
 
16
17
 
18
+ @trait
17
19
  class OrderByClauseMixin:
18
20
  """Mixin providing ORDER BY clause."""
19
21
 
20
- _expression: Optional[exp.Expression] = None
22
+ __slots__ = ()
23
+
24
+ # Type annotation for PyRight - this will be provided by the base class
25
+ _expression: Optional[exp.Expression]
21
26
 
22
27
  def order_by(self, *items: Union[str, exp.Ordered], desc: bool = False) -> Self:
23
28
  """Add ORDER BY clause.
@@ -50,10 +55,14 @@ class OrderByClauseMixin:
50
55
  return cast("Self", builder)
51
56
 
52
57
 
58
+ @trait
53
59
  class LimitOffsetClauseMixin:
54
60
  """Mixin providing LIMIT and OFFSET clauses."""
55
61
 
56
- _expression: Optional[exp.Expression] = None
62
+ __slots__ = ()
63
+
64
+ # Type annotation for PyRight - this will be provided by the base class
65
+ _expression: Optional[exp.Expression]
57
66
 
58
67
  def limit(self, value: int) -> Self:
59
68
  """Add LIMIT clause.
@@ -94,10 +103,13 @@ class LimitOffsetClauseMixin:
94
103
  return cast("Self", builder)
95
104
 
96
105
 
106
+ @trait
97
107
  class ReturningClauseMixin:
98
108
  """Mixin providing RETURNING clause."""
99
109
 
100
- _expression: Optional[exp.Expression] = None
110
+ __slots__ = ()
111
+ # Type annotation for PyRight - this will be provided by the base class
112
+ _expression: Optional[exp.Expression]
101
113
 
102
114
  def returning(self, *columns: Union[str, exp.Expression]) -> Self:
103
115
  """Add RETURNING clause to the statement.
@@ -2,6 +2,7 @@
2
2
 
3
3
  from typing import TYPE_CHECKING, Optional, Union, cast
4
4
 
5
+ from mypy_extensions import trait
5
6
  from sqlglot import exp
6
7
 
7
8
  if TYPE_CHECKING:
@@ -12,10 +13,14 @@ if TYPE_CHECKING:
12
13
  __all__ = ("PivotClauseMixin", "UnpivotClauseMixin")
13
14
 
14
15
 
16
+ @trait
15
17
  class PivotClauseMixin:
16
18
  """Mixin class to add PIVOT functionality to a Select."""
17
19
 
18
- _expression: "Optional[exp.Expression]" = None
20
+ __slots__ = ()
21
+ # Type annotation for PyRight - this will be provided by the base class
22
+ _expression: Optional[exp.Expression]
23
+
19
24
  dialect: "DialectType" = None
20
25
 
21
26
  def pivot(
@@ -79,10 +84,14 @@ class PivotClauseMixin:
79
84
  return cast("Select", self)
80
85
 
81
86
 
87
+ @trait
82
88
  class UnpivotClauseMixin:
83
89
  """Mixin class to add UNPIVOT functionality to a Select."""
84
90
 
85
- _expression: "Optional[exp.Expression]" = None
91
+ __slots__ = ()
92
+ # Type annotation for PyRight - this will be provided by the base class
93
+ _expression: Optional[exp.Expression]
94
+
86
95
  dialect: "DialectType" = None
87
96
 
88
97
  def unpivot(