sqlspec 0.16.1__py3-none-any.whl → 0.17.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.
- sqlspec/__init__.py +11 -1
- sqlspec/_sql.py +18 -412
- sqlspec/adapters/aiosqlite/__init__.py +11 -1
- sqlspec/adapters/aiosqlite/config.py +137 -165
- sqlspec/adapters/aiosqlite/driver.py +21 -10
- sqlspec/adapters/aiosqlite/pool.py +492 -0
- sqlspec/adapters/duckdb/__init__.py +2 -0
- sqlspec/adapters/duckdb/config.py +11 -235
- sqlspec/adapters/duckdb/pool.py +243 -0
- sqlspec/adapters/sqlite/__init__.py +2 -0
- sqlspec/adapters/sqlite/config.py +4 -115
- sqlspec/adapters/sqlite/pool.py +140 -0
- sqlspec/base.py +147 -26
- sqlspec/builder/__init__.py +6 -0
- sqlspec/builder/_insert.py +177 -12
- sqlspec/builder/_parsing_utils.py +53 -2
- sqlspec/builder/mixins/_join_operations.py +148 -7
- sqlspec/builder/mixins/_merge_operations.py +102 -16
- sqlspec/builder/mixins/_select_operations.py +311 -6
- sqlspec/builder/mixins/_update_operations.py +49 -34
- sqlspec/builder/mixins/_where_clause.py +85 -13
- sqlspec/core/compiler.py +7 -5
- sqlspec/driver/_common.py +9 -1
- sqlspec/loader.py +27 -54
- sqlspec/storage/registry.py +2 -2
- sqlspec/typing.py +53 -99
- {sqlspec-0.16.1.dist-info → sqlspec-0.17.0.dist-info}/METADATA +1 -1
- {sqlspec-0.16.1.dist-info → sqlspec-0.17.0.dist-info}/RECORD +32 -29
- {sqlspec-0.16.1.dist-info → sqlspec-0.17.0.dist-info}/WHEEL +0 -0
- {sqlspec-0.16.1.dist-info → sqlspec-0.17.0.dist-info}/entry_points.txt +0 -0
- {sqlspec-0.16.1.dist-info → sqlspec-0.17.0.dist-info}/licenses/LICENSE +0 -0
- {sqlspec-0.16.1.dist-info → sqlspec-0.17.0.dist-info}/licenses/NOTICE +0 -0
|
@@ -9,10 +9,13 @@ from typing import Any, Final, Optional, Union, cast
|
|
|
9
9
|
|
|
10
10
|
from sqlglot import exp, maybe_parse, parse_one
|
|
11
11
|
|
|
12
|
+
from sqlspec.core.parameters import ParameterStyle
|
|
12
13
|
from sqlspec.utils.type_guards import has_expression_attr, has_parameter_builder
|
|
13
14
|
|
|
14
15
|
|
|
15
|
-
def parse_column_expression(
|
|
16
|
+
def parse_column_expression(
|
|
17
|
+
column_input: Union[str, exp.Expression, Any], builder: Optional[Any] = None
|
|
18
|
+
) -> exp.Expression:
|
|
16
19
|
"""Parse a column input that might be a complex expression.
|
|
17
20
|
|
|
18
21
|
Handles cases like:
|
|
@@ -22,9 +25,11 @@ def parse_column_expression(column_input: Union[str, exp.Expression, Any]) -> ex
|
|
|
22
25
|
- Function calls: "MAX(price)" -> Max(this=Column(price))
|
|
23
26
|
- Complex expressions: "CASE WHEN ... END" -> Case(...)
|
|
24
27
|
- Custom Column objects from our builder
|
|
28
|
+
- SQL objects with raw SQL expressions
|
|
25
29
|
|
|
26
30
|
Args:
|
|
27
|
-
column_input: String, SQLGlot expression, or Column object
|
|
31
|
+
column_input: String, SQLGlot expression, SQL object, or Column object
|
|
32
|
+
builder: Optional builder instance for parameter merging
|
|
28
33
|
|
|
29
34
|
Returns:
|
|
30
35
|
exp.Expression: Parsed SQLGlot expression
|
|
@@ -32,6 +37,26 @@ def parse_column_expression(column_input: Union[str, exp.Expression, Any]) -> ex
|
|
|
32
37
|
if isinstance(column_input, exp.Expression):
|
|
33
38
|
return column_input
|
|
34
39
|
|
|
40
|
+
# Handle SQL objects (from sql.raw with parameters)
|
|
41
|
+
if hasattr(column_input, "expression") and hasattr(column_input, "sql"):
|
|
42
|
+
# This is likely a SQL object
|
|
43
|
+
expression = getattr(column_input, "expression", None)
|
|
44
|
+
if expression is not None and isinstance(expression, exp.Expression):
|
|
45
|
+
# Merge parameters from SQL object into builder if available
|
|
46
|
+
if builder and hasattr(column_input, "parameters") and hasattr(builder, "add_parameter"):
|
|
47
|
+
sql_parameters = getattr(column_input, "parameters", {})
|
|
48
|
+
for param_name, param_value in sql_parameters.items():
|
|
49
|
+
builder.add_parameter(param_value, name=param_name)
|
|
50
|
+
return cast("exp.Expression", expression)
|
|
51
|
+
# If expression is None, fall back to parsing the raw SQL
|
|
52
|
+
sql_text = getattr(column_input, "sql", "")
|
|
53
|
+
# Merge parameters even when parsing raw SQL
|
|
54
|
+
if builder and hasattr(column_input, "parameters") and hasattr(builder, "add_parameter"):
|
|
55
|
+
sql_parameters = getattr(column_input, "parameters", {})
|
|
56
|
+
for param_name, param_value in sql_parameters.items():
|
|
57
|
+
builder.add_parameter(param_value, name=param_name)
|
|
58
|
+
return exp.maybe_parse(sql_text) or exp.column(str(sql_text))
|
|
59
|
+
|
|
35
60
|
if has_expression_attr(column_input):
|
|
36
61
|
try:
|
|
37
62
|
attr_value = column_input._expression
|
|
@@ -127,6 +152,32 @@ def parse_condition_expression(
|
|
|
127
152
|
if not isinstance(condition_input, str):
|
|
128
153
|
condition_input = str(condition_input)
|
|
129
154
|
|
|
155
|
+
# Convert database-specific parameter styles to SQLGlot-compatible format
|
|
156
|
+
# This ensures that placeholders like $1, %s, :1 are properly recognized as parameters
|
|
157
|
+
from sqlspec.core.parameters import ParameterValidator
|
|
158
|
+
|
|
159
|
+
validator = ParameterValidator()
|
|
160
|
+
param_info = validator.extract_parameters(condition_input)
|
|
161
|
+
|
|
162
|
+
# If we found parameters, convert incompatible ones to SQLGlot-compatible format
|
|
163
|
+
if param_info:
|
|
164
|
+
# Convert problematic parameter styles to :param_N format for SQLGlot
|
|
165
|
+
converted_condition = condition_input
|
|
166
|
+
for param in reversed(param_info): # Reverse to preserve positions
|
|
167
|
+
if param.style in {
|
|
168
|
+
ParameterStyle.NUMERIC,
|
|
169
|
+
ParameterStyle.POSITIONAL_PYFORMAT,
|
|
170
|
+
ParameterStyle.POSITIONAL_COLON,
|
|
171
|
+
}:
|
|
172
|
+
# Convert $1, %s, :1 to :param_0, :param_1, etc.
|
|
173
|
+
placeholder = f":param_{param.ordinal}"
|
|
174
|
+
converted_condition = (
|
|
175
|
+
converted_condition[: param.position]
|
|
176
|
+
+ placeholder
|
|
177
|
+
+ converted_condition[param.position + len(param.placeholder_text) :]
|
|
178
|
+
)
|
|
179
|
+
condition_input = converted_condition
|
|
180
|
+
|
|
130
181
|
try:
|
|
131
182
|
return exp.condition(condition_input)
|
|
132
183
|
except Exception:
|
|
@@ -9,9 +9,11 @@ from sqlspec.exceptions import SQLBuilderError
|
|
|
9
9
|
from sqlspec.utils.type_guards import has_query_builder_parameters
|
|
10
10
|
|
|
11
11
|
if TYPE_CHECKING:
|
|
12
|
+
from sqlspec.builder._column import ColumnExpression
|
|
13
|
+
from sqlspec.core.statement import SQL
|
|
12
14
|
from sqlspec.protocols import SQLBuilderProtocol
|
|
13
15
|
|
|
14
|
-
__all__ = ("JoinClauseMixin"
|
|
16
|
+
__all__ = ("JoinBuilder", "JoinClauseMixin")
|
|
15
17
|
|
|
16
18
|
|
|
17
19
|
@trait
|
|
@@ -26,7 +28,7 @@ class JoinClauseMixin:
|
|
|
26
28
|
def join(
|
|
27
29
|
self,
|
|
28
30
|
table: Union[str, exp.Expression, Any],
|
|
29
|
-
on: Optional[Union[str, exp.Expression]] = None,
|
|
31
|
+
on: Optional[Union[str, exp.Expression, "SQL"]] = None,
|
|
30
32
|
alias: Optional[str] = None,
|
|
31
33
|
join_type: str = "INNER",
|
|
32
34
|
) -> Self:
|
|
@@ -56,7 +58,33 @@ class JoinClauseMixin:
|
|
|
56
58
|
table_expr = table
|
|
57
59
|
on_expr: Optional[exp.Expression] = None
|
|
58
60
|
if on is not None:
|
|
59
|
-
|
|
61
|
+
if isinstance(on, str):
|
|
62
|
+
on_expr = exp.condition(on)
|
|
63
|
+
elif hasattr(on, "expression") and hasattr(on, "sql"):
|
|
64
|
+
# Handle SQL objects (from sql.raw with parameters)
|
|
65
|
+
expression = getattr(on, "expression", None)
|
|
66
|
+
if expression is not None and isinstance(expression, exp.Expression):
|
|
67
|
+
# Merge parameters from SQL object into builder
|
|
68
|
+
if hasattr(on, "parameters") and hasattr(builder, "add_parameter"):
|
|
69
|
+
sql_parameters = getattr(on, "parameters", {})
|
|
70
|
+
for param_name, param_value in sql_parameters.items():
|
|
71
|
+
builder.add_parameter(param_value, name=param_name)
|
|
72
|
+
on_expr = expression
|
|
73
|
+
else:
|
|
74
|
+
# If expression is None, fall back to parsing the raw SQL
|
|
75
|
+
sql_text = getattr(on, "sql", "")
|
|
76
|
+
# Merge parameters even when parsing raw SQL
|
|
77
|
+
if hasattr(on, "parameters") and hasattr(builder, "add_parameter"):
|
|
78
|
+
sql_parameters = getattr(on, "parameters", {})
|
|
79
|
+
for param_name, param_value in sql_parameters.items():
|
|
80
|
+
builder.add_parameter(param_value, name=param_name)
|
|
81
|
+
on_expr = exp.maybe_parse(sql_text) or exp.condition(str(sql_text))
|
|
82
|
+
# For other types (should be exp.Expression)
|
|
83
|
+
elif isinstance(on, exp.Expression):
|
|
84
|
+
on_expr = on
|
|
85
|
+
else:
|
|
86
|
+
# Last resort - convert to string and parse
|
|
87
|
+
on_expr = exp.condition(str(on))
|
|
60
88
|
join_type_upper = join_type.upper()
|
|
61
89
|
if join_type_upper == "INNER":
|
|
62
90
|
join_expr = exp.Join(this=table_expr, on=on_expr)
|
|
@@ -73,22 +101,22 @@ class JoinClauseMixin:
|
|
|
73
101
|
return cast("Self", builder)
|
|
74
102
|
|
|
75
103
|
def inner_join(
|
|
76
|
-
self, table: Union[str, exp.Expression, Any], on: Union[str, exp.Expression], alias: Optional[str] = None
|
|
104
|
+
self, table: Union[str, exp.Expression, Any], on: Union[str, exp.Expression, "SQL"], alias: Optional[str] = None
|
|
77
105
|
) -> Self:
|
|
78
106
|
return self.join(table, on, alias, "INNER")
|
|
79
107
|
|
|
80
108
|
def left_join(
|
|
81
|
-
self, table: Union[str, exp.Expression, Any], on: Union[str, exp.Expression], alias: Optional[str] = None
|
|
109
|
+
self, table: Union[str, exp.Expression, Any], on: Union[str, exp.Expression, "SQL"], alias: Optional[str] = None
|
|
82
110
|
) -> Self:
|
|
83
111
|
return self.join(table, on, alias, "LEFT")
|
|
84
112
|
|
|
85
113
|
def right_join(
|
|
86
|
-
self, table: Union[str, exp.Expression, Any], on: Union[str, exp.Expression], alias: Optional[str] = None
|
|
114
|
+
self, table: Union[str, exp.Expression, Any], on: Union[str, exp.Expression, "SQL"], alias: Optional[str] = None
|
|
87
115
|
) -> Self:
|
|
88
116
|
return self.join(table, on, alias, "RIGHT")
|
|
89
117
|
|
|
90
118
|
def full_join(
|
|
91
|
-
self, table: Union[str, exp.Expression, Any], on: Union[str, exp.Expression], alias: Optional[str] = None
|
|
119
|
+
self, table: Union[str, exp.Expression, Any], on: Union[str, exp.Expression, "SQL"], alias: Optional[str] = None
|
|
92
120
|
) -> Self:
|
|
93
121
|
return self.join(table, on, alias, "FULL")
|
|
94
122
|
|
|
@@ -120,3 +148,116 @@ class JoinClauseMixin:
|
|
|
120
148
|
join_expr = exp.Join(this=table_expr, kind="CROSS")
|
|
121
149
|
builder._expression = builder._expression.join(join_expr, copy=False)
|
|
122
150
|
return cast("Self", builder)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@trait
|
|
154
|
+
class JoinBuilder:
|
|
155
|
+
"""Builder for JOIN operations with fluent syntax.
|
|
156
|
+
|
|
157
|
+
Example:
|
|
158
|
+
```python
|
|
159
|
+
from sqlspec import sql
|
|
160
|
+
|
|
161
|
+
# sql.left_join_("posts").on("users.id = posts.user_id")
|
|
162
|
+
join_clause = sql.left_join_("posts").on(
|
|
163
|
+
"users.id = posts.user_id"
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
# Or with query builder
|
|
167
|
+
query = (
|
|
168
|
+
sql.select("users.name", "posts.title")
|
|
169
|
+
.from_("users")
|
|
170
|
+
.join(
|
|
171
|
+
sql.left_join_("posts").on(
|
|
172
|
+
"users.id = posts.user_id"
|
|
173
|
+
)
|
|
174
|
+
)
|
|
175
|
+
)
|
|
176
|
+
```
|
|
177
|
+
"""
|
|
178
|
+
|
|
179
|
+
def __init__(self, join_type: str) -> None:
|
|
180
|
+
"""Initialize the join builder.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
join_type: Type of join (inner, left, right, full, cross)
|
|
184
|
+
"""
|
|
185
|
+
self._join_type = join_type.upper()
|
|
186
|
+
self._table: Optional[Union[str, exp.Expression]] = None
|
|
187
|
+
self._condition: Optional[exp.Expression] = None
|
|
188
|
+
self._alias: Optional[str] = None
|
|
189
|
+
|
|
190
|
+
def __eq__(self, other: object) -> "ColumnExpression": # type: ignore[override]
|
|
191
|
+
"""Equal to (==) - not typically used but needed for type consistency."""
|
|
192
|
+
from sqlspec.builder._column import ColumnExpression
|
|
193
|
+
|
|
194
|
+
# JoinBuilder doesn't have a direct expression, so this is a placeholder
|
|
195
|
+
# In practice, this shouldn't be called as joins are used differently
|
|
196
|
+
placeholder_expr = exp.Literal.string(f"join_{self._join_type.lower()}")
|
|
197
|
+
if other is None:
|
|
198
|
+
return ColumnExpression(exp.Is(this=placeholder_expr, expression=exp.Null()))
|
|
199
|
+
return ColumnExpression(exp.EQ(this=placeholder_expr, expression=exp.convert(other)))
|
|
200
|
+
|
|
201
|
+
def __hash__(self) -> int:
|
|
202
|
+
"""Make JoinBuilder hashable."""
|
|
203
|
+
return hash(id(self))
|
|
204
|
+
|
|
205
|
+
def __call__(self, table: Union[str, exp.Expression], alias: Optional[str] = None) -> Self:
|
|
206
|
+
"""Set the table to join.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
table: Table name or expression to join
|
|
210
|
+
alias: Optional alias for the table
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
Self for method chaining
|
|
214
|
+
"""
|
|
215
|
+
self._table = table
|
|
216
|
+
self._alias = alias
|
|
217
|
+
return self
|
|
218
|
+
|
|
219
|
+
def on(self, condition: Union[str, exp.Expression]) -> exp.Expression:
|
|
220
|
+
"""Set the join condition and build the JOIN expression.
|
|
221
|
+
|
|
222
|
+
Args:
|
|
223
|
+
condition: JOIN condition (e.g., "users.id = posts.user_id")
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
Complete JOIN expression
|
|
227
|
+
"""
|
|
228
|
+
if not self._table:
|
|
229
|
+
msg = "Table must be set before calling .on()"
|
|
230
|
+
raise SQLBuilderError(msg)
|
|
231
|
+
|
|
232
|
+
# Parse the condition
|
|
233
|
+
condition_expr: exp.Expression
|
|
234
|
+
if isinstance(condition, str):
|
|
235
|
+
parsed: Optional[exp.Expression] = exp.maybe_parse(condition)
|
|
236
|
+
condition_expr = parsed or exp.condition(condition)
|
|
237
|
+
else:
|
|
238
|
+
condition_expr = condition
|
|
239
|
+
|
|
240
|
+
# Build table expression
|
|
241
|
+
table_expr: exp.Expression
|
|
242
|
+
if isinstance(self._table, str):
|
|
243
|
+
table_expr = exp.to_table(self._table)
|
|
244
|
+
if self._alias:
|
|
245
|
+
table_expr = exp.alias_(table_expr, self._alias)
|
|
246
|
+
else:
|
|
247
|
+
table_expr = self._table
|
|
248
|
+
if self._alias:
|
|
249
|
+
table_expr = exp.alias_(table_expr, self._alias)
|
|
250
|
+
|
|
251
|
+
# Create the appropriate join type using same pattern as existing JoinClauseMixin
|
|
252
|
+
if self._join_type == "INNER JOIN":
|
|
253
|
+
return exp.Join(this=table_expr, on=condition_expr)
|
|
254
|
+
if self._join_type == "LEFT JOIN":
|
|
255
|
+
return exp.Join(this=table_expr, on=condition_expr, side="LEFT")
|
|
256
|
+
if self._join_type == "RIGHT JOIN":
|
|
257
|
+
return exp.Join(this=table_expr, on=condition_expr, side="RIGHT")
|
|
258
|
+
if self._join_type == "FULL JOIN":
|
|
259
|
+
return exp.Join(this=table_expr, on=condition_expr, side="FULL", kind="OUTER")
|
|
260
|
+
if self._join_type == "CROSS JOIN":
|
|
261
|
+
# CROSS JOIN doesn't use ON condition
|
|
262
|
+
return exp.Join(this=table_expr, kind="CROSS")
|
|
263
|
+
return exp.Join(this=table_expr, on=condition_expr)
|
|
@@ -179,14 +179,23 @@ class MergeMatchedClauseMixin:
|
|
|
179
179
|
whens.append("expressions", when_clause)
|
|
180
180
|
|
|
181
181
|
def when_matched_then_update(
|
|
182
|
-
self,
|
|
182
|
+
self,
|
|
183
|
+
set_values: Optional[dict[str, Any]] = None,
|
|
184
|
+
condition: Optional[Union[str, exp.Expression]] = None,
|
|
185
|
+
**kwargs: Any,
|
|
183
186
|
) -> Self:
|
|
184
187
|
"""Define the UPDATE action for matched rows.
|
|
185
188
|
|
|
189
|
+
Supports:
|
|
190
|
+
- when_matched_then_update({"column": value})
|
|
191
|
+
- when_matched_then_update(column=value, other_column=other_value)
|
|
192
|
+
- when_matched_then_update({"column": value}, other_column=other_value)
|
|
193
|
+
|
|
186
194
|
Args:
|
|
187
195
|
set_values: A dictionary of column names and their new values to set.
|
|
188
196
|
The values will be parameterized.
|
|
189
197
|
condition: An optional additional condition for this specific action.
|
|
198
|
+
**kwargs: Column-value pairs to update on match.
|
|
190
199
|
|
|
191
200
|
Raises:
|
|
192
201
|
SQLBuilderError: If the condition type is unsupported.
|
|
@@ -194,14 +203,48 @@ class MergeMatchedClauseMixin:
|
|
|
194
203
|
Returns:
|
|
195
204
|
The current builder instance for method chaining.
|
|
196
205
|
"""
|
|
206
|
+
# Combine set_values dict and kwargs
|
|
207
|
+
all_values = dict(set_values or {}, **kwargs)
|
|
208
|
+
|
|
209
|
+
if not all_values:
|
|
210
|
+
msg = "No update values provided. Use set_values dict or kwargs."
|
|
211
|
+
raise SQLBuilderError(msg)
|
|
212
|
+
|
|
197
213
|
update_expressions: list[exp.EQ] = []
|
|
198
|
-
for col, val in
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
214
|
+
for col, val in all_values.items():
|
|
215
|
+
if hasattr(val, "expression") and hasattr(val, "sql"):
|
|
216
|
+
# Handle SQL objects (from sql.raw with parameters)
|
|
217
|
+
expression = getattr(val, "expression", None)
|
|
218
|
+
if expression is not None and isinstance(expression, exp.Expression):
|
|
219
|
+
# Merge parameters from SQL object into builder
|
|
220
|
+
if hasattr(val, "parameters"):
|
|
221
|
+
sql_parameters = getattr(val, "parameters", {})
|
|
222
|
+
for param_name, param_value in sql_parameters.items():
|
|
223
|
+
self.add_parameter(param_value, name=param_name)
|
|
224
|
+
value_expr = expression
|
|
225
|
+
else:
|
|
226
|
+
# If expression is None, fall back to parsing the raw SQL
|
|
227
|
+
sql_text = getattr(val, "sql", "")
|
|
228
|
+
# Merge parameters even when parsing raw SQL
|
|
229
|
+
if hasattr(val, "parameters"):
|
|
230
|
+
sql_parameters = getattr(val, "parameters", {})
|
|
231
|
+
for param_name, param_value in sql_parameters.items():
|
|
232
|
+
self.add_parameter(param_value, name=param_name)
|
|
233
|
+
# Check if sql_text is callable (like Expression.sql method)
|
|
234
|
+
if callable(sql_text):
|
|
235
|
+
sql_text = str(val)
|
|
236
|
+
value_expr = exp.maybe_parse(sql_text) or exp.convert(str(sql_text))
|
|
237
|
+
elif isinstance(val, exp.Expression):
|
|
238
|
+
value_expr = val
|
|
239
|
+
else:
|
|
240
|
+
column_name = col if isinstance(col, str) else str(col)
|
|
241
|
+
if "." in column_name:
|
|
242
|
+
column_name = column_name.split(".")[-1]
|
|
243
|
+
param_name = self._generate_unique_parameter_name(column_name)
|
|
244
|
+
param_name = self.add_parameter(val, name=param_name)[1]
|
|
245
|
+
value_expr = exp.Placeholder(this=param_name)
|
|
246
|
+
|
|
247
|
+
update_expressions.append(exp.EQ(this=exp.column(col), expression=value_expr))
|
|
205
248
|
|
|
206
249
|
when_args: dict[str, Any] = {"matched": True, "then": exp.Update(expressions=update_expressions)}
|
|
207
250
|
|
|
@@ -386,15 +429,24 @@ class MergeNotMatchedBySourceClauseMixin:
|
|
|
386
429
|
raise NotImplementedError(msg)
|
|
387
430
|
|
|
388
431
|
def when_not_matched_by_source_then_update(
|
|
389
|
-
self,
|
|
432
|
+
self,
|
|
433
|
+
set_values: Optional[dict[str, Any]] = None,
|
|
434
|
+
condition: Optional[Union[str, exp.Expression]] = None,
|
|
435
|
+
**kwargs: Any,
|
|
390
436
|
) -> Self:
|
|
391
437
|
"""Define the UPDATE action for rows not matched by source.
|
|
392
438
|
|
|
393
439
|
This is useful for handling rows that exist in the target but not in the source.
|
|
394
440
|
|
|
441
|
+
Supports:
|
|
442
|
+
- when_not_matched_by_source_then_update({"column": value})
|
|
443
|
+
- when_not_matched_by_source_then_update(column=value, other_column=other_value)
|
|
444
|
+
- when_not_matched_by_source_then_update({"column": value}, other_column=other_value)
|
|
445
|
+
|
|
395
446
|
Args:
|
|
396
447
|
set_values: A dictionary of column names and their new values to set.
|
|
397
448
|
condition: An optional additional condition for this specific action.
|
|
449
|
+
**kwargs: Column-value pairs to update when not matched by source.
|
|
398
450
|
|
|
399
451
|
Raises:
|
|
400
452
|
SQLBuilderError: If the condition type is unsupported.
|
|
@@ -402,14 +454,48 @@ class MergeNotMatchedBySourceClauseMixin:
|
|
|
402
454
|
Returns:
|
|
403
455
|
The current builder instance for method chaining.
|
|
404
456
|
"""
|
|
457
|
+
# Combine set_values dict and kwargs
|
|
458
|
+
all_values = dict(set_values or {}, **kwargs)
|
|
459
|
+
|
|
460
|
+
if not all_values:
|
|
461
|
+
msg = "No update values provided. Use set_values dict or kwargs."
|
|
462
|
+
raise SQLBuilderError(msg)
|
|
463
|
+
|
|
405
464
|
update_expressions: list[exp.EQ] = []
|
|
406
|
-
for col, val in
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
465
|
+
for col, val in all_values.items():
|
|
466
|
+
if hasattr(val, "expression") and hasattr(val, "sql"):
|
|
467
|
+
# Handle SQL objects (from sql.raw with parameters)
|
|
468
|
+
expression = getattr(val, "expression", None)
|
|
469
|
+
if expression is not None and isinstance(expression, exp.Expression):
|
|
470
|
+
# Merge parameters from SQL object into builder
|
|
471
|
+
if hasattr(val, "parameters"):
|
|
472
|
+
sql_parameters = getattr(val, "parameters", {})
|
|
473
|
+
for param_name, param_value in sql_parameters.items():
|
|
474
|
+
self.add_parameter(param_value, name=param_name)
|
|
475
|
+
value_expr = expression
|
|
476
|
+
else:
|
|
477
|
+
# If expression is None, fall back to parsing the raw SQL
|
|
478
|
+
sql_text = getattr(val, "sql", "")
|
|
479
|
+
# Merge parameters even when parsing raw SQL
|
|
480
|
+
if hasattr(val, "parameters"):
|
|
481
|
+
sql_parameters = getattr(val, "parameters", {})
|
|
482
|
+
for param_name, param_value in sql_parameters.items():
|
|
483
|
+
self.add_parameter(param_value, name=param_name)
|
|
484
|
+
# Check if sql_text is callable (like Expression.sql method)
|
|
485
|
+
if callable(sql_text):
|
|
486
|
+
sql_text = str(val)
|
|
487
|
+
value_expr = exp.maybe_parse(sql_text) or exp.convert(str(sql_text))
|
|
488
|
+
elif isinstance(val, exp.Expression):
|
|
489
|
+
value_expr = val
|
|
490
|
+
else:
|
|
491
|
+
column_name = col if isinstance(col, str) else str(col)
|
|
492
|
+
if "." in column_name:
|
|
493
|
+
column_name = column_name.split(".")[-1]
|
|
494
|
+
param_name = self._generate_unique_parameter_name(column_name)
|
|
495
|
+
param_name = self.add_parameter(val, name=param_name)[1]
|
|
496
|
+
value_expr = exp.Placeholder(this=param_name)
|
|
497
|
+
|
|
498
|
+
update_expressions.append(exp.EQ(this=exp.column(col), expression=value_expr))
|
|
413
499
|
|
|
414
500
|
when_args: dict[str, Any] = {
|
|
415
501
|
"matched": False,
|