sqlspec 0.16.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 (39) hide show
  1. sqlspec/_sql.py +448 -15
  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 +59 -44
  8. sqlspec/builder/_merge.py +17 -2
  9. sqlspec/builder/_parsing_utils.py +11 -11
  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 -34
  15. sqlspec/builder/mixins/_join_operations.py +11 -4
  16. sqlspec/builder/mixins/_merge_operations.py +81 -21
  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 +12 -8
  20. sqlspec/builder/mixins/_update_operations.py +37 -14
  21. sqlspec/builder/mixins/_where_clause.py +55 -43
  22. sqlspec/core/cache.py +26 -28
  23. sqlspec/core/compiler.py +58 -37
  24. sqlspec/core/parameters.py +80 -52
  25. sqlspec/core/result.py +30 -17
  26. sqlspec/core/statement.py +31 -21
  27. sqlspec/driver/_async.py +76 -46
  28. sqlspec/driver/_common.py +25 -6
  29. sqlspec/driver/_sync.py +73 -43
  30. sqlspec/driver/mixins/_result_tools.py +51 -22
  31. sqlspec/driver/mixins/_sql_translator.py +61 -11
  32. sqlspec/protocols.py +7 -0
  33. sqlspec/utils/type_guards.py +7 -3
  34. {sqlspec-0.16.0.dist-info → sqlspec-0.16.1.dist-info}/METADATA +1 -1
  35. {sqlspec-0.16.0.dist-info → sqlspec-0.16.1.dist-info}/RECORD +39 -39
  36. {sqlspec-0.16.0.dist-info → sqlspec-0.16.1.dist-info}/WHEEL +0 -0
  37. {sqlspec-0.16.0.dist-info → sqlspec-0.16.1.dist-info}/entry_points.txt +0 -0
  38. {sqlspec-0.16.0.dist-info → sqlspec-0.16.1.dist-info}/licenses/LICENSE +0 -0
  39. {sqlspec-0.16.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,37 +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 i, v in enumerate(values):
82
- if isinstance(v, exp.Expression):
83
- row_exprs.append(v)
84
- else:
85
- # Try to use column name if available, otherwise use position-based name
86
- try:
87
- _columns = self._columns # type: ignore[attr-defined]
88
- if _columns and i < len(_columns):
89
- column_name = str(_columns[i]).split(".")[-1] if "." in str(_columns[i]) else str(_columns[i])
90
- param_name = self._generate_unique_parameter_name(column_name) # type: ignore[attr-defined]
91
- else:
92
- param_name = self._generate_unique_parameter_name(f"value_{i + 1}") # type: ignore[attr-defined]
93
- except AttributeError:
94
- param_name = self._generate_unique_parameter_name(f"value_{i + 1}") # type: ignore[attr-defined]
95
- _, param_name = self.add_parameter(v, name=param_name) # type: ignore[attr-defined]
96
- 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
+
97
178
  values_expr = exp.Values(expressions=[row_exprs])
98
179
  self._expression.set("expression", values_expr)
99
180
  return self
@@ -110,10 +191,21 @@ class InsertValuesMixin:
110
191
  return self.values(*values)
111
192
 
112
193
 
194
+ @trait
113
195
  class InsertFromSelectMixin:
114
196
  """Mixin providing INSERT ... SELECT support for INSERT builders."""
115
197
 
116
- _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)
117
209
 
118
210
  def from_select(self, select_builder: Any) -> Self:
119
211
  """Sets the INSERT source to a SELECT statement.
@@ -128,7 +220,7 @@ class InsertFromSelectMixin:
128
220
  SQLBuilderError: If the table is not set or the select_builder is invalid.
129
221
  """
130
222
  try:
131
- if not self._table: # type: ignore[attr-defined]
223
+ if not self._table:
132
224
  msg = "The target table must be set using .into() before adding values."
133
225
  raise SQLBuilderError(msg)
134
226
  except AttributeError:
@@ -139,11 +231,11 @@ class InsertFromSelectMixin:
139
231
  if not isinstance(self._expression, exp.Insert):
140
232
  msg = "Cannot set INSERT source on a non-INSERT expression."
141
233
  raise SQLBuilderError(msg)
142
- subquery_parameters = select_builder._parameters # pyright: ignore[attr-defined]
234
+ subquery_parameters = select_builder._parameters
143
235
  if subquery_parameters:
144
236
  for p_name, p_value in subquery_parameters.items():
145
- self.add_parameter(p_value, name=p_name) # type: ignore[attr-defined]
146
- select_expr = select_builder._expression # pyright: ignore[attr-defined]
237
+ self.add_parameter(p_value, name=p_name)
238
+ select_expr = select_builder._expression
147
239
  if select_expr and isinstance(select_expr, exp.Select):
148
240
  self._expression.set("expression", select_expr.copy())
149
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:
@@ -175,8 +199,8 @@ class MergeMatchedClauseMixin:
175
199
  column_name = col if isinstance(col, str) else str(col)
176
200
  if "." in column_name:
177
201
  column_name = column_name.split(".")[-1]
178
- param_name = self._generate_unique_parameter_name(column_name) # type: ignore[attr-defined]
179
- param_name = self.add_parameter(val, name=param_name)[1] # type: ignore[attr-defined]
202
+ param_name = self._generate_unique_parameter_name(column_name)
203
+ param_name = self.add_parameter(val, name=param_name)[1]
180
204
  update_expressions.append(exp.EQ(this=exp.column(col), expression=exp.var(param_name)))
181
205
 
182
206
  when_args: dict[str, Any] = {"matched": True, "then": exp.Update(expressions=update_expressions)}
@@ -238,10 +262,28 @@ class MergeMatchedClauseMixin:
238
262
  return self
239
263
 
240
264
 
265
+ @trait
241
266
  class MergeNotMatchedClauseMixin:
242
267
  """Mixin providing WHEN NOT MATCHED THEN ... clauses for MERGE builders."""
243
268
 
244
- _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)
245
287
 
246
288
  def when_not_matched_then_insert(
247
289
  self,
@@ -278,8 +320,8 @@ class MergeNotMatchedClauseMixin:
278
320
  column_name = columns[i] if isinstance(columns[i], str) else str(columns[i])
279
321
  if "." in column_name:
280
322
  column_name = column_name.split(".")[-1]
281
- param_name = self._generate_unique_parameter_name(column_name) # type: ignore[attr-defined]
282
- param_name = self.add_parameter(val, name=param_name)[1] # type: ignore[attr-defined]
323
+ param_name = self._generate_unique_parameter_name(column_name)
324
+ param_name = self.add_parameter(val, name=param_name)[1]
283
325
  parameterized_values.append(exp.var(param_name))
284
326
 
285
327
  insert_args["this"] = exp.Tuple(expressions=[exp.column(c) for c in columns])
@@ -316,14 +358,32 @@ class MergeNotMatchedClauseMixin:
316
358
  when_args["this"] = condition_expr
317
359
 
318
360
  when_clause = exp.When(**when_args)
319
- self._add_when_clause(when_clause) # type: ignore[attr-defined]
361
+ self._add_when_clause(when_clause)
320
362
  return self
321
363
 
322
364
 
365
+ @trait
323
366
  class MergeNotMatchedBySourceClauseMixin:
324
367
  """Mixin providing WHEN NOT MATCHED BY SOURCE THEN ... clauses for MERGE builders."""
325
368
 
326
- _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)
327
387
 
328
388
  def when_not_matched_by_source_then_update(
329
389
  self, set_values: dict[str, Any], condition: Optional[Union[str, exp.Expression]] = None
@@ -347,8 +407,8 @@ class MergeNotMatchedBySourceClauseMixin:
347
407
  column_name = col if isinstance(col, str) else str(col)
348
408
  if "." in column_name:
349
409
  column_name = column_name.split(".")[-1]
350
- param_name = self._generate_unique_parameter_name(column_name) # type: ignore[attr-defined]
351
- param_name = self.add_parameter(val, name=param_name)[1] # type: ignore[attr-defined]
410
+ param_name = self._generate_unique_parameter_name(column_name)
411
+ param_name = self.add_parameter(val, name=param_name)[1]
352
412
  update_expressions.append(exp.EQ(this=exp.column(col), expression=exp.var(param_name)))
353
413
 
354
414
  when_args: dict[str, Any] = {
@@ -375,7 +435,7 @@ class MergeNotMatchedBySourceClauseMixin:
375
435
  when_args["this"] = condition_expr
376
436
 
377
437
  when_clause = exp.When(**when_args)
378
- self._add_when_clause(when_clause) # type: ignore[attr-defined]
438
+ self._add_when_clause(when_clause)
379
439
  return self
380
440
 
381
441
  def when_not_matched_by_source_then_delete(self, condition: Optional[Union[str, exp.Expression]] = None) -> Self:
@@ -412,5 +472,5 @@ class MergeNotMatchedBySourceClauseMixin:
412
472
  when_args["this"] = condition_expr
413
473
 
414
474
  when_clause = exp.When(**when_args)
415
- self._add_when_clause(when_clause) # type: ignore[attr-defined]
475
+ self._add_when_clause(when_clause)
416
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(
@@ -3,6 +3,7 @@
3
3
  from dataclasses import dataclass
4
4
  from typing import TYPE_CHECKING, Any, Optional, Union, cast
5
5
 
6
+ from mypy_extensions import trait
6
7
  from sqlglot import exp
7
8
  from typing_extensions import Self
8
9
 
@@ -11,17 +12,20 @@ from sqlspec.exceptions import SQLBuilderError
11
12
  from sqlspec.utils.type_guards import has_query_builder_parameters, is_expression
12
13
 
13
14
  if TYPE_CHECKING:
14
- from sqlspec.builder._base import QueryBuilder
15
15
  from sqlspec.builder._column import Column, FunctionColumn
16
16
  from sqlspec.protocols import SelectBuilderProtocol, SQLBuilderProtocol
17
17
 
18
18
  __all__ = ("CaseBuilder", "SelectClauseMixin")
19
19
 
20
20
 
21
+ @trait
21
22
  class SelectClauseMixin:
22
23
  """Consolidated mixin providing all SELECT-related clauses and functionality."""
23
24
 
24
- _expression: Optional[exp.Expression] = None
25
+ __slots__ = ()
26
+
27
+ # Type annotation for PyRight - this will be provided by the base class
28
+ _expression: Optional[exp.Expression]
25
29
 
26
30
  def select(self, *columns: Union[str, exp.Expression, "Column", "FunctionColumn"]) -> Self:
27
31
  """Add columns to SELECT clause.
@@ -529,7 +533,7 @@ class SelectClauseMixin:
529
533
  Returns:
530
534
  CaseBuilder: A CaseBuilder instance for building the CASE expression.
531
535
  """
532
- builder = cast("QueryBuilder", self) # pyright: ignore
536
+ builder = cast("SelectBuilderProtocol", self)
533
537
  return CaseBuilder(builder, alias)
534
538
 
535
539
 
@@ -537,15 +541,15 @@ class SelectClauseMixin:
537
541
  class CaseBuilder:
538
542
  """Builder for CASE expressions."""
539
543
 
540
- _parent: "QueryBuilder" # pyright: ignore
544
+ _parent: "SelectBuilderProtocol"
541
545
  _alias: Optional[str]
542
546
  _case_expr: exp.Case
543
547
 
544
- def __init__(self, parent: "QueryBuilder", alias: "Optional[str]" = None) -> None:
548
+ def __init__(self, parent: "SelectBuilderProtocol", alias: "Optional[str]" = None) -> None:
545
549
  """Initialize CaseBuilder.
546
550
 
547
551
  Args:
548
- parent: The parent builder.
552
+ parent: The parent builder with select capabilities.
549
553
  alias: Optional alias for the CASE expression.
550
554
  """
551
555
  self._parent = parent
@@ -589,11 +593,11 @@ class CaseBuilder:
589
593
  self._case_expr.set("default", value_expr)
590
594
  return self
591
595
 
592
- def end(self) -> "QueryBuilder":
596
+ def end(self) -> "SelectBuilderProtocol":
593
597
  """Finalize the CASE expression and add it to the SELECT clause.
594
598
 
595
599
  Returns:
596
600
  The parent builder instance.
597
601
  """
598
602
  select_expr = exp.alias_(self._case_expr, self._alias) if self._alias else self._case_expr
599
- return cast("QueryBuilder", self._parent.select(select_expr)) # type: ignore[attr-defined]
603
+ return self._parent.select(select_expr)