sqlspec 0.25.0__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 (84) hide show
  1. sqlspec/_serialization.py +223 -21
  2. sqlspec/_sql.py +12 -50
  3. sqlspec/_typing.py +9 -0
  4. sqlspec/adapters/adbc/config.py +8 -1
  5. sqlspec/adapters/adbc/data_dictionary.py +290 -0
  6. sqlspec/adapters/adbc/driver.py +127 -18
  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 +63 -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 +6 -0
  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/builder/_base.py +82 -42
  45. sqlspec/builder/_column.py +57 -24
  46. sqlspec/builder/_ddl.py +84 -34
  47. sqlspec/builder/_insert.py +30 -52
  48. sqlspec/builder/_parsing_utils.py +104 -8
  49. sqlspec/builder/_select.py +147 -2
  50. sqlspec/builder/mixins/_cte_and_set_ops.py +1 -2
  51. sqlspec/builder/mixins/_join_operations.py +14 -30
  52. sqlspec/builder/mixins/_merge_operations.py +167 -61
  53. sqlspec/builder/mixins/_order_limit_operations.py +3 -10
  54. sqlspec/builder/mixins/_select_operations.py +3 -9
  55. sqlspec/builder/mixins/_update_operations.py +3 -22
  56. sqlspec/builder/mixins/_where_clause.py +4 -10
  57. sqlspec/cli.py +246 -140
  58. sqlspec/config.py +33 -19
  59. sqlspec/core/cache.py +2 -2
  60. sqlspec/core/compiler.py +56 -1
  61. sqlspec/core/parameters.py +7 -3
  62. sqlspec/core/statement.py +5 -0
  63. sqlspec/core/type_conversion.py +234 -0
  64. sqlspec/driver/__init__.py +6 -3
  65. sqlspec/driver/_async.py +106 -3
  66. sqlspec/driver/_common.py +156 -4
  67. sqlspec/driver/_sync.py +106 -3
  68. sqlspec/exceptions.py +5 -0
  69. sqlspec/migrations/__init__.py +4 -3
  70. sqlspec/migrations/base.py +153 -14
  71. sqlspec/migrations/commands.py +34 -96
  72. sqlspec/migrations/context.py +145 -0
  73. sqlspec/migrations/loaders.py +25 -8
  74. sqlspec/migrations/runner.py +352 -82
  75. sqlspec/typing.py +2 -0
  76. sqlspec/utils/config_resolver.py +153 -0
  77. sqlspec/utils/serializers.py +50 -2
  78. {sqlspec-0.25.0.dist-info → sqlspec-0.26.0.dist-info}/METADATA +1 -1
  79. sqlspec-0.26.0.dist-info/RECORD +157 -0
  80. sqlspec-0.25.0.dist-info/RECORD +0 -139
  81. {sqlspec-0.25.0.dist-info → sqlspec-0.26.0.dist-info}/WHEEL +0 -0
  82. {sqlspec-0.25.0.dist-info → sqlspec-0.26.0.dist-info}/entry_points.txt +0 -0
  83. {sqlspec-0.25.0.dist-info → sqlspec-0.26.0.dist-info}/licenses/LICENSE +0 -0
  84. {sqlspec-0.25.0.dist-info → sqlspec-0.26.0.dist-info}/licenses/NOTICE +0 -0
@@ -11,6 +11,7 @@ from mypy_extensions import trait
11
11
  from sqlglot import exp
12
12
  from typing_extensions import Self
13
13
 
14
+ from sqlspec.builder._parsing_utils import extract_sql_object_expression
14
15
  from sqlspec.exceptions import SQLBuilderError
15
16
  from sqlspec.utils.type_guards import has_query_builder_parameters
16
17
 
@@ -30,7 +31,6 @@ class MergeIntoClauseMixin:
30
31
 
31
32
  __slots__ = ()
32
33
 
33
- # Type annotations for PyRight - these will be provided by the base class
34
34
  def get_expression(self) -> Optional[exp.Expression]: ...
35
35
  def set_expression(self, expression: exp.Expression) -> None: ...
36
36
 
@@ -50,7 +50,6 @@ class MergeIntoClauseMixin:
50
50
  self.set_expression(exp.Merge(this=None, using=None, on=None, whens=exp.Whens(expressions=[])))
51
51
  current_expr = self.get_expression()
52
52
 
53
- # Type guard: current_expr is now guaranteed to be an Expression
54
53
  assert current_expr is not None
55
54
  current_expr.set("this", exp.to_table(table, alias=alias) if isinstance(table, str) else table)
56
55
  return self
@@ -62,7 +61,6 @@ class MergeUsingClauseMixin:
62
61
 
63
62
  __slots__ = ()
64
63
 
65
- # Type annotations for PyRight - these will be provided by the base class
66
64
  def get_expression(self) -> Optional[exp.Expression]: ...
67
65
  def set_expression(self, expression: exp.Expression) -> None: ...
68
66
 
@@ -71,6 +69,11 @@ class MergeUsingClauseMixin:
71
69
  msg = "Method must be provided by QueryBuilder subclass"
72
70
  raise NotImplementedError(msg)
73
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
+
74
77
  def using(self, source: Union[str, exp.Expression, Any], alias: Optional[str] = None) -> Self:
75
78
  """Set the source data for the MERGE operation (USING clause).
76
79
 
@@ -90,11 +93,35 @@ class MergeUsingClauseMixin:
90
93
  self.set_expression(exp.Merge(this=None, using=None, on=None, whens=exp.Whens(expressions=[])))
91
94
  current_expr = self.get_expression()
92
95
 
93
- # Type guard: current_expr is now guaranteed to be an Expression
94
96
  assert current_expr is not None
95
97
  source_expr: exp.Expression
96
98
  if isinstance(source, str):
97
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)
98
125
  elif has_query_builder_parameters(source) and hasattr(source, "_expression"):
99
126
  subquery_builder_parameters = source.parameters
100
127
  if subquery_builder_parameters:
@@ -121,7 +148,6 @@ class MergeOnClauseMixin:
121
148
 
122
149
  __slots__ = ()
123
150
 
124
- # Type annotations for PyRight - these will be provided by the base class
125
151
  def get_expression(self) -> Optional[exp.Expression]: ...
126
152
  def set_expression(self, expression: exp.Expression) -> None: ...
127
153
 
@@ -143,7 +169,6 @@ class MergeOnClauseMixin:
143
169
  self.set_expression(exp.Merge(this=None, using=None, on=None, whens=exp.Whens(expressions=[])))
144
170
  current_expr = self.get_expression()
145
171
 
146
- # Type guard: current_expr is now guaranteed to be an Expression
147
172
  assert current_expr is not None
148
173
  condition_expr: exp.Expression
149
174
  if isinstance(condition, str):
@@ -170,7 +195,6 @@ class MergeMatchedClauseMixin:
170
195
 
171
196
  __slots__ = ()
172
197
 
173
- # Type annotations for PyRight - these will be provided by the base class
174
198
  def get_expression(self) -> Optional[exp.Expression]: ...
175
199
  def set_expression(self, expression: exp.Expression) -> None: ...
176
200
 
@@ -184,6 +208,42 @@ class MergeMatchedClauseMixin:
184
208
  msg = "Method must be provided by QueryBuilder subclass"
185
209
  raise NotImplementedError(msg)
186
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
+
187
247
  def _add_when_clause(self, when_clause: exp.When) -> None:
188
248
  """Helper to add a WHEN clause to the MERGE statement.
189
249
 
@@ -195,7 +255,6 @@ class MergeMatchedClauseMixin:
195
255
  self.set_expression(exp.Merge(this=None, using=None, on=None, whens=exp.Whens(expressions=[])))
196
256
  current_expr = self.get_expression()
197
257
 
198
- # Type guard: current_expr is now guaranteed to be an Expression
199
258
  assert current_expr is not None
200
259
  whens = current_expr.args.get("whens")
201
260
  if not whens:
@@ -230,7 +289,11 @@ class MergeMatchedClauseMixin:
230
289
  The current builder instance for method chaining.
231
290
  """
232
291
  # Combine set_values dict and kwargs
233
- 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)
234
297
 
235
298
  if not all_values:
236
299
  msg = "No update values provided. Use set_values dict or kwargs."
@@ -239,35 +302,17 @@ class MergeMatchedClauseMixin:
239
302
  update_expressions: list[exp.EQ] = []
240
303
  for col, val in all_values.items():
241
304
  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))
305
+ value_expr = extract_sql_object_expression(val, builder=self)
263
306
  elif isinstance(val, exp.Expression):
264
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)
265
310
  else:
266
311
  column_name = col if isinstance(col, str) else str(col)
267
312
  if "." in column_name:
268
313
  column_name = column_name.split(".")[-1]
269
314
  param_name = self._generate_unique_parameter_name(column_name)
270
- param_name = self.add_parameter(val, name=param_name)[1]
315
+ _, param_name = self.add_parameter(val, name=param_name)
271
316
  value_expr = exp.Placeholder(this=param_name)
272
317
 
273
318
  update_expressions.append(exp.EQ(this=exp.column(col), expression=value_expr))
@@ -337,7 +382,6 @@ class MergeNotMatchedClauseMixin:
337
382
 
338
383
  __slots__ = ()
339
384
 
340
- # Type annotations for PyRight - these will be provided by the base class
341
385
  def get_expression(self) -> Optional[exp.Expression]: ...
342
386
  def set_expression(self, expression: exp.Expression) -> None: ...
343
387
 
@@ -351,6 +395,54 @@ class MergeNotMatchedClauseMixin:
351
395
  msg = "Method must be provided by QueryBuilder subclass"
352
396
  raise NotImplementedError(msg)
353
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
+
354
446
  def _add_when_clause(self, when_clause: exp.When) -> None:
355
447
  """Helper to add a WHEN clause to the MERGE statement - provided by QueryBuilder."""
356
448
  msg = "Method must be provided by QueryBuilder subclass"
@@ -388,12 +480,16 @@ class MergeNotMatchedClauseMixin:
388
480
 
389
481
  parameterized_values: list[exp.Expression] = []
390
482
  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())
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))
397
493
 
398
494
  insert_args["this"] = exp.Tuple(expressions=[exp.column(c) for c in columns])
399
495
  insert_args["expression"] = exp.Tuple(expressions=parameterized_values)
@@ -439,7 +535,6 @@ class MergeNotMatchedBySourceClauseMixin:
439
535
 
440
536
  __slots__ = ()
441
537
 
442
- # Type annotations for PyRight - these will be provided by the base class
443
538
  def get_expression(self) -> Optional[exp.Expression]: ...
444
539
  def set_expression(self, expression: exp.Expression) -> None: ...
445
540
 
@@ -458,6 +553,35 @@ class MergeNotMatchedBySourceClauseMixin:
458
553
  msg = "Method must be provided by QueryBuilder subclass"
459
554
  raise NotImplementedError(msg)
460
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
+
461
585
  def when_not_matched_by_source_then_update(
462
586
  self,
463
587
  set_values: Optional[dict[str, Any]] = None,
@@ -494,35 +618,17 @@ class MergeNotMatchedBySourceClauseMixin:
494
618
  update_expressions: list[exp.EQ] = []
495
619
  for col, val in all_values.items():
496
620
  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))
621
+ value_expr = extract_sql_object_expression(val, builder=self)
518
622
  elif isinstance(val, exp.Expression):
519
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)
520
626
  else:
521
627
  column_name = col if isinstance(col, str) else str(col)
522
628
  if "." in column_name:
523
629
  column_name = column_name.split(".")[-1]
524
630
  param_name = self._generate_unique_parameter_name(column_name)
525
- param_name = self.add_parameter(val, name=param_name)[1]
631
+ _, param_name = self.add_parameter(val, name=param_name)
526
632
  value_expr = exp.Placeholder(this=param_name)
527
633
 
528
634
  update_expressions.append(exp.EQ(this=exp.column(col), expression=value_expr))
@@ -11,7 +11,7 @@ from mypy_extensions import trait
11
11
  from sqlglot import exp
12
12
  from typing_extensions import Self
13
13
 
14
- from sqlspec.builder._parsing_utils import parse_order_expression
14
+ from sqlspec.builder._parsing_utils import extract_expression, parse_order_expression
15
15
  from sqlspec.exceptions import SQLBuilderError
16
16
 
17
17
  if TYPE_CHECKING:
@@ -29,7 +29,6 @@ class OrderByClauseMixin:
29
29
 
30
30
  __slots__ = ()
31
31
 
32
- # Type annotation for PyRight - this will be provided by the base class
33
32
  _expression: Optional[exp.Expression]
34
33
 
35
34
  def order_by(self, *items: Union[str, exp.Ordered, "Column"], desc: bool = False) -> Self:
@@ -58,9 +57,7 @@ class OrderByClauseMixin:
58
57
  order_item = order_item.desc()
59
58
  else:
60
59
  # Extract expression from Column objects or use as-is for sqlglot expressions
61
- from sqlspec._sql import SQLFactory
62
-
63
- extracted_item = SQLFactory._extract_expression(item)
60
+ extracted_item = extract_expression(item)
64
61
  order_item = extracted_item
65
62
  if desc and not isinstance(item, exp.Ordered):
66
63
  order_item = order_item.desc()
@@ -75,7 +72,6 @@ class LimitOffsetClauseMixin:
75
72
 
76
73
  __slots__ = ()
77
74
 
78
- # Type annotation for PyRight - this will be provided by the base class
79
75
  _expression: Optional[exp.Expression]
80
76
 
81
77
  def limit(self, value: int) -> Self:
@@ -122,7 +118,6 @@ class ReturningClauseMixin:
122
118
  """Mixin providing RETURNING clause."""
123
119
 
124
120
  __slots__ = ()
125
- # Type annotation for PyRight - this will be provided by the base class
126
121
  _expression: Optional[exp.Expression]
127
122
 
128
123
  def returning(self, *columns: Union[str, exp.Expression, "Column", "ExpressionWrapper", "Case"]) -> Self:
@@ -145,8 +140,6 @@ class ReturningClauseMixin:
145
140
  msg = "RETURNING is only supported for INSERT, UPDATE, and DELETE statements."
146
141
  raise SQLBuilderError(msg)
147
142
  # Extract expressions from various wrapper types
148
- from sqlspec._sql import SQLFactory
149
-
150
- returning_exprs = [SQLFactory._extract_expression(c) for c in columns]
143
+ returning_exprs = [extract_expression(c) for c in columns]
151
144
  self._expression.set("returning", exp.Returning(expressions=returning_exprs))
152
145
  return self
@@ -11,7 +11,7 @@ from mypy_extensions import trait
11
11
  from sqlglot import exp
12
12
  from typing_extensions import Self
13
13
 
14
- 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
15
15
  from sqlspec.exceptions import SQLBuilderError
16
16
  from sqlspec.utils.type_guards import has_query_builder_parameters, is_expression
17
17
 
@@ -29,7 +29,6 @@ class SelectClauseMixin:
29
29
 
30
30
  __slots__ = ()
31
31
 
32
- # Type annotations for PyRight - these will be provided by the base class
33
32
  def get_expression(self) -> Optional[exp.Expression]: ...
34
33
  def set_expression(self, expression: exp.Expression) -> None: ...
35
34
 
@@ -865,12 +864,9 @@ class Case:
865
864
  Returns:
866
865
  Self for method chaining.
867
866
  """
868
- from sqlspec._sql import SQLFactory
869
-
870
867
  cond_expr = exp.maybe_parse(condition) or exp.column(condition) if isinstance(condition, str) else condition
871
- val_expr = SQLFactory._to_expression(value)
868
+ val_expr = to_expression(value)
872
869
 
873
- # SQLGlot uses exp.If for CASE WHEN clauses, not exp.When
874
870
  when_clause = exp.If(this=cond_expr, true=val_expr)
875
871
  self._conditions.append(when_clause)
876
872
  return self
@@ -884,9 +880,7 @@ class Case:
884
880
  Returns:
885
881
  Self for method chaining.
886
882
  """
887
- from sqlspec._sql import SQLFactory
888
-
889
- self._default = SQLFactory._to_expression(value)
883
+ self._default = to_expression(value)
890
884
  return self
891
885
 
892
886
  def end(self) -> Self:
@@ -6,12 +6,13 @@ table specification, SET clauses, and FROM clauses.
6
6
  """
7
7
 
8
8
  from collections.abc import Mapping
9
- from typing import Any, Optional, Union, cast
9
+ from typing import Any, Optional, Union
10
10
 
11
11
  from mypy_extensions import trait
12
12
  from sqlglot import exp
13
13
  from typing_extensions import Self
14
14
 
15
+ from sqlspec.builder._parsing_utils import extract_sql_object_expression
15
16
  from sqlspec.exceptions import SQLBuilderError
16
17
  from sqlspec.utils.type_guards import has_query_builder_parameters
17
18
 
@@ -26,7 +27,6 @@ class UpdateTableClauseMixin:
26
27
 
27
28
  __slots__ = ()
28
29
 
29
- # Type annotations for PyRight - these will be provided by the base class
30
30
  def get_expression(self) -> Optional[exp.Expression]: ...
31
31
  def set_expression(self, expression: exp.Expression) -> None: ...
32
32
 
@@ -58,7 +58,6 @@ class UpdateSetClauseMixin:
58
58
 
59
59
  __slots__ = ()
60
60
 
61
- # Type annotations for PyRight - these will be provided by the base class
62
61
  def get_expression(self) -> Optional[exp.Expression]: ...
63
62
  def set_expression(self, expression: exp.Expression) -> None: ...
64
63
 
@@ -93,24 +92,7 @@ class UpdateSetClauseMixin:
93
92
  self.add_parameter(p_value, name=p_name)
94
93
  return value_expr
95
94
  if hasattr(val, "expression") and hasattr(val, "sql"):
96
- # Handle SQL objects (from sql.raw with parameters)
97
- expression = getattr(val, "expression", None)
98
- if expression is not None and isinstance(expression, exp.Expression):
99
- # Merge parameters from SQL object into builder
100
- if hasattr(val, "parameters"):
101
- sql_parameters = getattr(val, "parameters", {})
102
- for param_name, param_value in sql_parameters.items():
103
- self.add_parameter(param_value, name=param_name)
104
- return cast("exp.Expression", expression)
105
- # If expression is None, fall back to parsing the raw SQL
106
- sql_text = getattr(val, "sql", "")
107
- # Merge parameters even when parsing raw SQL
108
- if hasattr(val, "parameters"):
109
- sql_parameters = getattr(val, "parameters", {})
110
- for param_name, param_value in sql_parameters.items():
111
- self.add_parameter(param_value, name=param_name)
112
- parsed_expr = exp.maybe_parse(sql_text)
113
- return parsed_expr if parsed_expr is not None else exp.convert(str(sql_text))
95
+ return extract_sql_object_expression(val, builder=self)
114
96
  column_name = col if isinstance(col, str) else str(col)
115
97
  if "." in column_name:
116
98
  column_name = column_name.split(".")[-1]
@@ -170,7 +152,6 @@ class UpdateFromClauseMixin:
170
152
 
171
153
  __slots__ = ()
172
154
 
173
- # Type annotations for PyRight - these will be provided by the base class
174
155
  def get_expression(self) -> Optional[exp.Expression]: ...
175
156
  def set_expression(self, expression: exp.Expression) -> None: ...
176
157
 
@@ -16,6 +16,8 @@ from sqlglot import exp
16
16
  from typing_extensions import Self
17
17
 
18
18
  from sqlspec.builder._parsing_utils import extract_column_name, parse_column_expression, parse_condition_expression
19
+ from sqlspec.core.parameters import ParameterStyle, ParameterValidator
20
+ from sqlspec.core.statement import SQL
19
21
  from sqlspec.exceptions import SQLBuilderError
20
22
  from sqlspec.utils.type_guards import (
21
23
  has_expression_and_parameters,
@@ -299,15 +301,11 @@ class WhereClauseMixin:
299
301
  raise SQLBuilderError(msg)
300
302
 
301
303
  # Check if condition contains parameter placeholders
302
- from sqlspec.core.parameters import ParameterStyle, ParameterValidator
303
-
304
304
  validator = ParameterValidator()
305
305
  param_info = validator.extract_parameters(condition)
306
306
 
307
307
  if param_info:
308
308
  # String condition with placeholders - create SQL object with parameters
309
- from sqlspec import sql as sql_factory
310
-
311
309
  # Create parameter mapping based on the detected parameter info
312
310
  param_dict = dict(kwargs) # Start with named parameters
313
311
 
@@ -327,7 +325,7 @@ class WhereClauseMixin:
327
325
  param_dict[f"param_{i}"] = value
328
326
 
329
327
  # Create SQL object with parameters that will be processed correctly
330
- condition = sql_factory.raw(condition, **param_dict)
328
+ condition = SQL(condition, param_dict)
331
329
  # Fall through to existing SQL object handling logic
332
330
 
333
331
  elif len(values) == 1 and not kwargs:
@@ -819,15 +817,11 @@ class WhereClauseMixin:
819
817
  raise SQLBuilderError(msg)
820
818
 
821
819
  # Check if condition contains parameter placeholders
822
- from sqlspec.core.parameters import ParameterStyle, ParameterValidator
823
-
824
820
  validator = ParameterValidator()
825
821
  param_info = validator.extract_parameters(condition)
826
822
 
827
823
  if param_info:
828
824
  # String condition with placeholders - create SQL object with parameters
829
- from sqlspec import sql as sql_factory
830
-
831
825
  # Create parameter mapping based on the detected parameter info
832
826
  param_dict = dict(kwargs) # Start with named parameters
833
827
 
@@ -847,7 +841,7 @@ class WhereClauseMixin:
847
841
  param_dict[f"param_{i}"] = value
848
842
 
849
843
  # Create SQL object with parameters that will be processed correctly
850
- condition = sql_factory.raw(condition, **param_dict)
844
+ condition = SQL(condition, param_dict)
851
845
  # Fall through to existing SQL object handling logic
852
846
 
853
847
  elif len(values) == 1 and not kwargs: