sqlspec 0.25.0__py3-none-any.whl → 0.27.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.

Files changed (199) hide show
  1. sqlspec/__init__.py +7 -15
  2. sqlspec/_serialization.py +256 -24
  3. sqlspec/_typing.py +71 -52
  4. sqlspec/adapters/adbc/_types.py +1 -1
  5. sqlspec/adapters/adbc/adk/__init__.py +5 -0
  6. sqlspec/adapters/adbc/adk/store.py +870 -0
  7. sqlspec/adapters/adbc/config.py +69 -12
  8. sqlspec/adapters/adbc/data_dictionary.py +340 -0
  9. sqlspec/adapters/adbc/driver.py +266 -58
  10. sqlspec/adapters/adbc/litestar/__init__.py +5 -0
  11. sqlspec/adapters/adbc/litestar/store.py +504 -0
  12. sqlspec/adapters/adbc/type_converter.py +153 -0
  13. sqlspec/adapters/aiosqlite/_types.py +1 -1
  14. sqlspec/adapters/aiosqlite/adk/__init__.py +5 -0
  15. sqlspec/adapters/aiosqlite/adk/store.py +527 -0
  16. sqlspec/adapters/aiosqlite/config.py +88 -15
  17. sqlspec/adapters/aiosqlite/data_dictionary.py +149 -0
  18. sqlspec/adapters/aiosqlite/driver.py +143 -40
  19. sqlspec/adapters/aiosqlite/litestar/__init__.py +5 -0
  20. sqlspec/adapters/aiosqlite/litestar/store.py +281 -0
  21. sqlspec/adapters/aiosqlite/pool.py +7 -7
  22. sqlspec/adapters/asyncmy/__init__.py +7 -1
  23. sqlspec/adapters/asyncmy/_types.py +2 -2
  24. sqlspec/adapters/asyncmy/adk/__init__.py +5 -0
  25. sqlspec/adapters/asyncmy/adk/store.py +493 -0
  26. sqlspec/adapters/asyncmy/config.py +68 -23
  27. sqlspec/adapters/asyncmy/data_dictionary.py +161 -0
  28. sqlspec/adapters/asyncmy/driver.py +313 -58
  29. sqlspec/adapters/asyncmy/litestar/__init__.py +5 -0
  30. sqlspec/adapters/asyncmy/litestar/store.py +296 -0
  31. sqlspec/adapters/asyncpg/__init__.py +2 -1
  32. sqlspec/adapters/asyncpg/_type_handlers.py +71 -0
  33. sqlspec/adapters/asyncpg/_types.py +11 -7
  34. sqlspec/adapters/asyncpg/adk/__init__.py +5 -0
  35. sqlspec/adapters/asyncpg/adk/store.py +450 -0
  36. sqlspec/adapters/asyncpg/config.py +59 -35
  37. sqlspec/adapters/asyncpg/data_dictionary.py +173 -0
  38. sqlspec/adapters/asyncpg/driver.py +170 -25
  39. sqlspec/adapters/asyncpg/litestar/__init__.py +5 -0
  40. sqlspec/adapters/asyncpg/litestar/store.py +253 -0
  41. sqlspec/adapters/bigquery/_types.py +1 -1
  42. sqlspec/adapters/bigquery/adk/__init__.py +5 -0
  43. sqlspec/adapters/bigquery/adk/store.py +576 -0
  44. sqlspec/adapters/bigquery/config.py +27 -10
  45. sqlspec/adapters/bigquery/data_dictionary.py +149 -0
  46. sqlspec/adapters/bigquery/driver.py +368 -142
  47. sqlspec/adapters/bigquery/litestar/__init__.py +5 -0
  48. sqlspec/adapters/bigquery/litestar/store.py +327 -0
  49. sqlspec/adapters/bigquery/type_converter.py +125 -0
  50. sqlspec/adapters/duckdb/_types.py +1 -1
  51. sqlspec/adapters/duckdb/adk/__init__.py +14 -0
  52. sqlspec/adapters/duckdb/adk/store.py +553 -0
  53. sqlspec/adapters/duckdb/config.py +80 -20
  54. sqlspec/adapters/duckdb/data_dictionary.py +163 -0
  55. sqlspec/adapters/duckdb/driver.py +167 -45
  56. sqlspec/adapters/duckdb/litestar/__init__.py +5 -0
  57. sqlspec/adapters/duckdb/litestar/store.py +332 -0
  58. sqlspec/adapters/duckdb/pool.py +4 -4
  59. sqlspec/adapters/duckdb/type_converter.py +133 -0
  60. sqlspec/adapters/oracledb/_numpy_handlers.py +133 -0
  61. sqlspec/adapters/oracledb/_types.py +20 -2
  62. sqlspec/adapters/oracledb/adk/__init__.py +5 -0
  63. sqlspec/adapters/oracledb/adk/store.py +1745 -0
  64. sqlspec/adapters/oracledb/config.py +122 -32
  65. sqlspec/adapters/oracledb/data_dictionary.py +509 -0
  66. sqlspec/adapters/oracledb/driver.py +353 -91
  67. sqlspec/adapters/oracledb/litestar/__init__.py +5 -0
  68. sqlspec/adapters/oracledb/litestar/store.py +767 -0
  69. sqlspec/adapters/oracledb/migrations.py +348 -73
  70. sqlspec/adapters/oracledb/type_converter.py +207 -0
  71. sqlspec/adapters/psqlpy/_type_handlers.py +44 -0
  72. sqlspec/adapters/psqlpy/_types.py +2 -1
  73. sqlspec/adapters/psqlpy/adk/__init__.py +5 -0
  74. sqlspec/adapters/psqlpy/adk/store.py +482 -0
  75. sqlspec/adapters/psqlpy/config.py +46 -17
  76. sqlspec/adapters/psqlpy/data_dictionary.py +172 -0
  77. sqlspec/adapters/psqlpy/driver.py +123 -209
  78. sqlspec/adapters/psqlpy/litestar/__init__.py +5 -0
  79. sqlspec/adapters/psqlpy/litestar/store.py +272 -0
  80. sqlspec/adapters/psqlpy/type_converter.py +102 -0
  81. sqlspec/adapters/psycopg/_type_handlers.py +80 -0
  82. sqlspec/adapters/psycopg/_types.py +2 -1
  83. sqlspec/adapters/psycopg/adk/__init__.py +5 -0
  84. sqlspec/adapters/psycopg/adk/store.py +944 -0
  85. sqlspec/adapters/psycopg/config.py +69 -35
  86. sqlspec/adapters/psycopg/data_dictionary.py +331 -0
  87. sqlspec/adapters/psycopg/driver.py +238 -81
  88. sqlspec/adapters/psycopg/litestar/__init__.py +5 -0
  89. sqlspec/adapters/psycopg/litestar/store.py +554 -0
  90. sqlspec/adapters/sqlite/__init__.py +2 -1
  91. sqlspec/adapters/sqlite/_type_handlers.py +86 -0
  92. sqlspec/adapters/sqlite/_types.py +1 -1
  93. sqlspec/adapters/sqlite/adk/__init__.py +5 -0
  94. sqlspec/adapters/sqlite/adk/store.py +572 -0
  95. sqlspec/adapters/sqlite/config.py +87 -15
  96. sqlspec/adapters/sqlite/data_dictionary.py +149 -0
  97. sqlspec/adapters/sqlite/driver.py +137 -54
  98. sqlspec/adapters/sqlite/litestar/__init__.py +5 -0
  99. sqlspec/adapters/sqlite/litestar/store.py +318 -0
  100. sqlspec/adapters/sqlite/pool.py +18 -9
  101. sqlspec/base.py +45 -26
  102. sqlspec/builder/__init__.py +73 -4
  103. sqlspec/builder/_base.py +162 -89
  104. sqlspec/builder/_column.py +62 -29
  105. sqlspec/builder/_ddl.py +180 -121
  106. sqlspec/builder/_delete.py +5 -4
  107. sqlspec/builder/_dml.py +388 -0
  108. sqlspec/{_sql.py → builder/_factory.py} +53 -94
  109. sqlspec/builder/_insert.py +32 -131
  110. sqlspec/builder/_join.py +375 -0
  111. sqlspec/builder/_merge.py +446 -11
  112. sqlspec/builder/_parsing_utils.py +111 -17
  113. sqlspec/builder/_select.py +1457 -24
  114. sqlspec/builder/_update.py +11 -42
  115. sqlspec/cli.py +307 -194
  116. sqlspec/config.py +252 -67
  117. sqlspec/core/__init__.py +5 -4
  118. sqlspec/core/cache.py +17 -17
  119. sqlspec/core/compiler.py +62 -9
  120. sqlspec/core/filters.py +37 -37
  121. sqlspec/core/hashing.py +9 -9
  122. sqlspec/core/parameters.py +83 -48
  123. sqlspec/core/result.py +102 -46
  124. sqlspec/core/splitter.py +16 -17
  125. sqlspec/core/statement.py +36 -30
  126. sqlspec/core/type_conversion.py +235 -0
  127. sqlspec/driver/__init__.py +7 -6
  128. sqlspec/driver/_async.py +188 -151
  129. sqlspec/driver/_common.py +285 -80
  130. sqlspec/driver/_sync.py +188 -152
  131. sqlspec/driver/mixins/_result_tools.py +20 -236
  132. sqlspec/driver/mixins/_sql_translator.py +4 -4
  133. sqlspec/exceptions.py +75 -7
  134. sqlspec/extensions/adk/__init__.py +53 -0
  135. sqlspec/extensions/adk/_types.py +51 -0
  136. sqlspec/extensions/adk/converters.py +172 -0
  137. sqlspec/extensions/adk/migrations/0001_create_adk_tables.py +144 -0
  138. sqlspec/extensions/adk/migrations/__init__.py +0 -0
  139. sqlspec/extensions/adk/service.py +181 -0
  140. sqlspec/extensions/adk/store.py +536 -0
  141. sqlspec/extensions/aiosql/adapter.py +73 -53
  142. sqlspec/extensions/litestar/__init__.py +21 -4
  143. sqlspec/extensions/litestar/cli.py +54 -10
  144. sqlspec/extensions/litestar/config.py +59 -266
  145. sqlspec/extensions/litestar/handlers.py +46 -17
  146. sqlspec/extensions/litestar/migrations/0001_create_session_table.py +137 -0
  147. sqlspec/extensions/litestar/migrations/__init__.py +3 -0
  148. sqlspec/extensions/litestar/plugin.py +324 -223
  149. sqlspec/extensions/litestar/providers.py +25 -25
  150. sqlspec/extensions/litestar/store.py +265 -0
  151. sqlspec/loader.py +30 -49
  152. sqlspec/migrations/__init__.py +4 -3
  153. sqlspec/migrations/base.py +302 -39
  154. sqlspec/migrations/commands.py +611 -144
  155. sqlspec/migrations/context.py +142 -0
  156. sqlspec/migrations/fix.py +199 -0
  157. sqlspec/migrations/loaders.py +68 -23
  158. sqlspec/migrations/runner.py +543 -107
  159. sqlspec/migrations/tracker.py +237 -21
  160. sqlspec/migrations/utils.py +51 -3
  161. sqlspec/migrations/validation.py +177 -0
  162. sqlspec/protocols.py +66 -36
  163. sqlspec/storage/_utils.py +98 -0
  164. sqlspec/storage/backends/fsspec.py +134 -106
  165. sqlspec/storage/backends/local.py +78 -51
  166. sqlspec/storage/backends/obstore.py +278 -162
  167. sqlspec/storage/registry.py +75 -39
  168. sqlspec/typing.py +16 -84
  169. sqlspec/utils/config_resolver.py +153 -0
  170. sqlspec/utils/correlation.py +4 -5
  171. sqlspec/utils/data_transformation.py +3 -2
  172. sqlspec/utils/deprecation.py +9 -8
  173. sqlspec/utils/fixtures.py +4 -4
  174. sqlspec/utils/logging.py +46 -6
  175. sqlspec/utils/module_loader.py +2 -2
  176. sqlspec/utils/schema.py +288 -0
  177. sqlspec/utils/serializers.py +50 -2
  178. sqlspec/utils/sync_tools.py +21 -17
  179. sqlspec/utils/text.py +1 -2
  180. sqlspec/utils/type_guards.py +111 -20
  181. sqlspec/utils/version.py +433 -0
  182. {sqlspec-0.25.0.dist-info → sqlspec-0.27.0.dist-info}/METADATA +40 -21
  183. sqlspec-0.27.0.dist-info/RECORD +207 -0
  184. sqlspec/builder/mixins/__init__.py +0 -55
  185. sqlspec/builder/mixins/_cte_and_set_ops.py +0 -254
  186. sqlspec/builder/mixins/_delete_operations.py +0 -50
  187. sqlspec/builder/mixins/_insert_operations.py +0 -282
  188. sqlspec/builder/mixins/_join_operations.py +0 -389
  189. sqlspec/builder/mixins/_merge_operations.py +0 -592
  190. sqlspec/builder/mixins/_order_limit_operations.py +0 -152
  191. sqlspec/builder/mixins/_pivot_operations.py +0 -157
  192. sqlspec/builder/mixins/_select_operations.py +0 -936
  193. sqlspec/builder/mixins/_update_operations.py +0 -218
  194. sqlspec/builder/mixins/_where_clause.py +0 -1304
  195. sqlspec-0.25.0.dist-info/RECORD +0 -139
  196. sqlspec-0.25.0.dist-info/licenses/NOTICE +0 -29
  197. {sqlspec-0.25.0.dist-info → sqlspec-0.27.0.dist-info}/WHEEL +0 -0
  198. {sqlspec-0.25.0.dist-info → sqlspec-0.27.0.dist-info}/entry_points.txt +0 -0
  199. {sqlspec-0.25.0.dist-info → sqlspec-0.27.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,592 +0,0 @@
1
- # pyright: reportPrivateUsage=false
2
- """MERGE operation mixins.
3
-
4
- Provides mixins for MERGE statement functionality including INTO,
5
- USING, ON, WHEN MATCHED, and WHEN NOT MATCHED clauses.
6
- """
7
-
8
- from typing import Any, Optional, Union
9
-
10
- from mypy_extensions import trait
11
- from sqlglot import exp
12
- from typing_extensions import Self
13
-
14
- from sqlspec.exceptions import SQLBuilderError
15
- from sqlspec.utils.type_guards import has_query_builder_parameters
16
-
17
- __all__ = (
18
- "MergeIntoClauseMixin",
19
- "MergeMatchedClauseMixin",
20
- "MergeNotMatchedBySourceClauseMixin",
21
- "MergeNotMatchedClauseMixin",
22
- "MergeOnClauseMixin",
23
- "MergeUsingClauseMixin",
24
- )
25
-
26
-
27
- @trait
28
- class MergeIntoClauseMixin:
29
- """Mixin providing INTO clause for MERGE builders."""
30
-
31
- __slots__ = ()
32
-
33
- # Type annotations for PyRight - these will be provided by the base class
34
- def get_expression(self) -> Optional[exp.Expression]: ...
35
- def set_expression(self, expression: exp.Expression) -> None: ...
36
-
37
- def into(self, table: Union[str, exp.Expression], alias: Optional[str] = None) -> Self:
38
- """Set the target table for the MERGE operation (INTO clause).
39
-
40
- Args:
41
- table: The target table name or expression for the MERGE operation.
42
- Can be a string (table name) or an sqlglot Expression.
43
- alias: Optional alias for the target table.
44
-
45
- Returns:
46
- The current builder instance for method chaining.
47
- """
48
- current_expr = self.get_expression()
49
- if current_expr is None or not isinstance(current_expr, exp.Merge):
50
- self.set_expression(exp.Merge(this=None, using=None, on=None, whens=exp.Whens(expressions=[])))
51
- current_expr = self.get_expression()
52
-
53
- # Type guard: current_expr is now guaranteed to be an Expression
54
- assert current_expr is not None
55
- current_expr.set("this", exp.to_table(table, alias=alias) if isinstance(table, str) else table)
56
- return self
57
-
58
-
59
- @trait
60
- class MergeUsingClauseMixin:
61
- """Mixin providing USING clause for MERGE builders."""
62
-
63
- __slots__ = ()
64
-
65
- # Type annotations for PyRight - these will be provided by the base class
66
- def get_expression(self) -> Optional[exp.Expression]: ...
67
- def set_expression(self, expression: exp.Expression) -> None: ...
68
-
69
- def add_parameter(self, value: Any, name: Optional[str] = None) -> tuple[Any, str]:
70
- """Add parameter - provided by QueryBuilder."""
71
- msg = "Method must be provided by QueryBuilder subclass"
72
- raise NotImplementedError(msg)
73
-
74
- def using(self, source: Union[str, exp.Expression, Any], alias: Optional[str] = None) -> Self:
75
- """Set the source data for the MERGE operation (USING clause).
76
-
77
- Args:
78
- source: The source data for the MERGE operation.
79
- Can be a string (table name), an sqlglot Expression, or a SelectBuilder instance.
80
- alias: Optional alias for the source table.
81
-
82
- Returns:
83
- The current builder instance for method chaining.
84
-
85
- Raises:
86
- SQLBuilderError: If the current expression is not a MERGE statement or if the source type is unsupported.
87
- """
88
- current_expr = self.get_expression()
89
- if current_expr is None or not isinstance(current_expr, exp.Merge):
90
- self.set_expression(exp.Merge(this=None, using=None, on=None, whens=exp.Whens(expressions=[])))
91
- current_expr = self.get_expression()
92
-
93
- # Type guard: current_expr is now guaranteed to be an Expression
94
- assert current_expr is not None
95
- source_expr: exp.Expression
96
- if isinstance(source, str):
97
- source_expr = exp.to_table(source, alias=alias)
98
- elif has_query_builder_parameters(source) and hasattr(source, "_expression"):
99
- subquery_builder_parameters = source.parameters
100
- if subquery_builder_parameters:
101
- for p_name, p_value in subquery_builder_parameters.items():
102
- self.add_parameter(p_value, name=p_name)
103
-
104
- subquery_exp = exp.paren(getattr(source, "_expression", exp.select()))
105
- source_expr = exp.alias_(subquery_exp, alias) if alias else subquery_exp
106
- elif isinstance(source, exp.Expression):
107
- source_expr = source
108
- if alias:
109
- source_expr = exp.alias_(source_expr, alias)
110
- else:
111
- msg = f"Unsupported source type for USING clause: {type(source)}"
112
- raise SQLBuilderError(msg)
113
-
114
- current_expr.set("using", source_expr)
115
- return self
116
-
117
-
118
- @trait
119
- class MergeOnClauseMixin:
120
- """Mixin providing ON clause for MERGE builders."""
121
-
122
- __slots__ = ()
123
-
124
- # Type annotations for PyRight - these will be provided by the base class
125
- def get_expression(self) -> Optional[exp.Expression]: ...
126
- def set_expression(self, expression: exp.Expression) -> None: ...
127
-
128
- def on(self, condition: Union[str, exp.Expression]) -> Self:
129
- """Set the join condition for the MERGE operation (ON clause).
130
-
131
- Args:
132
- condition: The join condition for the MERGE operation.
133
- Can be a string (SQL condition) or an sqlglot Expression.
134
-
135
- Returns:
136
- The current builder instance for method chaining.
137
-
138
- Raises:
139
- SQLBuilderError: If the current expression is not a MERGE statement or if the condition type is unsupported.
140
- """
141
- current_expr = self.get_expression()
142
- if current_expr is None or not isinstance(current_expr, exp.Merge):
143
- self.set_expression(exp.Merge(this=None, using=None, on=None, whens=exp.Whens(expressions=[])))
144
- current_expr = self.get_expression()
145
-
146
- # Type guard: current_expr is now guaranteed to be an Expression
147
- assert current_expr is not None
148
- condition_expr: exp.Expression
149
- if isinstance(condition, str):
150
- parsed_condition: Optional[exp.Expression] = exp.maybe_parse(
151
- condition, dialect=getattr(self, "dialect", None)
152
- )
153
- if not parsed_condition:
154
- msg = f"Could not parse ON condition: {condition}"
155
- raise SQLBuilderError(msg)
156
- condition_expr = parsed_condition
157
- elif isinstance(condition, exp.Expression):
158
- condition_expr = condition
159
- else:
160
- msg = f"Unsupported condition type for ON clause: {type(condition)}"
161
- raise SQLBuilderError(msg)
162
-
163
- current_expr.set("on", condition_expr)
164
- return self
165
-
166
-
167
- @trait
168
- class MergeMatchedClauseMixin:
169
- """Mixin providing WHEN MATCHED THEN ... clauses for MERGE builders."""
170
-
171
- __slots__ = ()
172
-
173
- # Type annotations for PyRight - these will be provided by the base class
174
- def get_expression(self) -> Optional[exp.Expression]: ...
175
- def set_expression(self, expression: exp.Expression) -> None: ...
176
-
177
- def add_parameter(self, value: Any, name: Optional[str] = None) -> tuple[Any, str]:
178
- """Add parameter - provided by QueryBuilder."""
179
- msg = "Method must be provided by QueryBuilder subclass"
180
- raise NotImplementedError(msg)
181
-
182
- def _generate_unique_parameter_name(self, base_name: str) -> str:
183
- """Generate unique parameter name - provided by QueryBuilder."""
184
- msg = "Method must be provided by QueryBuilder subclass"
185
- raise NotImplementedError(msg)
186
-
187
- def _add_when_clause(self, when_clause: exp.When) -> None:
188
- """Helper to add a WHEN clause to the MERGE statement.
189
-
190
- Args:
191
- when_clause: The WHEN clause to add.
192
- """
193
- current_expr = self.get_expression()
194
- if current_expr is None or not isinstance(current_expr, exp.Merge):
195
- self.set_expression(exp.Merge(this=None, using=None, on=None, whens=exp.Whens(expressions=[])))
196
- current_expr = self.get_expression()
197
-
198
- # Type guard: current_expr is now guaranteed to be an Expression
199
- assert current_expr is not None
200
- whens = current_expr.args.get("whens")
201
- if not whens:
202
- whens = exp.Whens(expressions=[])
203
- current_expr.set("whens", whens)
204
-
205
- whens.append("expressions", when_clause)
206
-
207
- def when_matched_then_update(
208
- self,
209
- set_values: Optional[dict[str, Any]] = None,
210
- condition: Optional[Union[str, exp.Expression]] = None,
211
- **kwargs: Any,
212
- ) -> Self:
213
- """Define the UPDATE action for matched rows.
214
-
215
- Supports:
216
- - when_matched_then_update({"column": value})
217
- - when_matched_then_update(column=value, other_column=other_value)
218
- - when_matched_then_update({"column": value}, other_column=other_value)
219
-
220
- Args:
221
- set_values: A dictionary of column names and their new values to set.
222
- The values will be parameterized.
223
- condition: An optional additional condition for this specific action.
224
- **kwargs: Column-value pairs to update on match.
225
-
226
- Raises:
227
- SQLBuilderError: If the condition type is unsupported.
228
-
229
- Returns:
230
- The current builder instance for method chaining.
231
- """
232
- # Combine set_values dict and kwargs
233
- all_values = dict(set_values or {}, **kwargs)
234
-
235
- if not all_values:
236
- msg = "No update values provided. Use set_values dict or kwargs."
237
- raise SQLBuilderError(msg)
238
-
239
- update_expressions: list[exp.EQ] = []
240
- for col, val in all_values.items():
241
- if hasattr(val, "expression") and hasattr(val, "sql"):
242
- # Handle SQL objects (from sql.raw with parameters)
243
- expression = getattr(val, "expression", None)
244
- if expression is not None and isinstance(expression, exp.Expression):
245
- # Merge parameters from SQL object into builder
246
- if hasattr(val, "parameters"):
247
- sql_parameters = getattr(val, "parameters", {})
248
- for param_name, param_value in sql_parameters.items():
249
- self.add_parameter(param_value, name=param_name)
250
- value_expr = expression
251
- else:
252
- # If expression is None, fall back to parsing the raw SQL
253
- sql_text = getattr(val, "sql", "")
254
- # Merge parameters even when parsing raw SQL
255
- if hasattr(val, "parameters"):
256
- sql_parameters = getattr(val, "parameters", {})
257
- for param_name, param_value in sql_parameters.items():
258
- self.add_parameter(param_value, name=param_name)
259
- # Check if sql_text is callable (like Expression.sql method)
260
- if callable(sql_text):
261
- sql_text = str(val)
262
- value_expr = exp.maybe_parse(sql_text) or exp.convert(str(sql_text))
263
- elif isinstance(val, exp.Expression):
264
- value_expr = val
265
- else:
266
- column_name = col if isinstance(col, str) else str(col)
267
- if "." in column_name:
268
- column_name = column_name.split(".")[-1]
269
- param_name = self._generate_unique_parameter_name(column_name)
270
- param_name = self.add_parameter(val, name=param_name)[1]
271
- value_expr = exp.Placeholder(this=param_name)
272
-
273
- update_expressions.append(exp.EQ(this=exp.column(col), expression=value_expr))
274
-
275
- when_args: dict[str, Any] = {"matched": True, "then": exp.Update(expressions=update_expressions)}
276
-
277
- if condition:
278
- condition_expr: exp.Expression
279
- if isinstance(condition, str):
280
- parsed_cond: Optional[exp.Expression] = exp.maybe_parse(
281
- condition, dialect=getattr(self, "dialect", None)
282
- )
283
- if not parsed_cond:
284
- msg = f"Could not parse WHEN clause condition: {condition}"
285
- raise SQLBuilderError(msg)
286
- condition_expr = parsed_cond
287
- elif isinstance(condition, exp.Expression):
288
- condition_expr = condition
289
- else:
290
- msg = f"Unsupported condition type for WHEN clause: {type(condition)}"
291
- raise SQLBuilderError(msg)
292
- when_args["this"] = condition_expr
293
-
294
- when_clause = exp.When(**when_args)
295
- self._add_when_clause(when_clause)
296
- return self
297
-
298
- def when_matched_then_delete(self, condition: Optional[Union[str, exp.Expression]] = None) -> Self:
299
- """Define the DELETE action for matched rows.
300
-
301
- Args:
302
- condition: An optional additional condition for this specific action.
303
-
304
- Raises:
305
- SQLBuilderError: If the condition type is unsupported.
306
-
307
- Returns:
308
- The current builder instance for method chaining.
309
- """
310
- when_args: dict[str, Any] = {"matched": True, "then": exp.Delete()}
311
-
312
- if condition:
313
- condition_expr: exp.Expression
314
- if isinstance(condition, str):
315
- parsed_cond: Optional[exp.Expression] = exp.maybe_parse(
316
- condition, dialect=getattr(self, "dialect", None)
317
- )
318
- if not parsed_cond:
319
- msg = f"Could not parse WHEN clause condition: {condition}"
320
- raise SQLBuilderError(msg)
321
- condition_expr = parsed_cond
322
- elif isinstance(condition, exp.Expression):
323
- condition_expr = condition
324
- else:
325
- msg = f"Unsupported condition type for WHEN clause: {type(condition)}"
326
- raise SQLBuilderError(msg)
327
- when_args["this"] = condition_expr
328
-
329
- when_clause = exp.When(**when_args)
330
- self._add_when_clause(when_clause)
331
- return self
332
-
333
-
334
- @trait
335
- class MergeNotMatchedClauseMixin:
336
- """Mixin providing WHEN NOT MATCHED THEN ... clauses for MERGE builders."""
337
-
338
- __slots__ = ()
339
-
340
- # Type annotations for PyRight - these will be provided by the base class
341
- def get_expression(self) -> Optional[exp.Expression]: ...
342
- def set_expression(self, expression: exp.Expression) -> None: ...
343
-
344
- def add_parameter(self, value: Any, name: Optional[str] = None) -> tuple[Any, str]:
345
- """Add parameter - provided by QueryBuilder."""
346
- msg = "Method must be provided by QueryBuilder subclass"
347
- raise NotImplementedError(msg)
348
-
349
- def _generate_unique_parameter_name(self, base_name: str) -> str:
350
- """Generate unique parameter name - provided by QueryBuilder."""
351
- msg = "Method must be provided by QueryBuilder subclass"
352
- raise NotImplementedError(msg)
353
-
354
- def _add_when_clause(self, when_clause: exp.When) -> None:
355
- """Helper to add a WHEN clause to the MERGE statement - provided by QueryBuilder."""
356
- msg = "Method must be provided by QueryBuilder subclass"
357
- raise NotImplementedError(msg)
358
-
359
- def when_not_matched_then_insert(
360
- self,
361
- columns: Optional[list[str]] = None,
362
- values: Optional[list[Any]] = None,
363
- condition: Optional[Union[str, exp.Expression]] = None,
364
- by_target: bool = True,
365
- ) -> Self:
366
- """Define the INSERT action for rows not matched.
367
-
368
- Args:
369
- columns: A list of column names to insert into. If None, implies INSERT DEFAULT VALUES or matching source columns.
370
- values: A list of values corresponding to the columns.
371
- These values will be parameterized. If None, implies INSERT DEFAULT VALUES or subquery source.
372
- condition: An optional additional condition for this specific action.
373
- by_target: If True (default), condition is "WHEN NOT MATCHED [BY TARGET]".
374
- If False, condition is "WHEN NOT MATCHED BY SOURCE".
375
-
376
- Returns:
377
- The current builder instance for method chaining.
378
-
379
- Raises:
380
- SQLBuilderError: If columns and values are provided but do not match in length,
381
- or if columns are provided without values.
382
- """
383
- insert_args: dict[str, Any] = {}
384
- if columns and values:
385
- if len(columns) != len(values):
386
- msg = "Number of columns must match number of values for INSERT."
387
- raise SQLBuilderError(msg)
388
-
389
- parameterized_values: list[exp.Expression] = []
390
- for i, val in enumerate(values):
391
- column_name = columns[i] if isinstance(columns[i], str) else str(columns[i])
392
- if "." in column_name:
393
- column_name = column_name.split(".")[-1]
394
- param_name = self._generate_unique_parameter_name(column_name)
395
- param_name = self.add_parameter(val, name=param_name)[1]
396
- parameterized_values.append(exp.Placeholder())
397
-
398
- insert_args["this"] = exp.Tuple(expressions=[exp.column(c) for c in columns])
399
- insert_args["expression"] = exp.Tuple(expressions=parameterized_values)
400
- elif columns and not values:
401
- msg = "Specifying columns without values for INSERT action is complex and not fully supported yet. Consider providing full expressions."
402
- raise SQLBuilderError(msg)
403
- elif not columns and not values:
404
- pass
405
- else:
406
- msg = "Cannot specify values without columns for INSERT action."
407
- raise SQLBuilderError(msg)
408
-
409
- when_args: dict[str, Any] = {"matched": False, "then": exp.Insert(**insert_args)}
410
-
411
- if not by_target:
412
- when_args["source"] = True
413
-
414
- if condition:
415
- condition_expr: exp.Expression
416
- if isinstance(condition, str):
417
- parsed_cond: Optional[exp.Expression] = exp.maybe_parse(
418
- condition, dialect=getattr(self, "dialect", None)
419
- )
420
- if not parsed_cond:
421
- msg = f"Could not parse WHEN clause condition: {condition}"
422
- raise SQLBuilderError(msg)
423
- condition_expr = parsed_cond
424
- elif isinstance(condition, exp.Expression):
425
- condition_expr = condition
426
- else:
427
- msg = f"Unsupported condition type for WHEN clause: {type(condition)}"
428
- raise SQLBuilderError(msg)
429
- when_args["this"] = condition_expr
430
-
431
- when_clause = exp.When(**when_args)
432
- self._add_when_clause(when_clause)
433
- return self
434
-
435
-
436
- @trait
437
- class MergeNotMatchedBySourceClauseMixin:
438
- """Mixin providing WHEN NOT MATCHED BY SOURCE THEN ... clauses for MERGE builders."""
439
-
440
- __slots__ = ()
441
-
442
- # Type annotations for PyRight - these will be provided by the base class
443
- def get_expression(self) -> Optional[exp.Expression]: ...
444
- def set_expression(self, expression: exp.Expression) -> None: ...
445
-
446
- def add_parameter(self, value: Any, name: Optional[str] = None) -> tuple[Any, str]:
447
- """Add parameter - provided by QueryBuilder."""
448
- msg = "Method must be provided by QueryBuilder subclass"
449
- raise NotImplementedError(msg)
450
-
451
- def _generate_unique_parameter_name(self, base_name: str) -> str:
452
- """Generate unique parameter name - provided by QueryBuilder."""
453
- msg = "Method must be provided by QueryBuilder subclass"
454
- raise NotImplementedError(msg)
455
-
456
- def _add_when_clause(self, when_clause: exp.When) -> None:
457
- """Helper to add a WHEN clause to the MERGE statement - provided by QueryBuilder."""
458
- msg = "Method must be provided by QueryBuilder subclass"
459
- raise NotImplementedError(msg)
460
-
461
- def when_not_matched_by_source_then_update(
462
- self,
463
- set_values: Optional[dict[str, Any]] = None,
464
- condition: Optional[Union[str, exp.Expression]] = None,
465
- **kwargs: Any,
466
- ) -> Self:
467
- """Define the UPDATE action for rows not matched by source.
468
-
469
- This is useful for handling rows that exist in the target but not in the source.
470
-
471
- Supports:
472
- - when_not_matched_by_source_then_update({"column": value})
473
- - when_not_matched_by_source_then_update(column=value, other_column=other_value)
474
- - when_not_matched_by_source_then_update({"column": value}, other_column=other_value)
475
-
476
- Args:
477
- set_values: A dictionary of column names and their new values to set.
478
- condition: An optional additional condition for this specific action.
479
- **kwargs: Column-value pairs to update when not matched by source.
480
-
481
- Raises:
482
- SQLBuilderError: If the condition type is unsupported.
483
-
484
- Returns:
485
- The current builder instance for method chaining.
486
- """
487
- # Combine set_values dict and kwargs
488
- all_values = dict(set_values or {}, **kwargs)
489
-
490
- if not all_values:
491
- msg = "No update values provided. Use set_values dict or kwargs."
492
- raise SQLBuilderError(msg)
493
-
494
- update_expressions: list[exp.EQ] = []
495
- for col, val in all_values.items():
496
- if hasattr(val, "expression") and hasattr(val, "sql"):
497
- # Handle SQL objects (from sql.raw with parameters)
498
- expression = getattr(val, "expression", None)
499
- if expression is not None and isinstance(expression, exp.Expression):
500
- # Merge parameters from SQL object into builder
501
- if hasattr(val, "parameters"):
502
- sql_parameters = getattr(val, "parameters", {})
503
- for param_name, param_value in sql_parameters.items():
504
- self.add_parameter(param_value, name=param_name)
505
- value_expr = expression
506
- else:
507
- # If expression is None, fall back to parsing the raw SQL
508
- sql_text = getattr(val, "sql", "")
509
- # Merge parameters even when parsing raw SQL
510
- if hasattr(val, "parameters"):
511
- sql_parameters = getattr(val, "parameters", {})
512
- for param_name, param_value in sql_parameters.items():
513
- self.add_parameter(param_value, name=param_name)
514
- # Check if sql_text is callable (like Expression.sql method)
515
- if callable(sql_text):
516
- sql_text = str(val)
517
- value_expr = exp.maybe_parse(sql_text) or exp.convert(str(sql_text))
518
- elif isinstance(val, exp.Expression):
519
- value_expr = val
520
- else:
521
- column_name = col if isinstance(col, str) else str(col)
522
- if "." in column_name:
523
- column_name = column_name.split(".")[-1]
524
- param_name = self._generate_unique_parameter_name(column_name)
525
- param_name = self.add_parameter(val, name=param_name)[1]
526
- value_expr = exp.Placeholder(this=param_name)
527
-
528
- update_expressions.append(exp.EQ(this=exp.column(col), expression=value_expr))
529
-
530
- when_args: dict[str, Any] = {
531
- "matched": False,
532
- "source": True,
533
- "then": exp.Update(expressions=update_expressions),
534
- }
535
-
536
- if condition:
537
- condition_expr: exp.Expression
538
- if isinstance(condition, str):
539
- parsed_cond: Optional[exp.Expression] = exp.maybe_parse(
540
- condition, dialect=getattr(self, "dialect", None)
541
- )
542
- if not parsed_cond:
543
- msg = f"Could not parse WHEN clause condition: {condition}"
544
- raise SQLBuilderError(msg)
545
- condition_expr = parsed_cond
546
- elif isinstance(condition, exp.Expression):
547
- condition_expr = condition
548
- else:
549
- msg = f"Unsupported condition type for WHEN clause: {type(condition)}"
550
- raise SQLBuilderError(msg)
551
- when_args["this"] = condition_expr
552
-
553
- when_clause = exp.When(**when_args)
554
- self._add_when_clause(when_clause)
555
- return self
556
-
557
- def when_not_matched_by_source_then_delete(self, condition: Optional[Union[str, exp.Expression]] = None) -> Self:
558
- """Define the DELETE action for rows not matched by source.
559
-
560
- This is useful for cleaning up rows that exist in the target but not in the source.
561
-
562
- Args:
563
- condition: An optional additional condition for this specific action.
564
-
565
- Raises:
566
- SQLBuilderError: If the condition type is unsupported.
567
-
568
- Returns:
569
- The current builder instance for method chaining.
570
- """
571
- when_args: dict[str, Any] = {"matched": False, "source": True, "then": exp.Delete()}
572
-
573
- if condition:
574
- condition_expr: exp.Expression
575
- if isinstance(condition, str):
576
- parsed_cond: Optional[exp.Expression] = exp.maybe_parse(
577
- condition, dialect=getattr(self, "dialect", None)
578
- )
579
- if not parsed_cond:
580
- msg = f"Could not parse WHEN clause condition: {condition}"
581
- raise SQLBuilderError(msg)
582
- condition_expr = parsed_cond
583
- elif isinstance(condition, exp.Expression):
584
- condition_expr = condition
585
- else:
586
- msg = f"Unsupported condition type for WHEN clause: {type(condition)}"
587
- raise SQLBuilderError(msg)
588
- when_args["this"] = condition_expr
589
-
590
- when_clause = exp.When(**when_args)
591
- self._add_when_clause(when_clause)
592
- return self