sqlspec 0.16.0__cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.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.
- 51ff5a9eadfdefd49f98__mypyc.cpython-310-x86_64-linux-gnu.so +0 -0
- sqlspec/__init__.py +92 -0
- sqlspec/__main__.py +12 -0
- sqlspec/__metadata__.py +14 -0
- sqlspec/_serialization.py +77 -0
- sqlspec/_sql.py +1347 -0
- sqlspec/_typing.py +680 -0
- sqlspec/adapters/__init__.py +0 -0
- sqlspec/adapters/adbc/__init__.py +5 -0
- sqlspec/adapters/adbc/_types.py +12 -0
- sqlspec/adapters/adbc/config.py +361 -0
- sqlspec/adapters/adbc/driver.py +512 -0
- sqlspec/adapters/aiosqlite/__init__.py +19 -0
- sqlspec/adapters/aiosqlite/_types.py +13 -0
- sqlspec/adapters/aiosqlite/config.py +253 -0
- sqlspec/adapters/aiosqlite/driver.py +248 -0
- sqlspec/adapters/asyncmy/__init__.py +19 -0
- sqlspec/adapters/asyncmy/_types.py +12 -0
- sqlspec/adapters/asyncmy/config.py +180 -0
- sqlspec/adapters/asyncmy/driver.py +274 -0
- sqlspec/adapters/asyncpg/__init__.py +21 -0
- sqlspec/adapters/asyncpg/_types.py +17 -0
- sqlspec/adapters/asyncpg/config.py +229 -0
- sqlspec/adapters/asyncpg/driver.py +344 -0
- sqlspec/adapters/bigquery/__init__.py +18 -0
- sqlspec/adapters/bigquery/_types.py +12 -0
- sqlspec/adapters/bigquery/config.py +298 -0
- sqlspec/adapters/bigquery/driver.py +558 -0
- sqlspec/adapters/duckdb/__init__.py +22 -0
- sqlspec/adapters/duckdb/_types.py +12 -0
- sqlspec/adapters/duckdb/config.py +504 -0
- sqlspec/adapters/duckdb/driver.py +368 -0
- sqlspec/adapters/oracledb/__init__.py +32 -0
- sqlspec/adapters/oracledb/_types.py +14 -0
- sqlspec/adapters/oracledb/config.py +317 -0
- sqlspec/adapters/oracledb/driver.py +538 -0
- sqlspec/adapters/psqlpy/__init__.py +16 -0
- sqlspec/adapters/psqlpy/_types.py +11 -0
- sqlspec/adapters/psqlpy/config.py +214 -0
- sqlspec/adapters/psqlpy/driver.py +530 -0
- sqlspec/adapters/psycopg/__init__.py +32 -0
- sqlspec/adapters/psycopg/_types.py +17 -0
- sqlspec/adapters/psycopg/config.py +426 -0
- sqlspec/adapters/psycopg/driver.py +796 -0
- sqlspec/adapters/sqlite/__init__.py +15 -0
- sqlspec/adapters/sqlite/_types.py +11 -0
- sqlspec/adapters/sqlite/config.py +240 -0
- sqlspec/adapters/sqlite/driver.py +294 -0
- sqlspec/base.py +571 -0
- sqlspec/builder/__init__.py +62 -0
- sqlspec/builder/_base.py +440 -0
- sqlspec/builder/_column.py +324 -0
- sqlspec/builder/_ddl.py +1383 -0
- sqlspec/builder/_ddl_utils.py +104 -0
- sqlspec/builder/_delete.py +77 -0
- sqlspec/builder/_insert.py +241 -0
- sqlspec/builder/_merge.py +56 -0
- sqlspec/builder/_parsing_utils.py +140 -0
- sqlspec/builder/_select.py +174 -0
- sqlspec/builder/_update.py +186 -0
- sqlspec/builder/mixins/__init__.py +55 -0
- sqlspec/builder/mixins/_cte_and_set_ops.py +195 -0
- sqlspec/builder/mixins/_delete_operations.py +36 -0
- sqlspec/builder/mixins/_insert_operations.py +152 -0
- sqlspec/builder/mixins/_join_operations.py +115 -0
- sqlspec/builder/mixins/_merge_operations.py +416 -0
- sqlspec/builder/mixins/_order_limit_operations.py +123 -0
- sqlspec/builder/mixins/_pivot_operations.py +144 -0
- sqlspec/builder/mixins/_select_operations.py +599 -0
- sqlspec/builder/mixins/_update_operations.py +164 -0
- sqlspec/builder/mixins/_where_clause.py +609 -0
- sqlspec/cli.py +247 -0
- sqlspec/config.py +395 -0
- sqlspec/core/__init__.py +63 -0
- sqlspec/core/cache.cpython-310-x86_64-linux-gnu.so +0 -0
- sqlspec/core/cache.py +873 -0
- sqlspec/core/compiler.cpython-310-x86_64-linux-gnu.so +0 -0
- sqlspec/core/compiler.py +396 -0
- sqlspec/core/filters.cpython-310-x86_64-linux-gnu.so +0 -0
- sqlspec/core/filters.py +830 -0
- sqlspec/core/hashing.cpython-310-x86_64-linux-gnu.so +0 -0
- sqlspec/core/hashing.py +310 -0
- sqlspec/core/parameters.cpython-310-x86_64-linux-gnu.so +0 -0
- sqlspec/core/parameters.py +1209 -0
- sqlspec/core/result.cpython-310-x86_64-linux-gnu.so +0 -0
- sqlspec/core/result.py +664 -0
- sqlspec/core/splitter.cpython-310-x86_64-linux-gnu.so +0 -0
- sqlspec/core/splitter.py +819 -0
- sqlspec/core/statement.cpython-310-x86_64-linux-gnu.so +0 -0
- sqlspec/core/statement.py +666 -0
- sqlspec/driver/__init__.py +19 -0
- sqlspec/driver/_async.py +472 -0
- sqlspec/driver/_common.py +612 -0
- sqlspec/driver/_sync.py +473 -0
- sqlspec/driver/mixins/__init__.py +6 -0
- sqlspec/driver/mixins/_result_tools.py +164 -0
- sqlspec/driver/mixins/_sql_translator.py +36 -0
- sqlspec/exceptions.py +193 -0
- sqlspec/extensions/__init__.py +0 -0
- sqlspec/extensions/aiosql/__init__.py +10 -0
- sqlspec/extensions/aiosql/adapter.py +461 -0
- sqlspec/extensions/litestar/__init__.py +6 -0
- sqlspec/extensions/litestar/_utils.py +52 -0
- sqlspec/extensions/litestar/cli.py +48 -0
- sqlspec/extensions/litestar/config.py +92 -0
- sqlspec/extensions/litestar/handlers.py +260 -0
- sqlspec/extensions/litestar/plugin.py +145 -0
- sqlspec/extensions/litestar/providers.py +454 -0
- sqlspec/loader.cpython-310-x86_64-linux-gnu.so +0 -0
- sqlspec/loader.py +760 -0
- sqlspec/migrations/__init__.py +35 -0
- sqlspec/migrations/base.py +414 -0
- sqlspec/migrations/commands.py +443 -0
- sqlspec/migrations/loaders.py +402 -0
- sqlspec/migrations/runner.py +213 -0
- sqlspec/migrations/tracker.py +140 -0
- sqlspec/migrations/utils.py +129 -0
- sqlspec/protocols.py +400 -0
- sqlspec/py.typed +0 -0
- sqlspec/storage/__init__.py +23 -0
- sqlspec/storage/backends/__init__.py +0 -0
- sqlspec/storage/backends/base.py +163 -0
- sqlspec/storage/backends/fsspec.py +386 -0
- sqlspec/storage/backends/obstore.py +459 -0
- sqlspec/storage/capabilities.py +102 -0
- sqlspec/storage/registry.py +239 -0
- sqlspec/typing.py +299 -0
- sqlspec/utils/__init__.py +3 -0
- sqlspec/utils/correlation.py +150 -0
- sqlspec/utils/deprecation.py +106 -0
- sqlspec/utils/fixtures.cpython-310-x86_64-linux-gnu.so +0 -0
- sqlspec/utils/fixtures.py +58 -0
- sqlspec/utils/logging.py +127 -0
- sqlspec/utils/module_loader.py +89 -0
- sqlspec/utils/serializers.py +4 -0
- sqlspec/utils/singleton.py +32 -0
- sqlspec/utils/sync_tools.cpython-310-x86_64-linux-gnu.so +0 -0
- sqlspec/utils/sync_tools.py +237 -0
- sqlspec/utils/text.cpython-310-x86_64-linux-gnu.so +0 -0
- sqlspec/utils/text.py +96 -0
- sqlspec/utils/type_guards.cpython-310-x86_64-linux-gnu.so +0 -0
- sqlspec/utils/type_guards.py +1135 -0
- sqlspec-0.16.0.dist-info/METADATA +365 -0
- sqlspec-0.16.0.dist-info/RECORD +148 -0
- sqlspec-0.16.0.dist-info/WHEEL +4 -0
- sqlspec-0.16.0.dist-info/entry_points.txt +2 -0
- sqlspec-0.16.0.dist-info/licenses/LICENSE +21 -0
- sqlspec-0.16.0.dist-info/licenses/NOTICE +29 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""Insert operation mixins for SQL builders."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Sequence
|
|
4
|
+
from typing import Any, Optional, Union
|
|
5
|
+
|
|
6
|
+
from sqlglot import exp
|
|
7
|
+
from typing_extensions import Self
|
|
8
|
+
|
|
9
|
+
from sqlspec.exceptions import SQLBuilderError
|
|
10
|
+
|
|
11
|
+
__all__ = ("InsertFromSelectMixin", "InsertIntoClauseMixin", "InsertValuesMixin")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class InsertIntoClauseMixin:
|
|
15
|
+
"""Mixin providing INTO clause for INSERT builders."""
|
|
16
|
+
|
|
17
|
+
_expression: Optional[exp.Expression] = None
|
|
18
|
+
|
|
19
|
+
def into(self, table: str) -> Self:
|
|
20
|
+
"""Set the target table for the INSERT statement.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
table: The name of the table to insert data into.
|
|
24
|
+
|
|
25
|
+
Raises:
|
|
26
|
+
SQLBuilderError: If the current expression is not an INSERT statement.
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
The current builder instance for method chaining.
|
|
30
|
+
"""
|
|
31
|
+
if self._expression is None:
|
|
32
|
+
self._expression = exp.Insert()
|
|
33
|
+
if not isinstance(self._expression, exp.Insert):
|
|
34
|
+
msg = "Cannot set target table on a non-INSERT expression."
|
|
35
|
+
raise SQLBuilderError(msg)
|
|
36
|
+
|
|
37
|
+
setattr(self, "_table", table)
|
|
38
|
+
self._expression.set("this", exp.to_table(table))
|
|
39
|
+
return self
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class InsertValuesMixin:
|
|
43
|
+
"""Mixin providing VALUES and columns methods for INSERT builders."""
|
|
44
|
+
|
|
45
|
+
_expression: Optional[exp.Expression] = None
|
|
46
|
+
|
|
47
|
+
def columns(self, *columns: Union[str, exp.Expression]) -> Self:
|
|
48
|
+
"""Set the columns for the INSERT statement and synchronize the _columns attribute on the builder."""
|
|
49
|
+
if self._expression is None:
|
|
50
|
+
self._expression = exp.Insert()
|
|
51
|
+
if not isinstance(self._expression, exp.Insert):
|
|
52
|
+
msg = "Cannot set columns on a non-INSERT expression."
|
|
53
|
+
raise SQLBuilderError(msg)
|
|
54
|
+
column_exprs = [exp.column(col) if isinstance(col, str) else col for col in columns]
|
|
55
|
+
self._expression.set("columns", column_exprs)
|
|
56
|
+
try:
|
|
57
|
+
cols = self._columns # type: ignore[attr-defined]
|
|
58
|
+
if not columns:
|
|
59
|
+
cols.clear()
|
|
60
|
+
else:
|
|
61
|
+
cols[:] = [col.name if isinstance(col, exp.Column) else str(col) for col in columns]
|
|
62
|
+
except AttributeError:
|
|
63
|
+
pass
|
|
64
|
+
return self
|
|
65
|
+
|
|
66
|
+
def values(self, *values: Any) -> Self:
|
|
67
|
+
"""Add a row of values to the INSERT statement, validating against _columns if set."""
|
|
68
|
+
if self._expression is None:
|
|
69
|
+
self._expression = exp.Insert()
|
|
70
|
+
if not isinstance(self._expression, exp.Insert):
|
|
71
|
+
msg = "Cannot add values to a non-INSERT expression."
|
|
72
|
+
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)})."
|
|
77
|
+
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))
|
|
97
|
+
values_expr = exp.Values(expressions=[row_exprs])
|
|
98
|
+
self._expression.set("expression", values_expr)
|
|
99
|
+
return self
|
|
100
|
+
|
|
101
|
+
def add_values(self, values: Sequence[Any]) -> Self:
|
|
102
|
+
"""Add a row of values to the INSERT statement (alternative signature).
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
values: Sequence of values for the row.
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
The current builder instance for method chaining.
|
|
109
|
+
"""
|
|
110
|
+
return self.values(*values)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class InsertFromSelectMixin:
|
|
114
|
+
"""Mixin providing INSERT ... SELECT support for INSERT builders."""
|
|
115
|
+
|
|
116
|
+
_expression: Optional[exp.Expression] = None
|
|
117
|
+
|
|
118
|
+
def from_select(self, select_builder: Any) -> Self:
|
|
119
|
+
"""Sets the INSERT source to a SELECT statement.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
select_builder: A SelectBuilder instance representing the SELECT query.
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
The current builder instance for method chaining.
|
|
126
|
+
|
|
127
|
+
Raises:
|
|
128
|
+
SQLBuilderError: If the table is not set or the select_builder is invalid.
|
|
129
|
+
"""
|
|
130
|
+
try:
|
|
131
|
+
if not self._table: # type: ignore[attr-defined]
|
|
132
|
+
msg = "The target table must be set using .into() before adding values."
|
|
133
|
+
raise SQLBuilderError(msg)
|
|
134
|
+
except AttributeError:
|
|
135
|
+
msg = "The target table must be set using .into() before adding values."
|
|
136
|
+
raise SQLBuilderError(msg)
|
|
137
|
+
if self._expression is None:
|
|
138
|
+
self._expression = exp.Insert()
|
|
139
|
+
if not isinstance(self._expression, exp.Insert):
|
|
140
|
+
msg = "Cannot set INSERT source on a non-INSERT expression."
|
|
141
|
+
raise SQLBuilderError(msg)
|
|
142
|
+
subquery_parameters = select_builder._parameters # pyright: ignore[attr-defined]
|
|
143
|
+
if subquery_parameters:
|
|
144
|
+
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]
|
|
147
|
+
if select_expr and isinstance(select_expr, exp.Select):
|
|
148
|
+
self._expression.set("expression", select_expr.copy())
|
|
149
|
+
else:
|
|
150
|
+
msg = "SelectBuilder must have a valid SELECT expression."
|
|
151
|
+
raise SQLBuilderError(msg)
|
|
152
|
+
return self
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
from typing import TYPE_CHECKING, Any, Optional, Union, cast
|
|
2
|
+
|
|
3
|
+
from sqlglot import exp
|
|
4
|
+
from typing_extensions import Self
|
|
5
|
+
|
|
6
|
+
from sqlspec.builder._parsing_utils import parse_table_expression
|
|
7
|
+
from sqlspec.exceptions import SQLBuilderError
|
|
8
|
+
from sqlspec.utils.type_guards import has_query_builder_parameters
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from sqlspec.protocols import SQLBuilderProtocol
|
|
12
|
+
|
|
13
|
+
__all__ = ("JoinClauseMixin",)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class JoinClauseMixin:
|
|
17
|
+
"""Mixin providing JOIN clause methods for SELECT builders."""
|
|
18
|
+
|
|
19
|
+
def join(
|
|
20
|
+
self,
|
|
21
|
+
table: Union[str, exp.Expression, Any],
|
|
22
|
+
on: Optional[Union[str, exp.Expression]] = None,
|
|
23
|
+
alias: Optional[str] = None,
|
|
24
|
+
join_type: str = "INNER",
|
|
25
|
+
) -> Self:
|
|
26
|
+
builder = cast("SQLBuilderProtocol", self)
|
|
27
|
+
if builder._expression is None:
|
|
28
|
+
builder._expression = exp.Select()
|
|
29
|
+
if not isinstance(builder._expression, exp.Select):
|
|
30
|
+
msg = "JOIN clause is only supported for SELECT statements."
|
|
31
|
+
raise SQLBuilderError(msg)
|
|
32
|
+
table_expr: exp.Expression
|
|
33
|
+
if isinstance(table, str):
|
|
34
|
+
table_expr = parse_table_expression(table, alias)
|
|
35
|
+
elif has_query_builder_parameters(table):
|
|
36
|
+
if hasattr(table, "_expression") and getattr(table, "_expression", None) is not None:
|
|
37
|
+
table_expr_value = getattr(table, "_expression", None)
|
|
38
|
+
if table_expr_value is not None:
|
|
39
|
+
subquery_exp = exp.paren(table_expr_value.copy()) # pyright: ignore
|
|
40
|
+
else:
|
|
41
|
+
subquery_exp = exp.paren(exp.Anonymous(this=""))
|
|
42
|
+
table_expr = exp.alias_(subquery_exp, alias) if alias else subquery_exp
|
|
43
|
+
else:
|
|
44
|
+
subquery = table.build() # pyright: ignore
|
|
45
|
+
sql_str = subquery.sql if hasattr(subquery, "sql") and not callable(subquery.sql) else str(subquery)
|
|
46
|
+
subquery_exp = exp.paren(exp.maybe_parse(sql_str, dialect=getattr(builder, "dialect", None)))
|
|
47
|
+
table_expr = exp.alias_(subquery_exp, alias) if alias else subquery_exp
|
|
48
|
+
else:
|
|
49
|
+
table_expr = table
|
|
50
|
+
on_expr: Optional[exp.Expression] = None
|
|
51
|
+
if on is not None:
|
|
52
|
+
on_expr = exp.condition(on) if isinstance(on, str) else on
|
|
53
|
+
join_type_upper = join_type.upper()
|
|
54
|
+
if join_type_upper == "INNER":
|
|
55
|
+
join_expr = exp.Join(this=table_expr, on=on_expr)
|
|
56
|
+
elif join_type_upper == "LEFT":
|
|
57
|
+
join_expr = exp.Join(this=table_expr, on=on_expr, side="LEFT")
|
|
58
|
+
elif join_type_upper == "RIGHT":
|
|
59
|
+
join_expr = exp.Join(this=table_expr, on=on_expr, side="RIGHT")
|
|
60
|
+
elif join_type_upper == "FULL":
|
|
61
|
+
join_expr = exp.Join(this=table_expr, on=on_expr, side="FULL", kind="OUTER")
|
|
62
|
+
else:
|
|
63
|
+
msg = f"Unsupported join type: {join_type}"
|
|
64
|
+
raise SQLBuilderError(msg)
|
|
65
|
+
builder._expression = builder._expression.join(join_expr, copy=False)
|
|
66
|
+
return cast("Self", builder)
|
|
67
|
+
|
|
68
|
+
def inner_join(
|
|
69
|
+
self, table: Union[str, exp.Expression, Any], on: Union[str, exp.Expression], alias: Optional[str] = None
|
|
70
|
+
) -> Self:
|
|
71
|
+
return self.join(table, on, alias, "INNER")
|
|
72
|
+
|
|
73
|
+
def left_join(
|
|
74
|
+
self, table: Union[str, exp.Expression, Any], on: Union[str, exp.Expression], alias: Optional[str] = None
|
|
75
|
+
) -> Self:
|
|
76
|
+
return self.join(table, on, alias, "LEFT")
|
|
77
|
+
|
|
78
|
+
def right_join(
|
|
79
|
+
self, table: Union[str, exp.Expression, Any], on: Union[str, exp.Expression], alias: Optional[str] = None
|
|
80
|
+
) -> Self:
|
|
81
|
+
return self.join(table, on, alias, "RIGHT")
|
|
82
|
+
|
|
83
|
+
def full_join(
|
|
84
|
+
self, table: Union[str, exp.Expression, Any], on: Union[str, exp.Expression], alias: Optional[str] = None
|
|
85
|
+
) -> Self:
|
|
86
|
+
return self.join(table, on, alias, "FULL")
|
|
87
|
+
|
|
88
|
+
def cross_join(self, table: Union[str, exp.Expression, Any], alias: Optional[str] = None) -> Self:
|
|
89
|
+
builder = cast("SQLBuilderProtocol", self)
|
|
90
|
+
if builder._expression is None:
|
|
91
|
+
builder._expression = exp.Select()
|
|
92
|
+
if not isinstance(builder._expression, exp.Select):
|
|
93
|
+
msg = "Cannot add cross join to a non-SELECT expression."
|
|
94
|
+
raise SQLBuilderError(msg)
|
|
95
|
+
table_expr: exp.Expression
|
|
96
|
+
if isinstance(table, str):
|
|
97
|
+
table_expr = parse_table_expression(table, alias)
|
|
98
|
+
elif has_query_builder_parameters(table):
|
|
99
|
+
if hasattr(table, "_expression") and getattr(table, "_expression", None) is not None:
|
|
100
|
+
table_expr_value = getattr(table, "_expression", None)
|
|
101
|
+
if table_expr_value is not None:
|
|
102
|
+
subquery_exp = exp.paren(table_expr_value.copy()) # pyright: ignore
|
|
103
|
+
else:
|
|
104
|
+
subquery_exp = exp.paren(exp.Anonymous(this=""))
|
|
105
|
+
table_expr = exp.alias_(subquery_exp, alias) if alias else subquery_exp
|
|
106
|
+
else:
|
|
107
|
+
subquery = table.build() # pyright: ignore
|
|
108
|
+
sql_str = subquery.sql if hasattr(subquery, "sql") and not callable(subquery.sql) else str(subquery)
|
|
109
|
+
subquery_exp = exp.paren(exp.maybe_parse(sql_str, dialect=getattr(builder, "dialect", None)))
|
|
110
|
+
table_expr = exp.alias_(subquery_exp, alias) if alias else subquery_exp
|
|
111
|
+
else:
|
|
112
|
+
table_expr = table
|
|
113
|
+
join_expr = exp.Join(this=table_expr, kind="CROSS")
|
|
114
|
+
builder._expression = builder._expression.join(join_expr, copy=False)
|
|
115
|
+
return cast("Self", builder)
|
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
"""Merge operation mixins for SQL builders."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Optional, Union
|
|
4
|
+
|
|
5
|
+
from sqlglot import exp
|
|
6
|
+
from typing_extensions import Self
|
|
7
|
+
|
|
8
|
+
from sqlspec.exceptions import SQLBuilderError
|
|
9
|
+
from sqlspec.utils.type_guards import has_query_builder_parameters
|
|
10
|
+
|
|
11
|
+
__all__ = (
|
|
12
|
+
"MergeIntoClauseMixin",
|
|
13
|
+
"MergeMatchedClauseMixin",
|
|
14
|
+
"MergeNotMatchedBySourceClauseMixin",
|
|
15
|
+
"MergeNotMatchedClauseMixin",
|
|
16
|
+
"MergeOnClauseMixin",
|
|
17
|
+
"MergeUsingClauseMixin",
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class MergeIntoClauseMixin:
|
|
22
|
+
"""Mixin providing INTO clause for MERGE builders."""
|
|
23
|
+
|
|
24
|
+
_expression: Optional[exp.Expression] = None
|
|
25
|
+
|
|
26
|
+
def into(self, table: Union[str, exp.Expression], alias: Optional[str] = None) -> Self:
|
|
27
|
+
"""Set the target table for the MERGE operation (INTO clause).
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
table: The target table name or expression for the MERGE operation.
|
|
31
|
+
Can be a string (table name) or an sqlglot Expression.
|
|
32
|
+
alias: Optional alias for the target table.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
The current builder instance for method chaining.
|
|
36
|
+
"""
|
|
37
|
+
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.set("this", exp.to_table(table, alias=alias) if isinstance(table, str) else table)
|
|
42
|
+
return self
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class MergeUsingClauseMixin:
|
|
46
|
+
"""Mixin providing USING clause for MERGE builders."""
|
|
47
|
+
|
|
48
|
+
_expression: Optional[exp.Expression] = None
|
|
49
|
+
|
|
50
|
+
def using(self, source: Union[str, exp.Expression, Any], alias: Optional[str] = None) -> Self:
|
|
51
|
+
"""Set the source data for the MERGE operation (USING clause).
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
source: The source data for the MERGE operation.
|
|
55
|
+
Can be a string (table name), an sqlglot Expression, or a SelectBuilder instance.
|
|
56
|
+
alias: Optional alias for the source table.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
The current builder instance for method chaining.
|
|
60
|
+
|
|
61
|
+
Raises:
|
|
62
|
+
SQLBuilderError: If the current expression is not a MERGE statement or if the source type is unsupported.
|
|
63
|
+
"""
|
|
64
|
+
if self._expression is None:
|
|
65
|
+
self._expression = exp.Merge(this=None, using=None, on=None, whens=exp.Whens(expressions=[]))
|
|
66
|
+
if not isinstance(self._expression, exp.Merge):
|
|
67
|
+
self._expression = exp.Merge(this=None, using=None, on=None, whens=exp.Whens(expressions=[]))
|
|
68
|
+
|
|
69
|
+
source_expr: exp.Expression
|
|
70
|
+
if isinstance(source, str):
|
|
71
|
+
source_expr = exp.to_table(source, alias=alias)
|
|
72
|
+
elif has_query_builder_parameters(source) and hasattr(source, "_expression"):
|
|
73
|
+
subquery_builder_parameters = source.parameters
|
|
74
|
+
if subquery_builder_parameters:
|
|
75
|
+
for p_name, p_value in subquery_builder_parameters.items():
|
|
76
|
+
self.add_parameter(p_value, name=p_name) # type: ignore[attr-defined]
|
|
77
|
+
|
|
78
|
+
subquery_exp = exp.paren(getattr(source, "_expression", exp.select()))
|
|
79
|
+
source_expr = exp.alias_(subquery_exp, alias) if alias else subquery_exp
|
|
80
|
+
elif isinstance(source, exp.Expression):
|
|
81
|
+
source_expr = source
|
|
82
|
+
if alias:
|
|
83
|
+
source_expr = exp.alias_(source_expr, alias)
|
|
84
|
+
else:
|
|
85
|
+
msg = f"Unsupported source type for USING clause: {type(source)}"
|
|
86
|
+
raise SQLBuilderError(msg)
|
|
87
|
+
|
|
88
|
+
self._expression.set("using", source_expr)
|
|
89
|
+
return self
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class MergeOnClauseMixin:
|
|
93
|
+
"""Mixin providing ON clause for MERGE builders."""
|
|
94
|
+
|
|
95
|
+
_expression: Optional[exp.Expression] = None
|
|
96
|
+
|
|
97
|
+
def on(self, condition: Union[str, exp.Expression]) -> Self:
|
|
98
|
+
"""Set the join condition for the MERGE operation (ON clause).
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
condition: The join condition for the MERGE operation.
|
|
102
|
+
Can be a string (SQL condition) or an sqlglot Expression.
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
The current builder instance for method chaining.
|
|
106
|
+
|
|
107
|
+
Raises:
|
|
108
|
+
SQLBuilderError: If the current expression is not a MERGE statement or if the condition type is unsupported.
|
|
109
|
+
"""
|
|
110
|
+
if self._expression is None:
|
|
111
|
+
self._expression = exp.Merge(this=None, using=None, on=None, whens=exp.Whens(expressions=[]))
|
|
112
|
+
if not isinstance(self._expression, exp.Merge):
|
|
113
|
+
self._expression = exp.Merge(this=None, using=None, on=None, whens=exp.Whens(expressions=[]))
|
|
114
|
+
|
|
115
|
+
condition_expr: exp.Expression
|
|
116
|
+
if isinstance(condition, str):
|
|
117
|
+
parsed_condition: Optional[exp.Expression] = exp.maybe_parse(
|
|
118
|
+
condition, dialect=getattr(self, "dialect", None)
|
|
119
|
+
)
|
|
120
|
+
if not parsed_condition:
|
|
121
|
+
msg = f"Could not parse ON condition: {condition}"
|
|
122
|
+
raise SQLBuilderError(msg)
|
|
123
|
+
condition_expr = parsed_condition
|
|
124
|
+
elif isinstance(condition, exp.Expression):
|
|
125
|
+
condition_expr = condition
|
|
126
|
+
else:
|
|
127
|
+
msg = f"Unsupported condition type for ON clause: {type(condition)}"
|
|
128
|
+
raise SQLBuilderError(msg)
|
|
129
|
+
|
|
130
|
+
self._expression.set("on", condition_expr)
|
|
131
|
+
return self
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class MergeMatchedClauseMixin:
|
|
135
|
+
"""Mixin providing WHEN MATCHED THEN ... clauses for MERGE builders."""
|
|
136
|
+
|
|
137
|
+
_expression: Optional[exp.Expression] = None
|
|
138
|
+
|
|
139
|
+
def _add_when_clause(self, when_clause: exp.When) -> None:
|
|
140
|
+
"""Helper to add a WHEN clause to the MERGE statement.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
when_clause: The WHEN clause to add.
|
|
144
|
+
"""
|
|
145
|
+
if self._expression is None:
|
|
146
|
+
self._expression = exp.Merge(this=None, using=None, on=None, whens=exp.Whens(expressions=[]))
|
|
147
|
+
if not isinstance(self._expression, exp.Merge):
|
|
148
|
+
self._expression = exp.Merge(this=None, using=None, on=None, whens=exp.Whens(expressions=[]))
|
|
149
|
+
|
|
150
|
+
whens = self._expression.args.get("whens")
|
|
151
|
+
if not whens:
|
|
152
|
+
whens = exp.Whens(expressions=[])
|
|
153
|
+
self._expression.set("whens", whens)
|
|
154
|
+
|
|
155
|
+
whens.append("expressions", when_clause)
|
|
156
|
+
|
|
157
|
+
def when_matched_then_update(
|
|
158
|
+
self, set_values: dict[str, Any], condition: Optional[Union[str, exp.Expression]] = None
|
|
159
|
+
) -> Self:
|
|
160
|
+
"""Define the UPDATE action for matched rows.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
set_values: A dictionary of column names and their new values to set.
|
|
164
|
+
The values will be parameterized.
|
|
165
|
+
condition: An optional additional condition for this specific action.
|
|
166
|
+
|
|
167
|
+
Raises:
|
|
168
|
+
SQLBuilderError: If the condition type is unsupported.
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
The current builder instance for method chaining.
|
|
172
|
+
"""
|
|
173
|
+
update_expressions: list[exp.EQ] = []
|
|
174
|
+
for col, val in set_values.items():
|
|
175
|
+
column_name = col if isinstance(col, str) else str(col)
|
|
176
|
+
if "." in column_name:
|
|
177
|
+
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]
|
|
180
|
+
update_expressions.append(exp.EQ(this=exp.column(col), expression=exp.var(param_name)))
|
|
181
|
+
|
|
182
|
+
when_args: dict[str, Any] = {"matched": True, "then": exp.Update(expressions=update_expressions)}
|
|
183
|
+
|
|
184
|
+
if condition:
|
|
185
|
+
condition_expr: exp.Expression
|
|
186
|
+
if isinstance(condition, str):
|
|
187
|
+
parsed_cond: Optional[exp.Expression] = exp.maybe_parse(
|
|
188
|
+
condition, dialect=getattr(self, "dialect", None)
|
|
189
|
+
)
|
|
190
|
+
if not parsed_cond:
|
|
191
|
+
msg = f"Could not parse WHEN clause condition: {condition}"
|
|
192
|
+
raise SQLBuilderError(msg)
|
|
193
|
+
condition_expr = parsed_cond
|
|
194
|
+
elif isinstance(condition, exp.Expression):
|
|
195
|
+
condition_expr = condition
|
|
196
|
+
else:
|
|
197
|
+
msg = f"Unsupported condition type for WHEN clause: {type(condition)}"
|
|
198
|
+
raise SQLBuilderError(msg)
|
|
199
|
+
when_args["this"] = condition_expr
|
|
200
|
+
|
|
201
|
+
when_clause = exp.When(**when_args)
|
|
202
|
+
self._add_when_clause(when_clause)
|
|
203
|
+
return self
|
|
204
|
+
|
|
205
|
+
def when_matched_then_delete(self, condition: Optional[Union[str, exp.Expression]] = None) -> Self:
|
|
206
|
+
"""Define the DELETE action for matched rows.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
condition: An optional additional condition for this specific action.
|
|
210
|
+
|
|
211
|
+
Raises:
|
|
212
|
+
SQLBuilderError: If the condition type is unsupported.
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
The current builder instance for method chaining.
|
|
216
|
+
"""
|
|
217
|
+
when_args: dict[str, Any] = {"matched": True, "then": exp.Delete()}
|
|
218
|
+
|
|
219
|
+
if condition:
|
|
220
|
+
condition_expr: exp.Expression
|
|
221
|
+
if isinstance(condition, str):
|
|
222
|
+
parsed_cond: Optional[exp.Expression] = exp.maybe_parse(
|
|
223
|
+
condition, dialect=getattr(self, "dialect", None)
|
|
224
|
+
)
|
|
225
|
+
if not parsed_cond:
|
|
226
|
+
msg = f"Could not parse WHEN clause condition: {condition}"
|
|
227
|
+
raise SQLBuilderError(msg)
|
|
228
|
+
condition_expr = parsed_cond
|
|
229
|
+
elif isinstance(condition, exp.Expression):
|
|
230
|
+
condition_expr = condition
|
|
231
|
+
else:
|
|
232
|
+
msg = f"Unsupported condition type for WHEN clause: {type(condition)}"
|
|
233
|
+
raise SQLBuilderError(msg)
|
|
234
|
+
when_args["this"] = condition_expr
|
|
235
|
+
|
|
236
|
+
when_clause = exp.When(**when_args)
|
|
237
|
+
self._add_when_clause(when_clause)
|
|
238
|
+
return self
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
class MergeNotMatchedClauseMixin:
|
|
242
|
+
"""Mixin providing WHEN NOT MATCHED THEN ... clauses for MERGE builders."""
|
|
243
|
+
|
|
244
|
+
_expression: Optional[exp.Expression] = None
|
|
245
|
+
|
|
246
|
+
def when_not_matched_then_insert(
|
|
247
|
+
self,
|
|
248
|
+
columns: Optional[list[str]] = None,
|
|
249
|
+
values: Optional[list[Any]] = None,
|
|
250
|
+
condition: Optional[Union[str, exp.Expression]] = None,
|
|
251
|
+
by_target: bool = True,
|
|
252
|
+
) -> Self:
|
|
253
|
+
"""Define the INSERT action for rows not matched.
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
columns: A list of column names to insert into. If None, implies INSERT DEFAULT VALUES or matching source columns.
|
|
257
|
+
values: A list of values corresponding to the columns.
|
|
258
|
+
These values will be parameterized. If None, implies INSERT DEFAULT VALUES or subquery source.
|
|
259
|
+
condition: An optional additional condition for this specific action.
|
|
260
|
+
by_target: If True (default), condition is "WHEN NOT MATCHED [BY TARGET]".
|
|
261
|
+
If False, condition is "WHEN NOT MATCHED BY SOURCE".
|
|
262
|
+
|
|
263
|
+
Returns:
|
|
264
|
+
The current builder instance for method chaining.
|
|
265
|
+
|
|
266
|
+
Raises:
|
|
267
|
+
SQLBuilderError: If columns and values are provided but do not match in length,
|
|
268
|
+
or if columns are provided without values.
|
|
269
|
+
"""
|
|
270
|
+
insert_args: dict[str, Any] = {}
|
|
271
|
+
if columns and values:
|
|
272
|
+
if len(columns) != len(values):
|
|
273
|
+
msg = "Number of columns must match number of values for INSERT."
|
|
274
|
+
raise SQLBuilderError(msg)
|
|
275
|
+
|
|
276
|
+
parameterized_values: list[exp.Expression] = []
|
|
277
|
+
for i, val in enumerate(values):
|
|
278
|
+
column_name = columns[i] if isinstance(columns[i], str) else str(columns[i])
|
|
279
|
+
if "." in column_name:
|
|
280
|
+
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]
|
|
283
|
+
parameterized_values.append(exp.var(param_name))
|
|
284
|
+
|
|
285
|
+
insert_args["this"] = exp.Tuple(expressions=[exp.column(c) for c in columns])
|
|
286
|
+
insert_args["expression"] = exp.Tuple(expressions=parameterized_values)
|
|
287
|
+
elif columns and not values:
|
|
288
|
+
msg = "Specifying columns without values for INSERT action is complex and not fully supported yet. Consider providing full expressions."
|
|
289
|
+
raise SQLBuilderError(msg)
|
|
290
|
+
elif not columns and not values:
|
|
291
|
+
pass
|
|
292
|
+
else:
|
|
293
|
+
msg = "Cannot specify values without columns for INSERT action."
|
|
294
|
+
raise SQLBuilderError(msg)
|
|
295
|
+
|
|
296
|
+
when_args: dict[str, Any] = {"matched": False, "then": exp.Insert(**insert_args)}
|
|
297
|
+
|
|
298
|
+
if not by_target:
|
|
299
|
+
when_args["source"] = True
|
|
300
|
+
|
|
301
|
+
if condition:
|
|
302
|
+
condition_expr: exp.Expression
|
|
303
|
+
if isinstance(condition, str):
|
|
304
|
+
parsed_cond: Optional[exp.Expression] = exp.maybe_parse(
|
|
305
|
+
condition, dialect=getattr(self, "dialect", None)
|
|
306
|
+
)
|
|
307
|
+
if not parsed_cond:
|
|
308
|
+
msg = f"Could not parse WHEN clause condition: {condition}"
|
|
309
|
+
raise SQLBuilderError(msg)
|
|
310
|
+
condition_expr = parsed_cond
|
|
311
|
+
elif isinstance(condition, exp.Expression):
|
|
312
|
+
condition_expr = condition
|
|
313
|
+
else:
|
|
314
|
+
msg = f"Unsupported condition type for WHEN clause: {type(condition)}"
|
|
315
|
+
raise SQLBuilderError(msg)
|
|
316
|
+
when_args["this"] = condition_expr
|
|
317
|
+
|
|
318
|
+
when_clause = exp.When(**when_args)
|
|
319
|
+
self._add_when_clause(when_clause) # type: ignore[attr-defined]
|
|
320
|
+
return self
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
class MergeNotMatchedBySourceClauseMixin:
|
|
324
|
+
"""Mixin providing WHEN NOT MATCHED BY SOURCE THEN ... clauses for MERGE builders."""
|
|
325
|
+
|
|
326
|
+
_expression: Optional[exp.Expression] = None
|
|
327
|
+
|
|
328
|
+
def when_not_matched_by_source_then_update(
|
|
329
|
+
self, set_values: dict[str, Any], condition: Optional[Union[str, exp.Expression]] = None
|
|
330
|
+
) -> Self:
|
|
331
|
+
"""Define the UPDATE action for rows not matched by source.
|
|
332
|
+
|
|
333
|
+
This is useful for handling rows that exist in the target but not in the source.
|
|
334
|
+
|
|
335
|
+
Args:
|
|
336
|
+
set_values: A dictionary of column names and their new values to set.
|
|
337
|
+
condition: An optional additional condition for this specific action.
|
|
338
|
+
|
|
339
|
+
Raises:
|
|
340
|
+
SQLBuilderError: If the condition type is unsupported.
|
|
341
|
+
|
|
342
|
+
Returns:
|
|
343
|
+
The current builder instance for method chaining.
|
|
344
|
+
"""
|
|
345
|
+
update_expressions: list[exp.EQ] = []
|
|
346
|
+
for col, val in set_values.items():
|
|
347
|
+
column_name = col if isinstance(col, str) else str(col)
|
|
348
|
+
if "." in column_name:
|
|
349
|
+
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]
|
|
352
|
+
update_expressions.append(exp.EQ(this=exp.column(col), expression=exp.var(param_name)))
|
|
353
|
+
|
|
354
|
+
when_args: dict[str, Any] = {
|
|
355
|
+
"matched": False,
|
|
356
|
+
"source": True,
|
|
357
|
+
"then": exp.Update(expressions=update_expressions),
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if condition:
|
|
361
|
+
condition_expr: exp.Expression
|
|
362
|
+
if isinstance(condition, str):
|
|
363
|
+
parsed_cond: Optional[exp.Expression] = exp.maybe_parse(
|
|
364
|
+
condition, dialect=getattr(self, "dialect", None)
|
|
365
|
+
)
|
|
366
|
+
if not parsed_cond:
|
|
367
|
+
msg = f"Could not parse WHEN clause condition: {condition}"
|
|
368
|
+
raise SQLBuilderError(msg)
|
|
369
|
+
condition_expr = parsed_cond
|
|
370
|
+
elif isinstance(condition, exp.Expression):
|
|
371
|
+
condition_expr = condition
|
|
372
|
+
else:
|
|
373
|
+
msg = f"Unsupported condition type for WHEN clause: {type(condition)}"
|
|
374
|
+
raise SQLBuilderError(msg)
|
|
375
|
+
when_args["this"] = condition_expr
|
|
376
|
+
|
|
377
|
+
when_clause = exp.When(**when_args)
|
|
378
|
+
self._add_when_clause(when_clause) # type: ignore[attr-defined]
|
|
379
|
+
return self
|
|
380
|
+
|
|
381
|
+
def when_not_matched_by_source_then_delete(self, condition: Optional[Union[str, exp.Expression]] = None) -> Self:
|
|
382
|
+
"""Define the DELETE action for rows not matched by source.
|
|
383
|
+
|
|
384
|
+
This is useful for cleaning up rows that exist in the target but not in the source.
|
|
385
|
+
|
|
386
|
+
Args:
|
|
387
|
+
condition: An optional additional condition for this specific action.
|
|
388
|
+
|
|
389
|
+
Raises:
|
|
390
|
+
SQLBuilderError: If the condition type is unsupported.
|
|
391
|
+
|
|
392
|
+
Returns:
|
|
393
|
+
The current builder instance for method chaining.
|
|
394
|
+
"""
|
|
395
|
+
when_args: dict[str, Any] = {"matched": False, "source": True, "then": exp.Delete()}
|
|
396
|
+
|
|
397
|
+
if condition:
|
|
398
|
+
condition_expr: exp.Expression
|
|
399
|
+
if isinstance(condition, str):
|
|
400
|
+
parsed_cond: Optional[exp.Expression] = exp.maybe_parse(
|
|
401
|
+
condition, dialect=getattr(self, "dialect", None)
|
|
402
|
+
)
|
|
403
|
+
if not parsed_cond:
|
|
404
|
+
msg = f"Could not parse WHEN clause condition: {condition}"
|
|
405
|
+
raise SQLBuilderError(msg)
|
|
406
|
+
condition_expr = parsed_cond
|
|
407
|
+
elif isinstance(condition, exp.Expression):
|
|
408
|
+
condition_expr = condition
|
|
409
|
+
else:
|
|
410
|
+
msg = f"Unsupported condition type for WHEN clause: {type(condition)}"
|
|
411
|
+
raise SQLBuilderError(msg)
|
|
412
|
+
when_args["this"] = condition_expr
|
|
413
|
+
|
|
414
|
+
when_clause = exp.When(**when_args)
|
|
415
|
+
self._add_when_clause(when_clause) # type: ignore[attr-defined]
|
|
416
|
+
return self
|