sqlspec 0.11.1__py3-none-any.whl → 0.12.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 (155) hide show
  1. sqlspec/__init__.py +16 -3
  2. sqlspec/_serialization.py +3 -10
  3. sqlspec/_sql.py +1147 -0
  4. sqlspec/_typing.py +343 -41
  5. sqlspec/adapters/adbc/__init__.py +2 -6
  6. sqlspec/adapters/adbc/config.py +474 -149
  7. sqlspec/adapters/adbc/driver.py +330 -621
  8. sqlspec/adapters/aiosqlite/__init__.py +2 -6
  9. sqlspec/adapters/aiosqlite/config.py +143 -57
  10. sqlspec/adapters/aiosqlite/driver.py +269 -431
  11. sqlspec/adapters/asyncmy/__init__.py +3 -8
  12. sqlspec/adapters/asyncmy/config.py +247 -202
  13. sqlspec/adapters/asyncmy/driver.py +218 -436
  14. sqlspec/adapters/asyncpg/__init__.py +4 -7
  15. sqlspec/adapters/asyncpg/config.py +329 -176
  16. sqlspec/adapters/asyncpg/driver.py +417 -487
  17. sqlspec/adapters/bigquery/__init__.py +2 -2
  18. sqlspec/adapters/bigquery/config.py +407 -0
  19. sqlspec/adapters/bigquery/driver.py +600 -553
  20. sqlspec/adapters/duckdb/__init__.py +4 -1
  21. sqlspec/adapters/duckdb/config.py +432 -321
  22. sqlspec/adapters/duckdb/driver.py +392 -406
  23. sqlspec/adapters/oracledb/__init__.py +3 -8
  24. sqlspec/adapters/oracledb/config.py +625 -0
  25. sqlspec/adapters/oracledb/driver.py +548 -921
  26. sqlspec/adapters/psqlpy/__init__.py +4 -7
  27. sqlspec/adapters/psqlpy/config.py +372 -203
  28. sqlspec/adapters/psqlpy/driver.py +197 -533
  29. sqlspec/adapters/psycopg/__init__.py +3 -8
  30. sqlspec/adapters/psycopg/config.py +725 -0
  31. sqlspec/adapters/psycopg/driver.py +734 -694
  32. sqlspec/adapters/sqlite/__init__.py +2 -6
  33. sqlspec/adapters/sqlite/config.py +146 -81
  34. sqlspec/adapters/sqlite/driver.py +242 -405
  35. sqlspec/base.py +220 -784
  36. sqlspec/config.py +354 -0
  37. sqlspec/driver/__init__.py +22 -0
  38. sqlspec/driver/_async.py +252 -0
  39. sqlspec/driver/_common.py +338 -0
  40. sqlspec/driver/_sync.py +261 -0
  41. sqlspec/driver/mixins/__init__.py +17 -0
  42. sqlspec/driver/mixins/_pipeline.py +523 -0
  43. sqlspec/driver/mixins/_result_utils.py +122 -0
  44. sqlspec/driver/mixins/_sql_translator.py +35 -0
  45. sqlspec/driver/mixins/_storage.py +993 -0
  46. sqlspec/driver/mixins/_type_coercion.py +131 -0
  47. sqlspec/exceptions.py +299 -7
  48. sqlspec/extensions/aiosql/__init__.py +10 -0
  49. sqlspec/extensions/aiosql/adapter.py +474 -0
  50. sqlspec/extensions/litestar/__init__.py +1 -6
  51. sqlspec/extensions/litestar/_utils.py +1 -5
  52. sqlspec/extensions/litestar/config.py +5 -6
  53. sqlspec/extensions/litestar/handlers.py +13 -12
  54. sqlspec/extensions/litestar/plugin.py +22 -24
  55. sqlspec/extensions/litestar/providers.py +37 -55
  56. sqlspec/loader.py +528 -0
  57. sqlspec/service/__init__.py +3 -0
  58. sqlspec/service/base.py +24 -0
  59. sqlspec/service/pagination.py +26 -0
  60. sqlspec/statement/__init__.py +21 -0
  61. sqlspec/statement/builder/__init__.py +54 -0
  62. sqlspec/statement/builder/_ddl_utils.py +119 -0
  63. sqlspec/statement/builder/_parsing_utils.py +135 -0
  64. sqlspec/statement/builder/base.py +328 -0
  65. sqlspec/statement/builder/ddl.py +1379 -0
  66. sqlspec/statement/builder/delete.py +80 -0
  67. sqlspec/statement/builder/insert.py +274 -0
  68. sqlspec/statement/builder/merge.py +95 -0
  69. sqlspec/statement/builder/mixins/__init__.py +65 -0
  70. sqlspec/statement/builder/mixins/_aggregate_functions.py +151 -0
  71. sqlspec/statement/builder/mixins/_case_builder.py +91 -0
  72. sqlspec/statement/builder/mixins/_common_table_expr.py +91 -0
  73. sqlspec/statement/builder/mixins/_delete_from.py +34 -0
  74. sqlspec/statement/builder/mixins/_from.py +61 -0
  75. sqlspec/statement/builder/mixins/_group_by.py +119 -0
  76. sqlspec/statement/builder/mixins/_having.py +35 -0
  77. sqlspec/statement/builder/mixins/_insert_from_select.py +48 -0
  78. sqlspec/statement/builder/mixins/_insert_into.py +36 -0
  79. sqlspec/statement/builder/mixins/_insert_values.py +69 -0
  80. sqlspec/statement/builder/mixins/_join.py +110 -0
  81. sqlspec/statement/builder/mixins/_limit_offset.py +53 -0
  82. sqlspec/statement/builder/mixins/_merge_clauses.py +405 -0
  83. sqlspec/statement/builder/mixins/_order_by.py +46 -0
  84. sqlspec/statement/builder/mixins/_pivot.py +82 -0
  85. sqlspec/statement/builder/mixins/_returning.py +37 -0
  86. sqlspec/statement/builder/mixins/_select_columns.py +60 -0
  87. sqlspec/statement/builder/mixins/_set_ops.py +122 -0
  88. sqlspec/statement/builder/mixins/_unpivot.py +80 -0
  89. sqlspec/statement/builder/mixins/_update_from.py +54 -0
  90. sqlspec/statement/builder/mixins/_update_set.py +91 -0
  91. sqlspec/statement/builder/mixins/_update_table.py +29 -0
  92. sqlspec/statement/builder/mixins/_where.py +374 -0
  93. sqlspec/statement/builder/mixins/_window_functions.py +86 -0
  94. sqlspec/statement/builder/protocols.py +20 -0
  95. sqlspec/statement/builder/select.py +206 -0
  96. sqlspec/statement/builder/update.py +178 -0
  97. sqlspec/statement/filters.py +571 -0
  98. sqlspec/statement/parameters.py +736 -0
  99. sqlspec/statement/pipelines/__init__.py +67 -0
  100. sqlspec/statement/pipelines/analyzers/__init__.py +9 -0
  101. sqlspec/statement/pipelines/analyzers/_analyzer.py +649 -0
  102. sqlspec/statement/pipelines/base.py +315 -0
  103. sqlspec/statement/pipelines/context.py +119 -0
  104. sqlspec/statement/pipelines/result_types.py +41 -0
  105. sqlspec/statement/pipelines/transformers/__init__.py +8 -0
  106. sqlspec/statement/pipelines/transformers/_expression_simplifier.py +256 -0
  107. sqlspec/statement/pipelines/transformers/_literal_parameterizer.py +623 -0
  108. sqlspec/statement/pipelines/transformers/_remove_comments.py +66 -0
  109. sqlspec/statement/pipelines/transformers/_remove_hints.py +81 -0
  110. sqlspec/statement/pipelines/validators/__init__.py +23 -0
  111. sqlspec/statement/pipelines/validators/_dml_safety.py +275 -0
  112. sqlspec/statement/pipelines/validators/_parameter_style.py +297 -0
  113. sqlspec/statement/pipelines/validators/_performance.py +703 -0
  114. sqlspec/statement/pipelines/validators/_security.py +990 -0
  115. sqlspec/statement/pipelines/validators/base.py +67 -0
  116. sqlspec/statement/result.py +527 -0
  117. sqlspec/statement/splitter.py +701 -0
  118. sqlspec/statement/sql.py +1198 -0
  119. sqlspec/storage/__init__.py +15 -0
  120. sqlspec/storage/backends/__init__.py +0 -0
  121. sqlspec/storage/backends/base.py +166 -0
  122. sqlspec/storage/backends/fsspec.py +315 -0
  123. sqlspec/storage/backends/obstore.py +464 -0
  124. sqlspec/storage/protocol.py +170 -0
  125. sqlspec/storage/registry.py +315 -0
  126. sqlspec/typing.py +157 -36
  127. sqlspec/utils/correlation.py +155 -0
  128. sqlspec/utils/deprecation.py +3 -6
  129. sqlspec/utils/fixtures.py +6 -11
  130. sqlspec/utils/logging.py +135 -0
  131. sqlspec/utils/module_loader.py +45 -43
  132. sqlspec/utils/serializers.py +4 -0
  133. sqlspec/utils/singleton.py +6 -8
  134. sqlspec/utils/sync_tools.py +15 -27
  135. sqlspec/utils/text.py +58 -26
  136. {sqlspec-0.11.1.dist-info → sqlspec-0.12.1.dist-info}/METADATA +97 -26
  137. sqlspec-0.12.1.dist-info/RECORD +145 -0
  138. sqlspec/adapters/bigquery/config/__init__.py +0 -3
  139. sqlspec/adapters/bigquery/config/_common.py +0 -40
  140. sqlspec/adapters/bigquery/config/_sync.py +0 -87
  141. sqlspec/adapters/oracledb/config/__init__.py +0 -9
  142. sqlspec/adapters/oracledb/config/_asyncio.py +0 -186
  143. sqlspec/adapters/oracledb/config/_common.py +0 -131
  144. sqlspec/adapters/oracledb/config/_sync.py +0 -186
  145. sqlspec/adapters/psycopg/config/__init__.py +0 -19
  146. sqlspec/adapters/psycopg/config/_async.py +0 -169
  147. sqlspec/adapters/psycopg/config/_common.py +0 -56
  148. sqlspec/adapters/psycopg/config/_sync.py +0 -168
  149. sqlspec/filters.py +0 -331
  150. sqlspec/mixins.py +0 -305
  151. sqlspec/statement.py +0 -378
  152. sqlspec-0.11.1.dist-info/RECORD +0 -69
  153. {sqlspec-0.11.1.dist-info → sqlspec-0.12.1.dist-info}/WHEEL +0 -0
  154. {sqlspec-0.11.1.dist-info → sqlspec-0.12.1.dist-info}/licenses/LICENSE +0 -0
  155. {sqlspec-0.11.1.dist-info → sqlspec-0.12.1.dist-info}/licenses/NOTICE +0 -0
@@ -0,0 +1,110 @@
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.exceptions import SQLBuilderError
7
+ from sqlspec.statement.builder._parsing_utils import parse_table_expression
8
+
9
+ if TYPE_CHECKING:
10
+ from sqlspec.statement.builder.protocols import BuilderProtocol
11
+
12
+ __all__ = ("JoinClauseMixin",)
13
+
14
+
15
+ class JoinClauseMixin:
16
+ """Mixin providing JOIN clause methods for SELECT builders."""
17
+
18
+ def join(
19
+ self,
20
+ table: Union[str, exp.Expression, Any],
21
+ on: Optional[Union[str, exp.Expression]] = None,
22
+ alias: Optional[str] = None,
23
+ join_type: str = "INNER",
24
+ ) -> Self:
25
+ builder = cast("BuilderProtocol", self)
26
+ if builder._expression is None:
27
+ builder._expression = exp.Select()
28
+ if not isinstance(builder._expression, exp.Select):
29
+ msg = "JOIN clause is only supported for SELECT statements."
30
+ raise SQLBuilderError(msg)
31
+ table_expr: exp.Expression
32
+ if isinstance(table, str):
33
+ table_expr = parse_table_expression(table, alias)
34
+ elif hasattr(table, "build"):
35
+ # Handle builder objects with build() method
36
+ # Work directly with AST when possible to avoid string parsing
37
+ if hasattr(table, "_expression") and getattr(table, "_expression", None) is not None:
38
+ subquery_exp = exp.paren(table._expression.copy()) # pyright: ignore
39
+ table_expr = exp.alias_(subquery_exp, alias) if alias else subquery_exp
40
+ else:
41
+ # Fallback to string parsing
42
+ subquery = table.build() # pyright: ignore
43
+ subquery_exp = exp.paren(exp.maybe_parse(subquery.sql, dialect=getattr(builder, "dialect", None)))
44
+ table_expr = exp.alias_(subquery_exp, alias) if alias else subquery_exp
45
+ # Parameter merging logic can be added here if needed
46
+ else:
47
+ table_expr = table
48
+ on_expr: Optional[exp.Expression] = None
49
+ if on is not None:
50
+ on_expr = exp.condition(on) if isinstance(on, str) else on
51
+ join_type_upper = join_type.upper()
52
+ if join_type_upper == "INNER":
53
+ join_expr = exp.Join(this=table_expr, on=on_expr)
54
+ elif join_type_upper == "LEFT":
55
+ join_expr = exp.Join(this=table_expr, on=on_expr, side="LEFT")
56
+ elif join_type_upper == "RIGHT":
57
+ join_expr = exp.Join(this=table_expr, on=on_expr, side="RIGHT")
58
+ elif join_type_upper == "FULL":
59
+ join_expr = exp.Join(this=table_expr, on=on_expr, side="FULL", kind="OUTER")
60
+ else:
61
+ msg = f"Unsupported join type: {join_type}"
62
+ raise SQLBuilderError(msg)
63
+ builder._expression = builder._expression.join(join_expr, copy=False)
64
+ return cast("Self", builder)
65
+
66
+ def inner_join(
67
+ self, table: Union[str, exp.Expression, Any], on: Union[str, exp.Expression], alias: Optional[str] = None
68
+ ) -> Self:
69
+ return self.join(table, on, alias, "INNER")
70
+
71
+ def left_join(
72
+ self, table: Union[str, exp.Expression, Any], on: Union[str, exp.Expression], alias: Optional[str] = None
73
+ ) -> Self:
74
+ return self.join(table, on, alias, "LEFT")
75
+
76
+ def right_join(
77
+ self, table: Union[str, exp.Expression, Any], on: Union[str, exp.Expression], alias: Optional[str] = None
78
+ ) -> Self:
79
+ return self.join(table, on, alias, "RIGHT")
80
+
81
+ def full_join(
82
+ self, table: Union[str, exp.Expression, Any], on: Union[str, exp.Expression], alias: Optional[str] = None
83
+ ) -> Self:
84
+ return self.join(table, on, alias, "FULL")
85
+
86
+ def cross_join(self, table: Union[str, exp.Expression, Any], alias: Optional[str] = None) -> Self:
87
+ builder = cast("BuilderProtocol", self)
88
+ if builder._expression is None:
89
+ builder._expression = exp.Select()
90
+ if not isinstance(builder._expression, exp.Select):
91
+ msg = "Cannot add cross join to a non-SELECT expression."
92
+ raise SQLBuilderError(msg)
93
+ table_expr: exp.Expression
94
+ if isinstance(table, str):
95
+ table_expr = parse_table_expression(table, alias)
96
+ elif hasattr(table, "build"):
97
+ # Handle builder objects with build() method
98
+ if hasattr(table, "_expression") and getattr(table, "_expression", None) is not None:
99
+ subquery_exp = exp.paren(table._expression.copy()) # pyright: ignore
100
+ table_expr = exp.alias_(subquery_exp, alias) if alias else subquery_exp
101
+ else:
102
+ # Fallback to string parsing
103
+ subquery = table.build() # pyright: ignore
104
+ subquery_exp = exp.paren(exp.maybe_parse(subquery.sql, dialect=getattr(builder, "dialect", None)))
105
+ table_expr = exp.alias_(subquery_exp, alias) if alias else subquery_exp
106
+ else:
107
+ table_expr = table
108
+ join_expr = exp.Join(this=table_expr, kind="CROSS")
109
+ builder._expression = builder._expression.join(join_expr, copy=False)
110
+ return cast("Self", builder)
@@ -0,0 +1,53 @@
1
+ from typing import TYPE_CHECKING, cast
2
+
3
+ from sqlglot import exp
4
+ from typing_extensions import Self
5
+
6
+ if TYPE_CHECKING:
7
+ from sqlspec.statement.builder.protocols import BuilderProtocol
8
+
9
+ from sqlspec.exceptions import SQLBuilderError
10
+
11
+ __all__ = ("LimitOffsetClauseMixin",)
12
+
13
+
14
+ class LimitOffsetClauseMixin:
15
+ """Mixin providing LIMIT and OFFSET clauses for SELECT builders."""
16
+
17
+ def limit(self, value: int) -> Self:
18
+ """Add LIMIT clause.
19
+
20
+ Args:
21
+ value: The maximum number of rows to return.
22
+
23
+ Raises:
24
+ SQLBuilderError: If the current expression is not a SELECT statement.
25
+
26
+ Returns:
27
+ The current builder instance for method chaining.
28
+ """
29
+ builder = cast("BuilderProtocol", self)
30
+ if not isinstance(builder._expression, exp.Select):
31
+ msg = "LIMIT is only supported for SELECT statements."
32
+ raise SQLBuilderError(msg)
33
+ builder._expression = builder._expression.limit(exp.Literal.number(value), copy=False)
34
+ return cast("Self", builder)
35
+
36
+ def offset(self, value: int) -> Self:
37
+ """Add OFFSET clause.
38
+
39
+ Args:
40
+ value: The number of rows to skip before starting to return rows.
41
+
42
+ Raises:
43
+ SQLBuilderError: If the current expression is not a SELECT statement.
44
+
45
+ Returns:
46
+ The current builder instance for method chaining.
47
+ """
48
+ builder = cast("BuilderProtocol", self)
49
+ if not isinstance(builder._expression, exp.Select):
50
+ msg = "OFFSET is only supported for SELECT statements."
51
+ raise SQLBuilderError(msg)
52
+ builder._expression = builder._expression.offset(exp.Literal.number(value), copy=False)
53
+ return cast("Self", builder)
@@ -0,0 +1,405 @@
1
+ from typing import Any, Optional, Union
2
+
3
+ from sqlglot import exp
4
+ from typing_extensions import Self
5
+
6
+ from sqlspec.exceptions import SQLBuilderError
7
+
8
+ __all__ = (
9
+ "MergeIntoClauseMixin",
10
+ "MergeMatchedClauseMixin",
11
+ "MergeNotMatchedBySourceClauseMixin",
12
+ "MergeNotMatchedClauseMixin",
13
+ "MergeOnClauseMixin",
14
+ "MergeUsingClauseMixin",
15
+ )
16
+
17
+
18
+ class MergeIntoClauseMixin:
19
+ """Mixin providing INTO clause for MERGE builders."""
20
+
21
+ _expression: Optional[exp.Expression] = None
22
+
23
+ def into(self, table: Union[str, exp.Expression], alias: Optional[str] = None) -> Self:
24
+ """Set the target table for the MERGE operation (INTO clause).
25
+
26
+ Args:
27
+ table: The target table name or expression for the MERGE operation.
28
+ Can be a string (table name) or an sqlglot Expression.
29
+ alias: Optional alias for the target table.
30
+
31
+ Returns:
32
+ The current builder instance for method chaining.
33
+ """
34
+ if self._expression is None:
35
+ self._expression = exp.Merge(this=None, using=None, on=None, whens=exp.Whens(expressions=[])) # pyright: ignore
36
+ if not isinstance(self._expression, exp.Merge): # pyright: ignore
37
+ self._expression = exp.Merge(this=None, using=None, on=None, whens=exp.Whens(expressions=[])) # pyright: ignore
38
+ self._expression.set("this", exp.to_table(table, alias=alias) if isinstance(table, str) else table)
39
+ return self
40
+
41
+
42
+ class MergeUsingClauseMixin:
43
+ """Mixin providing USING clause for MERGE builders."""
44
+
45
+ _expression: Optional[exp.Expression] = None
46
+
47
+ def using(self, source: Union[str, exp.Expression, Any], alias: Optional[str] = None) -> Self:
48
+ """Set the source data for the MERGE operation (USING clause).
49
+
50
+ Args:
51
+ source: The source data for the MERGE operation.
52
+ Can be a string (table name), an sqlglot Expression, or a SelectBuilder instance.
53
+ alias: Optional alias for the source table.
54
+
55
+ Returns:
56
+ The current builder instance for method chaining.
57
+
58
+ Raises:
59
+ SQLBuilderError: If the current expression is not a MERGE statement or if the source type is unsupported.
60
+ """
61
+ if self._expression is None:
62
+ self._expression = exp.Merge(this=None, using=None, on=None, whens=exp.Whens(expressions=[]))
63
+ if not isinstance(self._expression, exp.Merge):
64
+ self._expression = exp.Merge(this=None, using=None, on=None, whens=exp.Whens(expressions=[]))
65
+
66
+ source_expr: exp.Expression
67
+ if isinstance(source, str):
68
+ source_expr = exp.to_table(source, alias=alias)
69
+ elif hasattr(source, "_parameters") and hasattr(source, "_expression"):
70
+ # Merge parameters from the SELECT builder or other builder
71
+ subquery_builder_params = getattr(source, "_parameters", {})
72
+ if subquery_builder_params:
73
+ for p_name, p_value in subquery_builder_params.items():
74
+ self.add_parameter(p_value, name=p_name) # type: ignore[attr-defined]
75
+
76
+ subquery_exp = exp.paren(getattr(source, "_expression", exp.select()))
77
+ source_expr = exp.alias_(subquery_exp, alias) if alias else subquery_exp
78
+ elif isinstance(source, exp.Expression):
79
+ source_expr = source
80
+ if alias:
81
+ source_expr = exp.alias_(source_expr, alias)
82
+ else:
83
+ msg = f"Unsupported source type for USING clause: {type(source)}"
84
+ raise SQLBuilderError(msg)
85
+
86
+ self._expression.set("using", source_expr)
87
+ return self
88
+
89
+
90
+ class MergeOnClauseMixin:
91
+ """Mixin providing ON clause for MERGE builders."""
92
+
93
+ _expression: Optional[exp.Expression] = None
94
+
95
+ def on(self, condition: Union[str, exp.Expression]) -> Self:
96
+ """Set the join condition for the MERGE operation (ON clause).
97
+
98
+ Args:
99
+ condition: The join condition for the MERGE operation.
100
+ Can be a string (SQL condition) or an sqlglot Expression.
101
+
102
+ Returns:
103
+ The current builder instance for method chaining.
104
+
105
+ Raises:
106
+ SQLBuilderError: If the current expression is not a MERGE statement or if the condition type is unsupported.
107
+ """
108
+ if self._expression is None:
109
+ self._expression = exp.Merge(this=None, using=None, on=None, whens=exp.Whens(expressions=[]))
110
+ if not isinstance(self._expression, exp.Merge):
111
+ self._expression = exp.Merge(this=None, using=None, on=None, whens=exp.Whens(expressions=[]))
112
+
113
+ condition_expr: exp.Expression
114
+ if isinstance(condition, str):
115
+ parsed_condition: Optional[exp.Expression] = exp.maybe_parse(
116
+ condition, dialect=getattr(self, "dialect", None)
117
+ )
118
+ if not parsed_condition:
119
+ msg = f"Could not parse ON condition: {condition}"
120
+ raise SQLBuilderError(msg)
121
+ condition_expr = parsed_condition
122
+ elif isinstance(condition, exp.Expression):
123
+ condition_expr = condition
124
+ else:
125
+ msg = f"Unsupported condition type for ON clause: {type(condition)}"
126
+ raise SQLBuilderError(msg)
127
+
128
+ self._expression.set("on", condition_expr)
129
+ return self
130
+
131
+
132
+ class MergeMatchedClauseMixin:
133
+ """Mixin providing WHEN MATCHED THEN ... clauses for MERGE builders."""
134
+
135
+ _expression: Optional[exp.Expression] = None
136
+
137
+ def _add_when_clause(self, when_clause: exp.When) -> None:
138
+ """Helper to add a WHEN clause to the MERGE statement.
139
+
140
+ Args:
141
+ when_clause: The WHEN clause to add.
142
+ """
143
+ if self._expression is None:
144
+ self._expression = exp.Merge(this=None, using=None, on=None, whens=exp.Whens(expressions=[]))
145
+ if not isinstance(self._expression, exp.Merge):
146
+ self._expression = exp.Merge(this=None, using=None, on=None, whens=exp.Whens(expressions=[]))
147
+
148
+ # Get or create the whens object
149
+ whens = self._expression.args.get("whens")
150
+ if not whens:
151
+ whens = exp.Whens(expressions=[])
152
+ self._expression.set("whens", whens)
153
+
154
+ # Add the when clause to the whens expressions using SQLGlot's append method
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
+ param_name = self.add_parameter(val)[1] # type: ignore[attr-defined]
176
+ update_expressions.append(exp.EQ(this=exp.column(col), expression=exp.var(param_name)))
177
+
178
+ when_args: dict[str, Any] = {"matched": True, "then": exp.Update(expressions=update_expressions)}
179
+
180
+ if condition:
181
+ condition_expr: exp.Expression
182
+ if isinstance(condition, str):
183
+ parsed_cond: Optional[exp.Expression] = exp.maybe_parse(
184
+ condition, dialect=getattr(self, "dialect", None)
185
+ )
186
+ if not parsed_cond:
187
+ msg = f"Could not parse WHEN clause condition: {condition}"
188
+ raise SQLBuilderError(msg)
189
+ condition_expr = parsed_cond
190
+ elif isinstance(condition, exp.Expression):
191
+ condition_expr = condition
192
+ else:
193
+ msg = f"Unsupported condition type for WHEN clause: {type(condition)}"
194
+ raise SQLBuilderError(msg)
195
+ when_args["this"] = condition_expr
196
+
197
+ when_clause = exp.When(**when_args)
198
+ self._add_when_clause(when_clause)
199
+ return self
200
+
201
+ def when_matched_then_delete(self, condition: Optional[Union[str, exp.Expression]] = None) -> Self:
202
+ """Define the DELETE action for matched rows.
203
+
204
+ Args:
205
+ condition: An optional additional condition for this specific action.
206
+
207
+ Raises:
208
+ SQLBuilderError: If the condition type is unsupported.
209
+
210
+ Returns:
211
+ The current builder instance for method chaining.
212
+ """
213
+ when_args: dict[str, Any] = {"matched": True, "then": exp.Delete()}
214
+
215
+ if condition:
216
+ condition_expr: exp.Expression
217
+ if isinstance(condition, str):
218
+ parsed_cond: Optional[exp.Expression] = exp.maybe_parse(
219
+ condition, dialect=getattr(self, "dialect", None)
220
+ )
221
+ if not parsed_cond:
222
+ msg = f"Could not parse WHEN clause condition: {condition}"
223
+ raise SQLBuilderError(msg)
224
+ condition_expr = parsed_cond
225
+ elif isinstance(condition, exp.Expression):
226
+ condition_expr = condition
227
+ else:
228
+ msg = f"Unsupported condition type for WHEN clause: {type(condition)}"
229
+ raise SQLBuilderError(msg)
230
+ when_args["this"] = condition_expr
231
+
232
+ when_clause = exp.When(**when_args)
233
+ self._add_when_clause(when_clause)
234
+ return self
235
+
236
+
237
+ class MergeNotMatchedClauseMixin:
238
+ """Mixin providing WHEN NOT MATCHED THEN ... clauses for MERGE builders."""
239
+
240
+ _expression: Optional[exp.Expression] = None
241
+
242
+ def when_not_matched_then_insert(
243
+ self,
244
+ columns: Optional[list[str]] = None,
245
+ values: Optional[list[Any]] = None,
246
+ condition: Optional[Union[str, exp.Expression]] = None,
247
+ by_target: bool = True,
248
+ ) -> Self:
249
+ """Define the INSERT action for rows not matched.
250
+
251
+ Args:
252
+ columns: A list of column names to insert into. If None, implies INSERT DEFAULT VALUES or matching source columns.
253
+ values: A list of values corresponding to the columns.
254
+ These values will be parameterized. If None, implies INSERT DEFAULT VALUES or subquery source.
255
+ condition: An optional additional condition for this specific action.
256
+ by_target: If True (default), condition is "WHEN NOT MATCHED [BY TARGET]".
257
+ If False, condition is "WHEN NOT MATCHED BY SOURCE".
258
+
259
+ Returns:
260
+ The current builder instance for method chaining.
261
+
262
+ Raises:
263
+ SQLBuilderError: If columns and values are provided but do not match in length,
264
+ or if columns are provided without values.
265
+ """
266
+ insert_args: dict[str, Any] = {}
267
+ if columns and values:
268
+ if len(columns) != len(values):
269
+ msg = "Number of columns must match number of values for INSERT."
270
+ raise SQLBuilderError(msg)
271
+
272
+ parameterized_values: list[exp.Expression] = []
273
+ for val in values:
274
+ param_name = self.add_parameter(val)[1] # type: ignore[attr-defined]
275
+ parameterized_values.append(exp.var(param_name))
276
+
277
+ insert_args["this"] = exp.Tuple(expressions=[exp.column(c) for c in columns])
278
+ insert_args["expression"] = exp.Tuple(expressions=parameterized_values)
279
+ elif columns and not values:
280
+ msg = "Specifying columns without values for INSERT action is complex and not fully supported yet. Consider providing full expressions."
281
+ raise SQLBuilderError(msg)
282
+ elif not columns and not values:
283
+ # INSERT DEFAULT VALUES case
284
+ pass
285
+ else:
286
+ msg = "Cannot specify values without columns for INSERT action."
287
+ raise SQLBuilderError(msg)
288
+
289
+ when_args: dict[str, Any] = {"matched": False, "then": exp.Insert(**insert_args)}
290
+
291
+ if not by_target:
292
+ when_args["source"] = True
293
+
294
+ if condition:
295
+ condition_expr: exp.Expression
296
+ if isinstance(condition, str):
297
+ parsed_cond: Optional[exp.Expression] = exp.maybe_parse(
298
+ condition, dialect=getattr(self, "dialect", None)
299
+ )
300
+ if not parsed_cond:
301
+ msg = f"Could not parse WHEN clause condition: {condition}"
302
+ raise SQLBuilderError(msg)
303
+ condition_expr = parsed_cond
304
+ elif isinstance(condition, exp.Expression):
305
+ condition_expr = condition
306
+ else:
307
+ msg = f"Unsupported condition type for WHEN clause: {type(condition)}"
308
+ raise SQLBuilderError(msg)
309
+ when_args["this"] = condition_expr
310
+
311
+ when_clause = exp.When(**when_args)
312
+ self._add_when_clause(when_clause) # type: ignore[attr-defined]
313
+ return self
314
+
315
+
316
+ class MergeNotMatchedBySourceClauseMixin:
317
+ """Mixin providing WHEN NOT MATCHED BY SOURCE THEN ... clauses for MERGE builders."""
318
+
319
+ _expression: Optional[exp.Expression] = None
320
+
321
+ def when_not_matched_by_source_then_update(
322
+ self, set_values: dict[str, Any], condition: Optional[Union[str, exp.Expression]] = None
323
+ ) -> Self:
324
+ """Define the UPDATE action for rows not matched by source.
325
+
326
+ This is useful for handling rows that exist in the target but not in the source.
327
+
328
+ Args:
329
+ set_values: A dictionary of column names and their new values to set.
330
+ condition: An optional additional condition for this specific action.
331
+
332
+ Raises:
333
+ SQLBuilderError: If the condition type is unsupported.
334
+
335
+ Returns:
336
+ The current builder instance for method chaining.
337
+ """
338
+ update_expressions: list[exp.EQ] = []
339
+ for col, val in set_values.items():
340
+ param_name = self.add_parameter(val)[1] # type: ignore[attr-defined]
341
+ update_expressions.append(exp.EQ(this=exp.column(col), expression=exp.var(param_name)))
342
+
343
+ when_args: dict[str, Any] = {
344
+ "matched": False,
345
+ "source": True,
346
+ "then": exp.Update(expressions=update_expressions),
347
+ }
348
+
349
+ if condition:
350
+ condition_expr: exp.Expression
351
+ if isinstance(condition, str):
352
+ parsed_cond: Optional[exp.Expression] = exp.maybe_parse(
353
+ condition, dialect=getattr(self, "dialect", None)
354
+ )
355
+ if not parsed_cond:
356
+ msg = f"Could not parse WHEN clause condition: {condition}"
357
+ raise SQLBuilderError(msg)
358
+ condition_expr = parsed_cond
359
+ elif isinstance(condition, exp.Expression):
360
+ condition_expr = condition
361
+ else:
362
+ msg = f"Unsupported condition type for WHEN clause: {type(condition)}"
363
+ raise SQLBuilderError(msg)
364
+ when_args["this"] = condition_expr
365
+
366
+ when_clause = exp.When(**when_args)
367
+ self._add_when_clause(when_clause) # type: ignore[attr-defined]
368
+ return self
369
+
370
+ def when_not_matched_by_source_then_delete(self, condition: Optional[Union[str, exp.Expression]] = None) -> Self:
371
+ """Define the DELETE action for rows not matched by source.
372
+
373
+ This is useful for cleaning up rows that exist in the target but not in the source.
374
+
375
+ Args:
376
+ condition: An optional additional condition for this specific action.
377
+
378
+ Raises:
379
+ SQLBuilderError: If the condition type is unsupported.
380
+
381
+ Returns:
382
+ The current builder instance for method chaining.
383
+ """
384
+ when_args: dict[str, Any] = {"matched": False, "source": True, "then": exp.Delete()}
385
+
386
+ if condition:
387
+ condition_expr: exp.Expression
388
+ if isinstance(condition, str):
389
+ parsed_cond: Optional[exp.Expression] = exp.maybe_parse(
390
+ condition, dialect=getattr(self, "dialect", None)
391
+ )
392
+ if not parsed_cond:
393
+ msg = f"Could not parse WHEN clause condition: {condition}"
394
+ raise SQLBuilderError(msg)
395
+ condition_expr = parsed_cond
396
+ elif isinstance(condition, exp.Expression):
397
+ condition_expr = condition
398
+ else:
399
+ msg = f"Unsupported condition type for WHEN clause: {type(condition)}"
400
+ raise SQLBuilderError(msg)
401
+ when_args["this"] = condition_expr
402
+
403
+ when_clause = exp.When(**when_args)
404
+ self._add_when_clause(when_clause) # type: ignore[attr-defined]
405
+ return self
@@ -0,0 +1,46 @@
1
+ from typing import TYPE_CHECKING, Union, cast
2
+
3
+ from sqlglot import exp
4
+ from typing_extensions import Self
5
+
6
+ from sqlspec.exceptions import SQLBuilderError
7
+ from sqlspec.statement.builder._parsing_utils import parse_order_expression
8
+
9
+ if TYPE_CHECKING:
10
+ from sqlspec.statement.builder.protocols import BuilderProtocol
11
+
12
+ __all__ = ("OrderByClauseMixin",)
13
+
14
+
15
+ class OrderByClauseMixin:
16
+ """Mixin providing ORDER BY clause for SELECT builders."""
17
+
18
+ def order_by(self, *items: Union[str, exp.Ordered], desc: bool = False) -> Self:
19
+ """Add ORDER BY clause.
20
+
21
+ Args:
22
+ *items: Columns to order by. Can be strings (column names) or sqlglot.exp.Ordered instances for specific directions (e.g., exp.column("name").desc()).
23
+ desc: Whether to order in descending order (applies to all items if they are strings).
24
+
25
+ Raises:
26
+ SQLBuilderError: If the current expression is not a SELECT statement or if the item type is unsupported.
27
+
28
+ Returns:
29
+ The current builder instance for method chaining.
30
+ """
31
+ builder = cast("BuilderProtocol", self)
32
+ if not isinstance(builder._expression, exp.Select):
33
+ msg = "ORDER BY is only supported for SELECT statements."
34
+ raise SQLBuilderError(msg)
35
+
36
+ current_expr = builder._expression
37
+ for item in items:
38
+ if isinstance(item, str):
39
+ order_item = parse_order_expression(item)
40
+ if desc:
41
+ order_item = order_item.desc()
42
+ else:
43
+ order_item = item
44
+ current_expr = current_expr.order_by(order_item, copy=False)
45
+ builder._expression = current_expr
46
+ return cast("Self", builder)