sqlspec 0.13.0__py3-none-any.whl → 0.14.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 (110) hide show
  1. sqlspec/__init__.py +39 -1
  2. sqlspec/adapters/adbc/config.py +4 -40
  3. sqlspec/adapters/adbc/driver.py +29 -16
  4. sqlspec/adapters/aiosqlite/config.py +15 -20
  5. sqlspec/adapters/aiosqlite/driver.py +36 -18
  6. sqlspec/adapters/asyncmy/config.py +16 -33
  7. sqlspec/adapters/asyncmy/driver.py +23 -16
  8. sqlspec/adapters/asyncpg/config.py +19 -61
  9. sqlspec/adapters/asyncpg/driver.py +41 -18
  10. sqlspec/adapters/bigquery/config.py +2 -43
  11. sqlspec/adapters/bigquery/driver.py +26 -14
  12. sqlspec/adapters/duckdb/config.py +2 -49
  13. sqlspec/adapters/duckdb/driver.py +35 -16
  14. sqlspec/adapters/oracledb/config.py +30 -83
  15. sqlspec/adapters/oracledb/driver.py +54 -27
  16. sqlspec/adapters/psqlpy/config.py +17 -57
  17. sqlspec/adapters/psqlpy/driver.py +28 -8
  18. sqlspec/adapters/psycopg/config.py +30 -73
  19. sqlspec/adapters/psycopg/driver.py +69 -24
  20. sqlspec/adapters/sqlite/config.py +3 -21
  21. sqlspec/adapters/sqlite/driver.py +50 -26
  22. sqlspec/cli.py +248 -0
  23. sqlspec/config.py +18 -20
  24. sqlspec/driver/_async.py +28 -10
  25. sqlspec/driver/_common.py +5 -4
  26. sqlspec/driver/_sync.py +28 -10
  27. sqlspec/driver/mixins/__init__.py +6 -0
  28. sqlspec/driver/mixins/_cache.py +114 -0
  29. sqlspec/driver/mixins/_pipeline.py +0 -4
  30. sqlspec/{service/base.py → driver/mixins/_query_tools.py} +86 -421
  31. sqlspec/driver/mixins/_result_utils.py +0 -2
  32. sqlspec/driver/mixins/_sql_translator.py +0 -2
  33. sqlspec/driver/mixins/_storage.py +4 -18
  34. sqlspec/driver/mixins/_type_coercion.py +0 -2
  35. sqlspec/driver/parameters.py +4 -4
  36. sqlspec/extensions/aiosql/adapter.py +4 -4
  37. sqlspec/extensions/litestar/__init__.py +2 -1
  38. sqlspec/extensions/litestar/cli.py +48 -0
  39. sqlspec/extensions/litestar/plugin.py +3 -0
  40. sqlspec/loader.py +1 -1
  41. sqlspec/migrations/__init__.py +23 -0
  42. sqlspec/migrations/base.py +390 -0
  43. sqlspec/migrations/commands.py +525 -0
  44. sqlspec/migrations/runner.py +215 -0
  45. sqlspec/migrations/tracker.py +153 -0
  46. sqlspec/migrations/utils.py +89 -0
  47. sqlspec/protocols.py +37 -3
  48. sqlspec/statement/builder/__init__.py +8 -8
  49. sqlspec/statement/builder/{column.py → _column.py} +82 -52
  50. sqlspec/statement/builder/{ddl.py → _ddl.py} +5 -5
  51. sqlspec/statement/builder/_ddl_utils.py +1 -1
  52. sqlspec/statement/builder/{delete.py → _delete.py} +1 -1
  53. sqlspec/statement/builder/{insert.py → _insert.py} +1 -1
  54. sqlspec/statement/builder/{merge.py → _merge.py} +1 -1
  55. sqlspec/statement/builder/_parsing_utils.py +5 -3
  56. sqlspec/statement/builder/{select.py → _select.py} +59 -61
  57. sqlspec/statement/builder/{update.py → _update.py} +2 -2
  58. sqlspec/statement/builder/mixins/__init__.py +24 -30
  59. sqlspec/statement/builder/mixins/{_set_ops.py → _cte_and_set_ops.py} +86 -2
  60. sqlspec/statement/builder/mixins/{_delete_from.py → _delete_operations.py} +2 -0
  61. sqlspec/statement/builder/mixins/{_insert_values.py → _insert_operations.py} +70 -1
  62. sqlspec/statement/builder/mixins/{_merge_clauses.py → _merge_operations.py} +2 -0
  63. sqlspec/statement/builder/mixins/_order_limit_operations.py +123 -0
  64. sqlspec/statement/builder/mixins/{_pivot.py → _pivot_operations.py} +71 -2
  65. sqlspec/statement/builder/mixins/_select_operations.py +612 -0
  66. sqlspec/statement/builder/mixins/{_update_set.py → _update_operations.py} +73 -2
  67. sqlspec/statement/builder/mixins/_where_clause.py +536 -0
  68. sqlspec/statement/cache.py +50 -0
  69. sqlspec/statement/filters.py +37 -8
  70. sqlspec/statement/parameters.py +154 -25
  71. sqlspec/statement/pipelines/__init__.py +1 -1
  72. sqlspec/statement/pipelines/context.py +4 -4
  73. sqlspec/statement/pipelines/transformers/_expression_simplifier.py +3 -3
  74. sqlspec/statement/pipelines/validators/_parameter_style.py +22 -22
  75. sqlspec/statement/pipelines/validators/_performance.py +1 -5
  76. sqlspec/statement/sql.py +246 -176
  77. sqlspec/utils/__init__.py +2 -1
  78. sqlspec/utils/statement_hashing.py +203 -0
  79. sqlspec/utils/type_guards.py +32 -0
  80. {sqlspec-0.13.0.dist-info → sqlspec-0.14.0.dist-info}/METADATA +1 -1
  81. sqlspec-0.14.0.dist-info/RECORD +143 -0
  82. sqlspec-0.14.0.dist-info/entry_points.txt +2 -0
  83. sqlspec/service/__init__.py +0 -4
  84. sqlspec/service/_util.py +0 -147
  85. sqlspec/service/pagination.py +0 -26
  86. sqlspec/statement/builder/mixins/_aggregate_functions.py +0 -250
  87. sqlspec/statement/builder/mixins/_case_builder.py +0 -91
  88. sqlspec/statement/builder/mixins/_common_table_expr.py +0 -90
  89. sqlspec/statement/builder/mixins/_from.py +0 -63
  90. sqlspec/statement/builder/mixins/_group_by.py +0 -118
  91. sqlspec/statement/builder/mixins/_having.py +0 -35
  92. sqlspec/statement/builder/mixins/_insert_from_select.py +0 -47
  93. sqlspec/statement/builder/mixins/_insert_into.py +0 -36
  94. sqlspec/statement/builder/mixins/_limit_offset.py +0 -53
  95. sqlspec/statement/builder/mixins/_order_by.py +0 -46
  96. sqlspec/statement/builder/mixins/_returning.py +0 -37
  97. sqlspec/statement/builder/mixins/_select_columns.py +0 -61
  98. sqlspec/statement/builder/mixins/_unpivot.py +0 -77
  99. sqlspec/statement/builder/mixins/_update_from.py +0 -55
  100. sqlspec/statement/builder/mixins/_update_table.py +0 -29
  101. sqlspec/statement/builder/mixins/_where.py +0 -401
  102. sqlspec/statement/builder/mixins/_window_functions.py +0 -86
  103. sqlspec/statement/parameter_manager.py +0 -220
  104. sqlspec/statement/sql_compiler.py +0 -140
  105. sqlspec-0.13.0.dist-info/RECORD +0 -150
  106. /sqlspec/statement/builder/{base.py → _base.py} +0 -0
  107. /sqlspec/statement/builder/mixins/{_join.py → _join_operations.py} +0 -0
  108. {sqlspec-0.13.0.dist-info → sqlspec-0.14.0.dist-info}/WHEEL +0 -0
  109. {sqlspec-0.13.0.dist-info → sqlspec-0.14.0.dist-info}/licenses/LICENSE +0 -0
  110. {sqlspec-0.13.0.dist-info → sqlspec-0.14.0.dist-info}/licenses/NOTICE +0 -0
@@ -18,37 +18,37 @@ class ColumnExpression:
18
18
  """Base class for column expressions that can be combined with operators."""
19
19
 
20
20
  def __init__(self, expression: exp.Expression) -> None:
21
- self._expr = expression
21
+ self._expression = expression
22
22
 
23
23
  def __and__(self, other: "ColumnExpression") -> "ColumnExpression":
24
24
  """Combine with AND operator (&)."""
25
25
  if not isinstance(other, ColumnExpression):
26
26
  return NotImplemented
27
- return ColumnExpression(exp.And(this=self._expr, expression=other._expr))
27
+ return ColumnExpression(exp.And(this=self._expression, expression=other._expression))
28
28
 
29
29
  def __or__(self, other: "ColumnExpression") -> "ColumnExpression":
30
30
  """Combine with OR operator (|)."""
31
31
  if not isinstance(other, ColumnExpression):
32
32
  return NotImplemented
33
- return ColumnExpression(exp.Or(this=self._expr, expression=other._expr))
33
+ return ColumnExpression(exp.Or(this=self._expression, expression=other._expression))
34
34
 
35
35
  def __invert__(self) -> "ColumnExpression":
36
36
  """Apply NOT operator (~)."""
37
- return ColumnExpression(exp.Not(this=self._expr))
37
+ return ColumnExpression(exp.Not(this=self._expression))
38
38
 
39
39
  def __bool__(self) -> bool:
40
40
  """Prevent accidental use of 'and'/'or' keywords."""
41
41
  msg = (
42
42
  "Cannot use 'and'/'or' operators on ColumnExpression. "
43
43
  "Use '&'/'|' operators instead. "
44
- f"Expression: {self._expr.sql()}"
44
+ f"Expression: {self._expression.sql()}"
45
45
  )
46
46
  raise TypeError(msg)
47
47
 
48
48
  @property
49
49
  def sqlglot_expression(self) -> exp.Expression:
50
50
  """Get the underlying SQLGlot expression."""
51
- return self._expr
51
+ return self._expression
52
52
 
53
53
 
54
54
  class Column:
@@ -60,60 +60,60 @@ class Column:
60
60
 
61
61
  # Create SQLGlot column expression
62
62
  if table:
63
- self._expr = exp.Column(this=exp.Identifier(this=name), table=exp.Identifier(this=table))
63
+ self._expression = exp.Column(this=exp.Identifier(this=name), table=exp.Identifier(this=table))
64
64
  else:
65
- self._expr = exp.Column(this=exp.Identifier(this=name))
65
+ self._expression = exp.Column(this=exp.Identifier(this=name))
66
66
 
67
67
  # Comparison operators
68
68
  def __eq__(self, other: object) -> ColumnExpression: # type: ignore[override]
69
69
  """Equal to (==)."""
70
70
  if other is None:
71
- return ColumnExpression(exp.Is(this=self._expr, expression=exp.Null()))
72
- return ColumnExpression(exp.EQ(this=self._expr, expression=exp.convert(other)))
71
+ return ColumnExpression(exp.Is(this=self._expression, expression=exp.Null()))
72
+ return ColumnExpression(exp.EQ(this=self._expression, expression=exp.convert(other)))
73
73
 
74
74
  def __ne__(self, other: object) -> ColumnExpression: # type: ignore[override]
75
75
  """Not equal to (!=)."""
76
76
  if other is None:
77
- return ColumnExpression(exp.Not(this=exp.Is(this=self._expr, expression=exp.Null())))
78
- return ColumnExpression(exp.NEQ(this=self._expr, expression=exp.convert(other)))
77
+ return ColumnExpression(exp.Not(this=exp.Is(this=self._expression, expression=exp.Null())))
78
+ return ColumnExpression(exp.NEQ(this=self._expression, expression=exp.convert(other)))
79
79
 
80
80
  def __gt__(self, other: Any) -> ColumnExpression:
81
81
  """Greater than (>)."""
82
- return ColumnExpression(exp.GT(this=self._expr, expression=exp.convert(other)))
82
+ return ColumnExpression(exp.GT(this=self._expression, expression=exp.convert(other)))
83
83
 
84
84
  def __ge__(self, other: Any) -> ColumnExpression:
85
85
  """Greater than or equal (>=)."""
86
- return ColumnExpression(exp.GTE(this=self._expr, expression=exp.convert(other)))
86
+ return ColumnExpression(exp.GTE(this=self._expression, expression=exp.convert(other)))
87
87
 
88
88
  def __lt__(self, other: Any) -> ColumnExpression:
89
89
  """Less than (<)."""
90
- return ColumnExpression(exp.LT(this=self._expr, expression=exp.convert(other)))
90
+ return ColumnExpression(exp.LT(this=self._expression, expression=exp.convert(other)))
91
91
 
92
92
  def __le__(self, other: Any) -> ColumnExpression:
93
93
  """Less than or equal (<=)."""
94
- return ColumnExpression(exp.LTE(this=self._expr, expression=exp.convert(other)))
94
+ return ColumnExpression(exp.LTE(this=self._expression, expression=exp.convert(other)))
95
95
 
96
96
  def __invert__(self) -> ColumnExpression:
97
97
  """Apply NOT operator (~)."""
98
- return ColumnExpression(exp.Not(this=self._expr))
98
+ return ColumnExpression(exp.Not(this=self._expression))
99
99
 
100
100
  # SQL-specific methods
101
101
  def like(self, pattern: str, escape: Optional[str] = None) -> ColumnExpression:
102
102
  """SQL LIKE pattern matching."""
103
103
  if escape:
104
- like_expr = exp.Like(this=self._expr, expression=exp.convert(pattern), escape=exp.convert(escape))
104
+ like_expr = exp.Like(this=self._expression, expression=exp.convert(pattern), escape=exp.convert(escape))
105
105
  else:
106
- like_expr = exp.Like(this=self._expr, expression=exp.convert(pattern))
106
+ like_expr = exp.Like(this=self._expression, expression=exp.convert(pattern))
107
107
  return ColumnExpression(like_expr)
108
108
 
109
109
  def ilike(self, pattern: str) -> ColumnExpression:
110
110
  """Case-insensitive LIKE."""
111
- return ColumnExpression(exp.ILike(this=self._expr, expression=exp.convert(pattern)))
111
+ return ColumnExpression(exp.ILike(this=self._expression, expression=exp.convert(pattern)))
112
112
 
113
113
  def in_(self, values: Iterable[Any]) -> ColumnExpression:
114
114
  """SQL IN clause."""
115
115
  converted_values = [exp.convert(v) for v in values]
116
- return ColumnExpression(exp.In(this=self._expr, expressions=converted_values))
116
+ return ColumnExpression(exp.In(this=self._expression, expressions=converted_values))
117
117
 
118
118
  def not_in(self, values: Iterable[Any]) -> ColumnExpression:
119
119
  """SQL NOT IN clause."""
@@ -121,15 +121,15 @@ class Column:
121
121
 
122
122
  def between(self, start: Any, end: Any) -> ColumnExpression:
123
123
  """SQL BETWEEN clause."""
124
- return ColumnExpression(exp.Between(this=self._expr, low=exp.convert(start), high=exp.convert(end)))
124
+ return ColumnExpression(exp.Between(this=self._expression, low=exp.convert(start), high=exp.convert(end)))
125
125
 
126
126
  def is_null(self) -> ColumnExpression:
127
127
  """SQL IS NULL."""
128
- return ColumnExpression(exp.Is(this=self._expr, expression=exp.Null()))
128
+ return ColumnExpression(exp.Is(this=self._expression, expression=exp.Null()))
129
129
 
130
130
  def is_not_null(self) -> ColumnExpression:
131
131
  """SQL IS NOT NULL."""
132
- return ColumnExpression(exp.Not(this=exp.Is(this=self._expr, expression=exp.Null())))
132
+ return ColumnExpression(exp.Not(this=exp.Is(this=self._expression, expression=exp.Null())))
133
133
 
134
134
  def not_like(self, pattern: str, escape: Optional[str] = None) -> ColumnExpression:
135
135
  """SQL NOT LIKE pattern matching."""
@@ -142,67 +142,97 @@ class Column:
142
142
  def any_(self, values: Iterable[Any]) -> ColumnExpression:
143
143
  """SQL = ANY(...) clause."""
144
144
  converted_values = [exp.convert(v) for v in values]
145
- return ColumnExpression(exp.EQ(this=self._expr, expression=exp.Any(expressions=converted_values)))
145
+ return ColumnExpression(exp.EQ(this=self._expression, expression=exp.Any(expressions=converted_values)))
146
146
 
147
147
  def not_any_(self, values: Iterable[Any]) -> ColumnExpression:
148
148
  """SQL <> ANY(...) clause."""
149
149
  converted_values = [exp.convert(v) for v in values]
150
- return ColumnExpression(exp.NEQ(this=self._expr, expression=exp.Any(expressions=converted_values)))
150
+ return ColumnExpression(exp.NEQ(this=self._expression, expression=exp.Any(expressions=converted_values)))
151
151
 
152
152
  # SQL Functions
153
153
  def lower(self) -> "FunctionColumn":
154
154
  """SQL LOWER() function."""
155
- return FunctionColumn(exp.Lower(this=self._expr))
155
+ return FunctionColumn(exp.Lower(this=self._expression))
156
156
 
157
157
  def upper(self) -> "FunctionColumn":
158
158
  """SQL UPPER() function."""
159
- return FunctionColumn(exp.Upper(this=self._expr))
159
+ return FunctionColumn(exp.Upper(this=self._expression))
160
160
 
161
161
  def length(self) -> "FunctionColumn":
162
162
  """SQL LENGTH() function."""
163
- return FunctionColumn(exp.Length(this=self._expr))
163
+ return FunctionColumn(exp.Length(this=self._expression))
164
164
 
165
165
  def trim(self) -> "FunctionColumn":
166
166
  """SQL TRIM() function."""
167
- return FunctionColumn(exp.Trim(this=self._expr))
167
+ return FunctionColumn(exp.Trim(this=self._expression))
168
168
 
169
169
  def abs(self) -> "FunctionColumn":
170
170
  """SQL ABS() function."""
171
- return FunctionColumn(exp.Abs(this=self._expr))
171
+ return FunctionColumn(exp.Abs(this=self._expression))
172
172
 
173
173
  def round(self, decimals: int = 0) -> "FunctionColumn":
174
174
  """SQL ROUND() function."""
175
175
  if decimals == 0:
176
- return FunctionColumn(exp.Round(this=self._expr))
177
- return FunctionColumn(exp.Round(this=self._expr, expression=exp.Literal.number(decimals)))
176
+ return FunctionColumn(exp.Round(this=self._expression))
177
+ return FunctionColumn(exp.Round(this=self._expression, expression=exp.Literal.number(decimals)))
178
178
 
179
179
  def floor(self) -> "FunctionColumn":
180
180
  """SQL FLOOR() function."""
181
- return FunctionColumn(exp.Floor(this=self._expr))
181
+ return FunctionColumn(exp.Floor(this=self._expression))
182
182
 
183
183
  def ceil(self) -> "FunctionColumn":
184
184
  """SQL CEIL() function."""
185
- return FunctionColumn(exp.Ceil(this=self._expr))
185
+ return FunctionColumn(exp.Ceil(this=self._expression))
186
186
 
187
187
  def substring(self, start: int, length: Optional[int] = None) -> "FunctionColumn":
188
188
  """SQL SUBSTRING() function."""
189
189
  args = [exp.Literal.number(start)]
190
190
  if length is not None:
191
191
  args.append(exp.Literal.number(length))
192
- return FunctionColumn(exp.Substring(this=self._expr, expressions=args))
192
+ return FunctionColumn(exp.Substring(this=self._expression, expressions=args))
193
193
 
194
194
  def coalesce(self, *values: Any) -> "FunctionColumn":
195
195
  """SQL COALESCE() function."""
196
- expressions = [self._expr] + [exp.convert(v) for v in values]
196
+ expressions = [self._expression] + [exp.convert(v) for v in values]
197
197
  return FunctionColumn(exp.Coalesce(expressions=expressions))
198
198
 
199
199
  def cast(self, data_type: str) -> "FunctionColumn":
200
200
  """SQL CAST() function."""
201
- return FunctionColumn(exp.Cast(this=self._expr, to=exp.DataType.build(data_type)))
201
+ return FunctionColumn(exp.Cast(this=self._expression, to=exp.DataType.build(data_type)))
202
+
203
+ # Aggregate functions
204
+ def count(self) -> "FunctionColumn":
205
+ """SQL COUNT() function."""
206
+ return FunctionColumn(exp.Count(this=self._expression))
207
+
208
+ def sum(self) -> "FunctionColumn":
209
+ """SQL SUM() function."""
210
+ return FunctionColumn(exp.Sum(this=self._expression))
211
+
212
+ def avg(self) -> "FunctionColumn":
213
+ """SQL AVG() function."""
214
+ return FunctionColumn(exp.Avg(this=self._expression))
215
+
216
+ def min(self) -> "FunctionColumn":
217
+ """SQL MIN() function."""
218
+ return FunctionColumn(exp.Min(this=self._expression))
219
+
220
+ def max(self) -> "FunctionColumn":
221
+ """SQL MAX() function."""
222
+ return FunctionColumn(exp.Max(this=self._expression))
223
+
224
+ def count_distinct(self) -> "FunctionColumn":
225
+ """SQL COUNT(DISTINCT column) function."""
226
+ return FunctionColumn(exp.Count(this=exp.Distinct(expressions=[self._expression])))
227
+
228
+ @staticmethod
229
+ def count_all() -> "FunctionColumn":
230
+ """SQL COUNT(*) function."""
231
+ return FunctionColumn(exp.Count(this=exp.Star()))
202
232
 
203
233
  def alias(self, alias_name: str) -> exp.Expression:
204
234
  """Create an aliased column expression."""
205
- return exp.Alias(this=self._expr, alias=alias_name)
235
+ return exp.Alias(this=self._expression, alias=alias_name)
206
236
 
207
237
  def __repr__(self) -> str:
208
238
  if self.table:
@@ -218,25 +248,25 @@ class FunctionColumn:
218
248
  """Represents the result of a SQL function call on a column."""
219
249
 
220
250
  def __init__(self, expression: exp.Expression) -> None:
221
- self._expr = expression
251
+ self._expression = expression
222
252
 
223
253
  def __eq__(self, other: object) -> ColumnExpression: # type: ignore[override]
224
- return ColumnExpression(exp.EQ(this=self._expr, expression=exp.convert(other)))
254
+ return ColumnExpression(exp.EQ(this=self._expression, expression=exp.convert(other)))
225
255
 
226
256
  def __ne__(self, other: object) -> ColumnExpression: # type: ignore[override]
227
- return ColumnExpression(exp.NEQ(this=self._expr, expression=exp.convert(other)))
257
+ return ColumnExpression(exp.NEQ(this=self._expression, expression=exp.convert(other)))
228
258
 
229
259
  def like(self, pattern: str) -> ColumnExpression:
230
- return ColumnExpression(exp.Like(this=self._expr, expression=exp.convert(pattern)))
260
+ return ColumnExpression(exp.Like(this=self._expression, expression=exp.convert(pattern)))
231
261
 
232
262
  def ilike(self, pattern: str) -> ColumnExpression:
233
263
  """Case-insensitive LIKE."""
234
- return ColumnExpression(exp.ILike(this=self._expr, expression=exp.convert(pattern)))
264
+ return ColumnExpression(exp.ILike(this=self._expression, expression=exp.convert(pattern)))
235
265
 
236
266
  def in_(self, values: Iterable[Any]) -> ColumnExpression:
237
267
  """SQL IN clause."""
238
268
  converted_values = [exp.convert(v) for v in values]
239
- return ColumnExpression(exp.In(this=self._expr, expressions=converted_values))
269
+ return ColumnExpression(exp.In(this=self._expression, expressions=converted_values))
240
270
 
241
271
  def not_in_(self, values: Iterable[Any]) -> ColumnExpression:
242
272
  """SQL NOT IN clause."""
@@ -252,32 +282,32 @@ class FunctionColumn:
252
282
 
253
283
  def between(self, start: Any, end: Any) -> ColumnExpression:
254
284
  """SQL BETWEEN clause."""
255
- return ColumnExpression(exp.Between(this=self._expr, low=exp.convert(start), high=exp.convert(end)))
285
+ return ColumnExpression(exp.Between(this=self._expression, low=exp.convert(start), high=exp.convert(end)))
256
286
 
257
287
  def is_null(self) -> ColumnExpression:
258
288
  """SQL IS NULL."""
259
- return ColumnExpression(exp.Is(this=self._expr, expression=exp.Null()))
289
+ return ColumnExpression(exp.Is(this=self._expression, expression=exp.Null()))
260
290
 
261
291
  def is_not_null(self) -> ColumnExpression:
262
292
  """SQL IS NOT NULL."""
263
- return ColumnExpression(exp.Not(this=exp.Is(this=self._expr, expression=exp.Null())))
293
+ return ColumnExpression(exp.Not(this=exp.Is(this=self._expression, expression=exp.Null())))
264
294
 
265
295
  def any_(self, values: Iterable[Any]) -> ColumnExpression:
266
296
  """SQL = ANY(...) clause."""
267
297
  converted_values = [exp.convert(v) for v in values]
268
- return ColumnExpression(exp.EQ(this=self._expr, expression=exp.Any(expressions=converted_values)))
298
+ return ColumnExpression(exp.EQ(this=self._expression, expression=exp.Any(expressions=converted_values)))
269
299
 
270
300
  def not_any_(self, values: Iterable[Any]) -> ColumnExpression:
271
301
  """SQL <> ANY(...) clause."""
272
302
  converted_values = [exp.convert(v) for v in values]
273
- return ColumnExpression(exp.NEQ(this=self._expr, expression=exp.Any(expressions=converted_values)))
303
+ return ColumnExpression(exp.NEQ(this=self._expression, expression=exp.Any(expressions=converted_values)))
274
304
 
275
305
  def alias(self, alias_name: str) -> exp.Expression:
276
306
  """Create an aliased function expression."""
277
- return exp.Alias(this=self._expr, alias=alias_name)
307
+ return exp.Alias(this=self._expression, alias=alias_name)
278
308
 
279
309
  # Add other operators as needed...
280
310
 
281
311
  def __hash__(self) -> int:
282
312
  """Hash based on the SQL expression."""
283
- return hash(self._expr.sql() if has_sql_method(self._expr) else str(self._expr))
313
+ return hash(self._expression.sql() if has_sql_method(self._expression) else str(self._expression))
@@ -7,12 +7,12 @@ from sqlglot import exp
7
7
  from sqlglot.dialects.dialect import DialectType
8
8
  from typing_extensions import Self
9
9
 
10
+ from sqlspec.statement.builder._base import QueryBuilder, SafeQuery
10
11
  from sqlspec.statement.builder._ddl_utils import build_column_expression, build_constraint_expression
11
- from sqlspec.statement.builder.base import QueryBuilder, SafeQuery
12
12
  from sqlspec.statement.result import SQLResult
13
13
 
14
14
  if TYPE_CHECKING:
15
- from sqlspec.statement.builder.column import ColumnExpression
15
+ from sqlspec.statement.builder._column import ColumnExpression
16
16
  from sqlspec.statement.sql import SQL, SQLConfig
17
17
 
18
18
  __all__ = (
@@ -792,7 +792,7 @@ class CreateTableAsSelect(DDLBuilder):
792
792
 
793
793
  select_expr = None
794
794
  select_params = None
795
- from sqlspec.statement.builder.select import Select
795
+ from sqlspec.statement.builder._select import Select
796
796
  from sqlspec.statement.sql import SQL
797
797
 
798
798
  if isinstance(self._select_query, SQL):
@@ -909,7 +909,7 @@ class CreateMaterializedView(DDLBuilder):
909
909
 
910
910
  select_expr = None
911
911
  select_params = None
912
- from sqlspec.statement.builder.select import Select
912
+ from sqlspec.statement.builder._select import Select
913
913
  from sqlspec.statement.sql import SQL
914
914
 
915
915
  if isinstance(self._select_query, SQL):
@@ -1008,7 +1008,7 @@ class CreateView(DDLBuilder):
1008
1008
 
1009
1009
  select_expr = None
1010
1010
  select_params = None
1011
- from sqlspec.statement.builder.select import Select
1011
+ from sqlspec.statement.builder._select import Select
1012
1012
  from sqlspec.statement.sql import SQL
1013
1013
 
1014
1014
  if isinstance(self._select_query, SQL):
@@ -5,7 +5,7 @@ from typing import TYPE_CHECKING, Optional
5
5
  from sqlglot import exp
6
6
 
7
7
  if TYPE_CHECKING:
8
- from sqlspec.statement.builder.ddl import ColumnDefinition, ConstraintDefinition
8
+ from sqlspec.statement.builder._ddl import ColumnDefinition, ConstraintDefinition
9
9
 
10
10
  __all__ = ("build_column_expression", "build_constraint_expression")
11
11
 
@@ -9,7 +9,7 @@ from typing import Any, Optional
9
9
 
10
10
  from sqlglot import exp
11
11
 
12
- from sqlspec.statement.builder.base import QueryBuilder, SafeQuery
12
+ from sqlspec.statement.builder._base import QueryBuilder, SafeQuery
13
13
  from sqlspec.statement.builder.mixins import DeleteFromClauseMixin, ReturningClauseMixin, WhereClauseMixin
14
14
  from sqlspec.statement.result import SQLResult
15
15
  from sqlspec.typing import RowT
@@ -11,7 +11,7 @@ from sqlglot import exp
11
11
  from typing_extensions import Self
12
12
 
13
13
  from sqlspec.exceptions import SQLBuilderError
14
- from sqlspec.statement.builder.base import QueryBuilder
14
+ from sqlspec.statement.builder._base import QueryBuilder
15
15
  from sqlspec.statement.builder.mixins import (
16
16
  InsertFromSelectMixin,
17
17
  InsertIntoClauseMixin,
@@ -8,7 +8,7 @@ from dataclasses import dataclass
8
8
 
9
9
  from sqlglot import exp
10
10
 
11
- from sqlspec.statement.builder.base import QueryBuilder
11
+ from sqlspec.statement.builder._base import QueryBuilder
12
12
  from sqlspec.statement.builder.mixins import (
13
13
  MergeIntoClauseMixin,
14
14
  MergeMatchedClauseMixin,
@@ -9,6 +9,8 @@ from typing import Any, Optional, Union, cast
9
9
 
10
10
  from sqlglot import exp, maybe_parse, parse_one
11
11
 
12
+ from sqlspec.utils.type_guards import has_expression_attr, has_parameter_builder
13
+
12
14
 
13
15
  def parse_column_expression(column_input: Union[str, exp.Expression, Any]) -> exp.Expression:
14
16
  """Parse a column input that might be a complex expression.
@@ -31,8 +33,8 @@ def parse_column_expression(column_input: Union[str, exp.Expression, Any]) -> ex
31
33
  return column_input
32
34
 
33
35
  # Handle our custom Column objects
34
- if hasattr(column_input, "_expr"):
35
- attr_value = getattr(column_input, "_expr", None)
36
+ if has_expression_attr(column_input):
37
+ attr_value = getattr(column_input, "_expression", None)
36
38
  if isinstance(attr_value, exp.Expression):
37
39
  return attr_value
38
40
 
@@ -109,7 +111,7 @@ def parse_condition_expression(
109
111
  if value is None:
110
112
  return exp.Is(this=column_expr, expression=exp.null())
111
113
  # Use builder's parameter system if available
112
- if builder and hasattr(builder, "add_parameter"):
114
+ if builder and has_parameter_builder(builder):
113
115
  _, param_name = builder.add_parameter(value)
114
116
  return exp.EQ(this=column_expr, expression=exp.Placeholder(this=param_name))
115
117
  if isinstance(value, str):
@@ -11,23 +11,18 @@ from typing import Any, Optional, Union, cast
11
11
  from sqlglot import exp
12
12
  from typing_extensions import Self
13
13
 
14
- from sqlspec.statement.builder.base import QueryBuilder, SafeQuery
14
+ from sqlspec.statement.builder._base import QueryBuilder, SafeQuery
15
15
  from sqlspec.statement.builder.mixins import (
16
- AggregateFunctionsMixin,
17
- CaseBuilderMixin,
18
16
  CommonTableExpressionMixin,
19
- FromClauseMixin,
20
- GroupByClauseMixin,
21
17
  HavingClauseMixin,
22
18
  JoinClauseMixin,
23
19
  LimitOffsetClauseMixin,
24
20
  OrderByClauseMixin,
25
21
  PivotClauseMixin,
26
- SelectColumnsMixin,
22
+ SelectClauseMixin,
27
23
  SetOperationMixin,
28
24
  UnpivotClauseMixin,
29
25
  WhereClauseMixin,
30
- WindowFunctionsMixin,
31
26
  )
32
27
  from sqlspec.statement.result import SQLResult
33
28
  from sqlspec.typing import RowT
@@ -35,22 +30,21 @@ from sqlspec.typing import RowT
35
30
  __all__ = ("Select",)
36
31
 
37
32
 
33
+ # This pattern is formatted with the table name and compiled for each hint.
34
+ TABLE_HINT_PATTERN = r"\b{}\b(\s+AS\s+\w+)?"
35
+
36
+
38
37
  @dataclass
39
38
  class Select(
40
39
  QueryBuilder[RowT],
41
40
  WhereClauseMixin,
42
41
  OrderByClauseMixin,
43
42
  LimitOffsetClauseMixin,
44
- SelectColumnsMixin,
43
+ SelectClauseMixin,
45
44
  JoinClauseMixin,
46
- FromClauseMixin,
47
- GroupByClauseMixin,
48
45
  HavingClauseMixin,
49
46
  SetOperationMixin,
50
47
  CommonTableExpressionMixin,
51
- AggregateFunctionsMixin,
52
- WindowFunctionsMixin,
53
- CaseBuilderMixin,
54
48
  PivotClauseMixin,
55
49
  UnpivotClauseMixin,
56
50
  ):
@@ -95,14 +89,14 @@ class Select(
95
89
  """
96
90
  super().__init__(**kwargs)
97
91
 
98
- # Initialize fields from dataclass
92
+ # Manually initialize dataclass fields here because a custom __init__ is defined.
93
+ # This is necessary to support the `Select("col1", "col2")` shorthand initialization.
99
94
  self._with_parts = {}
100
95
  self._expression = None
101
96
  self._schema = None
102
97
  self._hints = []
103
98
 
104
- if self._expression is None:
105
- self._create_base_expression()
99
+ self._create_base_expression()
106
100
 
107
101
  # Add columns if provided - just a shorthand for .select()
108
102
  if columns:
@@ -174,48 +168,52 @@ class Select(
174
168
  """
175
169
  safe_query = super().build()
176
170
 
177
- if hasattr(self, "_hints") and self._hints:
178
- modified_expr = self._expression.copy() if self._expression else None
179
-
180
- if modified_expr and isinstance(modified_expr, exp.Select):
181
- statement_hints = [h["hint"] for h in self._hints if h.get("location") == "statement"]
182
- if statement_hints:
183
- # Parse each hint and create proper hint expressions
184
- hint_expressions = []
185
- for hint in statement_hints:
186
- try:
187
- # Try to parse hint as an expression (e.g., "INDEX(users idx_name)")
188
- hint_str = str(hint) # Ensure hint is a string
189
- hint_expr: Optional[exp.Expression] = exp.maybe_parse(hint_str, dialect=self.dialect_name)
190
- if hint_expr:
191
- hint_expressions.append(hint_expr)
192
- else:
193
- hint_expressions.append(exp.Anonymous(this=hint_str))
194
- except Exception: # noqa: PERF203
195
- hint_expressions.append(exp.Anonymous(this=str(hint)))
196
-
197
- if hint_expressions:
198
- hint_node = exp.Hint(expressions=hint_expressions)
199
- modified_expr.set("hint", hint_node)
200
-
201
- # For table-level hints, we'll fall back to comment injection in SQL
202
- # since SQLGlot doesn't have a standard way to attach hints to individual tables
203
- modified_sql = modified_expr.sql(dialect=self.dialect_name, pretty=True)
204
-
205
- table_hints = [h for h in self._hints if h.get("location") == "table" and h.get("table")]
206
- if table_hints:
207
- for th in table_hints:
208
- table = str(th["table"])
209
- hint = th["hint"]
210
- # More precise regex that captures the table and optional alias
211
- pattern = rf"\b{re.escape(table)}\b(\s+AS\s+\w+)?"
212
-
213
- def replacement_func(match: re.Match[str]) -> str:
214
- alias_part = match.group(1) or ""
215
- return f"/*+ {hint} */ {table}{alias_part}" # noqa: B023
216
-
217
- modified_sql = re.sub(pattern, replacement_func, modified_sql, flags=re.IGNORECASE, count=1)
218
-
219
- return SafeQuery(sql=modified_sql, parameters=safe_query.parameters, dialect=safe_query.dialect)
220
-
221
- return safe_query
171
+ if not self._hints:
172
+ return safe_query
173
+
174
+ modified_expr = self._expression.copy() if self._expression else self._create_base_expression()
175
+
176
+ if isinstance(modified_expr, exp.Select):
177
+ statement_hints = [h["hint"] for h in self._hints if h.get("location") == "statement"]
178
+ if statement_hints:
179
+ # Parse each hint and create proper hint expressions
180
+ hint_expressions = []
181
+
182
+ def parse_hint(hint: Any) -> exp.Expression:
183
+ """Parse a single hint with error handling."""
184
+ try:
185
+ # Try to parse hint as an expression (e.g., "INDEX(users idx_name)")
186
+ hint_str = str(hint) # Ensure hint is a string
187
+ hint_expr: Optional[exp.Expression] = exp.maybe_parse(hint_str, dialect=self.dialect_name)
188
+ if hint_expr:
189
+ return hint_expr
190
+ return exp.Anonymous(this=hint_str)
191
+ except Exception:
192
+ return exp.Anonymous(this=str(hint))
193
+
194
+ hint_expressions = [parse_hint(hint) for hint in statement_hints]
195
+
196
+ if hint_expressions:
197
+ hint_node = exp.Hint(expressions=hint_expressions)
198
+ modified_expr.set("hint", hint_node)
199
+
200
+ # For table-level hints, we'll fall back to comment injection in SQL
201
+ # since SQLGlot doesn't have a standard way to attach hints to individual tables
202
+ modified_sql = modified_expr.sql(dialect=self.dialect_name, pretty=True)
203
+
204
+ table_hints = [h for h in self._hints if h.get("location") == "table" and h.get("table")]
205
+ if table_hints:
206
+ for th in table_hints:
207
+ table = str(th["table"])
208
+ hint = th["hint"]
209
+ # More precise regex that captures the table and optional alias
210
+ pattern = TABLE_HINT_PATTERN.format(re.escape(table))
211
+ compiled_pattern = re.compile(pattern, re.IGNORECASE)
212
+
213
+ def replacement_func(match: re.Match[str]) -> str:
214
+ alias_part = match.group(1) or ""
215
+ return f"/*+ {hint} */ {table}{alias_part}" # noqa: B023
216
+
217
+ modified_sql = compiled_pattern.sub(replacement_func, modified_sql, count=1)
218
+
219
+ return SafeQuery(sql=modified_sql, parameters=safe_query.parameters, dialect=safe_query.dialect)
@@ -11,7 +11,7 @@ from sqlglot import exp
11
11
  from typing_extensions import Self
12
12
 
13
13
  from sqlspec.exceptions import SQLBuilderError
14
- from sqlspec.statement.builder.base import QueryBuilder, SafeQuery
14
+ from sqlspec.statement.builder._base import QueryBuilder, SafeQuery
15
15
  from sqlspec.statement.builder.mixins import (
16
16
  ReturningClauseMixin,
17
17
  UpdateFromClauseMixin,
@@ -23,7 +23,7 @@ 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 Select
26
+ from sqlspec.statement.builder._select import Select
27
27
 
28
28
  __all__ = ("Update",)
29
29