sql_fusion 1.0.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.
- sql_fusion/__init__.py +15 -0
- sql_fusion/composite_table.py +689 -0
- sql_fusion/operators.py +119 -0
- sql_fusion/query/__init__.py +0 -0
- sql_fusion/query/delete.py +84 -0
- sql_fusion/query/insert.py +72 -0
- sql_fusion/query/select.py +527 -0
- sql_fusion/query/update.py +76 -0
- sql_fusion-1.0.0.dist-info/METADATA +580 -0
- sql_fusion-1.0.0.dist-info/RECORD +12 -0
- sql_fusion-1.0.0.dist-info/WHEEL +4 -0
- sql_fusion-1.0.0.dist-info/entry_points.txt +3 -0
sql_fusion/__init__.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from .composite_table import Alias, Table, func
|
|
2
|
+
from .query.delete import delete
|
|
3
|
+
from .query.insert import insert
|
|
4
|
+
from .query.select import select
|
|
5
|
+
from .query.update import update
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"Alias",
|
|
9
|
+
"Table",
|
|
10
|
+
"delete",
|
|
11
|
+
"func",
|
|
12
|
+
"insert",
|
|
13
|
+
"select",
|
|
14
|
+
"update",
|
|
15
|
+
]
|
|
@@ -0,0 +1,689 @@
|
|
|
1
|
+
from copy import copy
|
|
2
|
+
from typing import Any, Callable, Self
|
|
3
|
+
|
|
4
|
+
from sql_fusion.operators import (
|
|
5
|
+
AbstractOperator,
|
|
6
|
+
EqualOperator,
|
|
7
|
+
GreaterThanOperator,
|
|
8
|
+
GreaterThanOrEqualOperator,
|
|
9
|
+
IlikeOperator,
|
|
10
|
+
InOperator,
|
|
11
|
+
LessThanOperator,
|
|
12
|
+
LessThanOrEqualOperator,
|
|
13
|
+
LikeOperator,
|
|
14
|
+
NotEqualOperator,
|
|
15
|
+
NotInOperator,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
CompileExpression = Callable[
|
|
19
|
+
[str, tuple[Any, ...]],
|
|
20
|
+
tuple[str, tuple[Any, ...]],
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class AliasRegistry:
|
|
25
|
+
"""Registry for managing unique table aliases."""
|
|
26
|
+
|
|
27
|
+
def __init__(self) -> None:
|
|
28
|
+
self._counter: int = 0
|
|
29
|
+
self._mapping: dict[Table, Alias] = {}
|
|
30
|
+
|
|
31
|
+
def get_next_alias(self) -> str:
|
|
32
|
+
"""Generate the next unique alias (a, b, c, ..., z, aa, ab, ...)."""
|
|
33
|
+
alias = ""
|
|
34
|
+
n = self._counter
|
|
35
|
+
while True:
|
|
36
|
+
alias = chr(ord("a") + (n % 26)) + alias
|
|
37
|
+
n //= 26
|
|
38
|
+
if n == 0:
|
|
39
|
+
break
|
|
40
|
+
self._counter += 1
|
|
41
|
+
return alias
|
|
42
|
+
|
|
43
|
+
def get_alias_for_table(self, table: Table) -> Alias:
|
|
44
|
+
if table not in self._mapping:
|
|
45
|
+
self._mapping[table] = Alias(self.get_next_alias())
|
|
46
|
+
return self._mapping[table]
|
|
47
|
+
|
|
48
|
+
def reset(self) -> None:
|
|
49
|
+
self._counter = 0
|
|
50
|
+
self._mapping.clear()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class AbstractQuery:
|
|
54
|
+
def __init__(
|
|
55
|
+
self,
|
|
56
|
+
table: Table | None,
|
|
57
|
+
columns: tuple[Column | Alias | FunctionCall, ...] = (),
|
|
58
|
+
) -> None:
|
|
59
|
+
self._table: Table | None = table
|
|
60
|
+
self._columns: tuple[Column | Alias | FunctionCall, ...] = columns
|
|
61
|
+
self._where_condition: Condition | None = None
|
|
62
|
+
self._ctes: list[tuple[str, AbstractQuery]] = []
|
|
63
|
+
self._with_recursive: bool = False
|
|
64
|
+
self._compile_expressions: list[CompileExpression] = []
|
|
65
|
+
self._before_clause_comments: dict[str, list[tuple[str, bool]]] = {}
|
|
66
|
+
self._after_clause_comments: dict[str, list[tuple[str, bool]]] = {}
|
|
67
|
+
self._alias_registry: AliasRegistry = AliasRegistry()
|
|
68
|
+
|
|
69
|
+
def _get_table(self) -> Table:
|
|
70
|
+
if self._table is None:
|
|
71
|
+
raise ValueError("FROM clause is required")
|
|
72
|
+
return self._table
|
|
73
|
+
|
|
74
|
+
def where(
|
|
75
|
+
self,
|
|
76
|
+
*conditions: Condition,
|
|
77
|
+
) -> Self:
|
|
78
|
+
qs = copy(self)
|
|
79
|
+
combined_condition: Condition | None = None
|
|
80
|
+
|
|
81
|
+
for condition in conditions:
|
|
82
|
+
if combined_condition is None:
|
|
83
|
+
combined_condition = condition
|
|
84
|
+
else:
|
|
85
|
+
combined_condition = combined_condition & condition
|
|
86
|
+
|
|
87
|
+
if combined_condition:
|
|
88
|
+
if qs._where_condition is None:
|
|
89
|
+
qs._where_condition = combined_condition
|
|
90
|
+
else:
|
|
91
|
+
qs._where_condition = qs._where_condition & combined_condition
|
|
92
|
+
|
|
93
|
+
return qs
|
|
94
|
+
|
|
95
|
+
def compile_expression(self, expression: CompileExpression) -> Self:
|
|
96
|
+
qs = copy(self)
|
|
97
|
+
qs._compile_expressions = self._compile_expressions.copy()
|
|
98
|
+
qs._compile_expressions.append(expression)
|
|
99
|
+
return qs
|
|
100
|
+
|
|
101
|
+
def comment(self, text: str, *, hint: bool = False) -> Self:
|
|
102
|
+
def _add_comment(
|
|
103
|
+
sql: str,
|
|
104
|
+
params: tuple[Any, ...],
|
|
105
|
+
) -> tuple[str, tuple[Any, ...]]:
|
|
106
|
+
prefix = "+ " if hint else " "
|
|
107
|
+
return f"/*{prefix}{text} */\n{sql}", params
|
|
108
|
+
|
|
109
|
+
return self.compile_expression(_add_comment)
|
|
110
|
+
|
|
111
|
+
def explain(
|
|
112
|
+
self,
|
|
113
|
+
*,
|
|
114
|
+
analyze: bool = False,
|
|
115
|
+
verbose: bool = False,
|
|
116
|
+
) -> Self:
|
|
117
|
+
def _add_explain(
|
|
118
|
+
sql: str,
|
|
119
|
+
params: tuple[Any, ...],
|
|
120
|
+
) -> tuple[str, tuple[Any, ...]]:
|
|
121
|
+
explain_parts = ["EXPLAIN"]
|
|
122
|
+
if analyze:
|
|
123
|
+
explain_parts.append("ANALYZE")
|
|
124
|
+
if verbose:
|
|
125
|
+
explain_parts.append("VERBOSE")
|
|
126
|
+
explain_parts.append(sql)
|
|
127
|
+
return " ".join(explain_parts), params
|
|
128
|
+
|
|
129
|
+
return self.compile_expression(_add_explain)
|
|
130
|
+
|
|
131
|
+
def analyze(self, *, verbose: bool = False) -> Self:
|
|
132
|
+
return self.explain(analyze=True, verbose=verbose)
|
|
133
|
+
|
|
134
|
+
def before_clause(
|
|
135
|
+
self,
|
|
136
|
+
clause: str,
|
|
137
|
+
text: str,
|
|
138
|
+
*,
|
|
139
|
+
hint: bool = False,
|
|
140
|
+
) -> Self:
|
|
141
|
+
qs = copy(self)
|
|
142
|
+
qs._before_clause_comments = {
|
|
143
|
+
key: value.copy()
|
|
144
|
+
for key, value in self._before_clause_comments.items()
|
|
145
|
+
}
|
|
146
|
+
qs._after_clause_comments = {
|
|
147
|
+
key: value.copy()
|
|
148
|
+
for key, value in self._after_clause_comments.items()
|
|
149
|
+
}
|
|
150
|
+
clause_key = clause.upper()
|
|
151
|
+
qs._before_clause_comments.setdefault(clause_key, []).append(
|
|
152
|
+
(text, hint),
|
|
153
|
+
)
|
|
154
|
+
return qs
|
|
155
|
+
|
|
156
|
+
def after_clause(
|
|
157
|
+
self,
|
|
158
|
+
clause: str,
|
|
159
|
+
text: str,
|
|
160
|
+
*,
|
|
161
|
+
hint: bool = False,
|
|
162
|
+
) -> Self:
|
|
163
|
+
qs = copy(self)
|
|
164
|
+
qs._before_clause_comments = {
|
|
165
|
+
key: value.copy()
|
|
166
|
+
for key, value in self._before_clause_comments.items()
|
|
167
|
+
}
|
|
168
|
+
qs._after_clause_comments = {
|
|
169
|
+
key: value.copy()
|
|
170
|
+
for key, value in self._after_clause_comments.items()
|
|
171
|
+
}
|
|
172
|
+
clause_key = clause.upper()
|
|
173
|
+
qs._after_clause_comments.setdefault(clause_key, []).append(
|
|
174
|
+
(text, hint),
|
|
175
|
+
)
|
|
176
|
+
return qs
|
|
177
|
+
|
|
178
|
+
def where_by(
|
|
179
|
+
self,
|
|
180
|
+
**kwargs: Any,
|
|
181
|
+
) -> Self:
|
|
182
|
+
qs = copy(self)
|
|
183
|
+
combined_condition: Condition | None = None
|
|
184
|
+
table = self._get_table()
|
|
185
|
+
self._alias_registry.get_alias_for_table(table)
|
|
186
|
+
|
|
187
|
+
for key, value in kwargs.items():
|
|
188
|
+
col: Column = Column(key)
|
|
189
|
+
col._attach_table(table) # pyright: ignore[reportPrivateUsage]
|
|
190
|
+
condition = Condition(
|
|
191
|
+
column=col,
|
|
192
|
+
operator=EqualOperator,
|
|
193
|
+
value=value,
|
|
194
|
+
)
|
|
195
|
+
if combined_condition is None:
|
|
196
|
+
combined_condition = condition
|
|
197
|
+
else:
|
|
198
|
+
combined_condition = combined_condition & condition
|
|
199
|
+
|
|
200
|
+
if combined_condition:
|
|
201
|
+
if qs._where_condition is None:
|
|
202
|
+
qs._where_condition = combined_condition
|
|
203
|
+
else:
|
|
204
|
+
qs._where_condition = qs._where_condition & combined_condition
|
|
205
|
+
|
|
206
|
+
return qs
|
|
207
|
+
|
|
208
|
+
def with_(self, *, recursive: bool = False, **ctes: AbstractQuery) -> Self:
|
|
209
|
+
if not ctes:
|
|
210
|
+
raise ValueError("No CTEs provided for with_")
|
|
211
|
+
|
|
212
|
+
qs = copy(self)
|
|
213
|
+
qs._ctes = self._ctes.copy()
|
|
214
|
+
qs._with_recursive = self._with_recursive or recursive
|
|
215
|
+
qs._ctes.extend(ctes.items())
|
|
216
|
+
|
|
217
|
+
return qs
|
|
218
|
+
|
|
219
|
+
def _build_with_clause(
|
|
220
|
+
self,
|
|
221
|
+
alias_registry: AliasRegistry | None = None,
|
|
222
|
+
) -> tuple[str, list[Any]]:
|
|
223
|
+
if not self._ctes:
|
|
224
|
+
return "", []
|
|
225
|
+
|
|
226
|
+
registry = alias_registry or self._alias_registry
|
|
227
|
+
with_parts: list[str] = []
|
|
228
|
+
params: list[Any] = []
|
|
229
|
+
|
|
230
|
+
for name, query in self._ctes:
|
|
231
|
+
query_sql, query_params = query.build_query(registry)
|
|
232
|
+
with_parts.append(f'"{name}" AS ({query_sql})')
|
|
233
|
+
params.extend(query_params)
|
|
234
|
+
|
|
235
|
+
recursive_part = " RECURSIVE" if self._with_recursive else ""
|
|
236
|
+
keyword = f"WITH{recursive_part}"
|
|
237
|
+
return self._build_clause(
|
|
238
|
+
"WITH",
|
|
239
|
+
keyword,
|
|
240
|
+
", ".join(with_parts),
|
|
241
|
+
), params
|
|
242
|
+
|
|
243
|
+
def _apply_compile_expressions(
|
|
244
|
+
self,
|
|
245
|
+
sql: str,
|
|
246
|
+
params: tuple[Any, ...],
|
|
247
|
+
) -> tuple[str, tuple[Any, ...]]:
|
|
248
|
+
for expression in self._compile_expressions:
|
|
249
|
+
sql, params = expression(sql, params)
|
|
250
|
+
|
|
251
|
+
return sql, params
|
|
252
|
+
|
|
253
|
+
def _build_clause(
|
|
254
|
+
self,
|
|
255
|
+
clause: str,
|
|
256
|
+
keyword: str,
|
|
257
|
+
body: str = "",
|
|
258
|
+
) -> str:
|
|
259
|
+
before_comments = self._render_clause_comments(
|
|
260
|
+
self._before_clause_comments.get(clause.upper(), []),
|
|
261
|
+
leading=False,
|
|
262
|
+
)
|
|
263
|
+
after_comments = self._render_clause_comments(
|
|
264
|
+
self._after_clause_comments.get(clause.upper(), []),
|
|
265
|
+
leading=True,
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
if body:
|
|
269
|
+
separator = "" if after_comments else " "
|
|
270
|
+
return (
|
|
271
|
+
f"{before_comments}{keyword}{after_comments}{separator}{body}"
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
return f"{before_comments}{keyword}{after_comments}"
|
|
275
|
+
|
|
276
|
+
@staticmethod
|
|
277
|
+
def _render_clause_comments(
|
|
278
|
+
comments: list[tuple[str, bool]],
|
|
279
|
+
*,
|
|
280
|
+
leading: bool,
|
|
281
|
+
) -> str:
|
|
282
|
+
if not comments:
|
|
283
|
+
return ""
|
|
284
|
+
|
|
285
|
+
rendered = [
|
|
286
|
+
f"/*+ {text} */" if hint else f"/* {text} */"
|
|
287
|
+
for text, hint in comments
|
|
288
|
+
]
|
|
289
|
+
if leading:
|
|
290
|
+
return "".join(f" {comment}\n" for comment in rendered)
|
|
291
|
+
return "".join(f"{comment}\n" for comment in rendered)
|
|
292
|
+
|
|
293
|
+
def build_query(
|
|
294
|
+
self,
|
|
295
|
+
alias_registry: AliasRegistry | None = None,
|
|
296
|
+
) -> tuple[str, tuple[Any, ...]]:
|
|
297
|
+
raise NotImplementedError()
|
|
298
|
+
|
|
299
|
+
def compile(self) -> tuple[str, tuple[Any, ...]]:
|
|
300
|
+
return self.build_query()
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
class ComparableExpression:
|
|
304
|
+
def _cond(
|
|
305
|
+
self,
|
|
306
|
+
operator: type[AbstractOperator],
|
|
307
|
+
other: object,
|
|
308
|
+
) -> Condition:
|
|
309
|
+
return Condition(column=self, operator=operator, value=other)
|
|
310
|
+
|
|
311
|
+
def __eq__(self, other: object) -> Condition: # type: ignore[override]
|
|
312
|
+
return self._cond(EqualOperator, other)
|
|
313
|
+
|
|
314
|
+
def __ne__(self, other: object) -> Condition: # type: ignore[override]
|
|
315
|
+
return self._cond(NotEqualOperator, other)
|
|
316
|
+
|
|
317
|
+
def __lt__(self, other: Any) -> Condition:
|
|
318
|
+
return self._cond(LessThanOperator, other)
|
|
319
|
+
|
|
320
|
+
def __gt__(self, other: Any) -> Condition:
|
|
321
|
+
return self._cond(GreaterThanOperator, other)
|
|
322
|
+
|
|
323
|
+
def __le__(self, other: Any) -> Condition:
|
|
324
|
+
return self._cond(LessThanOrEqualOperator, other)
|
|
325
|
+
|
|
326
|
+
def __ge__(self, other: Any) -> Condition:
|
|
327
|
+
return self._cond(GreaterThanOrEqualOperator, other)
|
|
328
|
+
|
|
329
|
+
def __hash__(self) -> int:
|
|
330
|
+
raise TypeError(f"unhashable type: '{type(self).__name__}'")
|
|
331
|
+
|
|
332
|
+
def get_ref(self, alias_registry: AliasRegistry) -> str:
|
|
333
|
+
raise NotImplementedError()
|
|
334
|
+
|
|
335
|
+
def _binary_expression(
|
|
336
|
+
self,
|
|
337
|
+
operator: str,
|
|
338
|
+
other: Any,
|
|
339
|
+
*,
|
|
340
|
+
reverse: bool = False,
|
|
341
|
+
) -> BinaryExpression:
|
|
342
|
+
if reverse:
|
|
343
|
+
return BinaryExpression(other, operator, self)
|
|
344
|
+
return BinaryExpression(self, operator, other)
|
|
345
|
+
|
|
346
|
+
def __add__(self, other: Any) -> BinaryExpression:
|
|
347
|
+
return self._binary_expression("+", other)
|
|
348
|
+
|
|
349
|
+
def __radd__(self, other: Any) -> BinaryExpression:
|
|
350
|
+
return self._binary_expression("+", other, reverse=True)
|
|
351
|
+
|
|
352
|
+
def __sub__(self, other: Any) -> BinaryExpression:
|
|
353
|
+
return self._binary_expression("-", other)
|
|
354
|
+
|
|
355
|
+
def __rsub__(self, other: Any) -> BinaryExpression:
|
|
356
|
+
return self._binary_expression("-", other, reverse=True)
|
|
357
|
+
|
|
358
|
+
def __mul__(self, other: Any) -> BinaryExpression:
|
|
359
|
+
return self._binary_expression("*", other)
|
|
360
|
+
|
|
361
|
+
def __rmul__(self, other: Any) -> BinaryExpression:
|
|
362
|
+
return self._binary_expression("*", other, reverse=True)
|
|
363
|
+
|
|
364
|
+
def __truediv__(self, other: Any) -> BinaryExpression:
|
|
365
|
+
return self._binary_expression("/", other)
|
|
366
|
+
|
|
367
|
+
def __rtruediv__(self, other: Any) -> BinaryExpression:
|
|
368
|
+
return self._binary_expression("/", other, reverse=True)
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
class BinaryExpression(ComparableExpression):
|
|
372
|
+
def __init__(self, left: Any, operator: str, right: Any) -> None:
|
|
373
|
+
self.left: Any = left
|
|
374
|
+
self.operator: str = operator
|
|
375
|
+
self.right: Any = right
|
|
376
|
+
|
|
377
|
+
@staticmethod
|
|
378
|
+
def _render_operand(
|
|
379
|
+
operand: Any,
|
|
380
|
+
alias_registry: AliasRegistry,
|
|
381
|
+
) -> tuple[str, tuple[Any, ...]]:
|
|
382
|
+
if isinstance(operand, BinaryExpression):
|
|
383
|
+
sql, params = operand.to_sql(alias_registry)
|
|
384
|
+
return f"({sql})", params
|
|
385
|
+
if isinstance(operand, FunctionCall):
|
|
386
|
+
return operand.to_sql(alias_registry)
|
|
387
|
+
if isinstance(operand, Column | Alias):
|
|
388
|
+
return operand.get_ref(alias_registry), tuple()
|
|
389
|
+
if isinstance(operand, ComparableExpression):
|
|
390
|
+
return operand.get_ref(alias_registry), tuple()
|
|
391
|
+
return "?", (operand,)
|
|
392
|
+
|
|
393
|
+
def to_sql(
|
|
394
|
+
self,
|
|
395
|
+
alias_registry: AliasRegistry,
|
|
396
|
+
) -> tuple[str, tuple[Any, ...]]:
|
|
397
|
+
left_sql, left_params = self._render_operand(self.left, alias_registry)
|
|
398
|
+
right_sql, right_params = self._render_operand(
|
|
399
|
+
self.right,
|
|
400
|
+
alias_registry,
|
|
401
|
+
)
|
|
402
|
+
return (
|
|
403
|
+
f"{left_sql} {self.operator} {right_sql}",
|
|
404
|
+
left_params + right_params,
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
def get_ref(self, alias_registry: AliasRegistry) -> str:
|
|
408
|
+
return self.to_sql(alias_registry)[0]
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
class Condition:
|
|
412
|
+
def __init__( # noqa: PLR0913
|
|
413
|
+
self,
|
|
414
|
+
column: ComparableExpression | FunctionCall | None = None,
|
|
415
|
+
operator: type[AbstractOperator] | None = None,
|
|
416
|
+
value: object | None = None,
|
|
417
|
+
is_and: bool = True,
|
|
418
|
+
left: Condition | None = None,
|
|
419
|
+
right: Condition | None = None,
|
|
420
|
+
negated: bool = False,
|
|
421
|
+
) -> None:
|
|
422
|
+
self.column: ComparableExpression | FunctionCall | None = column
|
|
423
|
+
self.operator: type[AbstractOperator] | None = operator
|
|
424
|
+
self.value: object | None = value
|
|
425
|
+
self.is_and: bool = is_and
|
|
426
|
+
self.left: Condition | None = left
|
|
427
|
+
self.right: Condition | None = right
|
|
428
|
+
self.negated: bool = negated
|
|
429
|
+
|
|
430
|
+
@staticmethod
|
|
431
|
+
def _render_expression(
|
|
432
|
+
value: ComparableExpression | FunctionCall,
|
|
433
|
+
alias_registry: AliasRegistry,
|
|
434
|
+
) -> tuple[str, tuple[Any, ...]]:
|
|
435
|
+
if isinstance(value, (BinaryExpression, FunctionCall)):
|
|
436
|
+
return value.to_sql(alias_registry)
|
|
437
|
+
return value.get_ref(alias_registry), tuple()
|
|
438
|
+
|
|
439
|
+
def __and__(self, other: Condition) -> Condition:
|
|
440
|
+
return Condition(is_and=True, left=self, right=other)
|
|
441
|
+
|
|
442
|
+
def __or__(self, other: Condition) -> Condition:
|
|
443
|
+
return Condition(is_and=False, left=self, right=other)
|
|
444
|
+
|
|
445
|
+
def __invert__(self) -> Condition:
|
|
446
|
+
result = copy(self)
|
|
447
|
+
result.negated = not self.negated
|
|
448
|
+
return result
|
|
449
|
+
|
|
450
|
+
def to_sql(
|
|
451
|
+
self,
|
|
452
|
+
alias_registry: AliasRegistry,
|
|
453
|
+
) -> tuple[str, tuple[Any, ...]]:
|
|
454
|
+
def apply_negation(
|
|
455
|
+
sql: str,
|
|
456
|
+
params: tuple[Any, ...],
|
|
457
|
+
) -> tuple[str, tuple[Any, ...]]:
|
|
458
|
+
if self.negated:
|
|
459
|
+
return (f"NOT ({sql})" if sql else "NOT", params)
|
|
460
|
+
return sql, params
|
|
461
|
+
|
|
462
|
+
if self.left and self.right:
|
|
463
|
+
left_sql, left_params = self.left.to_sql(alias_registry)
|
|
464
|
+
right_sql, right_params = self.right.to_sql(alias_registry)
|
|
465
|
+
operator_str: str = "AND" if self.is_and else "OR"
|
|
466
|
+
return apply_negation(
|
|
467
|
+
f"({left_sql} {operator_str} {right_sql})",
|
|
468
|
+
left_params + right_params,
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
if not self.column:
|
|
472
|
+
return apply_negation("", tuple())
|
|
473
|
+
|
|
474
|
+
col_ref, col_params = self._render_expression(
|
|
475
|
+
self.column,
|
|
476
|
+
alias_registry,
|
|
477
|
+
)
|
|
478
|
+
operator_class = self.operator
|
|
479
|
+
if operator_class is None:
|
|
480
|
+
return apply_negation(col_ref, col_params)
|
|
481
|
+
|
|
482
|
+
if isinstance(self.value, (ComparableExpression, FunctionCall)):
|
|
483
|
+
value_sql, value_params = self._render_expression(
|
|
484
|
+
self.value,
|
|
485
|
+
alias_registry,
|
|
486
|
+
)
|
|
487
|
+
sql, op_params = operator_class(col_ref).to_sql_ref(value_sql)
|
|
488
|
+
return apply_negation(sql, col_params + value_params + op_params)
|
|
489
|
+
|
|
490
|
+
if isinstance(self.value, AbstractQuery):
|
|
491
|
+
subquery_sql, subquery_params = self.value.build_query(
|
|
492
|
+
alias_registry,
|
|
493
|
+
)
|
|
494
|
+
return apply_negation(
|
|
495
|
+
f"{col_ref} {operator_class.sql_symbol} ({subquery_sql})",
|
|
496
|
+
col_params + subquery_params,
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
sql, op_params = operator_class(col_ref).to_sql(self.value)
|
|
500
|
+
return apply_negation(sql, col_params + op_params)
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
class Alias(ComparableExpression):
|
|
504
|
+
"""Represents a named SQL alias."""
|
|
505
|
+
|
|
506
|
+
def __init__(self, name: str) -> None:
|
|
507
|
+
self.name: str = name
|
|
508
|
+
|
|
509
|
+
def get_ref(self, alias_registry: AliasRegistry) -> str: # noqa: ARG002
|
|
510
|
+
return f'"{self.name}"'
|
|
511
|
+
|
|
512
|
+
def __repr__(self) -> str:
|
|
513
|
+
return f"Alias({self.name!r})"
|
|
514
|
+
|
|
515
|
+
def to_sql(self, alias_registry: AliasRegistry) -> str:
|
|
516
|
+
return self.get_ref(alias_registry)
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
class FunctionCall(ComparableExpression):
|
|
520
|
+
"""Represents a SQL function call with arguments."""
|
|
521
|
+
|
|
522
|
+
def __init__(self, name: str, *args: Any) -> None:
|
|
523
|
+
"""Initialize a function call.
|
|
524
|
+
|
|
525
|
+
Args:
|
|
526
|
+
name: The SQL function name (e.g., 'SUM', 'COUNT', 'MAX').
|
|
527
|
+
*args: Arguments to pass to the function.
|
|
528
|
+
|
|
529
|
+
"""
|
|
530
|
+
self.name: str = name
|
|
531
|
+
self.args: tuple[Any, ...] = args
|
|
532
|
+
self._alias: Alias | None = None
|
|
533
|
+
|
|
534
|
+
def as_(self, alias: Alias | str) -> FunctionCall:
|
|
535
|
+
"""Attach a named alias to the function call."""
|
|
536
|
+
result = copy(self)
|
|
537
|
+
result._alias = alias if isinstance(alias, Alias) else Alias(alias)
|
|
538
|
+
return result
|
|
539
|
+
|
|
540
|
+
def get_alias(self) -> Alias | None:
|
|
541
|
+
return self._alias
|
|
542
|
+
|
|
543
|
+
def to_sql(
|
|
544
|
+
self,
|
|
545
|
+
alias_registry: AliasRegistry,
|
|
546
|
+
*,
|
|
547
|
+
include_alias: bool = False,
|
|
548
|
+
) -> tuple[str, tuple[Any, ...]]:
|
|
549
|
+
"""Convert function call to SQL and extract parameters.
|
|
550
|
+
|
|
551
|
+
Returns:
|
|
552
|
+
Tuple of (sql_string, parameters_tuple)
|
|
553
|
+
|
|
554
|
+
"""
|
|
555
|
+
sql_args: list[str] = []
|
|
556
|
+
params: list[Any] = []
|
|
557
|
+
|
|
558
|
+
for arg in self.args:
|
|
559
|
+
if isinstance(arg, Column):
|
|
560
|
+
sql_args.append(arg.get_ref(alias_registry))
|
|
561
|
+
|
|
562
|
+
elif isinstance(arg, FunctionCall):
|
|
563
|
+
# Nested function call
|
|
564
|
+
nested_sql, nested_params = arg.to_sql(alias_registry)
|
|
565
|
+
sql_args.append(nested_sql)
|
|
566
|
+
params.extend(nested_params)
|
|
567
|
+
elif arg == "*":
|
|
568
|
+
# Special case for COUNT(*)
|
|
569
|
+
sql_args.append("*")
|
|
570
|
+
elif isinstance(arg, str):
|
|
571
|
+
# String literal - parameterized
|
|
572
|
+
sql_args.append("?")
|
|
573
|
+
params.append(arg)
|
|
574
|
+
elif isinstance(arg, (int, float)):
|
|
575
|
+
# Numeric literal - parameterized
|
|
576
|
+
sql_args.append("?")
|
|
577
|
+
params.append(arg)
|
|
578
|
+
else:
|
|
579
|
+
# Other types - parameterized
|
|
580
|
+
sql_args.append("?")
|
|
581
|
+
params.append(arg)
|
|
582
|
+
|
|
583
|
+
args_sql = ", ".join(sql_args)
|
|
584
|
+
sql = f"{self.name}({args_sql})"
|
|
585
|
+
if include_alias and self._alias is not None:
|
|
586
|
+
sql = f"{sql} AS {self._alias.get_ref(alias_registry)}"
|
|
587
|
+
return sql, tuple(params)
|
|
588
|
+
|
|
589
|
+
def __repr__(self) -> str:
|
|
590
|
+
args_repr = ", ".join(repr(arg) for arg in self.args)
|
|
591
|
+
if self._alias is None:
|
|
592
|
+
return f"FunctionCall({self.name}({args_repr}))"
|
|
593
|
+
return f"FunctionCall({self.name}({args_repr}) AS {self._alias!r})"
|
|
594
|
+
|
|
595
|
+
def __hash__(self) -> int:
|
|
596
|
+
raise TypeError(f"unhashable type: '{type(self).__name__}'")
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
class FunctionRegistry:
|
|
600
|
+
"""Dynamic SQL function registry using __getattr__.
|
|
601
|
+
|
|
602
|
+
Allows arbitrary function calls without defining each one explicitly.
|
|
603
|
+
Any attribute access
|
|
604
|
+
returns a callable that creates FunctionCall instances.
|
|
605
|
+
"""
|
|
606
|
+
|
|
607
|
+
def __getattr__(self, name: str) -> Callable[..., FunctionCall]:
|
|
608
|
+
"""Return a callable for creating FunctionCall instances.
|
|
609
|
+
|
|
610
|
+
Args:
|
|
611
|
+
name: The function name (e.g., 'sum', 'count', 'my_custom_func').
|
|
612
|
+
|
|
613
|
+
Returns:
|
|
614
|
+
A callable that creates FunctionCall instances with uppercase name.
|
|
615
|
+
|
|
616
|
+
"""
|
|
617
|
+
|
|
618
|
+
def function_call(*args: Any) -> FunctionCall:
|
|
619
|
+
return FunctionCall(name.upper(), *args)
|
|
620
|
+
|
|
621
|
+
return function_call
|
|
622
|
+
|
|
623
|
+
|
|
624
|
+
func = FunctionRegistry()
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
class Column(ComparableExpression):
|
|
628
|
+
def __init__(self, name: str) -> None:
|
|
629
|
+
self.name: str = name
|
|
630
|
+
|
|
631
|
+
def _attach_table(self, table: Table) -> None:
|
|
632
|
+
self.table = table
|
|
633
|
+
|
|
634
|
+
def get_ref(self, alias_registry: AliasRegistry) -> str:
|
|
635
|
+
alias = alias_registry.get_alias_for_table(self.table)
|
|
636
|
+
return f'"{alias.name}"."{self.name}"'
|
|
637
|
+
|
|
638
|
+
def like(self, pattern: str) -> Condition:
|
|
639
|
+
return Condition(column=self, operator=LikeOperator, value=pattern)
|
|
640
|
+
|
|
641
|
+
def ilike(self, pattern: str) -> Condition:
|
|
642
|
+
return Condition(column=self, operator=IlikeOperator, value=pattern)
|
|
643
|
+
|
|
644
|
+
def in_(self, values: tuple[Any, ...] | list[Any] | Any) -> Condition:
|
|
645
|
+
return Condition(column=self, operator=InOperator, value=values)
|
|
646
|
+
|
|
647
|
+
def not_in(self, values: tuple[Any, ...] | list[Any] | Any) -> Condition:
|
|
648
|
+
return Condition(column=self, operator=NotInOperator, value=values)
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
class Table:
|
|
652
|
+
def __init__(
|
|
653
|
+
self,
|
|
654
|
+
name: str | AbstractQuery,
|
|
655
|
+
) -> None:
|
|
656
|
+
self._table_name: str = ""
|
|
657
|
+
self._subquery: AbstractQuery | None = None
|
|
658
|
+
|
|
659
|
+
if isinstance(name, AbstractQuery):
|
|
660
|
+
self._subquery = name
|
|
661
|
+
else:
|
|
662
|
+
self._table_name = name
|
|
663
|
+
|
|
664
|
+
def get_name(self) -> str:
|
|
665
|
+
if self._subquery is not None:
|
|
666
|
+
raise ValueError("Table is a subquery, no name available")
|
|
667
|
+
return self._table_name
|
|
668
|
+
|
|
669
|
+
def to_sql(
|
|
670
|
+
self,
|
|
671
|
+
alias_registry: AliasRegistry | None = None,
|
|
672
|
+
) -> tuple[str, tuple[Any, ...]]:
|
|
673
|
+
if self._subquery is not None:
|
|
674
|
+
subquery_sql, subquery_params = self._subquery.build_query(
|
|
675
|
+
alias_registry,
|
|
676
|
+
)
|
|
677
|
+
return f"({subquery_sql})", subquery_params
|
|
678
|
+
|
|
679
|
+
return f'"{self._table_name}"', tuple()
|
|
680
|
+
|
|
681
|
+
def __getattr__(self, column_name: str) -> Column:
|
|
682
|
+
if column_name.startswith("_"):
|
|
683
|
+
raise AttributeError(
|
|
684
|
+
f"'{type(self).__name__}' "
|
|
685
|
+
f"object has no attribute '{column_name}'",
|
|
686
|
+
)
|
|
687
|
+
column = Column(column_name)
|
|
688
|
+
column._attach_table(self) # pyright: ignore[reportPrivateUsage]
|
|
689
|
+
return column
|