sqlspec 0.24.1__py3-none-any.whl → 0.26.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 (95) hide show
  1. sqlspec/_serialization.py +223 -21
  2. sqlspec/_sql.py +20 -62
  3. sqlspec/_typing.py +11 -0
  4. sqlspec/adapters/adbc/config.py +8 -1
  5. sqlspec/adapters/adbc/data_dictionary.py +290 -0
  6. sqlspec/adapters/adbc/driver.py +129 -20
  7. sqlspec/adapters/adbc/type_converter.py +159 -0
  8. sqlspec/adapters/aiosqlite/config.py +3 -0
  9. sqlspec/adapters/aiosqlite/data_dictionary.py +117 -0
  10. sqlspec/adapters/aiosqlite/driver.py +17 -3
  11. sqlspec/adapters/asyncmy/_types.py +1 -1
  12. sqlspec/adapters/asyncmy/config.py +11 -8
  13. sqlspec/adapters/asyncmy/data_dictionary.py +122 -0
  14. sqlspec/adapters/asyncmy/driver.py +31 -7
  15. sqlspec/adapters/asyncpg/config.py +3 -0
  16. sqlspec/adapters/asyncpg/data_dictionary.py +134 -0
  17. sqlspec/adapters/asyncpg/driver.py +19 -4
  18. sqlspec/adapters/bigquery/config.py +3 -0
  19. sqlspec/adapters/bigquery/data_dictionary.py +109 -0
  20. sqlspec/adapters/bigquery/driver.py +21 -3
  21. sqlspec/adapters/bigquery/type_converter.py +93 -0
  22. sqlspec/adapters/duckdb/_types.py +1 -1
  23. sqlspec/adapters/duckdb/config.py +2 -0
  24. sqlspec/adapters/duckdb/data_dictionary.py +124 -0
  25. sqlspec/adapters/duckdb/driver.py +32 -5
  26. sqlspec/adapters/duckdb/pool.py +1 -1
  27. sqlspec/adapters/duckdb/type_converter.py +103 -0
  28. sqlspec/adapters/oracledb/config.py +6 -0
  29. sqlspec/adapters/oracledb/data_dictionary.py +442 -0
  30. sqlspec/adapters/oracledb/driver.py +68 -9
  31. sqlspec/adapters/oracledb/migrations.py +51 -67
  32. sqlspec/adapters/oracledb/type_converter.py +132 -0
  33. sqlspec/adapters/psqlpy/config.py +3 -0
  34. sqlspec/adapters/psqlpy/data_dictionary.py +133 -0
  35. sqlspec/adapters/psqlpy/driver.py +23 -179
  36. sqlspec/adapters/psqlpy/type_converter.py +73 -0
  37. sqlspec/adapters/psycopg/config.py +8 -4
  38. sqlspec/adapters/psycopg/data_dictionary.py +257 -0
  39. sqlspec/adapters/psycopg/driver.py +40 -5
  40. sqlspec/adapters/sqlite/config.py +3 -0
  41. sqlspec/adapters/sqlite/data_dictionary.py +117 -0
  42. sqlspec/adapters/sqlite/driver.py +18 -3
  43. sqlspec/adapters/sqlite/pool.py +13 -4
  44. sqlspec/base.py +3 -4
  45. sqlspec/builder/_base.py +130 -48
  46. sqlspec/builder/_column.py +66 -24
  47. sqlspec/builder/_ddl.py +91 -41
  48. sqlspec/builder/_insert.py +40 -58
  49. sqlspec/builder/_parsing_utils.py +127 -12
  50. sqlspec/builder/_select.py +147 -2
  51. sqlspec/builder/_update.py +1 -1
  52. sqlspec/builder/mixins/_cte_and_set_ops.py +31 -23
  53. sqlspec/builder/mixins/_delete_operations.py +12 -7
  54. sqlspec/builder/mixins/_insert_operations.py +50 -36
  55. sqlspec/builder/mixins/_join_operations.py +15 -30
  56. sqlspec/builder/mixins/_merge_operations.py +210 -78
  57. sqlspec/builder/mixins/_order_limit_operations.py +4 -10
  58. sqlspec/builder/mixins/_pivot_operations.py +1 -0
  59. sqlspec/builder/mixins/_select_operations.py +44 -22
  60. sqlspec/builder/mixins/_update_operations.py +30 -37
  61. sqlspec/builder/mixins/_where_clause.py +52 -70
  62. sqlspec/cli.py +246 -140
  63. sqlspec/config.py +33 -19
  64. sqlspec/core/__init__.py +3 -2
  65. sqlspec/core/cache.py +298 -352
  66. sqlspec/core/compiler.py +61 -4
  67. sqlspec/core/filters.py +246 -213
  68. sqlspec/core/hashing.py +9 -11
  69. sqlspec/core/parameters.py +27 -10
  70. sqlspec/core/statement.py +72 -12
  71. sqlspec/core/type_conversion.py +234 -0
  72. sqlspec/driver/__init__.py +6 -3
  73. sqlspec/driver/_async.py +108 -5
  74. sqlspec/driver/_common.py +186 -17
  75. sqlspec/driver/_sync.py +108 -5
  76. sqlspec/driver/mixins/_result_tools.py +60 -7
  77. sqlspec/exceptions.py +5 -0
  78. sqlspec/loader.py +8 -9
  79. sqlspec/migrations/__init__.py +4 -3
  80. sqlspec/migrations/base.py +153 -14
  81. sqlspec/migrations/commands.py +34 -96
  82. sqlspec/migrations/context.py +145 -0
  83. sqlspec/migrations/loaders.py +25 -8
  84. sqlspec/migrations/runner.py +352 -82
  85. sqlspec/storage/backends/fsspec.py +1 -0
  86. sqlspec/typing.py +4 -0
  87. sqlspec/utils/config_resolver.py +153 -0
  88. sqlspec/utils/serializers.py +50 -2
  89. {sqlspec-0.24.1.dist-info → sqlspec-0.26.0.dist-info}/METADATA +1 -1
  90. sqlspec-0.26.0.dist-info/RECORD +157 -0
  91. sqlspec-0.24.1.dist-info/RECORD +0 -139
  92. {sqlspec-0.24.1.dist-info → sqlspec-0.26.0.dist-info}/WHEEL +0 -0
  93. {sqlspec-0.24.1.dist-info → sqlspec-0.26.0.dist-info}/entry_points.txt +0 -0
  94. {sqlspec-0.24.1.dist-info → sqlspec-0.26.0.dist-info}/licenses/LICENSE +0 -0
  95. {sqlspec-0.24.1.dist-info → sqlspec-0.26.0.dist-info}/licenses/NOTICE +0 -0
@@ -1,3 +1,4 @@
1
+ # pyright: reportPrivateUsage=false
1
2
  """MERGE operation mixins.
2
3
 
3
4
  Provides mixins for MERGE statement functionality including INTO,
@@ -10,6 +11,7 @@ from mypy_extensions import trait
10
11
  from sqlglot import exp
11
12
  from typing_extensions import Self
12
13
 
14
+ from sqlspec.builder._parsing_utils import extract_sql_object_expression
13
15
  from sqlspec.exceptions import SQLBuilderError
14
16
  from sqlspec.utils.type_guards import has_query_builder_parameters
15
17
 
@@ -28,7 +30,9 @@ class MergeIntoClauseMixin:
28
30
  """Mixin providing INTO clause for MERGE builders."""
29
31
 
30
32
  __slots__ = ()
31
- _expression: Optional[exp.Expression]
33
+
34
+ def get_expression(self) -> Optional[exp.Expression]: ...
35
+ def set_expression(self, expression: exp.Expression) -> None: ...
32
36
 
33
37
  def into(self, table: Union[str, exp.Expression], alias: Optional[str] = None) -> Self:
34
38
  """Set the target table for the MERGE operation (INTO clause).
@@ -41,11 +45,13 @@ class MergeIntoClauseMixin:
41
45
  Returns:
42
46
  The current builder instance for method chaining.
43
47
  """
44
- if self._expression is None:
45
- self._expression = exp.Merge(this=None, using=None, on=None, whens=exp.Whens(expressions=[]))
46
- if not isinstance(self._expression, exp.Merge):
47
- self._expression = exp.Merge(this=None, using=None, on=None, whens=exp.Whens(expressions=[]))
48
- self._expression.set("this", exp.to_table(table, alias=alias) if isinstance(table, str) else table)
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
+ assert current_expr is not None
54
+ current_expr.set("this", exp.to_table(table, alias=alias) if isinstance(table, str) else table)
49
55
  return self
50
56
 
51
57
 
@@ -54,13 +60,20 @@ class MergeUsingClauseMixin:
54
60
  """Mixin providing USING clause for MERGE builders."""
55
61
 
56
62
  __slots__ = ()
57
- _expression: Optional[exp.Expression]
63
+
64
+ def get_expression(self) -> Optional[exp.Expression]: ...
65
+ def set_expression(self, expression: exp.Expression) -> None: ...
58
66
 
59
67
  def add_parameter(self, value: Any, name: Optional[str] = None) -> tuple[Any, str]:
60
68
  """Add parameter - provided by QueryBuilder."""
61
69
  msg = "Method must be provided by QueryBuilder subclass"
62
70
  raise NotImplementedError(msg)
63
71
 
72
+ def _generate_unique_parameter_name(self, base_name: str) -> str:
73
+ """Generate unique parameter name - provided by QueryBuilder."""
74
+ msg = "Method must be provided by QueryBuilder subclass"
75
+ raise NotImplementedError(msg)
76
+
64
77
  def using(self, source: Union[str, exp.Expression, Any], alias: Optional[str] = None) -> Self:
65
78
  """Set the source data for the MERGE operation (USING clause).
66
79
 
@@ -75,14 +88,40 @@ class MergeUsingClauseMixin:
75
88
  Raises:
76
89
  SQLBuilderError: If the current expression is not a MERGE statement or if the source type is unsupported.
77
90
  """
78
- if self._expression is None:
79
- self._expression = exp.Merge(this=None, using=None, on=None, whens=exp.Whens(expressions=[]))
80
- if not isinstance(self._expression, exp.Merge):
81
- self._expression = exp.Merge(this=None, using=None, on=None, whens=exp.Whens(expressions=[]))
91
+ current_expr = self.get_expression()
92
+ if current_expr is None or not isinstance(current_expr, exp.Merge):
93
+ self.set_expression(exp.Merge(this=None, using=None, on=None, whens=exp.Whens(expressions=[])))
94
+ current_expr = self.get_expression()
82
95
 
96
+ assert current_expr is not None
83
97
  source_expr: exp.Expression
84
98
  if isinstance(source, str):
85
99
  source_expr = exp.to_table(source, alias=alias)
100
+ elif isinstance(source, dict):
101
+ columns = list(source.keys())
102
+ values = list(source.values())
103
+
104
+ parameterized_values: list[exp.Expression] = []
105
+ for col, val in zip(columns, values):
106
+ column_name = col if isinstance(col, str) else str(col)
107
+ if "." in column_name:
108
+ column_name = column_name.split(".")[-1]
109
+ param_name = self._generate_unique_parameter_name(column_name)
110
+ _, param_name = self.add_parameter(val, name=param_name)
111
+ parameterized_values.append(exp.Placeholder(this=param_name))
112
+
113
+ select_expr = exp.Select()
114
+ select_expressions = []
115
+ for i, col in enumerate(columns):
116
+ select_expressions.append(exp.alias_(parameterized_values[i], col))
117
+ select_expr.set("expressions", select_expressions)
118
+
119
+ from_expr = exp.From(this=exp.to_table("DUAL"))
120
+ select_expr.set("from", from_expr)
121
+
122
+ source_expr = exp.paren(select_expr)
123
+ if alias:
124
+ source_expr = exp.alias_(source_expr, alias, table=False)
86
125
  elif has_query_builder_parameters(source) and hasattr(source, "_expression"):
87
126
  subquery_builder_parameters = source.parameters
88
127
  if subquery_builder_parameters:
@@ -99,7 +138,7 @@ class MergeUsingClauseMixin:
99
138
  msg = f"Unsupported source type for USING clause: {type(source)}"
100
139
  raise SQLBuilderError(msg)
101
140
 
102
- self._expression.set("using", source_expr)
141
+ current_expr.set("using", source_expr)
103
142
  return self
104
143
 
105
144
 
@@ -108,7 +147,9 @@ class MergeOnClauseMixin:
108
147
  """Mixin providing ON clause for MERGE builders."""
109
148
 
110
149
  __slots__ = ()
111
- _expression: Optional[exp.Expression]
150
+
151
+ def get_expression(self) -> Optional[exp.Expression]: ...
152
+ def set_expression(self, expression: exp.Expression) -> None: ...
112
153
 
113
154
  def on(self, condition: Union[str, exp.Expression]) -> Self:
114
155
  """Set the join condition for the MERGE operation (ON clause).
@@ -123,11 +164,12 @@ class MergeOnClauseMixin:
123
164
  Raises:
124
165
  SQLBuilderError: If the current expression is not a MERGE statement or if the condition type is unsupported.
125
166
  """
126
- if self._expression is None:
127
- self._expression = exp.Merge(this=None, using=None, on=None, whens=exp.Whens(expressions=[]))
128
- if not isinstance(self._expression, exp.Merge):
129
- self._expression = exp.Merge(this=None, using=None, on=None, whens=exp.Whens(expressions=[]))
167
+ current_expr = self.get_expression()
168
+ if current_expr is None or not isinstance(current_expr, exp.Merge):
169
+ self.set_expression(exp.Merge(this=None, using=None, on=None, whens=exp.Whens(expressions=[])))
170
+ current_expr = self.get_expression()
130
171
 
172
+ assert current_expr is not None
131
173
  condition_expr: exp.Expression
132
174
  if isinstance(condition, str):
133
175
  parsed_condition: Optional[exp.Expression] = exp.maybe_parse(
@@ -143,7 +185,7 @@ class MergeOnClauseMixin:
143
185
  msg = f"Unsupported condition type for ON clause: {type(condition)}"
144
186
  raise SQLBuilderError(msg)
145
187
 
146
- self._expression.set("on", condition_expr)
188
+ current_expr.set("on", condition_expr)
147
189
  return self
148
190
 
149
191
 
@@ -152,7 +194,9 @@ class MergeMatchedClauseMixin:
152
194
  """Mixin providing WHEN MATCHED THEN ... clauses for MERGE builders."""
153
195
 
154
196
  __slots__ = ()
155
- _expression: Optional[exp.Expression]
197
+
198
+ def get_expression(self) -> Optional[exp.Expression]: ...
199
+ def set_expression(self, expression: exp.Expression) -> None: ...
156
200
 
157
201
  def add_parameter(self, value: Any, name: Optional[str] = None) -> tuple[Any, str]:
158
202
  """Add parameter - provided by QueryBuilder."""
@@ -164,21 +208,58 @@ class MergeMatchedClauseMixin:
164
208
  msg = "Method must be provided by QueryBuilder subclass"
165
209
  raise NotImplementedError(msg)
166
210
 
211
+ def _is_column_reference(self, value: str) -> bool:
212
+ """Check if a string value is a column reference rather than a literal.
213
+
214
+ Uses sqlglot to parse the value and determine if it represents a column
215
+ reference, function call, or other SQL expression rather than a literal.
216
+ """
217
+ if not isinstance(value, str):
218
+ return False
219
+
220
+ # If the string contains spaces and no SQL-like syntax, treat as literal
221
+ if " " in value and not any(x in value for x in [".", "(", ")", "*", "="]):
222
+ return False
223
+
224
+ # Only consider strings with dots (table.column), functions, or SQL keywords as column references
225
+ # Simple identifiers are treated as literals
226
+ if not any(x in value for x in [".", "(", ")"]):
227
+ # Check if it's a SQL keyword/function that should be treated as expression
228
+ sql_keywords = {"NULL", "CURRENT_TIMESTAMP", "CURRENT_DATE", "CURRENT_TIME", "DEFAULT"}
229
+ if value.upper() not in sql_keywords:
230
+ return False
231
+
232
+ try:
233
+ # Try to parse as SQL expression
234
+ parsed: Optional[exp.Expression] = exp.maybe_parse(value)
235
+ if parsed is None:
236
+ return False
237
+
238
+ # Check for SQL literals that should be treated as expressions
239
+ return isinstance(
240
+ parsed,
241
+ (exp.Dot, exp.Anonymous, exp.Func, exp.Null, exp.CurrentTimestamp, exp.CurrentDate, exp.CurrentTime),
242
+ )
243
+ except Exception:
244
+ # If parsing fails, treat as literal
245
+ return False
246
+
167
247
  def _add_when_clause(self, when_clause: exp.When) -> None:
168
248
  """Helper to add a WHEN clause to the MERGE statement.
169
249
 
170
250
  Args:
171
251
  when_clause: The WHEN clause to add.
172
252
  """
173
- if self._expression is None:
174
- self._expression = exp.Merge(this=None, using=None, on=None, whens=exp.Whens(expressions=[])) # type: ignore[misc]
175
- if not isinstance(self._expression, exp.Merge):
176
- self._expression = exp.Merge(this=None, using=None, on=None, whens=exp.Whens(expressions=[])) # type: ignore[misc]
253
+ current_expr = self.get_expression()
254
+ if current_expr is None or not isinstance(current_expr, exp.Merge):
255
+ self.set_expression(exp.Merge(this=None, using=None, on=None, whens=exp.Whens(expressions=[])))
256
+ current_expr = self.get_expression()
177
257
 
178
- whens = self._expression.args.get("whens")
258
+ assert current_expr is not None
259
+ whens = current_expr.args.get("whens")
179
260
  if not whens:
180
261
  whens = exp.Whens(expressions=[])
181
- self._expression.set("whens", whens)
262
+ current_expr.set("whens", whens)
182
263
 
183
264
  whens.append("expressions", when_clause)
184
265
 
@@ -208,7 +289,11 @@ class MergeMatchedClauseMixin:
208
289
  The current builder instance for method chaining.
209
290
  """
210
291
  # Combine set_values dict and kwargs
211
- all_values = dict(set_values or {}, **kwargs)
292
+ all_values = {}
293
+ if set_values:
294
+ all_values.update(set_values)
295
+ if kwargs:
296
+ all_values.update(kwargs)
212
297
 
213
298
  if not all_values:
214
299
  msg = "No update values provided. Use set_values dict or kwargs."
@@ -217,35 +302,17 @@ class MergeMatchedClauseMixin:
217
302
  update_expressions: list[exp.EQ] = []
218
303
  for col, val in all_values.items():
219
304
  if hasattr(val, "expression") and hasattr(val, "sql"):
220
- # Handle SQL objects (from sql.raw with parameters)
221
- expression = getattr(val, "expression", None)
222
- if expression is not None and isinstance(expression, exp.Expression):
223
- # Merge parameters from SQL object into builder
224
- if hasattr(val, "parameters"):
225
- sql_parameters = getattr(val, "parameters", {})
226
- for param_name, param_value in sql_parameters.items():
227
- self.add_parameter(param_value, name=param_name)
228
- value_expr = expression
229
- else:
230
- # If expression is None, fall back to parsing the raw SQL
231
- sql_text = getattr(val, "sql", "")
232
- # Merge parameters even when parsing raw SQL
233
- if hasattr(val, "parameters"):
234
- sql_parameters = getattr(val, "parameters", {})
235
- for param_name, param_value in sql_parameters.items():
236
- self.add_parameter(param_value, name=param_name)
237
- # Check if sql_text is callable (like Expression.sql method)
238
- if callable(sql_text):
239
- sql_text = str(val)
240
- value_expr = exp.maybe_parse(sql_text) or exp.convert(str(sql_text))
305
+ value_expr = extract_sql_object_expression(val, builder=self)
241
306
  elif isinstance(val, exp.Expression):
242
307
  value_expr = val
308
+ elif isinstance(val, str) and self._is_column_reference(val):
309
+ value_expr = exp.maybe_parse(val) or exp.column(val)
243
310
  else:
244
311
  column_name = col if isinstance(col, str) else str(col)
245
312
  if "." in column_name:
246
313
  column_name = column_name.split(".")[-1]
247
314
  param_name = self._generate_unique_parameter_name(column_name)
248
- param_name = self.add_parameter(val, name=param_name)[1]
315
+ _, param_name = self.add_parameter(val, name=param_name)
249
316
  value_expr = exp.Placeholder(this=param_name)
250
317
 
251
318
  update_expressions.append(exp.EQ(this=exp.column(col), expression=value_expr))
@@ -315,7 +382,8 @@ class MergeNotMatchedClauseMixin:
315
382
 
316
383
  __slots__ = ()
317
384
 
318
- _expression: Optional[exp.Expression]
385
+ def get_expression(self) -> Optional[exp.Expression]: ...
386
+ def set_expression(self, expression: exp.Expression) -> None: ...
319
387
 
320
388
  def add_parameter(self, value: Any, name: Optional[str] = None) -> tuple[Any, str]:
321
389
  """Add parameter - provided by QueryBuilder."""
@@ -327,6 +395,54 @@ class MergeNotMatchedClauseMixin:
327
395
  msg = "Method must be provided by QueryBuilder subclass"
328
396
  raise NotImplementedError(msg)
329
397
 
398
+ def _is_column_reference(self, value: str) -> bool:
399
+ """Check if a string value is a column reference rather than a literal.
400
+
401
+ Uses sqlglot to parse the value and determine if it represents a column
402
+ reference, function call, or other SQL expression rather than a literal.
403
+ """
404
+ if not isinstance(value, str):
405
+ return False
406
+
407
+ # If the string contains spaces and no SQL-like syntax, treat as literal
408
+ if " " in value and not any(x in value for x in [".", "(", ")", "*", "="]):
409
+ return False
410
+
411
+ try:
412
+ # Try to parse as SQL expression
413
+ parsed: Optional[exp.Expression] = exp.maybe_parse(value)
414
+ if parsed is None:
415
+ return False
416
+
417
+ # If it parses to a Column, Dot (table.column), Identifier, or other SQL constructs
418
+
419
+ except Exception:
420
+ # If parsing fails, fall back to conservative approach
421
+ # Only treat simple identifiers as column references
422
+ return (
423
+ value.replace("_", "").replace(".", "").isalnum()
424
+ and (value[0].isalpha() or value[0] == "_")
425
+ and " " not in value
426
+ and "'" not in value
427
+ and '"' not in value
428
+ )
429
+ return bool(
430
+ isinstance(
431
+ parsed,
432
+ (
433
+ exp.Column,
434
+ exp.Dot,
435
+ exp.Identifier,
436
+ exp.Anonymous,
437
+ exp.Func,
438
+ exp.Null,
439
+ exp.CurrentTimestamp,
440
+ exp.CurrentDate,
441
+ exp.CurrentTime,
442
+ ),
443
+ )
444
+ )
445
+
330
446
  def _add_when_clause(self, when_clause: exp.When) -> None:
331
447
  """Helper to add a WHEN clause to the MERGE statement - provided by QueryBuilder."""
332
448
  msg = "Method must be provided by QueryBuilder subclass"
@@ -364,12 +480,16 @@ class MergeNotMatchedClauseMixin:
364
480
 
365
481
  parameterized_values: list[exp.Expression] = []
366
482
  for i, val in enumerate(values):
367
- column_name = columns[i] if isinstance(columns[i], str) else str(columns[i])
368
- if "." in column_name:
369
- column_name = column_name.split(".")[-1]
370
- param_name = self._generate_unique_parameter_name(column_name)
371
- param_name = self.add_parameter(val, name=param_name)[1]
372
- parameterized_values.append(exp.Placeholder())
483
+ if isinstance(val, str) and self._is_column_reference(val):
484
+ # Handle column references (like "s.data") as column expressions, not parameters
485
+ parameterized_values.append(exp.maybe_parse(val) or exp.column(val))
486
+ else:
487
+ column_name = columns[i] if isinstance(columns[i], str) else str(columns[i])
488
+ if "." in column_name:
489
+ column_name = column_name.split(".")[-1]
490
+ param_name = self._generate_unique_parameter_name(column_name)
491
+ _, param_name = self.add_parameter(val, name=param_name)
492
+ parameterized_values.append(exp.Placeholder(this=param_name))
373
493
 
374
494
  insert_args["this"] = exp.Tuple(expressions=[exp.column(c) for c in columns])
375
495
  insert_args["expression"] = exp.Tuple(expressions=parameterized_values)
@@ -415,7 +535,8 @@ class MergeNotMatchedBySourceClauseMixin:
415
535
 
416
536
  __slots__ = ()
417
537
 
418
- _expression: Optional[exp.Expression]
538
+ def get_expression(self) -> Optional[exp.Expression]: ...
539
+ def set_expression(self, expression: exp.Expression) -> None: ...
419
540
 
420
541
  def add_parameter(self, value: Any, name: Optional[str] = None) -> tuple[Any, str]:
421
542
  """Add parameter - provided by QueryBuilder."""
@@ -432,6 +553,35 @@ class MergeNotMatchedBySourceClauseMixin:
432
553
  msg = "Method must be provided by QueryBuilder subclass"
433
554
  raise NotImplementedError(msg)
434
555
 
556
+ def _is_column_reference(self, value: str) -> bool:
557
+ """Check if a string value is a column reference rather than a literal.
558
+
559
+ Uses sqlglot to parse the value and determine if it represents a column
560
+ reference, function call, or other SQL expression rather than a literal.
561
+
562
+ Args:
563
+ value: The string value to check
564
+
565
+ Returns:
566
+ True if the value is a column reference, False if it's a literal
567
+ """
568
+ if not isinstance(value, str):
569
+ return False
570
+
571
+ try:
572
+ parsed: Optional[exp.Expression] = exp.maybe_parse(value)
573
+ if parsed is None:
574
+ return False
575
+
576
+ except Exception:
577
+ return False
578
+ return bool(
579
+ isinstance(
580
+ parsed,
581
+ (exp.Dot, exp.Anonymous, exp.Func, exp.Null, exp.CurrentTimestamp, exp.CurrentDate, exp.CurrentTime),
582
+ )
583
+ )
584
+
435
585
  def when_not_matched_by_source_then_update(
436
586
  self,
437
587
  set_values: Optional[dict[str, Any]] = None,
@@ -468,35 +618,17 @@ class MergeNotMatchedBySourceClauseMixin:
468
618
  update_expressions: list[exp.EQ] = []
469
619
  for col, val in all_values.items():
470
620
  if hasattr(val, "expression") and hasattr(val, "sql"):
471
- # Handle SQL objects (from sql.raw with parameters)
472
- expression = getattr(val, "expression", None)
473
- if expression is not None and isinstance(expression, exp.Expression):
474
- # Merge parameters from SQL object into builder
475
- if hasattr(val, "parameters"):
476
- sql_parameters = getattr(val, "parameters", {})
477
- for param_name, param_value in sql_parameters.items():
478
- self.add_parameter(param_value, name=param_name)
479
- value_expr = expression
480
- else:
481
- # If expression is None, fall back to parsing the raw SQL
482
- sql_text = getattr(val, "sql", "")
483
- # Merge parameters even when parsing raw SQL
484
- if hasattr(val, "parameters"):
485
- sql_parameters = getattr(val, "parameters", {})
486
- for param_name, param_value in sql_parameters.items():
487
- self.add_parameter(param_value, name=param_name)
488
- # Check if sql_text is callable (like Expression.sql method)
489
- if callable(sql_text):
490
- sql_text = str(val)
491
- value_expr = exp.maybe_parse(sql_text) or exp.convert(str(sql_text))
621
+ value_expr = extract_sql_object_expression(val, builder=self)
492
622
  elif isinstance(val, exp.Expression):
493
623
  value_expr = val
624
+ elif isinstance(val, str) and self._is_column_reference(val):
625
+ value_expr = exp.maybe_parse(val) or exp.column(val)
494
626
  else:
495
627
  column_name = col if isinstance(col, str) else str(col)
496
628
  if "." in column_name:
497
629
  column_name = column_name.split(".")[-1]
498
630
  param_name = self._generate_unique_parameter_name(column_name)
499
- param_name = self.add_parameter(val, name=param_name)[1]
631
+ _, param_name = self.add_parameter(val, name=param_name)
500
632
  value_expr = exp.Placeholder(this=param_name)
501
633
 
502
634
  update_expressions.append(exp.EQ(this=exp.column(col), expression=value_expr))
@@ -1,3 +1,4 @@
1
+ # pyright: reportPrivateUsage=false
1
2
  """ORDER BY, LIMIT, OFFSET, and RETURNING clause mixins.
2
3
 
3
4
  Provides mixins for query result ordering, limiting, and result
@@ -10,7 +11,7 @@ from mypy_extensions import trait
10
11
  from sqlglot import exp
11
12
  from typing_extensions import Self
12
13
 
13
- from sqlspec.builder._parsing_utils import parse_order_expression
14
+ from sqlspec.builder._parsing_utils import extract_expression, parse_order_expression
14
15
  from sqlspec.exceptions import SQLBuilderError
15
16
 
16
17
  if TYPE_CHECKING:
@@ -28,7 +29,6 @@ class OrderByClauseMixin:
28
29
 
29
30
  __slots__ = ()
30
31
 
31
- # Type annotation for PyRight - this will be provided by the base class
32
32
  _expression: Optional[exp.Expression]
33
33
 
34
34
  def order_by(self, *items: Union[str, exp.Ordered, "Column"], desc: bool = False) -> Self:
@@ -57,9 +57,7 @@ class OrderByClauseMixin:
57
57
  order_item = order_item.desc()
58
58
  else:
59
59
  # Extract expression from Column objects or use as-is for sqlglot expressions
60
- from sqlspec._sql import SQLFactory
61
-
62
- extracted_item = SQLFactory._extract_expression(item)
60
+ extracted_item = extract_expression(item)
63
61
  order_item = extracted_item
64
62
  if desc and not isinstance(item, exp.Ordered):
65
63
  order_item = order_item.desc()
@@ -74,7 +72,6 @@ class LimitOffsetClauseMixin:
74
72
 
75
73
  __slots__ = ()
76
74
 
77
- # Type annotation for PyRight - this will be provided by the base class
78
75
  _expression: Optional[exp.Expression]
79
76
 
80
77
  def limit(self, value: int) -> Self:
@@ -121,7 +118,6 @@ class ReturningClauseMixin:
121
118
  """Mixin providing RETURNING clause."""
122
119
 
123
120
  __slots__ = ()
124
- # Type annotation for PyRight - this will be provided by the base class
125
121
  _expression: Optional[exp.Expression]
126
122
 
127
123
  def returning(self, *columns: Union[str, exp.Expression, "Column", "ExpressionWrapper", "Case"]) -> Self:
@@ -144,8 +140,6 @@ class ReturningClauseMixin:
144
140
  msg = "RETURNING is only supported for INSERT, UPDATE, and DELETE statements."
145
141
  raise SQLBuilderError(msg)
146
142
  # Extract expressions from various wrapper types
147
- from sqlspec._sql import SQLFactory
148
-
149
- returning_exprs = [SQLFactory._extract_expression(c) for c in columns]
143
+ returning_exprs = [extract_expression(c) for c in columns]
150
144
  self._expression.set("returning", exp.Returning(expressions=returning_exprs))
151
145
  return self
@@ -1,3 +1,4 @@
1
+ # pyright: reportPrivateUsage=false
1
2
  """PIVOT and UNPIVOT operation mixins.
2
3
 
3
4
  Provides mixins for PIVOT and UNPIVOT operations in SELECT statements.
@@ -1,3 +1,4 @@
1
+ # pyright: reportPrivateUsage=false
1
2
  """SELECT clause mixins.
2
3
 
3
4
  Provides mixins for SELECT statement functionality including column selection,
@@ -10,7 +11,7 @@ from mypy_extensions import trait
10
11
  from sqlglot import exp
11
12
  from typing_extensions import Self
12
13
 
13
- from sqlspec.builder._parsing_utils import parse_column_expression, parse_table_expression
14
+ from sqlspec.builder._parsing_utils import parse_column_expression, parse_table_expression, to_expression
14
15
  from sqlspec.exceptions import SQLBuilderError
15
16
  from sqlspec.utils.type_guards import has_query_builder_parameters, is_expression
16
17
 
@@ -28,8 +29,8 @@ class SelectClauseMixin:
28
29
 
29
30
  __slots__ = ()
30
31
 
31
- # Type annotation for PyRight - this will be provided by the base class
32
- _expression: Optional[exp.Expression]
32
+ def get_expression(self) -> Optional[exp.Expression]: ...
33
+ def set_expression(self, expression: exp.Expression) -> None: ...
33
34
 
34
35
  def select(self, *columns: Union[str, exp.Expression, "Column", "FunctionColumn", "SQL", "Case"]) -> Self:
35
36
  """Add columns to SELECT clause.
@@ -41,13 +42,17 @@ class SelectClauseMixin:
41
42
  The current builder instance for method chaining.
42
43
  """
43
44
  builder = cast("SQLBuilderProtocol", self)
44
- if builder._expression is None:
45
- builder._expression = exp.Select()
46
- if not isinstance(builder._expression, exp.Select):
45
+ current_expr = self.get_expression()
46
+ if current_expr is None:
47
+ self.set_expression(exp.Select())
48
+ current_expr = self.get_expression()
49
+
50
+ if not isinstance(current_expr, exp.Select):
47
51
  msg = "Cannot add select columns to a non-SELECT expression."
48
52
  raise SQLBuilderError(msg)
49
53
  for column in columns:
50
- builder._expression = builder._expression.select(parse_column_expression(column, builder), copy=False)
54
+ current_expr = current_expr.select(parse_column_expression(column, builder), copy=False)
55
+ self.set_expression(current_expr)
51
56
  return cast("Self", builder)
52
57
 
53
58
  def distinct(self, *columns: Union[str, exp.Expression, "Column", "FunctionColumn", "SQL"]) -> Self:
@@ -129,13 +134,13 @@ class SelectClauseMixin:
129
134
  Returns:
130
135
  The current builder instance for method chaining.
131
136
  """
132
- if self._expression is None or not isinstance(self._expression, exp.Select):
137
+ current_expr = self.get_expression()
138
+ if current_expr is None or not isinstance(current_expr, exp.Select):
133
139
  return self
134
140
 
135
141
  for column in columns:
136
- self._expression = self._expression.group_by(
137
- exp.column(column) if isinstance(column, str) else column, copy=False
138
- )
142
+ current_expr = current_expr.group_by(exp.column(column) if isinstance(column, str) else column, copy=False)
143
+ self.set_expression(current_expr)
139
144
  return self
140
145
 
141
146
  def group_by_rollup(self, *columns: Union[str, exp.Expression]) -> Self:
@@ -480,9 +485,12 @@ class SelectClauseMixin:
480
485
  Returns:
481
486
  The current builder instance for method chaining.
482
487
  """
483
- if self._expression is None:
484
- self._expression = exp.Select()
485
- if not isinstance(self._expression, exp.Select):
488
+ current_expr = self.get_expression()
489
+ if current_expr is None:
490
+ self.set_expression(exp.Select())
491
+ current_expr = self.get_expression()
492
+
493
+ if not isinstance(current_expr, exp.Select):
486
494
  msg = "Cannot add window function to a non-SELECT expression."
487
495
  raise SQLBuilderError(msg)
488
496
 
@@ -525,7 +533,8 @@ class SelectClauseMixin:
525
533
  over_args["frame"] = frame_expr
526
534
 
527
535
  window_expr = exp.Window(this=func_expr_parsed, **over_args)
528
- self._expression.select(exp.alias_(window_expr, alias) if alias else window_expr, copy=False)
536
+ current_expr = current_expr.select(exp.alias_(window_expr, alias) if alias else window_expr, copy=False)
537
+ self.set_expression(current_expr)
529
538
  return self
530
539
 
531
540
  def case_(self, alias: "Optional[str]" = None) -> "CaseBuilder":
@@ -855,12 +864,9 @@ class Case:
855
864
  Returns:
856
865
  Self for method chaining.
857
866
  """
858
- from sqlspec._sql import SQLFactory
859
-
860
867
  cond_expr = exp.maybe_parse(condition) or exp.column(condition) if isinstance(condition, str) else condition
861
- val_expr = SQLFactory._to_expression(value)
868
+ val_expr = to_expression(value)
862
869
 
863
- # SQLGlot uses exp.If for CASE WHEN clauses, not exp.When
864
870
  when_clause = exp.If(this=cond_expr, true=val_expr)
865
871
  self._conditions.append(when_clause)
866
872
  return self
@@ -874,9 +880,7 @@ class Case:
874
880
  Returns:
875
881
  Self for method chaining.
876
882
  """
877
- from sqlspec._sql import SQLFactory
878
-
879
- self._default = SQLFactory._to_expression(value)
883
+ self._default = to_expression(value)
880
884
  return self
881
885
 
882
886
  def end(self) -> Self:
@@ -906,3 +910,21 @@ class Case:
906
910
  """
907
911
  case_expr = exp.Case(ifs=self._conditions, default=self._default)
908
912
  return cast("exp.Alias", exp.alias_(case_expr, alias))
913
+
914
+ @property
915
+ def conditions(self) -> "list[exp.If]":
916
+ """Get CASE conditions (public API).
917
+
918
+ Returns:
919
+ List of If expressions representing WHEN clauses
920
+ """
921
+ return self._conditions
922
+
923
+ @property
924
+ def default(self) -> Optional[exp.Expression]:
925
+ """Get CASE default value (public API).
926
+
927
+ Returns:
928
+ Default expression for the ELSE clause, or None
929
+ """
930
+ return self._default