sqlspec 0.26.0__py3-none-any.whl → 0.28.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 (212) hide show
  1. sqlspec/__init__.py +7 -15
  2. sqlspec/_serialization.py +55 -25
  3. sqlspec/_typing.py +155 -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 +880 -0
  7. sqlspec/adapters/adbc/config.py +62 -12
  8. sqlspec/adapters/adbc/data_dictionary.py +74 -2
  9. sqlspec/adapters/adbc/driver.py +226 -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 +44 -50
  13. sqlspec/adapters/aiosqlite/_types.py +1 -1
  14. sqlspec/adapters/aiosqlite/adk/__init__.py +5 -0
  15. sqlspec/adapters/aiosqlite/adk/store.py +536 -0
  16. sqlspec/adapters/aiosqlite/config.py +86 -16
  17. sqlspec/adapters/aiosqlite/data_dictionary.py +34 -2
  18. sqlspec/adapters/aiosqlite/driver.py +127 -38
  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 +1 -1
  24. sqlspec/adapters/asyncmy/adk/__init__.py +5 -0
  25. sqlspec/adapters/asyncmy/adk/store.py +503 -0
  26. sqlspec/adapters/asyncmy/config.py +59 -17
  27. sqlspec/adapters/asyncmy/data_dictionary.py +41 -2
  28. sqlspec/adapters/asyncmy/driver.py +293 -62
  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 +460 -0
  36. sqlspec/adapters/asyncpg/config.py +57 -36
  37. sqlspec/adapters/asyncpg/data_dictionary.py +48 -2
  38. sqlspec/adapters/asyncpg/driver.py +153 -23
  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 +585 -0
  44. sqlspec/adapters/bigquery/config.py +36 -11
  45. sqlspec/adapters/bigquery/data_dictionary.py +42 -2
  46. sqlspec/adapters/bigquery/driver.py +489 -144
  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 +55 -23
  50. sqlspec/adapters/duckdb/_types.py +2 -2
  51. sqlspec/adapters/duckdb/adk/__init__.py +14 -0
  52. sqlspec/adapters/duckdb/adk/store.py +563 -0
  53. sqlspec/adapters/duckdb/config.py +79 -21
  54. sqlspec/adapters/duckdb/data_dictionary.py +41 -2
  55. sqlspec/adapters/duckdb/driver.py +225 -44
  56. sqlspec/adapters/duckdb/litestar/__init__.py +5 -0
  57. sqlspec/adapters/duckdb/litestar/store.py +332 -0
  58. sqlspec/adapters/duckdb/pool.py +5 -5
  59. sqlspec/adapters/duckdb/type_converter.py +51 -21
  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 +1628 -0
  64. sqlspec/adapters/oracledb/config.py +120 -36
  65. sqlspec/adapters/oracledb/data_dictionary.py +87 -20
  66. sqlspec/adapters/oracledb/driver.py +475 -86
  67. sqlspec/adapters/oracledb/litestar/__init__.py +5 -0
  68. sqlspec/adapters/oracledb/litestar/store.py +765 -0
  69. sqlspec/adapters/oracledb/migrations.py +316 -25
  70. sqlspec/adapters/oracledb/type_converter.py +91 -16
  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 +483 -0
  75. sqlspec/adapters/psqlpy/config.py +45 -19
  76. sqlspec/adapters/psqlpy/data_dictionary.py +48 -2
  77. sqlspec/adapters/psqlpy/driver.py +108 -41
  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 +40 -11
  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 +962 -0
  85. sqlspec/adapters/psycopg/config.py +65 -37
  86. sqlspec/adapters/psycopg/data_dictionary.py +91 -3
  87. sqlspec/adapters/psycopg/driver.py +200 -78
  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 +582 -0
  95. sqlspec/adapters/sqlite/config.py +85 -16
  96. sqlspec/adapters/sqlite/data_dictionary.py +34 -2
  97. sqlspec/adapters/sqlite/driver.py +120 -52
  98. sqlspec/adapters/sqlite/litestar/__init__.py +5 -0
  99. sqlspec/adapters/sqlite/litestar/store.py +318 -0
  100. sqlspec/adapters/sqlite/pool.py +5 -5
  101. sqlspec/base.py +45 -26
  102. sqlspec/builder/__init__.py +73 -4
  103. sqlspec/builder/_base.py +91 -58
  104. sqlspec/builder/_column.py +5 -5
  105. sqlspec/builder/_ddl.py +98 -89
  106. sqlspec/builder/_delete.py +5 -4
  107. sqlspec/builder/_dml.py +388 -0
  108. sqlspec/{_sql.py → builder/_factory.py} +41 -44
  109. sqlspec/builder/_insert.py +5 -82
  110. sqlspec/builder/{mixins/_join_operations.py → _join.py} +145 -143
  111. sqlspec/builder/_merge.py +446 -11
  112. sqlspec/builder/_parsing_utils.py +9 -11
  113. sqlspec/builder/_select.py +1313 -25
  114. sqlspec/builder/_update.py +11 -42
  115. sqlspec/cli.py +76 -69
  116. sqlspec/config.py +331 -62
  117. sqlspec/core/__init__.py +5 -4
  118. sqlspec/core/cache.py +18 -18
  119. sqlspec/core/compiler.py +6 -8
  120. sqlspec/core/filters.py +55 -47
  121. sqlspec/core/hashing.py +9 -9
  122. sqlspec/core/parameters.py +76 -45
  123. sqlspec/core/result.py +234 -47
  124. sqlspec/core/splitter.py +16 -17
  125. sqlspec/core/statement.py +32 -31
  126. sqlspec/core/type_conversion.py +3 -2
  127. sqlspec/driver/__init__.py +1 -3
  128. sqlspec/driver/_async.py +183 -160
  129. sqlspec/driver/_common.py +197 -109
  130. sqlspec/driver/_sync.py +189 -161
  131. sqlspec/driver/mixins/_result_tools.py +20 -236
  132. sqlspec/driver/mixins/_sql_translator.py +4 -4
  133. sqlspec/exceptions.py +70 -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 +69 -61
  142. sqlspec/extensions/fastapi/__init__.py +21 -0
  143. sqlspec/extensions/fastapi/extension.py +331 -0
  144. sqlspec/extensions/fastapi/providers.py +543 -0
  145. sqlspec/extensions/flask/__init__.py +36 -0
  146. sqlspec/extensions/flask/_state.py +71 -0
  147. sqlspec/extensions/flask/_utils.py +40 -0
  148. sqlspec/extensions/flask/extension.py +389 -0
  149. sqlspec/extensions/litestar/__init__.py +21 -4
  150. sqlspec/extensions/litestar/cli.py +54 -10
  151. sqlspec/extensions/litestar/config.py +56 -266
  152. sqlspec/extensions/litestar/handlers.py +46 -17
  153. sqlspec/extensions/litestar/migrations/0001_create_session_table.py +137 -0
  154. sqlspec/extensions/litestar/migrations/__init__.py +3 -0
  155. sqlspec/extensions/litestar/plugin.py +349 -224
  156. sqlspec/extensions/litestar/providers.py +25 -25
  157. sqlspec/extensions/litestar/store.py +265 -0
  158. sqlspec/extensions/starlette/__init__.py +10 -0
  159. sqlspec/extensions/starlette/_state.py +25 -0
  160. sqlspec/extensions/starlette/_utils.py +52 -0
  161. sqlspec/extensions/starlette/extension.py +254 -0
  162. sqlspec/extensions/starlette/middleware.py +154 -0
  163. sqlspec/loader.py +30 -49
  164. sqlspec/migrations/base.py +200 -76
  165. sqlspec/migrations/commands.py +591 -62
  166. sqlspec/migrations/context.py +6 -9
  167. sqlspec/migrations/fix.py +199 -0
  168. sqlspec/migrations/loaders.py +47 -19
  169. sqlspec/migrations/runner.py +241 -75
  170. sqlspec/migrations/tracker.py +237 -21
  171. sqlspec/migrations/utils.py +51 -3
  172. sqlspec/migrations/validation.py +177 -0
  173. sqlspec/protocols.py +106 -36
  174. sqlspec/storage/_utils.py +85 -0
  175. sqlspec/storage/backends/fsspec.py +133 -107
  176. sqlspec/storage/backends/local.py +78 -51
  177. sqlspec/storage/backends/obstore.py +276 -168
  178. sqlspec/storage/registry.py +75 -39
  179. sqlspec/typing.py +30 -84
  180. sqlspec/utils/__init__.py +25 -4
  181. sqlspec/utils/arrow_helpers.py +81 -0
  182. sqlspec/utils/config_resolver.py +6 -6
  183. sqlspec/utils/correlation.py +4 -5
  184. sqlspec/utils/data_transformation.py +3 -2
  185. sqlspec/utils/deprecation.py +9 -8
  186. sqlspec/utils/fixtures.py +4 -4
  187. sqlspec/utils/logging.py +46 -6
  188. sqlspec/utils/module_loader.py +205 -5
  189. sqlspec/utils/portal.py +311 -0
  190. sqlspec/utils/schema.py +288 -0
  191. sqlspec/utils/serializers.py +113 -4
  192. sqlspec/utils/sync_tools.py +36 -22
  193. sqlspec/utils/text.py +1 -2
  194. sqlspec/utils/type_guards.py +136 -20
  195. sqlspec/utils/version.py +433 -0
  196. {sqlspec-0.26.0.dist-info → sqlspec-0.28.0.dist-info}/METADATA +41 -22
  197. sqlspec-0.28.0.dist-info/RECORD +221 -0
  198. sqlspec/builder/mixins/__init__.py +0 -55
  199. sqlspec/builder/mixins/_cte_and_set_ops.py +0 -253
  200. sqlspec/builder/mixins/_delete_operations.py +0 -50
  201. sqlspec/builder/mixins/_insert_operations.py +0 -282
  202. sqlspec/builder/mixins/_merge_operations.py +0 -698
  203. sqlspec/builder/mixins/_order_limit_operations.py +0 -145
  204. sqlspec/builder/mixins/_pivot_operations.py +0 -157
  205. sqlspec/builder/mixins/_select_operations.py +0 -930
  206. sqlspec/builder/mixins/_update_operations.py +0 -199
  207. sqlspec/builder/mixins/_where_clause.py +0 -1298
  208. sqlspec-0.26.0.dist-info/RECORD +0 -157
  209. sqlspec-0.26.0.dist-info/licenses/NOTICE +0 -29
  210. {sqlspec-0.26.0.dist-info → sqlspec-0.28.0.dist-info}/WHEEL +0 -0
  211. {sqlspec-0.26.0.dist-info → sqlspec-0.28.0.dist-info}/entry_points.txt +0 -0
  212. {sqlspec-0.26.0.dist-info → sqlspec-0.28.0.dist-info}/licenses/LICENSE +0 -0
sqlspec/builder/_merge.py CHANGED
@@ -4,24 +4,459 @@ Provides a fluent interface for building SQL MERGE queries with
4
4
  parameter binding and validation.
5
5
  """
6
6
 
7
- from typing import Any, Optional
7
+ from collections.abc import Mapping, Sequence
8
+ from itertools import starmap
9
+ from typing import Any
8
10
 
11
+ from mypy_extensions import trait
9
12
  from sqlglot import exp
13
+ from typing_extensions import Self
10
14
 
11
15
  from sqlspec.builder._base import QueryBuilder
12
- from sqlspec.builder.mixins import (
13
- MergeIntoClauseMixin,
14
- MergeMatchedClauseMixin,
15
- MergeNotMatchedBySourceClauseMixin,
16
- MergeNotMatchedClauseMixin,
17
- MergeOnClauseMixin,
18
- MergeUsingClauseMixin,
19
- )
16
+ from sqlspec.builder._parsing_utils import extract_sql_object_expression
20
17
  from sqlspec.core.result import SQLResult
18
+ from sqlspec.exceptions import SQLBuilderError
19
+ from sqlspec.utils.type_guards import has_query_builder_parameters
21
20
 
22
21
  __all__ = ("Merge",)
23
22
 
24
23
 
24
+ class _MergeAssignmentMixin:
25
+ """Shared assignment helpers for MERGE clause mixins."""
26
+
27
+ __slots__ = ()
28
+
29
+ def add_parameter(self, value: Any, name: str | None = None) -> tuple[Any, str]:
30
+ msg = "Method must be provided by QueryBuilder subclass"
31
+ raise NotImplementedError(msg)
32
+
33
+ def _generate_unique_parameter_name(self, base_name: str) -> str:
34
+ msg = "Method must be provided by QueryBuilder subclass"
35
+ raise NotImplementedError(msg)
36
+
37
+ def _is_column_reference(self, value: str) -> bool:
38
+ if not isinstance(value, str):
39
+ return False
40
+ candidate = value.strip()
41
+ sql_keywords = {"NULL", "CURRENT_TIMESTAMP", "CURRENT_DATE", "CURRENT_TIME", "DEFAULT"}
42
+ if candidate.upper() in sql_keywords:
43
+ return True
44
+ if "(" not in candidate and ")" not in candidate:
45
+ return False
46
+ try:
47
+ parsed: exp.Expression | None = exp.maybe_parse(candidate)
48
+ except Exception:
49
+ return False
50
+ if parsed is None:
51
+ return False
52
+ return isinstance(
53
+ parsed, (exp.Dot, exp.Anonymous, exp.Func, exp.Null, exp.CurrentTimestamp, exp.CurrentDate, exp.CurrentTime)
54
+ )
55
+
56
+ def _process_assignment(self, target_column: str, value: Any) -> exp.Expression:
57
+ column_identifier = exp.column(target_column) if isinstance(target_column, str) else target_column
58
+
59
+ if hasattr(value, "expression") and hasattr(value, "sql"):
60
+ value_expr = extract_sql_object_expression(value, builder=self)
61
+ return exp.EQ(this=column_identifier, expression=value_expr)
62
+ if isinstance(value, exp.Expression):
63
+ return exp.EQ(this=column_identifier, expression=value)
64
+ if isinstance(value, str) and self._is_column_reference(value):
65
+ parsed_expression: exp.Expression | None = exp.maybe_parse(value)
66
+ if parsed_expression is None:
67
+ msg = f"Could not parse assignment expression: {value}"
68
+ raise SQLBuilderError(msg)
69
+ return exp.EQ(this=column_identifier, expression=parsed_expression)
70
+
71
+ column_name = target_column if isinstance(target_column, str) else str(target_column)
72
+ column_leaf = column_name.split(".")[-1]
73
+ param_name = self._generate_unique_parameter_name(column_leaf)
74
+ _, param_name = self.add_parameter(value, name=param_name)
75
+ placeholder = exp.Placeholder(this=param_name)
76
+ return exp.EQ(this=column_identifier, expression=placeholder)
77
+
78
+
79
+ @trait
80
+ class MergeIntoClauseMixin:
81
+ """Mixin providing INTO clause for MERGE builders."""
82
+
83
+ __slots__ = ()
84
+
85
+ def get_expression(self) -> exp.Expression | None: ...
86
+ def set_expression(self, expression: exp.Expression) -> None: ...
87
+
88
+ def into(self, table: str | exp.Expression, alias: str | None = None) -> Self:
89
+ current_expr = self.get_expression()
90
+ if current_expr is None or not isinstance(current_expr, exp.Merge):
91
+ self.set_expression(exp.Merge(this=None, using=None, on=None, whens=exp.Whens(expressions=[])))
92
+ current_expr = self.get_expression()
93
+
94
+ assert current_expr is not None
95
+ current_expr.set("this", exp.to_table(table, alias=alias) if isinstance(table, str) else table)
96
+ return self
97
+
98
+
99
+ @trait
100
+ class MergeUsingClauseMixin(_MergeAssignmentMixin):
101
+ """Mixin providing USING clause for MERGE builders."""
102
+
103
+ __slots__ = ()
104
+
105
+ def get_expression(self) -> exp.Expression | None: ...
106
+ def set_expression(self, expression: exp.Expression) -> None: ...
107
+
108
+ def add_parameter(self, value: Any, name: str | None = None) -> tuple[Any, str]:
109
+ msg = "Method must be provided by QueryBuilder subclass"
110
+ raise NotImplementedError(msg)
111
+
112
+ def _generate_unique_parameter_name(self, base_name: str) -> str:
113
+ msg = "Method must be provided by QueryBuilder subclass"
114
+ raise NotImplementedError(msg)
115
+
116
+ def using(self, source: str | exp.Expression | Any, alias: str | None = None) -> Self:
117
+ current_expr = self.get_expression()
118
+ if current_expr is None or not isinstance(current_expr, exp.Merge):
119
+ self.set_expression(exp.Merge(this=None, using=None, on=None, whens=exp.Whens(expressions=[])))
120
+ current_expr = self.get_expression()
121
+
122
+ assert current_expr is not None
123
+ source_expr: exp.Expression
124
+ if isinstance(source, str):
125
+ source_expr = exp.to_table(source, alias=alias)
126
+ elif isinstance(source, dict):
127
+ columns = list(source.keys())
128
+ values = list(source.values())
129
+
130
+ parameterized_values: list[exp.Expression] = []
131
+ for column, value in zip(columns, values, strict=False):
132
+ column_name = column if isinstance(column, str) else str(column)
133
+ if "." in column_name:
134
+ column_name = column_name.split(".")[-1]
135
+ param_name = self._generate_unique_parameter_name(column_name)
136
+ _, param_name = self.add_parameter(value, name=param_name)
137
+ parameterized_values.append(exp.Placeholder(this=param_name))
138
+
139
+ select_expr = exp.Select()
140
+ select_expr.set(
141
+ "expressions", [exp.alias_(parameterized_values[index], column) for index, column in enumerate(columns)]
142
+ )
143
+ select_expr.set("from", exp.From(this=exp.to_table("DUAL")))
144
+
145
+ source_expr = exp.paren(select_expr)
146
+ if alias:
147
+ source_expr = exp.alias_(source_expr, alias, table=False)
148
+ elif has_query_builder_parameters(source) and hasattr(source, "_expression"):
149
+ parameters_obj = getattr(source, "parameters", None)
150
+ if isinstance(parameters_obj, dict):
151
+ for param_name, param_value in parameters_obj.items():
152
+ self.add_parameter(param_value, name=param_name)
153
+ elif isinstance(parameters_obj, (list, tuple)):
154
+ for param_value in parameters_obj:
155
+ self.add_parameter(param_value)
156
+ elif parameters_obj is not None:
157
+ self.add_parameter(parameters_obj)
158
+ subquery_expression_source = getattr(source, "_expression", None)
159
+ subquery_expression = (
160
+ exp.paren(subquery_expression_source)
161
+ if isinstance(subquery_expression_source, exp.Expression)
162
+ else exp.paren(exp.select())
163
+ )
164
+ source_expr = exp.alias_(subquery_expression, alias) if alias else subquery_expression
165
+ elif isinstance(source, exp.Expression):
166
+ source_expr = exp.alias_(source, alias) if alias else source
167
+ else:
168
+ msg = f"Unsupported source type for USING clause: {type(source)}"
169
+ raise SQLBuilderError(msg)
170
+
171
+ current_expr.set("using", source_expr)
172
+ return self
173
+
174
+
175
+ @trait
176
+ class MergeOnClauseMixin:
177
+ """Mixin providing ON clause for MERGE builders."""
178
+
179
+ __slots__ = ()
180
+
181
+ def get_expression(self) -> exp.Expression | None: ...
182
+ def set_expression(self, expression: exp.Expression) -> None: ...
183
+
184
+ def on(self, condition: str | exp.Expression) -> Self:
185
+ current_expr = self.get_expression()
186
+ if current_expr is None or not isinstance(current_expr, exp.Merge):
187
+ self.set_expression(exp.Merge(this=None, using=None, on=None, whens=exp.Whens(expressions=[])))
188
+ current_expr = self.get_expression()
189
+
190
+ assert current_expr is not None
191
+ if isinstance(condition, str):
192
+ parsed_condition: exp.Expression | None = exp.maybe_parse(condition, dialect=getattr(self, "dialect", None))
193
+ if parsed_condition is None:
194
+ msg = f"Could not parse ON condition: {condition}"
195
+ raise SQLBuilderError(msg)
196
+ condition_expr = parsed_condition
197
+ elif isinstance(condition, exp.Expression):
198
+ condition_expr = condition
199
+ else:
200
+ msg = f"Unsupported condition type for ON clause: {type(condition)}"
201
+ raise SQLBuilderError(msg)
202
+
203
+ current_expr.set("on", condition_expr)
204
+ return self
205
+
206
+
207
+ @trait
208
+ class MergeMatchedClauseMixin(_MergeAssignmentMixin):
209
+ """Mixin providing WHEN MATCHED THEN ... clauses for MERGE builders."""
210
+
211
+ __slots__ = ()
212
+
213
+ def get_expression(self) -> exp.Expression | None: ...
214
+ def set_expression(self, expression: exp.Expression) -> None: ...
215
+
216
+ def add_parameter(self, value: Any, name: str | None = None) -> tuple[Any, str]:
217
+ msg = "Method must be provided by QueryBuilder subclass"
218
+ raise NotImplementedError(msg)
219
+
220
+ def _generate_unique_parameter_name(self, base_name: str) -> str:
221
+ msg = "Method must be provided by QueryBuilder subclass"
222
+ raise NotImplementedError(msg)
223
+
224
+ def when_matched_then_update(
225
+ self,
226
+ set_values: dict[str, Any] | None = None,
227
+ condition: str | exp.Expression | None = None,
228
+ **assignments: Any,
229
+ ) -> Self:
230
+ current_expr = self.get_expression()
231
+ if current_expr is None or not isinstance(current_expr, exp.Merge):
232
+ self.set_expression(exp.Merge(this=None, using=None, on=None, whens=exp.Whens(expressions=[])))
233
+ current_expr = self.get_expression()
234
+
235
+ assert current_expr is not None
236
+ combined_assignments: dict[str, Any] = {}
237
+ if set_values:
238
+ combined_assignments.update(set_values)
239
+ if assignments:
240
+ combined_assignments.update(assignments)
241
+
242
+ if not combined_assignments:
243
+ msg = "No update values provided. Use set_values or keyword arguments."
244
+ raise SQLBuilderError(msg)
245
+
246
+ set_expressions = list(starmap(self._process_assignment, combined_assignments.items()))
247
+ update_expression = exp.Update(expressions=set_expressions)
248
+
249
+ when_kwargs: dict[str, Any] = {"matched": True, "then": update_expression}
250
+ if condition is not None:
251
+ if isinstance(condition, str):
252
+ parsed_condition: exp.Expression | None = exp.maybe_parse(
253
+ condition, dialect=getattr(self, "dialect", None)
254
+ )
255
+ if parsed_condition is None:
256
+ msg = f"Could not parse WHEN clause condition: {condition}"
257
+ raise SQLBuilderError(msg)
258
+ when_kwargs["this"] = parsed_condition
259
+ elif isinstance(condition, exp.Expression):
260
+ when_kwargs["this"] = condition
261
+ else:
262
+ msg = f"Unsupported condition type for WHEN clause: {type(condition)}"
263
+ raise SQLBuilderError(msg)
264
+
265
+ whens = current_expr.args.get("whens")
266
+ if not isinstance(whens, exp.Whens):
267
+ whens = exp.Whens(expressions=[])
268
+ current_expr.set("whens", whens)
269
+ whens.append("expressions", exp.When(**when_kwargs))
270
+ return self
271
+
272
+ def when_matched_then_delete(self, condition: str | exp.Expression | None = None) -> Self:
273
+ current_expr = self.get_expression()
274
+ if current_expr is None or not isinstance(current_expr, exp.Merge):
275
+ self.set_expression(exp.Merge(this=None, using=None, on=None, whens=exp.Whens(expressions=[])))
276
+ current_expr = self.get_expression()
277
+
278
+ assert current_expr is not None
279
+ when_kwargs: dict[str, Any] = {"matched": True, "then": exp.Delete()}
280
+ if condition is not None:
281
+ if isinstance(condition, str):
282
+ parsed_condition: exp.Expression | None = exp.maybe_parse(
283
+ condition, dialect=getattr(self, "dialect", None)
284
+ )
285
+ if parsed_condition is None:
286
+ msg = f"Could not parse WHEN clause condition: {condition}"
287
+ raise SQLBuilderError(msg)
288
+ when_kwargs["this"] = parsed_condition
289
+ elif isinstance(condition, exp.Expression):
290
+ when_kwargs["this"] = condition
291
+ else:
292
+ msg = f"Unsupported condition type for WHEN clause: {type(condition)}"
293
+ raise SQLBuilderError(msg)
294
+
295
+ whens = current_expr.args.get("whens")
296
+ if not isinstance(whens, exp.Whens):
297
+ whens = exp.Whens(expressions=[])
298
+ current_expr.set("whens", whens)
299
+ whens.append("expressions", exp.When(**when_kwargs))
300
+ return self
301
+
302
+
303
+ @trait
304
+ class MergeNotMatchedClauseMixin(_MergeAssignmentMixin):
305
+ """Mixin providing WHEN NOT MATCHED THEN ... clauses for MERGE builders."""
306
+
307
+ __slots__ = ()
308
+
309
+ def get_expression(self) -> exp.Expression | None: ...
310
+ def set_expression(self, expression: exp.Expression) -> None: ...
311
+
312
+ def add_parameter(self, value: Any, name: str | None = None) -> tuple[Any, str]:
313
+ msg = "Method must be provided by QueryBuilder subclass"
314
+ raise NotImplementedError(msg)
315
+
316
+ def _generate_unique_parameter_name(self, base_name: str) -> str:
317
+ msg = "Method must be provided by QueryBuilder subclass"
318
+ raise NotImplementedError(msg)
319
+
320
+ def when_not_matched_then_insert(
321
+ self,
322
+ columns: Mapping[str, Any] | Sequence[str] | None = None,
323
+ values: Sequence[Any] | None = None,
324
+ **value_kwargs: Any,
325
+ ) -> Self:
326
+ current_expr = self.get_expression()
327
+ if current_expr is None or not isinstance(current_expr, exp.Merge):
328
+ self.set_expression(exp.Merge(this=None, using=None, on=None, whens=exp.Whens(expressions=[])))
329
+ current_expr = self.get_expression()
330
+
331
+ assert current_expr is not None
332
+ insert_expr = exp.Insert()
333
+ column_names: list[str]
334
+ column_values: list[Any]
335
+
336
+ if isinstance(columns, Mapping):
337
+ combined = dict(columns)
338
+ if value_kwargs:
339
+ combined.update(value_kwargs)
340
+ column_names = list(combined.keys())
341
+ column_values = list(combined.values())
342
+ elif value_kwargs:
343
+ column_names = list(value_kwargs.keys())
344
+ column_values = list(value_kwargs.values())
345
+ else:
346
+ if columns is None or values is None:
347
+ msg = "Columns and values must be provided when not using keyword arguments."
348
+ raise SQLBuilderError(msg)
349
+ column_names = [str(column) for column in columns]
350
+ column_values = list(values)
351
+ if len(column_names) != len(column_values):
352
+ msg = "Number of columns must match number of values for MERGE insert"
353
+ raise SQLBuilderError(msg)
354
+
355
+ insert_columns = [exp.column(name) for name in column_names]
356
+
357
+ insert_values: list[exp.Expression] = []
358
+ for column_name, value in zip(column_names, column_values, strict=True):
359
+ if hasattr(value, "expression") and hasattr(value, "sql"):
360
+ insert_values.append(extract_sql_object_expression(value, builder=self))
361
+ elif isinstance(value, exp.Expression):
362
+ insert_values.append(value)
363
+ elif isinstance(value, str):
364
+ param_name = self._generate_unique_parameter_name(column_name.split(".")[-1])
365
+ _, param_name = self.add_parameter(value, name=param_name)
366
+ insert_values.append(exp.Placeholder(this=param_name))
367
+ else:
368
+ param_name = self._generate_unique_parameter_name(column_name.split(".")[-1])
369
+ _, param_name = self.add_parameter(value, name=param_name)
370
+ insert_values.append(exp.Placeholder(this=param_name))
371
+
372
+ insert_expr.set("this", exp.Tuple(expressions=insert_columns))
373
+ insert_expr.set("expression", exp.Tuple(expressions=insert_values))
374
+ whens = current_expr.args.get("whens")
375
+ if not isinstance(whens, exp.Whens):
376
+ whens = exp.Whens(expressions=[])
377
+ current_expr.set("whens", whens)
378
+ whens.append("expressions", exp.When(matched=False, then=insert_expr))
379
+ return self
380
+
381
+
382
+ @trait
383
+ class MergeNotMatchedBySourceClauseMixin(_MergeAssignmentMixin):
384
+ """Mixin providing WHEN NOT MATCHED BY SOURCE THEN ... clauses."""
385
+
386
+ __slots__ = ()
387
+
388
+ def get_expression(self) -> exp.Expression | None: ...
389
+ def set_expression(self, expression: exp.Expression) -> None: ...
390
+
391
+ def add_parameter(self, value: Any, name: str | None = None) -> tuple[Any, str]:
392
+ msg = "Method must be provided by QueryBuilder subclass"
393
+ raise NotImplementedError(msg)
394
+
395
+ def _generate_unique_parameter_name(self, base_name: str) -> str:
396
+ msg = "Method must be provided by QueryBuilder subclass"
397
+ raise NotImplementedError(msg)
398
+
399
+ def when_not_matched_by_source_then_update(
400
+ self, set_values: dict[str, Any] | None = None, **assignments: Any
401
+ ) -> Self:
402
+ current_expr = self.get_expression()
403
+ if current_expr is None or not isinstance(current_expr, exp.Merge):
404
+ self.set_expression(exp.Merge(this=None, using=None, on=None, whens=exp.Whens(expressions=[])))
405
+ current_expr = self.get_expression()
406
+
407
+ assert current_expr is not None
408
+ combined_assignments: dict[str, Any] = {}
409
+ if set_values:
410
+ combined_assignments.update(set_values)
411
+ if assignments:
412
+ combined_assignments.update(assignments)
413
+
414
+ if not combined_assignments:
415
+ msg = "No update values provided. Use set_values or keyword arguments."
416
+ raise SQLBuilderError(msg)
417
+
418
+ set_expressions: list[exp.Expression] = []
419
+ for column_name, value in combined_assignments.items():
420
+ column_identifier = exp.column(column_name)
421
+ if hasattr(value, "expression") and hasattr(value, "sql"):
422
+ value_expr = extract_sql_object_expression(value, builder=self)
423
+ elif isinstance(value, exp.Expression):
424
+ value_expr = value
425
+ elif isinstance(value, str) and self._is_column_reference(value):
426
+ parsed_value: exp.Expression | None = exp.maybe_parse(value)
427
+ if parsed_value is None:
428
+ msg = f"Could not parse assignment expression: {value}"
429
+ raise SQLBuilderError(msg)
430
+ value_expr = parsed_value
431
+ else:
432
+ param_name = self._generate_unique_parameter_name(column_name)
433
+ _, param_name = self.add_parameter(value, name=param_name)
434
+ value_expr = exp.Placeholder(this=param_name)
435
+ set_expressions.append(exp.EQ(this=column_identifier, expression=value_expr))
436
+
437
+ update_expr = exp.Update(expressions=set_expressions)
438
+ whens = current_expr.args.get("whens")
439
+ if not isinstance(whens, exp.Whens):
440
+ whens = exp.Whens(expressions=[])
441
+ current_expr.set("whens", whens)
442
+ whens.append("expressions", exp.When(matched=False, source=True, then=update_expr))
443
+ return self
444
+
445
+ def when_not_matched_by_source_then_delete(self) -> Self:
446
+ current_expr = self.get_expression()
447
+ if current_expr is None or not isinstance(current_expr, exp.Merge):
448
+ self.set_expression(exp.Merge(this=None, using=None, on=None, whens=exp.Whens(expressions=[])))
449
+ current_expr = self.get_expression()
450
+
451
+ assert current_expr is not None
452
+ whens = current_expr.args.get("whens")
453
+ if not isinstance(whens, exp.Whens):
454
+ whens = exp.Whens(expressions=[])
455
+ current_expr.set("whens", whens)
456
+ whens.append("expressions", exp.When(matched=False, source=True, then=exp.Delete()))
457
+ return self
458
+
459
+
25
460
  class Merge(
26
461
  QueryBuilder,
27
462
  MergeUsingClauseMixin,
@@ -38,9 +473,9 @@ class Merge(
38
473
  """
39
474
 
40
475
  __slots__ = ()
41
- _expression: Optional[exp.Expression]
476
+ _expression: exp.Expression | None
42
477
 
43
- def __init__(self, target_table: Optional[str] = None, **kwargs: Any) -> None:
478
+ def __init__(self, target_table: str | None = None, **kwargs: Any) -> None:
44
479
  """Initialize MERGE with optional target table.
45
480
 
46
481
  Args:
@@ -5,7 +5,7 @@ passed as strings to builder methods.
5
5
  """
6
6
 
7
7
  import contextlib
8
- from typing import Any, Final, Optional, Union, cast
8
+ from typing import Any, Final, cast
9
9
 
10
10
  from sqlglot import exp, maybe_parse, parse_one
11
11
 
@@ -18,7 +18,7 @@ from sqlspec.utils.type_guards import (
18
18
  )
19
19
 
20
20
 
21
- def extract_column_name(column: Union[str, exp.Column]) -> str:
21
+ def extract_column_name(column: str | exp.Column) -> str:
22
22
  """Extract column name from column expression for parameter naming.
23
23
 
24
24
  Args:
@@ -39,9 +39,7 @@ def extract_column_name(column: Union[str, exp.Column]) -> str:
39
39
  return "column"
40
40
 
41
41
 
42
- def parse_column_expression(
43
- column_input: Union[str, exp.Expression, Any], builder: Optional[Any] = None
44
- ) -> exp.Expression:
42
+ def parse_column_expression(column_input: str | exp.Expression | Any, builder: Any | None = None) -> exp.Expression:
45
43
  """Parse a column input that might be a complex expression.
46
44
 
47
45
  Handles cases like:
@@ -87,7 +85,7 @@ def parse_column_expression(
87
85
  return exp.maybe_parse(column_input) or exp.column(str(column_input))
88
86
 
89
87
 
90
- def parse_table_expression(table_input: str, explicit_alias: Optional[str] = None) -> exp.Expression:
88
+ def parse_table_expression(table_input: str, explicit_alias: str | None = None) -> exp.Expression:
91
89
  """Parses a table string that can be a name, a name with an alias, or a subquery string."""
92
90
  with contextlib.suppress(Exception):
93
91
  parsed = parse_one(f"SELECT * FROM {table_input}")
@@ -102,7 +100,7 @@ def parse_table_expression(table_input: str, explicit_alias: Optional[str] = Non
102
100
  return exp.to_table(table_input, alias=explicit_alias)
103
101
 
104
102
 
105
- def parse_order_expression(order_input: Union[str, exp.Expression]) -> exp.Expression:
103
+ def parse_order_expression(order_input: str | exp.Expression) -> exp.Expression:
106
104
  """Parse an ORDER BY expression that might include direction.
107
105
 
108
106
  Handles cases like:
@@ -129,7 +127,7 @@ def parse_order_expression(order_input: Union[str, exp.Expression]) -> exp.Expre
129
127
 
130
128
 
131
129
  def parse_condition_expression(
132
- condition_input: Union[str, exp.Expression, tuple[str, Any]], builder: "Any" = None
130
+ condition_input: str | exp.Expression | tuple[str, Any], builder: "Any" = None
133
131
  ) -> exp.Expression:
134
132
  """Parse a condition that might be complex SQL.
135
133
 
@@ -193,13 +191,13 @@ def parse_condition_expression(
193
191
  )
194
192
  condition_input = converted_condition
195
193
 
196
- parsed: Optional[exp.Expression] = exp.maybe_parse(condition_input)
194
+ parsed: exp.Expression | None = exp.maybe_parse(condition_input)
197
195
  if parsed:
198
196
  return parsed
199
197
  return exp.condition(condition_input)
200
198
 
201
199
 
202
- def extract_sql_object_expression(value: Any, builder: Optional[Any] = None) -> exp.Expression:
200
+ def extract_sql_object_expression(value: Any, builder: Any | None = None) -> exp.Expression:
203
201
  """Extract SQLGlot expression from SQL object value with parameter merging.
204
202
 
205
203
  Handles the common pattern of:
@@ -264,7 +262,7 @@ def extract_expression(value: Any) -> exp.Expression:
264
262
  """
265
263
  from sqlspec.builder._column import Column
266
264
  from sqlspec.builder._expression_wrappers import ExpressionWrapper
267
- from sqlspec.builder.mixins._select_operations import Case
265
+ from sqlspec.builder._select import Case
268
266
 
269
267
  if isinstance(value, str):
270
268
  return exp.column(value)