sqlspec 0.14.0__py3-none-any.whl → 0.15.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 (158) hide show
  1. sqlspec/__init__.py +50 -25
  2. sqlspec/__main__.py +12 -0
  3. sqlspec/__metadata__.py +1 -3
  4. sqlspec/_serialization.py +1 -2
  5. sqlspec/_sql.py +256 -120
  6. sqlspec/_typing.py +278 -142
  7. sqlspec/adapters/adbc/__init__.py +4 -3
  8. sqlspec/adapters/adbc/_types.py +12 -0
  9. sqlspec/adapters/adbc/config.py +115 -248
  10. sqlspec/adapters/adbc/driver.py +462 -353
  11. sqlspec/adapters/aiosqlite/__init__.py +18 -3
  12. sqlspec/adapters/aiosqlite/_types.py +13 -0
  13. sqlspec/adapters/aiosqlite/config.py +199 -129
  14. sqlspec/adapters/aiosqlite/driver.py +230 -269
  15. sqlspec/adapters/asyncmy/__init__.py +18 -3
  16. sqlspec/adapters/asyncmy/_types.py +12 -0
  17. sqlspec/adapters/asyncmy/config.py +80 -168
  18. sqlspec/adapters/asyncmy/driver.py +260 -225
  19. sqlspec/adapters/asyncpg/__init__.py +19 -4
  20. sqlspec/adapters/asyncpg/_types.py +17 -0
  21. sqlspec/adapters/asyncpg/config.py +82 -181
  22. sqlspec/adapters/asyncpg/driver.py +285 -383
  23. sqlspec/adapters/bigquery/__init__.py +17 -3
  24. sqlspec/adapters/bigquery/_types.py +12 -0
  25. sqlspec/adapters/bigquery/config.py +191 -258
  26. sqlspec/adapters/bigquery/driver.py +474 -646
  27. sqlspec/adapters/duckdb/__init__.py +14 -3
  28. sqlspec/adapters/duckdb/_types.py +12 -0
  29. sqlspec/adapters/duckdb/config.py +415 -351
  30. sqlspec/adapters/duckdb/driver.py +343 -413
  31. sqlspec/adapters/oracledb/__init__.py +19 -5
  32. sqlspec/adapters/oracledb/_types.py +14 -0
  33. sqlspec/adapters/oracledb/config.py +123 -379
  34. sqlspec/adapters/oracledb/driver.py +507 -560
  35. sqlspec/adapters/psqlpy/__init__.py +13 -3
  36. sqlspec/adapters/psqlpy/_types.py +11 -0
  37. sqlspec/adapters/psqlpy/config.py +93 -254
  38. sqlspec/adapters/psqlpy/driver.py +505 -234
  39. sqlspec/adapters/psycopg/__init__.py +19 -5
  40. sqlspec/adapters/psycopg/_types.py +17 -0
  41. sqlspec/adapters/psycopg/config.py +143 -403
  42. sqlspec/adapters/psycopg/driver.py +706 -872
  43. sqlspec/adapters/sqlite/__init__.py +14 -3
  44. sqlspec/adapters/sqlite/_types.py +11 -0
  45. sqlspec/adapters/sqlite/config.py +202 -118
  46. sqlspec/adapters/sqlite/driver.py +264 -303
  47. sqlspec/base.py +105 -9
  48. sqlspec/{statement/builder → builder}/__init__.py +12 -14
  49. sqlspec/{statement/builder → builder}/_base.py +120 -55
  50. sqlspec/{statement/builder → builder}/_column.py +17 -6
  51. sqlspec/{statement/builder → builder}/_ddl.py +46 -79
  52. sqlspec/{statement/builder → builder}/_ddl_utils.py +5 -10
  53. sqlspec/{statement/builder → builder}/_delete.py +6 -25
  54. sqlspec/{statement/builder → builder}/_insert.py +6 -64
  55. sqlspec/builder/_merge.py +56 -0
  56. sqlspec/{statement/builder → builder}/_parsing_utils.py +3 -10
  57. sqlspec/{statement/builder → builder}/_select.py +11 -56
  58. sqlspec/{statement/builder → builder}/_update.py +12 -18
  59. sqlspec/{statement/builder → builder}/mixins/__init__.py +10 -14
  60. sqlspec/{statement/builder → builder}/mixins/_cte_and_set_ops.py +48 -59
  61. sqlspec/{statement/builder → builder}/mixins/_insert_operations.py +22 -16
  62. sqlspec/{statement/builder → builder}/mixins/_join_operations.py +1 -3
  63. sqlspec/{statement/builder → builder}/mixins/_merge_operations.py +3 -5
  64. sqlspec/{statement/builder → builder}/mixins/_order_limit_operations.py +3 -3
  65. sqlspec/{statement/builder → builder}/mixins/_pivot_operations.py +4 -8
  66. sqlspec/{statement/builder → builder}/mixins/_select_operations.py +21 -36
  67. sqlspec/{statement/builder → builder}/mixins/_update_operations.py +3 -14
  68. sqlspec/{statement/builder → builder}/mixins/_where_clause.py +52 -79
  69. sqlspec/cli.py +4 -5
  70. sqlspec/config.py +180 -133
  71. sqlspec/core/__init__.py +63 -0
  72. sqlspec/core/cache.py +873 -0
  73. sqlspec/core/compiler.py +396 -0
  74. sqlspec/core/filters.py +828 -0
  75. sqlspec/core/hashing.py +310 -0
  76. sqlspec/core/parameters.py +1209 -0
  77. sqlspec/core/result.py +664 -0
  78. sqlspec/{statement → core}/splitter.py +321 -191
  79. sqlspec/core/statement.py +651 -0
  80. sqlspec/driver/__init__.py +7 -10
  81. sqlspec/driver/_async.py +387 -176
  82. sqlspec/driver/_common.py +527 -289
  83. sqlspec/driver/_sync.py +390 -172
  84. sqlspec/driver/mixins/__init__.py +2 -19
  85. sqlspec/driver/mixins/_result_tools.py +168 -0
  86. sqlspec/driver/mixins/_sql_translator.py +6 -3
  87. sqlspec/exceptions.py +5 -252
  88. sqlspec/extensions/aiosql/adapter.py +93 -96
  89. sqlspec/extensions/litestar/config.py +0 -1
  90. sqlspec/extensions/litestar/handlers.py +15 -26
  91. sqlspec/extensions/litestar/plugin.py +16 -14
  92. sqlspec/extensions/litestar/providers.py +17 -52
  93. sqlspec/loader.py +424 -105
  94. sqlspec/migrations/__init__.py +12 -0
  95. sqlspec/migrations/base.py +92 -68
  96. sqlspec/migrations/commands.py +24 -106
  97. sqlspec/migrations/loaders.py +402 -0
  98. sqlspec/migrations/runner.py +49 -51
  99. sqlspec/migrations/tracker.py +31 -44
  100. sqlspec/migrations/utils.py +64 -24
  101. sqlspec/protocols.py +7 -183
  102. sqlspec/storage/__init__.py +1 -1
  103. sqlspec/storage/backends/base.py +37 -40
  104. sqlspec/storage/backends/fsspec.py +136 -112
  105. sqlspec/storage/backends/obstore.py +138 -160
  106. sqlspec/storage/capabilities.py +5 -4
  107. sqlspec/storage/registry.py +57 -106
  108. sqlspec/typing.py +136 -115
  109. sqlspec/utils/__init__.py +2 -3
  110. sqlspec/utils/correlation.py +0 -3
  111. sqlspec/utils/deprecation.py +6 -6
  112. sqlspec/utils/fixtures.py +6 -6
  113. sqlspec/utils/logging.py +0 -2
  114. sqlspec/utils/module_loader.py +7 -12
  115. sqlspec/utils/singleton.py +0 -1
  116. sqlspec/utils/sync_tools.py +16 -37
  117. sqlspec/utils/text.py +12 -51
  118. sqlspec/utils/type_guards.py +443 -232
  119. {sqlspec-0.14.0.dist-info → sqlspec-0.15.0.dist-info}/METADATA +7 -2
  120. sqlspec-0.15.0.dist-info/RECORD +134 -0
  121. sqlspec-0.15.0.dist-info/entry_points.txt +2 -0
  122. sqlspec/driver/connection.py +0 -207
  123. sqlspec/driver/mixins/_cache.py +0 -114
  124. sqlspec/driver/mixins/_csv_writer.py +0 -91
  125. sqlspec/driver/mixins/_pipeline.py +0 -508
  126. sqlspec/driver/mixins/_query_tools.py +0 -796
  127. sqlspec/driver/mixins/_result_utils.py +0 -138
  128. sqlspec/driver/mixins/_storage.py +0 -912
  129. sqlspec/driver/mixins/_type_coercion.py +0 -128
  130. sqlspec/driver/parameters.py +0 -138
  131. sqlspec/statement/__init__.py +0 -21
  132. sqlspec/statement/builder/_merge.py +0 -95
  133. sqlspec/statement/cache.py +0 -50
  134. sqlspec/statement/filters.py +0 -625
  135. sqlspec/statement/parameters.py +0 -996
  136. sqlspec/statement/pipelines/__init__.py +0 -210
  137. sqlspec/statement/pipelines/analyzers/__init__.py +0 -9
  138. sqlspec/statement/pipelines/analyzers/_analyzer.py +0 -646
  139. sqlspec/statement/pipelines/context.py +0 -115
  140. sqlspec/statement/pipelines/transformers/__init__.py +0 -7
  141. sqlspec/statement/pipelines/transformers/_expression_simplifier.py +0 -88
  142. sqlspec/statement/pipelines/transformers/_literal_parameterizer.py +0 -1247
  143. sqlspec/statement/pipelines/transformers/_remove_comments_and_hints.py +0 -76
  144. sqlspec/statement/pipelines/validators/__init__.py +0 -23
  145. sqlspec/statement/pipelines/validators/_dml_safety.py +0 -290
  146. sqlspec/statement/pipelines/validators/_parameter_style.py +0 -370
  147. sqlspec/statement/pipelines/validators/_performance.py +0 -714
  148. sqlspec/statement/pipelines/validators/_security.py +0 -967
  149. sqlspec/statement/result.py +0 -435
  150. sqlspec/statement/sql.py +0 -1774
  151. sqlspec/utils/cached_property.py +0 -25
  152. sqlspec/utils/statement_hashing.py +0 -203
  153. sqlspec-0.14.0.dist-info/RECORD +0 -143
  154. sqlspec-0.14.0.dist-info/entry_points.txt +0 -2
  155. /sqlspec/{statement/builder → builder}/mixins/_delete_operations.py +0 -0
  156. {sqlspec-0.14.0.dist-info → sqlspec-0.15.0.dist-info}/WHEEL +0 -0
  157. {sqlspec-0.14.0.dist-info → sqlspec-0.15.0.dist-info}/licenses/LICENSE +0 -0
  158. {sqlspec-0.14.0.dist-info → sqlspec-0.15.0.dist-info}/licenses/NOTICE +0 -0
@@ -7,7 +7,7 @@ from sqlglot import exp
7
7
  if TYPE_CHECKING:
8
8
  from sqlglot.dialects.dialect import DialectType
9
9
 
10
- from sqlspec.statement.builder._select import Select
10
+ from sqlspec.builder._select import Select
11
11
 
12
12
  __all__ = ("PivotClauseMixin", "UnpivotClauseMixin")
13
13
 
@@ -56,12 +56,10 @@ class PivotClauseMixin:
56
56
  for val in pivot_values:
57
57
  if isinstance(val, exp.Expression):
58
58
  pivot_value_exprs.append(val)
59
- elif isinstance(val, str):
60
- pivot_value_exprs.append(exp.Literal.string(val))
61
- elif isinstance(val, (int, float)):
62
- pivot_value_exprs.append(exp.Literal.number(val))
59
+ elif isinstance(val, (str, int, float)):
60
+ pivot_value_exprs.append(exp.convert(val))
63
61
  else:
64
- pivot_value_exprs.append(exp.Literal.string(str(val)))
62
+ pivot_value_exprs.append(exp.convert(str(val)))
65
63
 
66
64
  in_expr = exp.In(this=pivot_col_expr, expressions=pivot_value_exprs)
67
65
 
@@ -113,7 +111,6 @@ class UnpivotClauseMixin:
113
111
  """
114
112
  current_expr = self._expression
115
113
  if not isinstance(current_expr, exp.Select):
116
- # SelectBuilder's __init__ ensures _expression is exp.Select.
117
114
  msg = "Unpivot can only be applied to a Select expression managed by Select."
118
115
  raise TypeError(msg)
119
116
 
@@ -127,7 +124,6 @@ class UnpivotClauseMixin:
127
124
  elif isinstance(col_name_or_expr, str):
128
125
  unpivot_cols_exprs.append(exp.column(col_name_or_expr))
129
126
  else:
130
- # Fallback for other types, should ideally be an error or more specific handling
131
127
  unpivot_cols_exprs.append(exp.column(str(col_name_or_expr)))
132
128
 
133
129
  in_expr = exp.In(this=name_col_ident, expressions=unpivot_cols_exprs)
@@ -6,15 +6,14 @@ from typing import TYPE_CHECKING, Any, Optional, Union, cast
6
6
  from sqlglot import exp
7
7
  from typing_extensions import Self
8
8
 
9
+ from sqlspec.builder._parsing_utils import parse_column_expression, parse_table_expression
9
10
  from sqlspec.exceptions import SQLBuilderError
10
- from sqlspec.statement.builder._parsing_utils import parse_column_expression, parse_table_expression
11
11
  from sqlspec.utils.type_guards import has_query_builder_parameters, is_expression
12
12
 
13
13
  if TYPE_CHECKING:
14
+ from sqlspec.builder._base import QueryBuilder
15
+ from sqlspec.builder._column import Column, FunctionColumn
14
16
  from sqlspec.protocols import SelectBuilderProtocol, SQLBuilderProtocol
15
- from sqlspec.statement.builder._base import QueryBuilder
16
- from sqlspec.statement.builder._column import Column, FunctionColumn
17
- from sqlspec.typing import RowT
18
17
 
19
18
  __all__ = ("CaseBuilder", "SelectClauseMixin")
20
19
 
@@ -24,7 +23,6 @@ class SelectClauseMixin:
24
23
 
25
24
  _expression: Optional[exp.Expression] = None
26
25
 
27
- # SELECT and DISTINCT methods
28
26
  def select(self, *columns: Union[str, exp.Expression, "Column", "FunctionColumn"]) -> Self:
29
27
  """Add columns to SELECT clause.
30
28
 
@@ -69,7 +67,6 @@ class SelectClauseMixin:
69
67
  builder._expression.set("distinct", exp.Distinct(expressions=distinct_columns))
70
68
  return cast("Self", builder)
71
69
 
72
- # FROM clause method
73
70
  def from_(self, table: Union[str, exp.Expression, Any], alias: Optional[str] = None) -> Self:
74
71
  """Add FROM clause.
75
72
 
@@ -93,30 +90,27 @@ class SelectClauseMixin:
93
90
  if isinstance(table, str):
94
91
  from_expr = parse_table_expression(table, alias)
95
92
  elif is_expression(table):
96
- # Direct sqlglot expression - use as is
97
93
  from_expr = exp.alias_(table, alias) if alias else table
98
94
  elif has_query_builder_parameters(table):
99
- # Query builder with build() method
100
95
  subquery = table.build()
101
96
  sql_str = subquery.sql if hasattr(subquery, "sql") and not callable(subquery.sql) else str(subquery)
102
97
  subquery_exp = exp.paren(exp.maybe_parse(sql_str, dialect=getattr(builder, "dialect", None)))
103
98
  from_expr = exp.alias_(subquery_exp, alias) if alias else subquery_exp
104
- current_params = getattr(builder, "_parameters", None)
105
- merged_params = getattr(type(builder), "ParameterConverter", None)
106
- if merged_params and hasattr(subquery, "parameters"):
107
- subquery_params = getattr(subquery, "parameters", {})
108
- merged_params = merged_params.merge_parameters(
109
- parameters=subquery_params,
110
- args=current_params if isinstance(current_params, list) else None,
111
- kwargs=current_params if isinstance(current_params, dict) else {},
99
+ current_parameters = getattr(builder, "_parameters", None)
100
+ merged_parameters = getattr(type(builder), "ParameterConverter", None)
101
+ if merged_parameters and hasattr(subquery, "parameters"):
102
+ subquery_parameters = getattr(subquery, "parameters", {})
103
+ merged_parameters = merged_parameters.merge_parameters(
104
+ parameters=subquery_parameters,
105
+ args=current_parameters if isinstance(current_parameters, list) else None,
106
+ kwargs=current_parameters if isinstance(current_parameters, dict) else {},
112
107
  )
113
- setattr(builder, "_parameters", merged_params)
108
+ setattr(builder, "_parameters", merged_parameters)
114
109
  else:
115
110
  from_expr = table
116
111
  builder._expression = builder._expression.from_(from_expr, copy=False)
117
112
  return cast("Self", builder)
118
113
 
119
- # GROUP BY methods
120
114
  def group_by(self, *columns: Union[str, exp.Expression]) -> Self:
121
115
  """Add GROUP BY clause.
122
116
 
@@ -149,7 +143,6 @@ class SelectClauseMixin:
149
143
 
150
144
  Example:
151
145
  ```python
152
- # GROUP BY ROLLUP(product, region)
153
146
  query = (
154
147
  sql.select("product", "region", sql.sum("sales"))
155
148
  .from_("sales_data")
@@ -174,7 +167,6 @@ class SelectClauseMixin:
174
167
 
175
168
  Example:
176
169
  ```python
177
- # GROUP BY CUBE(product, region)
178
170
  query = (
179
171
  sql.select("product", "region", sql.sum("sales"))
180
172
  .from_("sales_data")
@@ -200,7 +192,6 @@ class SelectClauseMixin:
200
192
 
201
193
  Example:
202
194
  ```python
203
- # GROUP BY GROUPING SETS ((product), (region), ())
204
195
  query = (
205
196
  sql.select("product", "region", sql.sum("sales"))
206
197
  .from_("sales_data")
@@ -217,13 +208,11 @@ class SelectClauseMixin:
217
208
  columns = [exp.column(col) for col in column_set]
218
209
  set_expressions.append(exp.Tuple(expressions=columns))
219
210
  else:
220
- # Single column
221
211
  set_expressions.append(exp.column(column_set))
222
212
 
223
213
  grouping_sets_expr = exp.GroupingSets(expressions=set_expressions)
224
214
  return self.group_by(grouping_sets_expr)
225
215
 
226
- # Aggregate function methods
227
216
  def count_(self, column: "Union[str, exp.Expression]" = "*", alias: Optional[str] = None) -> Self:
228
217
  """Add COUNT function to SELECT clause.
229
218
 
@@ -440,8 +429,7 @@ class SelectClauseMixin:
440
429
  """
441
430
  builder = cast("SelectBuilderProtocol", self)
442
431
  col_expr = exp.column(column) if isinstance(column, str) else column
443
- # Use GroupConcat which SQLGlot can translate to STRING_AGG for Postgres
444
- string_agg_expr = exp.GroupConcat(this=col_expr, separator=exp.Literal.string(separator))
432
+ string_agg_expr = exp.GroupConcat(this=col_expr, separator=exp.convert(separator))
445
433
  select_expr = exp.alias_(string_agg_expr, alias) if alias else string_agg_expr
446
434
  return cast("Self", builder.select(select_expr))
447
435
 
@@ -461,7 +449,6 @@ class SelectClauseMixin:
461
449
  select_expr = exp.alias_(json_agg_expr, alias) if alias else json_agg_expr
462
450
  return cast("Self", builder.select(select_expr))
463
451
 
464
- # Window function method
465
452
  def window(
466
453
  self,
467
454
  function_expr: Union[str, exp.Expression],
@@ -501,20 +488,19 @@ class SelectClauseMixin:
501
488
  else:
502
489
  func_expr_parsed = function_expr
503
490
 
504
- over_args: dict[str, Any] = {} # Stringified dict
491
+ over_args: dict[str, Any] = {}
505
492
  if partition_by:
506
493
  if isinstance(partition_by, str):
507
494
  over_args["partition_by"] = [exp.column(partition_by)]
508
- elif isinstance(partition_by, list): # Check for list
495
+ elif isinstance(partition_by, list):
509
496
  over_args["partition_by"] = [exp.column(col) if isinstance(col, str) else col for col in partition_by]
510
- elif isinstance(partition_by, exp.Expression): # Check for exp.Expression
497
+ elif isinstance(partition_by, exp.Expression):
511
498
  over_args["partition_by"] = [partition_by]
512
499
 
513
500
  if order_by:
514
501
  if isinstance(order_by, str):
515
502
  over_args["order"] = exp.column(order_by).asc()
516
503
  elif isinstance(order_by, list):
517
- # Properly handle multiple ORDER BY columns using Order expression
518
504
  order_expressions: list[Union[exp.Expression, exp.Column]] = []
519
505
  for col in order_by:
520
506
  if isinstance(col, str):
@@ -534,7 +520,6 @@ class SelectClauseMixin:
534
520
  self._expression.select(exp.alias_(window_expr, alias) if alias else window_expr, copy=False)
535
521
  return self
536
522
 
537
- # CASE expression method
538
523
  def case_(self, alias: "Optional[str]" = None) -> "CaseBuilder":
539
524
  """Create a CASE expression for the SELECT clause.
540
525
 
@@ -544,7 +529,7 @@ class SelectClauseMixin:
544
529
  Returns:
545
530
  CaseBuilder: A CaseBuilder instance for building the CASE expression.
546
531
  """
547
- builder = cast("QueryBuilder[Any]", self) # pyright: ignore
532
+ builder = cast("QueryBuilder", self) # pyright: ignore
548
533
  return CaseBuilder(builder, alias)
549
534
 
550
535
 
@@ -552,11 +537,11 @@ class SelectClauseMixin:
552
537
  class CaseBuilder:
553
538
  """Builder for CASE expressions."""
554
539
 
555
- _parent: "QueryBuilder[Any]" # pyright: ignore
540
+ _parent: "QueryBuilder" # pyright: ignore
556
541
  _alias: Optional[str]
557
542
  _case_expr: exp.Case
558
543
 
559
- def __init__(self, parent: "QueryBuilder[Any]", alias: "Optional[str]" = None) -> None:
544
+ def __init__(self, parent: "QueryBuilder", alias: "Optional[str]" = None) -> None:
560
545
  """Initialize CaseBuilder.
561
546
 
562
547
  Args:
@@ -602,11 +587,11 @@ class CaseBuilder:
602
587
  self._case_expr.set("default", value_expr)
603
588
  return self
604
589
 
605
- def end(self) -> "QueryBuilder[RowT]":
590
+ def end(self) -> "QueryBuilder":
606
591
  """Finalize the CASE expression and add it to the SELECT clause.
607
592
 
608
593
  Returns:
609
594
  The parent builder instance.
610
595
  """
611
596
  select_expr = exp.alias_(self._case_expr, self._alias) if self._alias else self._case_expr
612
- return cast("QueryBuilder[Any]", self._parent.select(select_expr)) # type: ignore[attr-defined]
597
+ return cast("QueryBuilder", self._parent.select(select_expr)) # type: ignore[attr-defined]
@@ -68,20 +68,15 @@ class UpdateSetClauseMixin:
68
68
  msg = "Cannot add SET clause to non-UPDATE expression."
69
69
  raise SQLBuilderError(msg)
70
70
  assignments = []
71
- # (column, value) signature
72
71
  if len(args) == MIN_SET_ARGS and not kwargs:
73
72
  col, val = args
74
73
  col_expr = col if isinstance(col, exp.Column) else exp.column(col)
75
- # If value is an expression, use it directly
76
74
  if isinstance(val, exp.Expression):
77
75
  value_expr = val
78
76
  elif has_query_builder_parameters(val):
79
- # It's a builder (like SelectBuilder), convert to subquery
80
77
  subquery = val.build()
81
- # Parse the SQL and use as expression
82
78
  sql_str = subquery.sql if hasattr(subquery, "sql") and not callable(subquery.sql) else str(subquery)
83
79
  value_expr = exp.paren(exp.maybe_parse(sql_str, dialect=getattr(self, "dialect", None)))
84
- # Merge parameters from subquery
85
80
  if has_query_builder_parameters(val):
86
81
  for p_name, p_value in val.parameters.items():
87
82
  self.add_parameter(p_value, name=p_name) # type: ignore[attr-defined]
@@ -89,20 +84,15 @@ class UpdateSetClauseMixin:
89
84
  param_name = self.add_parameter(val)[1] # type: ignore[attr-defined]
90
85
  value_expr = exp.Placeholder(this=param_name)
91
86
  assignments.append(exp.EQ(this=col_expr, expression=value_expr))
92
- # mapping and/or kwargs
93
87
  elif (len(args) == 1 and isinstance(args[0], Mapping)) or kwargs:
94
88
  all_values = dict(args[0] if args else {}, **kwargs)
95
89
  for col, val in all_values.items():
96
- # If value is an expression, use it directly
97
90
  if isinstance(val, exp.Expression):
98
91
  value_expr = val
99
92
  elif has_query_builder_parameters(val):
100
- # It's a builder (like SelectBuilder), convert to subquery
101
93
  subquery = val.build()
102
- # Parse the SQL and use as expression
103
94
  sql_str = subquery.sql if hasattr(subquery, "sql") and not callable(subquery.sql) else str(subquery)
104
95
  value_expr = exp.paren(exp.maybe_parse(sql_str, dialect=getattr(self, "dialect", None)))
105
- # Merge parameters from subquery
106
96
  if has_query_builder_parameters(val):
107
97
  for p_name, p_value in val.parameters.items():
108
98
  self.add_parameter(p_value, name=p_name) # type: ignore[attr-defined]
@@ -113,7 +103,6 @@ class UpdateSetClauseMixin:
113
103
  else:
114
104
  msg = "Invalid arguments for set(): use (column, value), mapping, or kwargs."
115
105
  raise SQLBuilderError(msg)
116
- # Append to existing expressions instead of replacing
117
106
  existing = self._expression.args.get("expressions", [])
118
107
  self._expression.set("expressions", existing + assignments)
119
108
  return self
@@ -142,9 +131,9 @@ class UpdateFromClauseMixin:
142
131
  if isinstance(table, str):
143
132
  table_expr = exp.to_table(table, alias=alias)
144
133
  elif has_query_builder_parameters(table):
145
- subquery_builder_params = getattr(table, "_parameters", None)
146
- if subquery_builder_params:
147
- for p_name, p_value in subquery_builder_params.items():
134
+ subquery_builder_parameters = getattr(table, "_parameters", None)
135
+ if subquery_builder_parameters:
136
+ for p_name, p_value in subquery_builder_parameters.items():
148
137
  self.add_parameter(p_value, name=p_name) # type: ignore[attr-defined]
149
138
  subquery_exp = exp.paren(getattr(table, "_expression", exp.select()))
150
139
  table_expr = exp.alias_(subquery_exp, alias) if alias else subquery_exp
@@ -1,18 +1,18 @@
1
1
  # ruff: noqa: PLR2004
2
2
  """Consolidated WHERE and HAVING clause mixins."""
3
3
 
4
- from typing import TYPE_CHECKING, Any, Callable, Optional, Union, cast
4
+ from typing import TYPE_CHECKING, Any, Optional, Union, cast
5
5
 
6
6
  from sqlglot import exp
7
7
  from typing_extensions import Self
8
8
 
9
+ from sqlspec.builder._parsing_utils import parse_column_expression, parse_condition_expression
9
10
  from sqlspec.exceptions import SQLBuilderError
10
- from sqlspec.statement.builder._parsing_utils import parse_column_expression, parse_condition_expression
11
11
  from sqlspec.utils.type_guards import has_query_builder_parameters, has_sqlglot_expression, is_iterable_parameters
12
12
 
13
13
  if TYPE_CHECKING:
14
+ from sqlspec.builder._column import ColumnExpression
14
15
  from sqlspec.protocols import SQLBuilderProtocol
15
- from sqlspec.statement.builder._column import ColumnExpression
16
16
 
17
17
  __all__ = ("HavingClauseMixin", "WhereClauseMixin")
18
18
 
@@ -20,33 +20,6 @@ __all__ = ("HavingClauseMixin", "WhereClauseMixin")
20
20
  class WhereClauseMixin:
21
21
  """Mixin providing WHERE clause methods for SELECT, UPDATE, and DELETE builders."""
22
22
 
23
- def _create_operator_handler(self, operator_class: type[exp.Expression]) -> Callable:
24
- """Create a handler that properly parameterizes values."""
25
-
26
- def handler(self: "SQLBuilderProtocol", column_exp: exp.Expression, value: Any) -> exp.Expression:
27
- _, param_name = self.add_parameter(value)
28
- return operator_class(this=column_exp, expression=exp.Placeholder(this=param_name))
29
-
30
- return handler
31
-
32
- def _create_like_handler(self) -> Callable:
33
- """Create LIKE handler."""
34
-
35
- def handler(self: "SQLBuilderProtocol", column_exp: exp.Expression, value: Any) -> exp.Expression:
36
- _, param_name = self.add_parameter(value)
37
- return exp.Like(this=column_exp, expression=exp.Placeholder(this=param_name))
38
-
39
- return handler
40
-
41
- def _create_not_like_handler(self) -> Callable:
42
- """Create NOT LIKE handler."""
43
-
44
- def handler(self: "SQLBuilderProtocol", column_exp: exp.Expression, value: Any) -> exp.Expression:
45
- _, param_name = self.add_parameter(value)
46
- return exp.Not(this=exp.Like(this=column_exp, expression=exp.Placeholder(this=param_name)))
47
-
48
- return handler
49
-
50
23
  def _handle_in_operator(self, column_exp: exp.Expression, value: Any) -> exp.Expression:
51
24
  """Handle IN operator."""
52
25
  builder = cast("SQLBuilderProtocol", self)
@@ -126,7 +99,6 @@ class WhereClauseMixin:
126
99
  operator = str(condition[1]).upper()
127
100
  value = condition[2]
128
101
 
129
- # Handle simple operators
130
102
  if operator == "=":
131
103
  _, param_name = builder.add_parameter(value)
132
104
  return exp.EQ(this=column_exp, expression=exp.Placeholder(this=param_name))
@@ -152,7 +124,6 @@ class WhereClauseMixin:
152
124
  _, param_name = builder.add_parameter(value)
153
125
  return exp.Not(this=exp.Like(this=column_exp, expression=exp.Placeholder(this=param_name)))
154
126
 
155
- # Handle complex operators
156
127
  if operator == "IN":
157
128
  return self._handle_in_operator(column_exp, value)
158
129
  if operator == "NOT IN":
@@ -175,15 +146,20 @@ class WhereClauseMixin:
175
146
  def where(
176
147
  self,
177
148
  condition: Union[str, exp.Expression, exp.Condition, tuple[str, Any], tuple[str, str, Any], "ColumnExpression"],
149
+ value: Optional[Any] = None,
150
+ operator: Optional[str] = None,
178
151
  ) -> Self:
179
152
  """Add a WHERE clause to the statement.
180
153
 
181
154
  Args:
182
155
  condition: The condition for the WHERE clause. Can be:
183
- - A string condition
156
+ - A string condition (when value is None)
157
+ - A string column name (when value is provided)
184
158
  - A sqlglot Expression or Condition
185
159
  - A 2-tuple (column, value) for equality comparison
186
160
  - A 3-tuple (column, operator, value) for custom comparison
161
+ value: Value for comparison (when condition is a column name)
162
+ operator: Operator for comparison (when both condition and value provided)
187
163
 
188
164
  Raises:
189
165
  SQLBuilderError: If the current expression is not a supported statement type.
@@ -191,10 +167,7 @@ class WhereClauseMixin:
191
167
  Returns:
192
168
  The current builder instance for method chaining.
193
169
  """
194
- # Special case: if this is an Update and _expression is not exp.Update, raise the expected error for test coverage
195
- if self.__class__.__name__ == "Update" and not (
196
- hasattr(self, "_expression") and isinstance(getattr(self, "_expression", None), exp.Update)
197
- ):
170
+ if self.__class__.__name__ == "Update" and not isinstance(self._expression, exp.Update): # type: ignore[attr-defined]
198
171
  msg = "Cannot add WHERE clause to non-UPDATE expression"
199
172
  raise SQLBuilderError(msg)
200
173
 
@@ -203,35 +176,38 @@ class WhereClauseMixin:
203
176
  msg = "Cannot add WHERE clause: expression is not initialized."
204
177
  raise SQLBuilderError(msg)
205
178
 
206
- # Check if DELETE has a table set
207
179
  if isinstance(builder._expression, exp.Delete) and not builder._expression.args.get("this"):
208
180
  msg = "WHERE clause requires a table to be set. Use from() to set the table first."
209
181
  raise SQLBuilderError(msg)
210
182
 
211
- # Process different condition types
212
- if isinstance(condition, str):
183
+ if value is not None:
184
+ if not isinstance(condition, str):
185
+ msg = "When value is provided, condition must be a column name (string)"
186
+ raise SQLBuilderError(msg)
187
+
188
+ if operator is not None:
189
+ where_expr = self._process_tuple_condition((condition, operator, value))
190
+ else:
191
+ where_expr = self._process_tuple_condition((condition, value))
192
+ elif isinstance(condition, str):
213
193
  where_expr = parse_condition_expression(condition)
214
194
  elif isinstance(condition, (exp.Expression, exp.Condition)):
215
195
  where_expr = condition
216
196
  elif isinstance(condition, tuple):
217
197
  where_expr = self._process_tuple_condition(condition)
218
198
  elif has_query_builder_parameters(condition):
219
- # Handle ColumnExpression objects
220
199
  column_expr_obj = cast("ColumnExpression", condition)
221
200
  where_expr = column_expr_obj._expression # pyright: ignore
222
201
  elif has_sqlglot_expression(condition):
223
- # This is a ColumnExpression from our new Column syntax
224
- raw_expr = getattr(condition, "sqlglot_expression", None)
202
+ raw_expr = condition.sqlglot_expression # pyright: ignore[attr-defined]
225
203
  if raw_expr is not None:
226
204
  where_expr = builder._parameterize_expression(raw_expr)
227
205
  else:
228
- # Fallback if attribute exists but is None
229
206
  where_expr = parse_condition_expression(str(condition))
230
207
  else:
231
208
  msg = f"Unsupported condition type: {type(condition).__name__}"
232
209
  raise SQLBuilderError(msg)
233
210
 
234
- # Apply WHERE clause based on statement type
235
211
  if isinstance(builder._expression, (exp.Select, exp.Update, exp.Delete)):
236
212
  builder._expression = builder._expression.where(where_expr, copy=False)
237
213
  else:
@@ -302,7 +278,7 @@ class WhereClauseMixin:
302
278
  _, param_name = builder.add_parameter(pattern)
303
279
  col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
304
280
  if escape is not None:
305
- cond = exp.Like(this=col_expr, expression=exp.var(param_name), escape=exp.Literal.string(str(escape)))
281
+ cond = exp.Like(this=col_expr, expression=exp.var(param_name), escape=exp.convert(str(escape)))
306
282
  else:
307
283
  cond = col_expr.like(exp.var(param_name))
308
284
  condition: exp.Expression = cond
@@ -344,8 +320,8 @@ class WhereClauseMixin:
344
320
  subquery_exp: exp.Expression
345
321
  if has_query_builder_parameters(values):
346
322
  subquery = values.build() # pyright: ignore
347
- sql_str = getattr(subquery, "sql", str(subquery))
348
- subquery_exp = exp.paren(exp.maybe_parse(sql_str, dialect=getattr(builder, "dialect_name", None)))
323
+ sql_str = subquery.sql
324
+ subquery_exp = exp.paren(exp.maybe_parse(sql_str, dialect=builder.dialect_name)) # pyright: ignore
349
325
  else:
350
326
  subquery_exp = values # type: ignore[assignment]
351
327
  condition = col_expr.isin(subquery_exp)
@@ -353,11 +329,11 @@ class WhereClauseMixin:
353
329
  if not is_iterable_parameters(values) or isinstance(values, (str, bytes)):
354
330
  msg = "Unsupported type for 'values' in WHERE IN"
355
331
  raise SQLBuilderError(msg)
356
- params = []
332
+ parameters = []
357
333
  for v in values:
358
334
  _, param_name = builder.add_parameter(v)
359
- params.append(exp.var(param_name))
360
- condition = col_expr.isin(*params)
335
+ parameters.append(exp.var(param_name))
336
+ condition = col_expr.isin(*parameters)
361
337
  return self.where(condition)
362
338
 
363
339
  def where_not_in(self, column: Union[str, exp.Column], values: Any) -> Self:
@@ -368,8 +344,8 @@ class WhereClauseMixin:
368
344
  subquery_exp: exp.Expression
369
345
  if has_query_builder_parameters(values):
370
346
  subquery = values.build() # pyright: ignore
371
- sql_str = getattr(subquery, "sql", str(subquery))
372
- subquery_exp = exp.paren(exp.maybe_parse(sql_str, dialect=getattr(builder, "dialect_name", None)))
347
+
348
+ subquery_exp = exp.paren(exp.maybe_parse(subquery.sql, dialect=builder.dialect_name)) # pyright: ignore
373
349
  else:
374
350
  subquery_exp = values # type: ignore[assignment]
375
351
  condition = exp.Not(this=col_expr.isin(subquery_exp))
@@ -377,11 +353,11 @@ class WhereClauseMixin:
377
353
  if not is_iterable_parameters(values) or isinstance(values, (str, bytes)):
378
354
  msg = "Values for where_not_in must be a non-string iterable or subquery."
379
355
  raise SQLBuilderError(msg)
380
- params = []
356
+ parameters = []
381
357
  for v in values:
382
358
  _, param_name = builder.add_parameter(v)
383
- params.append(exp.var(param_name))
384
- condition = exp.Not(this=col_expr.isin(*params))
359
+ parameters.append(exp.var(param_name))
360
+ condition = exp.Not(this=col_expr.isin(*parameters))
385
361
  return self.where(condition)
386
362
 
387
363
  def where_null(self, column: Union[str, exp.Column]) -> Self:
@@ -397,15 +373,15 @@ class WhereClauseMixin:
397
373
  builder = cast("SQLBuilderProtocol", self)
398
374
  sub_expr: exp.Expression
399
375
  if has_query_builder_parameters(subquery):
400
- subquery_builder_params: dict[str, Any] = subquery.parameters
401
- if subquery_builder_params:
402
- for p_name, p_value in subquery_builder_params.items():
376
+ subquery_builder_parameters: dict[str, Any] = subquery.parameters
377
+ if subquery_builder_parameters:
378
+ for p_name, p_value in subquery_builder_parameters.items():
403
379
  builder.add_parameter(p_value, name=p_name)
404
380
  sub_sql_obj = subquery.build() # pyright: ignore
405
- sql_str = getattr(sub_sql_obj, "sql", str(sub_sql_obj))
406
- sub_expr = exp.maybe_parse(sql_str, dialect=getattr(builder, "dialect_name", None))
381
+
382
+ sub_expr = exp.maybe_parse(sub_sql_obj.sql, dialect=builder.dialect_name) # pyright: ignore
407
383
  else:
408
- sub_expr = exp.maybe_parse(str(subquery), dialect=getattr(builder, "dialect_name", None))
384
+ sub_expr = exp.maybe_parse(str(subquery), dialect=builder.dialect_name)
409
385
 
410
386
  if sub_expr is None:
411
387
  msg = "Could not parse subquery for EXISTS"
@@ -419,15 +395,14 @@ class WhereClauseMixin:
419
395
  builder = cast("SQLBuilderProtocol", self)
420
396
  sub_expr: exp.Expression
421
397
  if has_query_builder_parameters(subquery):
422
- subquery_builder_params: dict[str, Any] = subquery.parameters
423
- if subquery_builder_params:
424
- for p_name, p_value in subquery_builder_params.items():
398
+ subquery_builder_parameters: dict[str, Any] = subquery.parameters
399
+ if subquery_builder_parameters:
400
+ for p_name, p_value in subquery_builder_parameters.items():
425
401
  builder.add_parameter(p_value, name=p_name)
426
402
  sub_sql_obj = subquery.build() # pyright: ignore
427
- sql_str = getattr(sub_sql_obj, "sql", str(sub_sql_obj))
428
- sub_expr = exp.maybe_parse(sql_str, dialect=getattr(builder, "dialect_name", None))
403
+ sub_expr = exp.maybe_parse(sub_sql_obj.sql, dialect=builder.dialect_name) # pyright: ignore
429
404
  else:
430
- sub_expr = exp.maybe_parse(str(subquery), dialect=getattr(builder, "dialect_name", None))
405
+ sub_expr = exp.maybe_parse(str(subquery), dialect=builder.dialect_name)
431
406
 
432
407
  if sub_expr is None:
433
408
  msg = "Could not parse subquery for NOT EXISTS"
@@ -444,8 +419,7 @@ class WhereClauseMixin:
444
419
  subquery_exp: exp.Expression
445
420
  if has_query_builder_parameters(values):
446
421
  subquery = values.build() # pyright: ignore
447
- sql_str = getattr(subquery, "sql", str(subquery))
448
- subquery_exp = exp.paren(exp.maybe_parse(sql_str, dialect=getattr(builder, "dialect_name", None)))
422
+ subquery_exp = exp.paren(exp.maybe_parse(subquery.sql, dialect=builder.dialect_name)) # pyright: ignore
449
423
  else:
450
424
  subquery_exp = values # type: ignore[assignment]
451
425
  condition = exp.EQ(this=col_expr, expression=exp.Any(this=subquery_exp))
@@ -464,24 +438,23 @@ class WhereClauseMixin:
464
438
  if not is_iterable_parameters(values) or isinstance(values, bytes):
465
439
  msg = "Unsupported type for 'values' in WHERE ANY"
466
440
  raise SQLBuilderError(msg)
467
- params = []
441
+ parameters = []
468
442
  for v in values:
469
443
  _, param_name = builder.add_parameter(v)
470
- params.append(exp.var(param_name))
471
- tuple_expr = exp.Tuple(expressions=params)
444
+ parameters.append(exp.var(param_name))
445
+ tuple_expr = exp.Tuple(expressions=parameters)
472
446
  condition = exp.EQ(this=col_expr, expression=exp.Any(this=tuple_expr))
473
447
  return self.where(condition)
474
448
 
475
449
  def where_not_any(self, column: Union[str, exp.Column], values: Any) -> Self:
476
- """Add WHERE column != ANY(values) clause."""
450
+ """Add WHERE column <> ANY(values) clause."""
477
451
  builder = cast("SQLBuilderProtocol", self)
478
452
  col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
479
453
  if has_query_builder_parameters(values) or isinstance(values, exp.Expression):
480
454
  subquery_exp: exp.Expression
481
455
  if has_query_builder_parameters(values):
482
456
  subquery = values.build() # pyright: ignore
483
- sql_str = getattr(subquery, "sql", str(subquery))
484
- subquery_exp = exp.paren(exp.maybe_parse(sql_str, dialect=getattr(builder, "dialect_name", None)))
457
+ subquery_exp = exp.paren(exp.maybe_parse(subquery.sql, dialect=builder.dialect_name)) # pyright: ignore
485
458
  else:
486
459
  subquery_exp = values # type: ignore[assignment]
487
460
  condition = exp.NEQ(this=col_expr, expression=exp.Any(this=subquery_exp))
@@ -500,11 +473,11 @@ class WhereClauseMixin:
500
473
  if not is_iterable_parameters(values) or isinstance(values, bytes):
501
474
  msg = "Unsupported type for 'values' in WHERE NOT ANY"
502
475
  raise SQLBuilderError(msg)
503
- params = []
476
+ parameters = []
504
477
  for v in values:
505
478
  _, param_name = builder.add_parameter(v)
506
- params.append(exp.var(param_name))
507
- tuple_expr = exp.Tuple(expressions=params)
479
+ parameters.append(exp.var(param_name))
480
+ tuple_expr = exp.Tuple(expressions=parameters)
508
481
  condition = exp.NEQ(this=col_expr, expression=exp.Any(this=tuple_expr))
509
482
  return self.where(condition)
510
483
 
sqlspec/cli.py CHANGED
@@ -32,7 +32,7 @@ def get_sqlspec_group() -> "Group":
32
32
  @click.group(name="sqlspec")
33
33
  @click.option(
34
34
  "--config",
35
- help="Dotted path to SQLAlchemy config(s) (e.g. 'myapp.config.sqlspec_configs')",
35
+ help="Dotted path to SQLSpec config(s) (e.g. 'myapp.config.sqlspec_configs')",
36
36
  required=True,
37
37
  type=str,
38
38
  )
@@ -87,7 +87,7 @@ def add_migration_commands(database_group: Optional["Group"] = None) -> "Group":
87
87
  database_group = get_sqlspec_group()
88
88
 
89
89
  bind_key_option = click.option(
90
- "--bind-key", help="Specify which SQLAlchemy config to use by bind key", type=str, default=None
90
+ "--bind-key", help="Specify which SQLSpec config to use by bind key", type=str, default=None
91
91
  )
92
92
  verbose_option = click.option("--verbose", help="Enable verbose output.", type=bool, default=False, is_flag=True)
93
93
  no_prompt_option = click.option(
@@ -103,21 +103,20 @@ def add_migration_commands(database_group: Optional["Group"] = None) -> "Group":
103
103
  def get_config_by_bind_key(
104
104
  ctx: "click.Context", bind_key: Optional[str]
105
105
  ) -> "Union[AsyncDatabaseConfig[Any, Any, Any], SyncDatabaseConfig[Any, Any, Any]]":
106
- """Get the SQLAlchemy config for the specified bind key.
106
+ """Get the SQLSpec config for the specified bind key.
107
107
 
108
108
  Args:
109
109
  ctx: The click context.
110
110
  bind_key: The bind key to get the config for.
111
111
 
112
112
  Returns:
113
- The SQLAlchemy config for the specified bind key.
113
+ The SQLSpec config for the specified bind key.
114
114
  """
115
115
  configs = ctx.obj["configs"]
116
116
  if bind_key is None:
117
117
  return cast("Union[AsyncDatabaseConfig[Any, Any, Any], SyncDatabaseConfig[Any, Any, Any]]", configs[0])
118
118
 
119
119
  for config in configs:
120
- # Check if config has a name or identifier attribute
121
120
  config_name = getattr(config, "name", None) or getattr(config, "bind_key", None)
122
121
  if config_name == bind_key:
123
122
  return cast("Union[AsyncDatabaseConfig[Any, Any, Any], SyncDatabaseConfig[Any, Any, Any]]", config)