sqlspec 0.12.1__py3-none-any.whl → 0.13.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 (113) hide show
  1. sqlspec/_sql.py +21 -180
  2. sqlspec/adapters/adbc/config.py +10 -12
  3. sqlspec/adapters/adbc/driver.py +120 -118
  4. sqlspec/adapters/aiosqlite/config.py +3 -3
  5. sqlspec/adapters/aiosqlite/driver.py +116 -141
  6. sqlspec/adapters/asyncmy/config.py +3 -4
  7. sqlspec/adapters/asyncmy/driver.py +123 -135
  8. sqlspec/adapters/asyncpg/config.py +3 -7
  9. sqlspec/adapters/asyncpg/driver.py +98 -140
  10. sqlspec/adapters/bigquery/config.py +4 -5
  11. sqlspec/adapters/bigquery/driver.py +231 -181
  12. sqlspec/adapters/duckdb/config.py +3 -6
  13. sqlspec/adapters/duckdb/driver.py +132 -124
  14. sqlspec/adapters/oracledb/config.py +6 -5
  15. sqlspec/adapters/oracledb/driver.py +242 -259
  16. sqlspec/adapters/psqlpy/config.py +3 -7
  17. sqlspec/adapters/psqlpy/driver.py +118 -93
  18. sqlspec/adapters/psycopg/config.py +34 -30
  19. sqlspec/adapters/psycopg/driver.py +342 -214
  20. sqlspec/adapters/sqlite/config.py +3 -3
  21. sqlspec/adapters/sqlite/driver.py +150 -104
  22. sqlspec/config.py +0 -4
  23. sqlspec/driver/_async.py +89 -98
  24. sqlspec/driver/_common.py +52 -17
  25. sqlspec/driver/_sync.py +81 -105
  26. sqlspec/driver/connection.py +207 -0
  27. sqlspec/driver/mixins/_csv_writer.py +91 -0
  28. sqlspec/driver/mixins/_pipeline.py +38 -49
  29. sqlspec/driver/mixins/_result_utils.py +27 -9
  30. sqlspec/driver/mixins/_storage.py +149 -216
  31. sqlspec/driver/mixins/_type_coercion.py +3 -4
  32. sqlspec/driver/parameters.py +138 -0
  33. sqlspec/exceptions.py +10 -2
  34. sqlspec/extensions/aiosql/adapter.py +0 -10
  35. sqlspec/extensions/litestar/handlers.py +0 -1
  36. sqlspec/extensions/litestar/plugin.py +0 -3
  37. sqlspec/extensions/litestar/providers.py +0 -14
  38. sqlspec/loader.py +31 -118
  39. sqlspec/protocols.py +542 -0
  40. sqlspec/service/__init__.py +3 -2
  41. sqlspec/service/_util.py +147 -0
  42. sqlspec/service/base.py +1116 -9
  43. sqlspec/statement/builder/__init__.py +42 -32
  44. sqlspec/statement/builder/_ddl_utils.py +0 -10
  45. sqlspec/statement/builder/_parsing_utils.py +10 -4
  46. sqlspec/statement/builder/base.py +70 -23
  47. sqlspec/statement/builder/column.py +283 -0
  48. sqlspec/statement/builder/ddl.py +102 -65
  49. sqlspec/statement/builder/delete.py +23 -7
  50. sqlspec/statement/builder/insert.py +29 -15
  51. sqlspec/statement/builder/merge.py +4 -4
  52. sqlspec/statement/builder/mixins/_aggregate_functions.py +113 -14
  53. sqlspec/statement/builder/mixins/_common_table_expr.py +0 -1
  54. sqlspec/statement/builder/mixins/_delete_from.py +1 -1
  55. sqlspec/statement/builder/mixins/_from.py +10 -8
  56. sqlspec/statement/builder/mixins/_group_by.py +0 -1
  57. sqlspec/statement/builder/mixins/_insert_from_select.py +0 -1
  58. sqlspec/statement/builder/mixins/_insert_values.py +0 -2
  59. sqlspec/statement/builder/mixins/_join.py +20 -13
  60. sqlspec/statement/builder/mixins/_limit_offset.py +3 -3
  61. sqlspec/statement/builder/mixins/_merge_clauses.py +3 -4
  62. sqlspec/statement/builder/mixins/_order_by.py +2 -2
  63. sqlspec/statement/builder/mixins/_pivot.py +4 -7
  64. sqlspec/statement/builder/mixins/_select_columns.py +6 -5
  65. sqlspec/statement/builder/mixins/_unpivot.py +6 -9
  66. sqlspec/statement/builder/mixins/_update_from.py +2 -1
  67. sqlspec/statement/builder/mixins/_update_set.py +11 -8
  68. sqlspec/statement/builder/mixins/_where.py +61 -34
  69. sqlspec/statement/builder/select.py +32 -17
  70. sqlspec/statement/builder/update.py +25 -11
  71. sqlspec/statement/filters.py +39 -14
  72. sqlspec/statement/parameter_manager.py +220 -0
  73. sqlspec/statement/parameters.py +210 -79
  74. sqlspec/statement/pipelines/__init__.py +166 -23
  75. sqlspec/statement/pipelines/analyzers/_analyzer.py +22 -25
  76. sqlspec/statement/pipelines/context.py +35 -39
  77. sqlspec/statement/pipelines/transformers/__init__.py +2 -3
  78. sqlspec/statement/pipelines/transformers/_expression_simplifier.py +19 -187
  79. sqlspec/statement/pipelines/transformers/_literal_parameterizer.py +667 -43
  80. sqlspec/statement/pipelines/transformers/_remove_comments_and_hints.py +76 -0
  81. sqlspec/statement/pipelines/validators/_dml_safety.py +33 -18
  82. sqlspec/statement/pipelines/validators/_parameter_style.py +87 -14
  83. sqlspec/statement/pipelines/validators/_performance.py +38 -23
  84. sqlspec/statement/pipelines/validators/_security.py +39 -62
  85. sqlspec/statement/result.py +37 -129
  86. sqlspec/statement/splitter.py +0 -12
  87. sqlspec/statement/sql.py +885 -379
  88. sqlspec/statement/sql_compiler.py +140 -0
  89. sqlspec/storage/__init__.py +10 -2
  90. sqlspec/storage/backends/fsspec.py +82 -35
  91. sqlspec/storage/backends/obstore.py +66 -49
  92. sqlspec/storage/capabilities.py +101 -0
  93. sqlspec/storage/registry.py +56 -83
  94. sqlspec/typing.py +6 -434
  95. sqlspec/utils/cached_property.py +25 -0
  96. sqlspec/utils/correlation.py +0 -2
  97. sqlspec/utils/logging.py +0 -6
  98. sqlspec/utils/sync_tools.py +0 -4
  99. sqlspec/utils/text.py +0 -5
  100. sqlspec/utils/type_guards.py +892 -0
  101. {sqlspec-0.12.1.dist-info → sqlspec-0.13.0.dist-info}/METADATA +1 -1
  102. sqlspec-0.13.0.dist-info/RECORD +150 -0
  103. sqlspec/statement/builder/protocols.py +0 -20
  104. sqlspec/statement/pipelines/base.py +0 -315
  105. sqlspec/statement/pipelines/result_types.py +0 -41
  106. sqlspec/statement/pipelines/transformers/_remove_comments.py +0 -66
  107. sqlspec/statement/pipelines/transformers/_remove_hints.py +0 -81
  108. sqlspec/statement/pipelines/validators/base.py +0 -67
  109. sqlspec/storage/protocol.py +0 -170
  110. sqlspec-0.12.1.dist-info/RECORD +0 -145
  111. {sqlspec-0.12.1.dist-info → sqlspec-0.13.0.dist-info}/WHEEL +0 -0
  112. {sqlspec-0.12.1.dist-info → sqlspec-0.13.0.dist-info}/licenses/LICENSE +0 -0
  113. {sqlspec-0.12.1.dist-info → sqlspec-0.13.0.dist-info}/licenses/NOTICE +0 -0
@@ -7,7 +7,8 @@ from sqlspec.exceptions import SQLBuilderError
7
7
  from sqlspec.statement.builder._parsing_utils import parse_column_expression
8
8
 
9
9
  if TYPE_CHECKING:
10
- from sqlspec.statement.builder.protocols import BuilderProtocol
10
+ from sqlspec.protocols import SQLBuilderProtocol
11
+ from sqlspec.statement.builder.column import Column, FunctionColumn
11
12
 
12
13
  __all__ = ("SelectColumnsMixin",)
13
14
 
@@ -15,7 +16,7 @@ __all__ = ("SelectColumnsMixin",)
15
16
  class SelectColumnsMixin:
16
17
  """Mixin providing SELECT column and DISTINCT clauses for SELECT builders."""
17
18
 
18
- def select(self, *columns: Union[str, exp.Expression]) -> Self:
19
+ def select(self, *columns: Union[str, exp.Expression, "Column", "FunctionColumn"]) -> Self:
19
20
  """Add columns to SELECT clause.
20
21
 
21
22
  Raises:
@@ -24,7 +25,7 @@ class SelectColumnsMixin:
24
25
  Returns:
25
26
  The current builder instance for method chaining.
26
27
  """
27
- builder = cast("BuilderProtocol", self)
28
+ builder = cast("SQLBuilderProtocol", self)
28
29
  if builder._expression is None:
29
30
  builder._expression = exp.Select()
30
31
  if not isinstance(builder._expression, exp.Select):
@@ -34,7 +35,7 @@ class SelectColumnsMixin:
34
35
  builder._expression = builder._expression.select(parse_column_expression(column), copy=False)
35
36
  return cast("Self", builder)
36
37
 
37
- def distinct(self, *columns: Union[str, exp.Expression]) -> Self:
38
+ def distinct(self, *columns: Union[str, exp.Expression, "Column", "FunctionColumn"]) -> Self:
38
39
  """Add DISTINCT clause to SELECT.
39
40
 
40
41
  Args:
@@ -46,7 +47,7 @@ class SelectColumnsMixin:
46
47
  Returns:
47
48
  The current builder instance for method chaining.
48
49
  """
49
- builder = cast("BuilderProtocol", self)
50
+ builder = cast("SQLBuilderProtocol", self)
50
51
  if builder._expression is None:
51
52
  builder._expression = exp.Select()
52
53
  if not isinstance(builder._expression, exp.Select):
@@ -5,13 +5,13 @@ from sqlglot import exp
5
5
  if TYPE_CHECKING:
6
6
  from sqlglot.dialects.dialect import DialectType
7
7
 
8
- from sqlspec.statement.builder.select import SelectBuilder
8
+ from sqlspec.statement.builder.select import Select
9
9
 
10
10
  __all__ = ("UnpivotClauseMixin",)
11
11
 
12
12
 
13
13
  class UnpivotClauseMixin:
14
- """Mixin class to add UNPIVOT functionality to a SelectBuilder."""
14
+ """Mixin class to add UNPIVOT functionality to a Select."""
15
15
 
16
16
  _expression: "Optional[exp.Expression]" = None
17
17
  dialect: "DialectType" = None
@@ -22,7 +22,7 @@ class UnpivotClauseMixin:
22
22
  name_column_name: str,
23
23
  columns_to_unpivot: list[Union[str, exp.Expression]],
24
24
  alias: Optional[str] = None,
25
- ) -> "SelectBuilder":
25
+ ) -> "Select":
26
26
  """Adds an UNPIVOT clause to the SELECT statement.
27
27
 
28
28
  Example:
@@ -38,12 +38,12 @@ class UnpivotClauseMixin:
38
38
  TypeError: If the current expression is not a Select expression.
39
39
 
40
40
  Returns:
41
- The SelectBuilder instance for chaining.
41
+ The Select instance for chaining.
42
42
  """
43
43
  current_expr = self._expression
44
44
  if not isinstance(current_expr, exp.Select):
45
45
  # SelectBuilder's __init__ ensures _expression is exp.Select.
46
- msg = "Unpivot can only be applied to a Select expression managed by SelectBuilder."
46
+ msg = "Unpivot can only be applied to a Select expression managed by Select."
47
47
  raise TypeError(msg)
48
48
 
49
49
  value_col_ident = exp.to_identifier(value_column_name)
@@ -59,7 +59,6 @@ class UnpivotClauseMixin:
59
59
  # Fallback for other types, should ideally be an error or more specific handling
60
60
  unpivot_cols_exprs.append(exp.column(str(col_name_or_expr)))
61
61
 
62
- # Create the unpivot expression (stored as Pivot with unpivot=True)
63
62
  in_expr = exp.In(this=name_col_ident, expressions=unpivot_cols_exprs)
64
63
 
65
64
  unpivot_node = exp.Pivot(expressions=[value_col_ident], fields=[in_expr], unpivot=True)
@@ -67,14 +66,12 @@ class UnpivotClauseMixin:
67
66
  if alias:
68
67
  unpivot_node.set("alias", exp.TableAlias(this=exp.to_identifier(alias)))
69
68
 
70
- # Add unpivot to the table in the FROM clause
71
69
  from_clause = current_expr.args.get("from")
72
70
  if from_clause and isinstance(from_clause, exp.From):
73
71
  table = from_clause.this
74
72
  if isinstance(table, exp.Table):
75
- # Add to pivots array
76
73
  existing_pivots = table.args.get("pivots", [])
77
74
  existing_pivots.append(unpivot_node)
78
75
  table.set("pivots", existing_pivots)
79
76
 
80
- return cast("SelectBuilder", self)
77
+ return cast("Select", self)
@@ -4,6 +4,7 @@ from sqlglot import exp
4
4
  from typing_extensions import Self
5
5
 
6
6
  from sqlspec.exceptions import SQLBuilderError
7
+ from sqlspec.utils.type_guards import has_query_builder_parameters
7
8
 
8
9
  __all__ = ("UpdateFromClauseMixin",)
9
10
 
@@ -30,7 +31,7 @@ class UpdateFromClauseMixin:
30
31
  table_expr: exp.Expression
31
32
  if isinstance(table, str):
32
33
  table_expr = exp.to_table(table, alias=alias)
33
- elif hasattr(table, "build"):
34
+ elif has_query_builder_parameters(table):
34
35
  subquery_builder_params = getattr(table, "_parameters", None)
35
36
  if subquery_builder_params:
36
37
  for p_name, p_value in subquery_builder_params.items():
@@ -5,6 +5,7 @@ from sqlglot import exp
5
5
  from typing_extensions import Self
6
6
 
7
7
  from sqlspec.exceptions import SQLBuilderError
8
+ from sqlspec.utils.type_guards import has_query_builder_parameters
8
9
 
9
10
  __all__ = ("UpdateSetClauseMixin",)
10
11
 
@@ -49,14 +50,15 @@ class UpdateSetClauseMixin:
49
50
  # If value is an expression, use it directly
50
51
  if isinstance(val, exp.Expression):
51
52
  value_expr = val
52
- elif hasattr(val, "_expression") and hasattr(val, "build"):
53
+ elif has_query_builder_parameters(val):
53
54
  # It's a builder (like SelectBuilder), convert to subquery
54
55
  subquery = val.build()
55
56
  # Parse the SQL and use as expression
56
- value_expr = exp.paren(exp.maybe_parse(subquery.sql, dialect=getattr(self, "dialect", None)))
57
+ sql_str = subquery.sql if hasattr(subquery, "sql") and not callable(subquery.sql) else str(subquery)
58
+ value_expr = exp.paren(exp.maybe_parse(sql_str, dialect=getattr(self, "dialect", None)))
57
59
  # Merge parameters from subquery
58
- if hasattr(val, "_parameters"):
59
- for p_name, p_value in val._parameters.items():
60
+ if has_query_builder_parameters(val):
61
+ for p_name, p_value in val.parameters.items():
60
62
  self.add_parameter(p_value, name=p_name) # type: ignore[attr-defined]
61
63
  else:
62
64
  param_name = self.add_parameter(val)[1] # type: ignore[attr-defined]
@@ -69,14 +71,15 @@ class UpdateSetClauseMixin:
69
71
  # If value is an expression, use it directly
70
72
  if isinstance(val, exp.Expression):
71
73
  value_expr = val
72
- elif hasattr(val, "_expression") and hasattr(val, "build"):
74
+ elif has_query_builder_parameters(val):
73
75
  # It's a builder (like SelectBuilder), convert to subquery
74
76
  subquery = val.build()
75
77
  # Parse the SQL and use as expression
76
- value_expr = exp.paren(exp.maybe_parse(subquery.sql, dialect=getattr(self, "dialect", None)))
78
+ sql_str = subquery.sql if hasattr(subquery, "sql") and not callable(subquery.sql) else str(subquery)
79
+ value_expr = exp.paren(exp.maybe_parse(sql_str, dialect=getattr(self, "dialect", None)))
77
80
  # Merge parameters from subquery
78
- if hasattr(val, "_parameters"):
79
- for p_name, p_value in val._parameters.items():
81
+ if has_query_builder_parameters(val):
82
+ for p_name, p_value in val.parameters.items():
80
83
  self.add_parameter(p_value, name=p_name) # type: ignore[attr-defined]
81
84
  else:
82
85
  param_name = self.add_parameter(val)[1] # type: ignore[attr-defined]
@@ -6,9 +6,11 @@ from typing_extensions import Self
6
6
 
7
7
  from sqlspec.exceptions import SQLBuilderError
8
8
  from sqlspec.statement.builder._parsing_utils import parse_column_expression, parse_condition_expression
9
+ from sqlspec.utils.type_guards import has_query_builder_parameters, is_iterable_parameters
9
10
 
10
11
  if TYPE_CHECKING:
11
- from sqlspec.statement.builder.protocols import BuilderProtocol
12
+ from sqlspec.protocols import SQLBuilderProtocol
13
+ from sqlspec.statement.builder.column import ColumnExpression
12
14
 
13
15
  __all__ = ("WhereClauseMixin",)
14
16
 
@@ -17,7 +19,8 @@ class WhereClauseMixin:
17
19
  """Mixin providing WHERE clause methods for SELECT, UPDATE, and DELETE builders."""
18
20
 
19
21
  def where(
20
- self, condition: Union[str, exp.Expression, exp.Condition, tuple[str, Any], tuple[str, str, Any]]
22
+ self,
23
+ condition: Union[str, exp.Expression, exp.Condition, tuple[str, Any], tuple[str, str, Any], "ColumnExpression"],
21
24
  ) -> Self:
22
25
  """Add a WHERE clause to the statement.
23
26
 
@@ -34,14 +37,14 @@ class WhereClauseMixin:
34
37
  Returns:
35
38
  The current builder instance for method chaining.
36
39
  """
37
- # Special case: if this is an UpdateBuilder and _expression is not exp.Update, raise the expected error for test coverage
40
+ # Special case: if this is an Update and _expression is not exp.Update, raise the expected error for test coverage
38
41
 
39
- if self.__class__.__name__ == "UpdateBuilder" and not (
42
+ if self.__class__.__name__ == "Update" and not (
40
43
  hasattr(self, "_expression") and isinstance(getattr(self, "_expression", None), exp.Update)
41
44
  ):
42
45
  msg = "Cannot add WHERE clause to non-UPDATE expression"
43
46
  raise SQLBuilderError(msg)
44
- builder = cast("BuilderProtocol", self)
47
+ builder = cast("SQLBuilderProtocol", self)
45
48
  if builder._expression is None:
46
49
  msg = "Cannot add WHERE clause: expression is not initialized."
47
50
  raise SQLBuilderError(msg)
@@ -50,7 +53,6 @@ class WhereClauseMixin:
50
53
  msg = f"Cannot add WHERE clause to unsupported expression type: {type(builder._expression).__name__}."
51
54
  raise SQLBuilderError(msg)
52
55
 
53
- # Check if table is set for DELETE queries
54
56
  if isinstance(builder._expression, exp.Delete) and not builder._expression.args.get("this"):
55
57
  msg = "WHERE clause requires a table to be set. Use from() to set the table first."
56
58
  raise SQLBuilderError(msg)
@@ -58,7 +60,6 @@ class WhereClauseMixin:
58
60
  # Normalize the condition using enhanced parsing
59
61
  condition_expr: exp.Expression
60
62
  if isinstance(condition, tuple):
61
- # Handle tuple format with proper parameter binding
62
63
  if len(condition) == 2:
63
64
  # 2-tuple: (column, value) -> column = value
64
65
  param_name = builder.add_parameter(condition[1])[1]
@@ -87,7 +88,6 @@ class WhereClauseMixin:
87
88
  "any": exp.Any,
88
89
  }
89
90
  operator = operator.lower()
90
- # Handle special cases for NOT operators
91
91
  if operator == "not like":
92
92
  condition_expr = exp.Not(this=exp.Like(this=col_expr, expression=placeholder_expr))
93
93
  elif operator == "not in":
@@ -104,7 +104,20 @@ class WhereClauseMixin:
104
104
  else:
105
105
  msg = f"WHERE tuple must have 2 or 3 elements, got {len(condition)}"
106
106
  raise SQLBuilderError(msg)
107
+ # Handle ColumnExpression objects
108
+ elif hasattr(condition, "sqlglot_expression"):
109
+ # This is a ColumnExpression from our new Column syntax
110
+ raw_expr = getattr(condition, "sqlglot_expression", None)
111
+ if raw_expr is not None:
112
+ condition_expr = builder._parameterize_expression(raw_expr)
113
+ else:
114
+ # Fallback if attribute exists but is None
115
+ condition_expr = parse_condition_expression(str(condition))
107
116
  else:
117
+ # Existing logic for strings and raw SQLGlot expressions
118
+ # Convert to string if it's not a recognized type
119
+ if not isinstance(condition, (str, exp.Expression, tuple)):
120
+ condition = str(condition)
108
121
  condition_expr = parse_condition_expression(condition)
109
122
 
110
123
  # Use dialect if available for Delete
@@ -194,13 +207,16 @@ class WhereClauseMixin:
194
207
 
195
208
  def where_exists(self, subquery: "Union[str, Any]") -> "Self":
196
209
  sub_expr: exp.Expression
197
- if hasattr(subquery, "_parameters") and hasattr(subquery, "build"):
198
- subquery_builder_params: dict[str, Any] = subquery._parameters # pyright: ignore
210
+ if has_query_builder_parameters(subquery):
211
+ subquery_builder_params: dict[str, Any] = subquery.parameters
199
212
  if subquery_builder_params:
200
213
  for p_name, p_value in subquery_builder_params.items():
201
214
  self.add_parameter(p_value, name=p_name) # type: ignore[attr-defined]
202
215
  sub_sql_obj = subquery.build() # pyright: ignore
203
- sub_expr = exp.maybe_parse(sub_sql_obj.sql, dialect=getattr(self, "dialect_name", None))
216
+ sql_str = (
217
+ sub_sql_obj.sql if hasattr(sub_sql_obj, "sql") and not callable(sub_sql_obj.sql) else str(sub_sql_obj)
218
+ )
219
+ sub_expr = exp.maybe_parse(sql_str, dialect=getattr(self, "dialect_name", None))
204
220
  else:
205
221
  sub_expr = exp.maybe_parse(str(subquery), dialect=getattr(self, "dialect_name", None))
206
222
 
@@ -213,13 +229,16 @@ class WhereClauseMixin:
213
229
 
214
230
  def where_not_exists(self, subquery: "Union[str, Any]") -> "Self":
215
231
  sub_expr: exp.Expression
216
- if hasattr(subquery, "_parameters") and hasattr(subquery, "build"):
217
- subquery_builder_params: dict[str, Any] = subquery._parameters # pyright: ignore
232
+ if has_query_builder_parameters(subquery):
233
+ subquery_builder_params: dict[str, Any] = subquery.parameters
218
234
  if subquery_builder_params:
219
235
  for p_name, p_value in subquery_builder_params.items():
220
236
  self.add_parameter(p_value, name=p_name) # type: ignore[attr-defined]
221
237
  sub_sql_obj = subquery.build() # pyright: ignore
222
- sub_expr = exp.maybe_parse(sub_sql_obj.sql, dialect=getattr(self, "dialect_name", None))
238
+ sql_str = (
239
+ sub_sql_obj.sql if hasattr(sub_sql_obj, "sql") and not callable(sub_sql_obj.sql) else str(sub_sql_obj)
240
+ )
241
+ sub_expr = exp.maybe_parse(sql_str, dialect=getattr(self, "dialect_name", None))
223
242
  else:
224
243
  sub_expr = exp.maybe_parse(str(subquery), dialect=getattr(self, "dialect_name", None))
225
244
 
@@ -238,16 +257,18 @@ class WhereClauseMixin:
238
257
  """Add a WHERE ... IN (...) clause. Supports subqueries and iterables."""
239
258
  col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
240
259
  # Subquery support
241
- if hasattr(values, "build") or isinstance(values, exp.Expression):
242
- if hasattr(values, "build"):
260
+ if has_query_builder_parameters(values) or isinstance(values, exp.Expression):
261
+ subquery_exp: exp.Expression
262
+ if has_query_builder_parameters(values):
243
263
  subquery = values.build() # pyright: ignore
244
- subquery_exp = exp.paren(exp.maybe_parse(subquery.sql, dialect=getattr(self, "dialect_name", None)))
264
+ sql_str = subquery.sql if hasattr(subquery, "sql") and not callable(subquery.sql) else str(subquery)
265
+ subquery_exp = exp.paren(exp.maybe_parse(sql_str, dialect=getattr(self, "dialect_name", None)))
245
266
  else:
246
- subquery_exp = values
267
+ subquery_exp = values # type: ignore[assignment]
247
268
  condition = col_expr.isin(subquery_exp)
248
269
  return self.where(condition)
249
270
  # Iterable of values
250
- if not hasattr(values, "__iter__") or isinstance(values, (str, bytes)):
271
+ if not is_iterable_parameters(values) or isinstance(values, (str, bytes)):
251
272
  msg = "Unsupported type for 'values' in WHERE IN"
252
273
  raise SQLBuilderError(msg)
253
274
  params = []
@@ -260,15 +281,17 @@ class WhereClauseMixin:
260
281
  def where_not_in(self, column: "Union[str, exp.Column]", values: Any) -> "Self":
261
282
  """Add a WHERE ... NOT IN (...) clause. Supports subqueries and iterables."""
262
283
  col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
263
- if hasattr(values, "build") or isinstance(values, exp.Expression):
264
- if hasattr(values, "build"):
284
+ if has_query_builder_parameters(values) or isinstance(values, exp.Expression):
285
+ subquery_exp: exp.Expression
286
+ if has_query_builder_parameters(values):
265
287
  subquery = values.build() # pyright: ignore
266
- subquery_exp = exp.paren(exp.maybe_parse(subquery.sql, dialect=getattr(self, "dialect_name", None)))
288
+ sql_str = subquery.sql if hasattr(subquery, "sql") and not callable(subquery.sql) else str(subquery)
289
+ subquery_exp = exp.paren(exp.maybe_parse(sql_str, dialect=getattr(self, "dialect_name", None)))
267
290
  else:
268
- subquery_exp = values
291
+ subquery_exp = values # type: ignore[assignment]
269
292
  condition = exp.Not(this=col_expr.isin(subquery_exp))
270
293
  return self.where(condition)
271
- if not hasattr(values, "__iter__") or isinstance(values, (str, bytes)):
294
+ if not is_iterable_parameters(values) or isinstance(values, (str, bytes)):
272
295
  msg = "Values for where_not_in must be a non-string iterable or subquery."
273
296
  raise SQLBuilderError(msg)
274
297
  params = []
@@ -294,12 +317,14 @@ class WhereClauseMixin:
294
317
  The current builder instance for method chaining.
295
318
  """
296
319
  col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
297
- if hasattr(values, "build") or isinstance(values, exp.Expression):
298
- if hasattr(values, "build"):
320
+ if has_query_builder_parameters(values) or isinstance(values, exp.Expression):
321
+ subquery_exp: exp.Expression
322
+ if has_query_builder_parameters(values):
299
323
  subquery = values.build() # pyright: ignore
300
- subquery_exp = exp.paren(exp.maybe_parse(subquery.sql, dialect=getattr(self, "dialect_name", None)))
324
+ sql_str = subquery.sql if hasattr(subquery, "sql") and not callable(subquery.sql) else str(subquery)
325
+ subquery_exp = exp.paren(exp.maybe_parse(sql_str, dialect=getattr(self, "dialect_name", None)))
301
326
  else:
302
- subquery_exp = values
327
+ subquery_exp = values # type: ignore[assignment]
303
328
  condition = exp.EQ(this=col_expr, expression=exp.Any(this=subquery_exp))
304
329
  return self.where(condition)
305
330
  if isinstance(values, str):
@@ -317,7 +342,7 @@ class WhereClauseMixin:
317
342
  # If parsing fails, fall through to error
318
343
  msg = "Unsupported type for 'values' in WHERE ANY"
319
344
  raise SQLBuilderError(msg)
320
- if not hasattr(values, "__iter__") or isinstance(values, bytes):
345
+ if not is_iterable_parameters(values) or isinstance(values, bytes):
321
346
  msg = "Unsupported type for 'values' in WHERE ANY"
322
347
  raise SQLBuilderError(msg)
323
348
  params = []
@@ -339,12 +364,14 @@ class WhereClauseMixin:
339
364
  The current builder instance for method chaining.
340
365
  """
341
366
  col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
342
- if hasattr(values, "build") or isinstance(values, exp.Expression):
343
- if hasattr(values, "build"):
367
+ if has_query_builder_parameters(values) or isinstance(values, exp.Expression):
368
+ subquery_exp: exp.Expression
369
+ if has_query_builder_parameters(values):
344
370
  subquery = values.build() # pyright: ignore
345
- subquery_exp = exp.paren(exp.maybe_parse(subquery.sql, dialect=getattr(self, "dialect_name", None)))
371
+ sql_str = subquery.sql if hasattr(subquery, "sql") and not callable(subquery.sql) else str(subquery)
372
+ subquery_exp = exp.paren(exp.maybe_parse(sql_str, dialect=getattr(self, "dialect_name", None)))
346
373
  else:
347
- subquery_exp = values
374
+ subquery_exp = values # type: ignore[assignment]
348
375
  condition = exp.NEQ(this=col_expr, expression=exp.Any(this=subquery_exp))
349
376
  return self.where(condition)
350
377
  if isinstance(values, str):
@@ -362,7 +389,7 @@ class WhereClauseMixin:
362
389
  # If parsing fails, fall through to error
363
390
  msg = "Unsupported type for 'values' in WHERE NOT ANY"
364
391
  raise SQLBuilderError(msg)
365
- if not hasattr(values, "__iter__") or isinstance(values, bytes):
392
+ if not is_iterable_parameters(values) or isinstance(values, bytes):
366
393
  msg = "Unsupported type for 'values' in WHERE NOT ANY"
367
394
  raise SQLBuilderError(msg)
368
395
  params = []
@@ -6,7 +6,7 @@ with automatic parameter binding and validation.
6
6
 
7
7
  import re
8
8
  from dataclasses import dataclass, field
9
- from typing import Optional, Union, cast
9
+ from typing import Any, Optional, Union, cast
10
10
 
11
11
  from sqlglot import exp
12
12
  from typing_extensions import Self
@@ -32,11 +32,11 @@ from sqlspec.statement.builder.mixins import (
32
32
  from sqlspec.statement.result import SQLResult
33
33
  from sqlspec.typing import RowT
34
34
 
35
- __all__ = ("SelectBuilder",)
35
+ __all__ = ("Select",)
36
36
 
37
37
 
38
38
  @dataclass
39
- class SelectBuilder(
39
+ class Select(
40
40
  QueryBuilder[RowT],
41
41
  WhereClauseMixin,
42
42
  OrderByClauseMixin,
@@ -77,16 +77,37 @@ class SelectBuilder(
77
77
  _schema: The schema/model class for row typing, if set via as_schema().
78
78
  """
79
79
 
80
- _with_parts: "dict[str, Union[exp.CTE, SelectBuilder]]" = field(default_factory=dict, init=False)
80
+ _with_parts: "dict[str, Union[exp.CTE, Select]]" = field(default_factory=dict, init=False)
81
81
  _expression: Optional[exp.Expression] = field(default=None, init=False, repr=False, compare=False, hash=False)
82
82
  _schema: Optional[type[RowT]] = None
83
83
  _hints: "list[dict[str, object]]" = field(default_factory=list, init=False, repr=False)
84
84
 
85
- def __post_init__(self) -> "None":
86
- super().__post_init__()
85
+ def __init__(self, *columns: str, **kwargs: Any) -> None:
86
+ """Initialize SELECT with optional columns.
87
+
88
+ Args:
89
+ *columns: Column names to select (e.g., "id", "name", "u.email")
90
+ **kwargs: Additional QueryBuilder arguments (dialect, schema, etc.)
91
+
92
+ Examples:
93
+ Select("id", "name") # Shorthand for Select().select("id", "name")
94
+ Select() # Same as SelectBuilder() - start empty
95
+ """
96
+ super().__init__(**kwargs)
97
+
98
+ # Initialize fields from dataclass
99
+ self._with_parts = {}
100
+ self._expression = None
101
+ self._schema = None
102
+ self._hints = []
103
+
87
104
  if self._expression is None:
88
105
  self._create_base_expression()
89
106
 
107
+ # Add columns if provided - just a shorthand for .select()
108
+ if columns:
109
+ self.select(*columns)
110
+
90
111
  @property
91
112
  def _expected_result_type(self) -> "type[SQLResult[RowT]]":
92
113
  """Get the expected result type for SELECT operations.
@@ -102,8 +123,8 @@ class SelectBuilder(
102
123
  # At this point, self._expression is exp.Select
103
124
  return self._expression
104
125
 
105
- def as_schema(self, schema: "type[RowT]") -> "SelectBuilder[RowT]":
106
- """Return a new SelectBuilder instance parameterized with the given schema/model type.
126
+ def as_schema(self, schema: "type[RowT]") -> "Select[RowT]":
127
+ """Return a new Select instance parameterized with the given schema/model type.
107
128
 
108
129
  This enables type-safe result mapping: the returned builder will carry the schema type
109
130
  for static analysis and IDE autocompletion. The schema should be a class such as a Pydantic
@@ -113,15 +134,15 @@ class SelectBuilder(
113
134
  schema: The schema/model class to use for row typing (e.g., a Pydantic model, dataclass, or msgspec.Struct).
114
135
 
115
136
  Returns:
116
- SelectBuilder[RowT]: A new SelectBuilder instance with RowT set to the provided schema/model type.
137
+ Select[RowT]: A new Select instance with RowT set to the provided schema/model type.
117
138
  """
118
- new_builder = SelectBuilder()
139
+ new_builder = Select()
119
140
  new_builder._expression = self._expression.copy() if self._expression is not None else None
120
141
  new_builder._parameters = self._parameters.copy()
121
142
  new_builder._parameter_counter = self._parameter_counter
122
143
  new_builder.dialect = self.dialect
123
144
  new_builder._schema = schema # type: ignore[assignment]
124
- return cast("SelectBuilder[RowT]", new_builder)
145
+ return cast("Select[RowT]", new_builder)
125
146
 
126
147
  def with_hint(
127
148
  self,
@@ -151,15 +172,12 @@ class SelectBuilder(
151
172
  Returns:
152
173
  SafeQuery: A dataclass containing the SQL string and parameters.
153
174
  """
154
- # Call parent build method which handles CTEs and optimization
155
175
  safe_query = super().build()
156
176
 
157
- # Apply hints using SQLGlot's proper hint support (more robust than regex)
158
177
  if hasattr(self, "_hints") and self._hints:
159
178
  modified_expr = self._expression.copy() if self._expression else None
160
179
 
161
180
  if modified_expr and isinstance(modified_expr, exp.Select):
162
- # Apply statement-level hints using SQLGlot's Hint expression
163
181
  statement_hints = [h["hint"] for h in self._hints if h.get("location") == "statement"]
164
182
  if statement_hints:
165
183
  # Parse each hint and create proper hint expressions
@@ -172,12 +190,10 @@ class SelectBuilder(
172
190
  if hint_expr:
173
191
  hint_expressions.append(hint_expr)
174
192
  else:
175
- # Create a raw identifier for unparsable hints
176
193
  hint_expressions.append(exp.Anonymous(this=hint_str))
177
194
  except Exception: # noqa: PERF203
178
195
  hint_expressions.append(exp.Anonymous(this=str(hint)))
179
196
 
180
- # Create a Hint node and attach to SELECT
181
197
  if hint_expressions:
182
198
  hint_node = exp.Hint(expressions=hint_expressions)
183
199
  modified_expr.set("hint", hint_node)
@@ -186,7 +202,6 @@ class SelectBuilder(
186
202
  # since SQLGlot doesn't have a standard way to attach hints to individual tables
187
203
  modified_sql = modified_expr.sql(dialect=self.dialect_name, pretty=True)
188
204
 
189
- # Apply table-level hints via string manipulation (as fallback)
190
205
  table_hints = [h for h in self._hints if h.get("location") == "table" and h.get("table")]
191
206
  if table_hints:
192
207
  for th in table_hints:
@@ -5,7 +5,7 @@ with automatic parameter binding and validation.
5
5
  """
6
6
 
7
7
  from dataclasses import dataclass
8
- from typing import TYPE_CHECKING, Optional, Union
8
+ from typing import TYPE_CHECKING, Any, Optional, Union
9
9
 
10
10
  from sqlglot import exp
11
11
  from typing_extensions import Self
@@ -23,13 +23,13 @@ from sqlspec.statement.result import SQLResult
23
23
  from sqlspec.typing import RowT
24
24
 
25
25
  if TYPE_CHECKING:
26
- from sqlspec.statement.builder.select import SelectBuilder
26
+ from sqlspec.statement.builder.select import Select
27
27
 
28
- __all__ = ("UpdateBuilder",)
28
+ __all__ = ("Update",)
29
29
 
30
30
 
31
31
  @dataclass(unsafe_hash=True)
32
- class UpdateBuilder(
32
+ class Update(
33
33
  QueryBuilder[RowT],
34
34
  WhereClauseMixin,
35
35
  ReturningClauseMixin,
@@ -46,16 +46,21 @@ class UpdateBuilder(
46
46
  ```python
47
47
  # Basic UPDATE
48
48
  update_query = (
49
- UpdateBuilder()
49
+ Update()
50
50
  .table("users")
51
51
  .set(name="John Doe")
52
52
  .set(email="john@example.com")
53
53
  .where("id = 1")
54
54
  )
55
55
 
56
+ # Even more concise with constructor
57
+ update_query = (
58
+ Update("users").set(name="John Doe").where("id = 1")
59
+ )
60
+
56
61
  # UPDATE with parameterized conditions
57
62
  update_query = (
58
- UpdateBuilder()
63
+ Update()
59
64
  .table("users")
60
65
  .set(status="active")
61
66
  .where_eq("id", 123)
@@ -63,7 +68,7 @@ class UpdateBuilder(
63
68
 
64
69
  # UPDATE with FROM clause (PostgreSQL style)
65
70
  update_query = (
66
- UpdateBuilder()
71
+ Update()
67
72
  .table("users", "u")
68
73
  .set(name="Updated Name")
69
74
  .from_("profiles", "p")
@@ -72,6 +77,18 @@ class UpdateBuilder(
72
77
  ```
73
78
  """
74
79
 
80
+ def __init__(self, table: Optional[str] = None, **kwargs: Any) -> None:
81
+ """Initialize UPDATE with optional table.
82
+
83
+ Args:
84
+ table: Target table name
85
+ **kwargs: Additional QueryBuilder arguments
86
+ """
87
+ super().__init__(**kwargs)
88
+
89
+ if table:
90
+ self.table(table)
91
+
75
92
  @property
76
93
  def _expected_result_type(self) -> "type[SQLResult[RowT]]":
77
94
  """Return the expected result type for this builder."""
@@ -87,7 +104,7 @@ class UpdateBuilder(
87
104
 
88
105
  def join(
89
106
  self,
90
- table: "Union[str, exp.Expression, SelectBuilder[RowT]]",
107
+ table: "Union[str, exp.Expression, Select[RowT]]",
91
108
  on: "Union[str, exp.Expression]",
92
109
  alias: "Optional[str]" = None,
93
110
  join_type: str = "INNER",
@@ -141,7 +158,6 @@ class UpdateBuilder(
141
158
  msg = f"Unsupported join type: {join_type}"
142
159
  raise SQLBuilderError(msg)
143
160
 
144
- # Add join to the UPDATE expression
145
161
  if not self._expression.args.get("joins"):
146
162
  self._expression.set("joins", [])
147
163
  self._expression.args["joins"].append(join_expr)
@@ -165,12 +181,10 @@ class UpdateBuilder(
165
181
  msg = "No UPDATE expression to build or expression is of the wrong type."
166
182
  raise SQLBuilderError(msg)
167
183
 
168
- # Check that the table is set
169
184
  if getattr(self._expression, "this", None) is None:
170
185
  msg = "No table specified for UPDATE statement."
171
186
  raise SQLBuilderError(msg)
172
187
 
173
- # Check that at least one SET expression exists
174
188
  if not self._expression.args.get("expressions"):
175
189
  msg = "At least one SET clause must be specified for UPDATE statement."
176
190
  raise SQLBuilderError(msg)