iceaxe 0.7.1__cp313-cp313-macosx_11_0_arm64.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 iceaxe might be problematic. Click here for more details.
- iceaxe/__init__.py +20 -0
- iceaxe/__tests__/__init__.py +0 -0
- iceaxe/__tests__/benchmarks/__init__.py +0 -0
- iceaxe/__tests__/benchmarks/test_bulk_insert.py +45 -0
- iceaxe/__tests__/benchmarks/test_select.py +114 -0
- iceaxe/__tests__/conf_models.py +133 -0
- iceaxe/__tests__/conftest.py +204 -0
- iceaxe/__tests__/docker_helpers.py +208 -0
- iceaxe/__tests__/helpers.py +268 -0
- iceaxe/__tests__/migrations/__init__.py +0 -0
- iceaxe/__tests__/migrations/conftest.py +36 -0
- iceaxe/__tests__/migrations/test_action_sorter.py +237 -0
- iceaxe/__tests__/migrations/test_generator.py +140 -0
- iceaxe/__tests__/migrations/test_generics.py +91 -0
- iceaxe/__tests__/mountaineer/__init__.py +0 -0
- iceaxe/__tests__/mountaineer/dependencies/__init__.py +0 -0
- iceaxe/__tests__/mountaineer/dependencies/test_core.py +76 -0
- iceaxe/__tests__/schemas/__init__.py +0 -0
- iceaxe/__tests__/schemas/test_actions.py +1264 -0
- iceaxe/__tests__/schemas/test_cli.py +25 -0
- iceaxe/__tests__/schemas/test_db_memory_serializer.py +1525 -0
- iceaxe/__tests__/schemas/test_db_serializer.py +398 -0
- iceaxe/__tests__/schemas/test_db_stubs.py +190 -0
- iceaxe/__tests__/test_alias.py +83 -0
- iceaxe/__tests__/test_base.py +52 -0
- iceaxe/__tests__/test_comparison.py +383 -0
- iceaxe/__tests__/test_field.py +11 -0
- iceaxe/__tests__/test_helpers.py +9 -0
- iceaxe/__tests__/test_modifications.py +151 -0
- iceaxe/__tests__/test_queries.py +605 -0
- iceaxe/__tests__/test_queries_str.py +173 -0
- iceaxe/__tests__/test_session.py +1511 -0
- iceaxe/__tests__/test_text_search.py +287 -0
- iceaxe/alias_values.py +67 -0
- iceaxe/base.py +350 -0
- iceaxe/comparison.py +560 -0
- iceaxe/field.py +250 -0
- iceaxe/functions.py +906 -0
- iceaxe/generics.py +140 -0
- iceaxe/io.py +107 -0
- iceaxe/logging.py +91 -0
- iceaxe/migrations/__init__.py +5 -0
- iceaxe/migrations/action_sorter.py +98 -0
- iceaxe/migrations/cli.py +228 -0
- iceaxe/migrations/client_io.py +62 -0
- iceaxe/migrations/generator.py +404 -0
- iceaxe/migrations/migration.py +86 -0
- iceaxe/migrations/migrator.py +101 -0
- iceaxe/modifications.py +176 -0
- iceaxe/mountaineer/__init__.py +10 -0
- iceaxe/mountaineer/cli.py +74 -0
- iceaxe/mountaineer/config.py +46 -0
- iceaxe/mountaineer/dependencies/__init__.py +6 -0
- iceaxe/mountaineer/dependencies/core.py +67 -0
- iceaxe/postgres.py +133 -0
- iceaxe/py.typed +0 -0
- iceaxe/queries.py +1455 -0
- iceaxe/queries_str.py +294 -0
- iceaxe/schemas/__init__.py +0 -0
- iceaxe/schemas/actions.py +864 -0
- iceaxe/schemas/cli.py +30 -0
- iceaxe/schemas/db_memory_serializer.py +705 -0
- iceaxe/schemas/db_serializer.py +346 -0
- iceaxe/schemas/db_stubs.py +525 -0
- iceaxe/session.py +860 -0
- iceaxe/session_optimized.c +12035 -0
- iceaxe/session_optimized.cpython-313-darwin.so +0 -0
- iceaxe/session_optimized.pyx +212 -0
- iceaxe/sql_types.py +148 -0
- iceaxe/typing.py +73 -0
- iceaxe-0.7.1.dist-info/METADATA +261 -0
- iceaxe-0.7.1.dist-info/RECORD +75 -0
- iceaxe-0.7.1.dist-info/WHEEL +6 -0
- iceaxe-0.7.1.dist-info/licenses/LICENSE +21 -0
- iceaxe-0.7.1.dist-info/top_level.txt +1 -0
iceaxe/queries.py
ADDED
|
@@ -0,0 +1,1455 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from copy import copy
|
|
4
|
+
from dataclasses import dataclass, field as dataclass_field
|
|
5
|
+
from functools import wraps
|
|
6
|
+
from typing import Any, Generic, Literal, Type, TypeVar, TypeVarTuple, cast, overload
|
|
7
|
+
|
|
8
|
+
from iceaxe.alias_values import Alias
|
|
9
|
+
from iceaxe.base import (
|
|
10
|
+
DBFieldClassDefinition,
|
|
11
|
+
DBModelMetaclass,
|
|
12
|
+
TableBase,
|
|
13
|
+
)
|
|
14
|
+
from iceaxe.comparison import ComparisonGroupType, FieldComparison, FieldComparisonGroup
|
|
15
|
+
from iceaxe.functions import FunctionMetadata
|
|
16
|
+
from iceaxe.queries_str import (
|
|
17
|
+
QueryElementBase,
|
|
18
|
+
QueryLiteral,
|
|
19
|
+
sql,
|
|
20
|
+
)
|
|
21
|
+
from iceaxe.typing import (
|
|
22
|
+
ALL_ENUM_TYPES,
|
|
23
|
+
DATE_TYPES,
|
|
24
|
+
JSON_WRAPPER_FALLBACK,
|
|
25
|
+
PRIMITIVE_TYPES,
|
|
26
|
+
PRIMITIVE_WRAPPER_TYPES,
|
|
27
|
+
is_alias,
|
|
28
|
+
is_base_table,
|
|
29
|
+
is_column,
|
|
30
|
+
is_comparison,
|
|
31
|
+
is_comparison_group,
|
|
32
|
+
is_function_metadata,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
P = TypeVar("P")
|
|
36
|
+
|
|
37
|
+
SUPPORTED_SELECTS = (
|
|
38
|
+
TableBase
|
|
39
|
+
| DBModelMetaclass
|
|
40
|
+
| ALL_ENUM_TYPES
|
|
41
|
+
| PRIMITIVE_TYPES
|
|
42
|
+
| PRIMITIVE_WRAPPER_TYPES
|
|
43
|
+
| DATE_TYPES
|
|
44
|
+
| JSON_WRAPPER_FALLBACK
|
|
45
|
+
| None
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
T = TypeVar("T", bound=SUPPORTED_SELECTS)
|
|
49
|
+
T2 = TypeVar("T2", bound=SUPPORTED_SELECTS)
|
|
50
|
+
T3 = TypeVar("T3", bound=SUPPORTED_SELECTS)
|
|
51
|
+
T4 = TypeVar("T4", bound=SUPPORTED_SELECTS)
|
|
52
|
+
T5 = TypeVar("T5", bound=SUPPORTED_SELECTS)
|
|
53
|
+
T6 = TypeVar("T6", bound=SUPPORTED_SELECTS)
|
|
54
|
+
T7 = TypeVar("T7", bound=SUPPORTED_SELECTS)
|
|
55
|
+
T8 = TypeVar("T8", bound=SUPPORTED_SELECTS)
|
|
56
|
+
T9 = TypeVar("T9", bound=SUPPORTED_SELECTS)
|
|
57
|
+
T10 = TypeVar("T10", bound=SUPPORTED_SELECTS)
|
|
58
|
+
Ts = TypeVarTuple("Ts")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
QueryType = TypeVar("QueryType", bound=Literal["SELECT", "INSERT", "UPDATE", "DELETE"])
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
JoinType = Literal["INNER", "LEFT", "RIGHT", "FULL"]
|
|
65
|
+
OrderDirection = Literal["ASC", "DESC"]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def allow_branching(fn):
|
|
69
|
+
"""
|
|
70
|
+
Allows query method modifiers to implement their logic as if `self` is being
|
|
71
|
+
modified, but in the background we'll actually return a new instance of the
|
|
72
|
+
query builder to allow for branching of the same underlying query.
|
|
73
|
+
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
@wraps(fn)
|
|
77
|
+
def new_fn(self, *args, **kwargs):
|
|
78
|
+
self = copy(self)
|
|
79
|
+
return fn(self, *args, **kwargs)
|
|
80
|
+
|
|
81
|
+
return new_fn
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@dataclass
|
|
85
|
+
class ForUpdateConfig:
|
|
86
|
+
"""
|
|
87
|
+
Configuration for FOR UPDATE clause in SELECT queries.
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
nowait: bool = False
|
|
91
|
+
skip_locked: bool = False
|
|
92
|
+
of_tables: set[QueryElementBase] = dataclass_field(default_factory=set)
|
|
93
|
+
conditions_set: bool = False
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class QueryBuilder(Generic[P, QueryType]):
|
|
97
|
+
"""
|
|
98
|
+
The QueryBuilder owns all construction of the SQL string given
|
|
99
|
+
python method chaining. Each function call returns a reference to
|
|
100
|
+
self, so you can construct as many queries as you want in a single
|
|
101
|
+
line of code.
|
|
102
|
+
|
|
103
|
+
Internally we store most input-arguments as-is. We provide runtime
|
|
104
|
+
value-checking to make sure the right objects are being passed in to query
|
|
105
|
+
manipulation functions so our final build() will deterministically succeed
|
|
106
|
+
if the query build was successful.
|
|
107
|
+
|
|
108
|
+
Note that this runtime check-checking validates different types than the static
|
|
109
|
+
analysis. To satisfy Python logical operations (like `join(ModelA.id == ModelB.id)`) we
|
|
110
|
+
have many overloaded operators that return objects at runtime but are masked to their
|
|
111
|
+
Python types for the purposes of static analysis. This implementation detail should
|
|
112
|
+
be transparent to the user but is noted in case you see different types through
|
|
113
|
+
runtime inspection than you see during the typehints.
|
|
114
|
+
|
|
115
|
+
```python {{sticky: True}}
|
|
116
|
+
# Basic SELECT query
|
|
117
|
+
query = (
|
|
118
|
+
QueryBuilder()
|
|
119
|
+
.select(User)
|
|
120
|
+
.where(User.is_active == True)
|
|
121
|
+
.order_by(User.created_at, "DESC")
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
# Complex query with joins and aggregates
|
|
125
|
+
query = (
|
|
126
|
+
QueryBuilder()
|
|
127
|
+
.select((User.name, func.count(Order.id)))
|
|
128
|
+
.join(Order, Order.user_id == User.id)
|
|
129
|
+
.where(Order.status == "completed")
|
|
130
|
+
.group_by(User.name)
|
|
131
|
+
.having(func.count(Order.id) > 5)
|
|
132
|
+
)
|
|
133
|
+
```
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
def __init__(self):
|
|
137
|
+
self._query_type: QueryType | None = None
|
|
138
|
+
self._main_model: Type[TableBase] | None = None
|
|
139
|
+
|
|
140
|
+
self._return_typehint: P
|
|
141
|
+
|
|
142
|
+
self._where_conditions: list[FieldComparison | FieldComparisonGroup] = []
|
|
143
|
+
self._order_by_clauses: list[str] = []
|
|
144
|
+
self._join_clauses: list[str] = []
|
|
145
|
+
self._limit_value: int | None = None
|
|
146
|
+
self._offset_value: int | None = None
|
|
147
|
+
self._group_by_clauses: list[str] = []
|
|
148
|
+
self._having_conditions: list[FieldComparison] = []
|
|
149
|
+
self._distinct_on_fields: list[QueryElementBase] = []
|
|
150
|
+
self._for_update_config: ForUpdateConfig = ForUpdateConfig()
|
|
151
|
+
|
|
152
|
+
# Query specific params
|
|
153
|
+
self._update_values: list[tuple[DBFieldClassDefinition, Any]] = []
|
|
154
|
+
self._select_fields: list[QueryElementBase] = []
|
|
155
|
+
self._select_raw: list[
|
|
156
|
+
DBFieldClassDefinition | Type[TableBase] | FunctionMetadata | Alias
|
|
157
|
+
] = []
|
|
158
|
+
self._select_aggregate_count = 0
|
|
159
|
+
|
|
160
|
+
# Alias tracking
|
|
161
|
+
self._alias_mappings: dict[str, QueryElementBase] = {}
|
|
162
|
+
|
|
163
|
+
# Text
|
|
164
|
+
self._text_query: str | None = None
|
|
165
|
+
self._text_variables: list[Any] = []
|
|
166
|
+
|
|
167
|
+
@overload
|
|
168
|
+
def select(self, fields: T | Type[T]) -> QueryBuilder[T, Literal["SELECT"]]: ...
|
|
169
|
+
|
|
170
|
+
@overload
|
|
171
|
+
def select(
|
|
172
|
+
self,
|
|
173
|
+
fields: tuple[T | Type[T]],
|
|
174
|
+
) -> QueryBuilder[tuple[T], Literal["SELECT"]]: ...
|
|
175
|
+
|
|
176
|
+
@overload
|
|
177
|
+
def select(
|
|
178
|
+
self,
|
|
179
|
+
fields: tuple[T | Type[T], T2 | Type[T2]],
|
|
180
|
+
) -> QueryBuilder[tuple[T, T2], Literal["SELECT"]]: ...
|
|
181
|
+
|
|
182
|
+
@overload
|
|
183
|
+
def select(
|
|
184
|
+
self,
|
|
185
|
+
fields: tuple[T | Type[T], T2 | Type[T2], T3 | Type[T3]],
|
|
186
|
+
) -> QueryBuilder[tuple[T, T2, T3], Literal["SELECT"]]: ...
|
|
187
|
+
|
|
188
|
+
@overload
|
|
189
|
+
def select(
|
|
190
|
+
self,
|
|
191
|
+
fields: tuple[T | Type[T], T2 | Type[T2], T3 | Type[T3], T4 | Type[T4]],
|
|
192
|
+
) -> QueryBuilder[tuple[T, T2, T3, T4], Literal["SELECT"]]: ...
|
|
193
|
+
|
|
194
|
+
@overload
|
|
195
|
+
def select(
|
|
196
|
+
self,
|
|
197
|
+
fields: tuple[
|
|
198
|
+
T | Type[T], T2 | Type[T2], T3 | Type[T3], T4 | Type[T4], T5 | Type[T5]
|
|
199
|
+
],
|
|
200
|
+
) -> QueryBuilder[tuple[T, T2, T3, T4, T5], Literal["SELECT"]]: ...
|
|
201
|
+
|
|
202
|
+
@overload
|
|
203
|
+
def select(
|
|
204
|
+
self,
|
|
205
|
+
fields: tuple[
|
|
206
|
+
T | Type[T],
|
|
207
|
+
T2 | Type[T2],
|
|
208
|
+
T3 | Type[T3],
|
|
209
|
+
T4 | Type[T4],
|
|
210
|
+
T5 | Type[T5],
|
|
211
|
+
T6 | Type[T6],
|
|
212
|
+
],
|
|
213
|
+
) -> QueryBuilder[tuple[T, T2, T3, T4, T5, T6], Literal["SELECT"]]: ...
|
|
214
|
+
|
|
215
|
+
@overload
|
|
216
|
+
def select(
|
|
217
|
+
self,
|
|
218
|
+
fields: tuple[
|
|
219
|
+
T | Type[T],
|
|
220
|
+
T2 | Type[T2],
|
|
221
|
+
T3 | Type[T3],
|
|
222
|
+
T4 | Type[T4],
|
|
223
|
+
T5 | Type[T5],
|
|
224
|
+
T6 | Type[T6],
|
|
225
|
+
T7 | Type[T7],
|
|
226
|
+
],
|
|
227
|
+
) -> QueryBuilder[tuple[T, T2, T3, T4, T5, T6, T7], Literal["SELECT"]]: ...
|
|
228
|
+
|
|
229
|
+
@overload
|
|
230
|
+
def select(
|
|
231
|
+
self,
|
|
232
|
+
fields: tuple[
|
|
233
|
+
T | Type[T],
|
|
234
|
+
T2 | Type[T2],
|
|
235
|
+
T3 | Type[T3],
|
|
236
|
+
T4 | Type[T4],
|
|
237
|
+
T5 | Type[T5],
|
|
238
|
+
T6 | Type[T6],
|
|
239
|
+
T7 | Type[T7],
|
|
240
|
+
T8 | Type[T8],
|
|
241
|
+
],
|
|
242
|
+
) -> QueryBuilder[tuple[T, T2, T3, T4, T5, T6, T7, T8], Literal["SELECT"]]: ...
|
|
243
|
+
|
|
244
|
+
@overload
|
|
245
|
+
def select(
|
|
246
|
+
self,
|
|
247
|
+
fields: tuple[
|
|
248
|
+
T | Type[T],
|
|
249
|
+
T2 | Type[T2],
|
|
250
|
+
T3 | Type[T3],
|
|
251
|
+
T4 | Type[T4],
|
|
252
|
+
T5 | Type[T5],
|
|
253
|
+
T6 | Type[T6],
|
|
254
|
+
T7 | Type[T7],
|
|
255
|
+
T8 | Type[T8],
|
|
256
|
+
T9 | Type[T9],
|
|
257
|
+
],
|
|
258
|
+
) -> QueryBuilder[tuple[T, T2, T3, T4, T5, T6, T7, T8, T9], Literal["SELECT"]]: ...
|
|
259
|
+
|
|
260
|
+
@overload
|
|
261
|
+
def select(
|
|
262
|
+
self,
|
|
263
|
+
fields: tuple[
|
|
264
|
+
T | Type[T],
|
|
265
|
+
T2 | Type[T2],
|
|
266
|
+
T3 | Type[T3],
|
|
267
|
+
T4 | Type[T4],
|
|
268
|
+
T5 | Type[T5],
|
|
269
|
+
T6 | Type[T6],
|
|
270
|
+
T7 | Type[T7],
|
|
271
|
+
T8 | Type[T8],
|
|
272
|
+
T9 | Type[T9],
|
|
273
|
+
T10 | Type[T10],
|
|
274
|
+
],
|
|
275
|
+
) -> QueryBuilder[
|
|
276
|
+
tuple[T, T2, T3, T4, T5, T6, T7, T8, T9, T10], Literal["SELECT"]
|
|
277
|
+
]: ...
|
|
278
|
+
|
|
279
|
+
@overload
|
|
280
|
+
def select(
|
|
281
|
+
self,
|
|
282
|
+
fields: tuple[
|
|
283
|
+
T | Type[T],
|
|
284
|
+
T2 | Type[T2],
|
|
285
|
+
T3 | Type[T3],
|
|
286
|
+
T4 | Type[T4],
|
|
287
|
+
T5 | Type[T5],
|
|
288
|
+
T6 | Type[T6],
|
|
289
|
+
T7 | Type[T7],
|
|
290
|
+
T8 | Type[T8],
|
|
291
|
+
T9 | Type[T9],
|
|
292
|
+
T10 | Type[T10],
|
|
293
|
+
*Ts,
|
|
294
|
+
],
|
|
295
|
+
) -> QueryBuilder[
|
|
296
|
+
tuple[T, T2, T3, T4, T5, T6, T7, T8, T9, T10, *Ts], Literal["SELECT"]
|
|
297
|
+
]: ...
|
|
298
|
+
|
|
299
|
+
@allow_branching
|
|
300
|
+
def select(
|
|
301
|
+
self,
|
|
302
|
+
fields: (
|
|
303
|
+
T
|
|
304
|
+
| Type[T]
|
|
305
|
+
| tuple[T | Type[T]]
|
|
306
|
+
| tuple[T | Type[T], T2 | Type[T2]]
|
|
307
|
+
| tuple[T | Type[T], T2 | Type[T2], T3 | Type[T3]]
|
|
308
|
+
| tuple[T | Type[T], T2 | Type[T2], T3 | Type[T3], T4 | Type[T4]]
|
|
309
|
+
| tuple[
|
|
310
|
+
T | Type[T], T2 | Type[T2], T3 | Type[T3], T4 | Type[T4], T5 | Type[T5]
|
|
311
|
+
]
|
|
312
|
+
| tuple[
|
|
313
|
+
T | Type[T],
|
|
314
|
+
T2 | Type[T2],
|
|
315
|
+
T3 | Type[T3],
|
|
316
|
+
T4 | Type[T4],
|
|
317
|
+
T5 | Type[T5],
|
|
318
|
+
T6 | Type[T6],
|
|
319
|
+
]
|
|
320
|
+
| tuple[
|
|
321
|
+
T | Type[T],
|
|
322
|
+
T2 | Type[T2],
|
|
323
|
+
T3 | Type[T3],
|
|
324
|
+
T4 | Type[T4],
|
|
325
|
+
T5 | Type[T5],
|
|
326
|
+
T6 | Type[T6],
|
|
327
|
+
T7 | Type[T7],
|
|
328
|
+
]
|
|
329
|
+
| tuple[
|
|
330
|
+
T | Type[T],
|
|
331
|
+
T2 | Type[T2],
|
|
332
|
+
T3 | Type[T3],
|
|
333
|
+
T4 | Type[T4],
|
|
334
|
+
T5 | Type[T5],
|
|
335
|
+
T6 | Type[T6],
|
|
336
|
+
T7 | Type[T7],
|
|
337
|
+
T8 | Type[T8],
|
|
338
|
+
]
|
|
339
|
+
| tuple[
|
|
340
|
+
T | Type[T],
|
|
341
|
+
T2 | Type[T2],
|
|
342
|
+
T3 | Type[T3],
|
|
343
|
+
T4 | Type[T4],
|
|
344
|
+
T5 | Type[T5],
|
|
345
|
+
T6 | Type[T6],
|
|
346
|
+
T7 | Type[T7],
|
|
347
|
+
T8 | Type[T8],
|
|
348
|
+
T9 | Type[T9],
|
|
349
|
+
]
|
|
350
|
+
| tuple[
|
|
351
|
+
T | Type[T],
|
|
352
|
+
T2 | Type[T2],
|
|
353
|
+
T3 | Type[T3],
|
|
354
|
+
T4 | Type[T4],
|
|
355
|
+
T5 | Type[T5],
|
|
356
|
+
T6 | Type[T6],
|
|
357
|
+
T7 | Type[T7],
|
|
358
|
+
T8 | Type[T8],
|
|
359
|
+
T9 | Type[T9],
|
|
360
|
+
T10 | Type[T10],
|
|
361
|
+
]
|
|
362
|
+
| tuple[
|
|
363
|
+
T | Type[T],
|
|
364
|
+
T2 | Type[T2],
|
|
365
|
+
T3 | Type[T3],
|
|
366
|
+
T4 | Type[T4],
|
|
367
|
+
T5 | Type[T5],
|
|
368
|
+
T6 | Type[T6],
|
|
369
|
+
T7 | Type[T7],
|
|
370
|
+
T8 | Type[T8],
|
|
371
|
+
T9 | Type[T9],
|
|
372
|
+
T10 | Type[T10],
|
|
373
|
+
*Ts,
|
|
374
|
+
]
|
|
375
|
+
),
|
|
376
|
+
) -> (
|
|
377
|
+
QueryBuilder[T, Literal["SELECT"]]
|
|
378
|
+
| QueryBuilder[tuple[T], Literal["SELECT"]]
|
|
379
|
+
| QueryBuilder[tuple[T, T2], Literal["SELECT"]]
|
|
380
|
+
| QueryBuilder[tuple[T, T2, T3], Literal["SELECT"]]
|
|
381
|
+
| QueryBuilder[tuple[T, T2, T3, T4], Literal["SELECT"]]
|
|
382
|
+
| QueryBuilder[tuple[T, T2, T3, T4, T5], Literal["SELECT"]]
|
|
383
|
+
| QueryBuilder[tuple[T, T2, T3, T4, T5, T6], Literal["SELECT"]]
|
|
384
|
+
| QueryBuilder[tuple[T, T2, T3, T4, T5, T6, T7], Literal["SELECT"]]
|
|
385
|
+
| QueryBuilder[tuple[T, T2, T3, T4, T5, T6, T7, T8], Literal["SELECT"]]
|
|
386
|
+
| QueryBuilder[tuple[T, T2, T3, T4, T5, T6, T7, T8, T9], Literal["SELECT"]]
|
|
387
|
+
| QueryBuilder[tuple[T, T2, T3, T4, T5, T6, T7, T8, T9, T10], Literal["SELECT"]]
|
|
388
|
+
| QueryBuilder[
|
|
389
|
+
tuple[T, T2, T3, T4, T5, T6, T7, T8, T9, T10, *Ts], Literal["SELECT"]
|
|
390
|
+
]
|
|
391
|
+
):
|
|
392
|
+
"""
|
|
393
|
+
Creates a SELECT query to fetch data from the database.
|
|
394
|
+
|
|
395
|
+
```python {{sticky: True}}
|
|
396
|
+
# Select all fields from User
|
|
397
|
+
query = QueryBuilder().select(User)
|
|
398
|
+
|
|
399
|
+
# Select specific fields
|
|
400
|
+
query = QueryBuilder().select((User.id, User.name))
|
|
401
|
+
|
|
402
|
+
# Select with aggregation
|
|
403
|
+
query = QueryBuilder().select((
|
|
404
|
+
User.name,
|
|
405
|
+
func.count(Order.id).as_("order_count")
|
|
406
|
+
))
|
|
407
|
+
|
|
408
|
+
# Select from multiple tables
|
|
409
|
+
query = QueryBuilder().select((User, Order))
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
:param fields: The fields to select. Can be:
|
|
413
|
+
- A single field (e.g., User.id)
|
|
414
|
+
- A model class (e.g., User)
|
|
415
|
+
- A tuple of fields (e.g., (User.id, User.name))
|
|
416
|
+
- A tuple of model classes (e.g., (User, Post))
|
|
417
|
+
:return: A QueryBuilder instance configured for SELECT operations
|
|
418
|
+
|
|
419
|
+
"""
|
|
420
|
+
all_fields: tuple[
|
|
421
|
+
DBFieldClassDefinition | Type[TableBase] | FunctionMetadata, ...
|
|
422
|
+
]
|
|
423
|
+
if not isinstance(fields, tuple):
|
|
424
|
+
all_fields = (fields,) # type: ignore
|
|
425
|
+
else:
|
|
426
|
+
all_fields = fields # type: ignore
|
|
427
|
+
|
|
428
|
+
# Verify the field type
|
|
429
|
+
for field in all_fields:
|
|
430
|
+
if (
|
|
431
|
+
not is_column(field)
|
|
432
|
+
and not is_base_table(field)
|
|
433
|
+
and not is_alias(field)
|
|
434
|
+
and not is_function_metadata(field)
|
|
435
|
+
):
|
|
436
|
+
raise ValueError(
|
|
437
|
+
f"Invalid field type {field}. Must be:\n1. A column field\n2. A table\n3. A QueryLiteral\n4. A tuple of the above."
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
self._select_inner(all_fields)
|
|
441
|
+
|
|
442
|
+
return self # type: ignore
|
|
443
|
+
|
|
444
|
+
def _select_inner(
|
|
445
|
+
self,
|
|
446
|
+
fields: tuple[DBFieldClassDefinition | Type[TableBase] | FunctionMetadata, ...],
|
|
447
|
+
):
|
|
448
|
+
self._query_type = "SELECT" # type: ignore
|
|
449
|
+
self._return_typehint = fields # type: ignore
|
|
450
|
+
|
|
451
|
+
if not fields:
|
|
452
|
+
raise ValueError("At least one field must be selected")
|
|
453
|
+
|
|
454
|
+
# We always take the default FROM table as the first element
|
|
455
|
+
representative_field = fields[0]
|
|
456
|
+
if is_column(representative_field):
|
|
457
|
+
self._main_model = representative_field.root_model
|
|
458
|
+
elif is_base_table(representative_field):
|
|
459
|
+
self._main_model = representative_field
|
|
460
|
+
elif is_function_metadata(representative_field):
|
|
461
|
+
self._main_model = representative_field.original_field.root_model
|
|
462
|
+
|
|
463
|
+
for field in fields:
|
|
464
|
+
if is_column(field) or is_base_table(field):
|
|
465
|
+
self._select_fields.append(sql.select(field))
|
|
466
|
+
self._select_raw.append(field)
|
|
467
|
+
elif is_alias(field):
|
|
468
|
+
# Handle alias case
|
|
469
|
+
if is_function_metadata(field.value):
|
|
470
|
+
alias_value = field.value.literal
|
|
471
|
+
self._alias_mappings[field.name] = field.value.literal
|
|
472
|
+
else:
|
|
473
|
+
# For primitive types, just use the name as is
|
|
474
|
+
alias_value = field.name
|
|
475
|
+
self._select_fields.append(
|
|
476
|
+
QueryLiteral(f"{alias_value} AS {field.name}")
|
|
477
|
+
)
|
|
478
|
+
self._select_raw.append(field)
|
|
479
|
+
elif is_function_metadata(field):
|
|
480
|
+
# Handle function metadata with or without alias
|
|
481
|
+
if field.local_name:
|
|
482
|
+
# If there's an alias, use it and track the mapping
|
|
483
|
+
self._select_fields.append(
|
|
484
|
+
QueryLiteral(f"{field.literal} AS {field.local_name}")
|
|
485
|
+
)
|
|
486
|
+
self._alias_mappings[field.local_name] = field.literal
|
|
487
|
+
else:
|
|
488
|
+
# If no alias, generate one and track the mapping
|
|
489
|
+
field.local_name = f"aggregate_{self._select_aggregate_count}"
|
|
490
|
+
self._select_fields.append(
|
|
491
|
+
QueryLiteral(f"{field.literal} AS {field.local_name}")
|
|
492
|
+
)
|
|
493
|
+
self._alias_mappings[field.local_name] = field.literal
|
|
494
|
+
self._select_aggregate_count += 1
|
|
495
|
+
self._select_raw.append(field)
|
|
496
|
+
|
|
497
|
+
@allow_branching
|
|
498
|
+
def update(self, model: Type[TableBase]) -> QueryBuilder[None, Literal["UPDATE"]]:
|
|
499
|
+
"""
|
|
500
|
+
Creates a new update query for the given model. Returns the same
|
|
501
|
+
QueryBuilder that is now flagged as an UPDATE query.
|
|
502
|
+
|
|
503
|
+
"""
|
|
504
|
+
self._query_type = "UPDATE" # type: ignore
|
|
505
|
+
self._main_model = model
|
|
506
|
+
return self # type: ignore
|
|
507
|
+
|
|
508
|
+
@allow_branching
|
|
509
|
+
def delete(self, model: Type[TableBase]) -> QueryBuilder[None, Literal["DELETE"]]:
|
|
510
|
+
"""
|
|
511
|
+
Creates a new delete query for the given model. Returns the same
|
|
512
|
+
QueryBuilder that is now flagged as a DELETE query.
|
|
513
|
+
|
|
514
|
+
"""
|
|
515
|
+
self._query_type = "DELETE" # type: ignore
|
|
516
|
+
self._main_model = model
|
|
517
|
+
return self # type: ignore
|
|
518
|
+
|
|
519
|
+
@allow_branching
|
|
520
|
+
def where(self, *conditions: FieldComparison | FieldComparisonGroup | bool):
|
|
521
|
+
"""
|
|
522
|
+
Adds WHERE conditions to filter the query results. Multiple conditions are combined with AND.
|
|
523
|
+
For OR conditions, use the `or_` function.
|
|
524
|
+
|
|
525
|
+
```python {{sticky: True}}
|
|
526
|
+
# Simple condition
|
|
527
|
+
query = (
|
|
528
|
+
QueryBuilder()
|
|
529
|
+
.select(User)
|
|
530
|
+
.where(User.age >= 18)
|
|
531
|
+
)
|
|
532
|
+
|
|
533
|
+
# Multiple conditions (AND)
|
|
534
|
+
query = (
|
|
535
|
+
QueryBuilder()
|
|
536
|
+
.select(User)
|
|
537
|
+
.where(
|
|
538
|
+
User.age >= 18,
|
|
539
|
+
User.is_active == True
|
|
540
|
+
)
|
|
541
|
+
)
|
|
542
|
+
|
|
543
|
+
# Complex conditions with AND/OR
|
|
544
|
+
query = (
|
|
545
|
+
QueryBuilder()
|
|
546
|
+
.select(User)
|
|
547
|
+
.where(
|
|
548
|
+
and_(
|
|
549
|
+
User.age >= 18,
|
|
550
|
+
or_(
|
|
551
|
+
User.role == "admin",
|
|
552
|
+
User.permissions.contains("manage_users")
|
|
553
|
+
)
|
|
554
|
+
)
|
|
555
|
+
)
|
|
556
|
+
)
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
:param conditions: One or more boolean conditions using field comparisons
|
|
560
|
+
:return: The QueryBuilder instance for method chaining
|
|
561
|
+
|
|
562
|
+
"""
|
|
563
|
+
# During typechecking these seem like bool values, since they're the result
|
|
564
|
+
# of the comparison set. But at runtime they will be the whole object that
|
|
565
|
+
# gives the comparison. We can assert that's true here.
|
|
566
|
+
validated_comparisons: list[FieldComparison | FieldComparisonGroup] = []
|
|
567
|
+
for condition in conditions:
|
|
568
|
+
if not is_comparison(condition) and not is_comparison_group(condition):
|
|
569
|
+
raise ValueError(f"Invalid where condition: {condition}")
|
|
570
|
+
validated_comparisons.append(condition)
|
|
571
|
+
|
|
572
|
+
self._where_conditions += validated_comparisons
|
|
573
|
+
return self
|
|
574
|
+
|
|
575
|
+
@allow_branching
|
|
576
|
+
def order_by(self, field: Any, direction: OrderDirection = "ASC"):
|
|
577
|
+
"""
|
|
578
|
+
Adds an ORDER BY clause to sort the query results.
|
|
579
|
+
|
|
580
|
+
```python {{sticky: True}}
|
|
581
|
+
# Simple ascending sort
|
|
582
|
+
query = (
|
|
583
|
+
QueryBuilder()
|
|
584
|
+
.select(User)
|
|
585
|
+
.order_by(User.created_at)
|
|
586
|
+
)
|
|
587
|
+
|
|
588
|
+
# Descending sort
|
|
589
|
+
query = (
|
|
590
|
+
QueryBuilder()
|
|
591
|
+
.select(User)
|
|
592
|
+
.order_by(User.created_at, "DESC")
|
|
593
|
+
)
|
|
594
|
+
|
|
595
|
+
# Multiple sort criteria
|
|
596
|
+
query = (
|
|
597
|
+
QueryBuilder()
|
|
598
|
+
.select(User)
|
|
599
|
+
.order_by(User.last_name, "ASC")
|
|
600
|
+
.order_by(User.first_name, "ASC")
|
|
601
|
+
)
|
|
602
|
+
|
|
603
|
+
# Sort by aggregate function
|
|
604
|
+
query = (
|
|
605
|
+
QueryBuilder()
|
|
606
|
+
.select((User.name, func.count(Post.id)))
|
|
607
|
+
.join(Post, Post.user_id == User.id)
|
|
608
|
+
.group_by(User.name)
|
|
609
|
+
.order_by(func.count(Post.id), "DESC")
|
|
610
|
+
)
|
|
611
|
+
|
|
612
|
+
# Sort by aliased column
|
|
613
|
+
query = (
|
|
614
|
+
QueryBuilder()
|
|
615
|
+
.select((User, func.count(Post.id).as_("post_count")))
|
|
616
|
+
.join(Post, Post.user_id == User.id)
|
|
617
|
+
.group_by(User.name)
|
|
618
|
+
.order_by("post_count", "DESC")
|
|
619
|
+
)
|
|
620
|
+
```
|
|
621
|
+
|
|
622
|
+
:param field: The field to sort by (can be a column, function, or string for aliased columns)
|
|
623
|
+
:param direction: The sort direction, either "ASC" or "DESC"
|
|
624
|
+
:return: The QueryBuilder instance for method chaining
|
|
625
|
+
"""
|
|
626
|
+
if is_column(field):
|
|
627
|
+
field_token, _ = field.to_query()
|
|
628
|
+
elif is_function_metadata(field):
|
|
629
|
+
field_token = field.literal
|
|
630
|
+
elif isinstance(field, str):
|
|
631
|
+
# Just use the string as-is for raw SQL queries
|
|
632
|
+
field_token = QueryLiteral(field)
|
|
633
|
+
else:
|
|
634
|
+
raise ValueError(f"Invalid order by field: {field}")
|
|
635
|
+
|
|
636
|
+
self._order_by_clauses.append(f"{field_token} {direction}")
|
|
637
|
+
return self
|
|
638
|
+
|
|
639
|
+
@allow_branching
|
|
640
|
+
def join(self, table: Type[TableBase], on: bool, join_type: JoinType = "INNER"):
|
|
641
|
+
"""
|
|
642
|
+
Adds a JOIN clause to combine data from multiple tables.
|
|
643
|
+
|
|
644
|
+
```python {{sticky: True}}
|
|
645
|
+
# Inner join
|
|
646
|
+
query = (
|
|
647
|
+
QueryBuilder()
|
|
648
|
+
.select((User.name, Order.total))
|
|
649
|
+
.join(Order, Order.user_id == User.id)
|
|
650
|
+
)
|
|
651
|
+
|
|
652
|
+
# Left join
|
|
653
|
+
query = (
|
|
654
|
+
QueryBuilder()
|
|
655
|
+
.select((User.name, func.count(Order.id)))
|
|
656
|
+
.join(Order, Order.user_id == User.id, "LEFT")
|
|
657
|
+
.group_by(User.name)
|
|
658
|
+
)
|
|
659
|
+
|
|
660
|
+
# Multiple joins
|
|
661
|
+
query = (
|
|
662
|
+
QueryBuilder()
|
|
663
|
+
.select((User.name, Order.id, Product.name))
|
|
664
|
+
.join(Order, Order.user_id == User.id)
|
|
665
|
+
.join(Product, Product.id == Order.product_id)
|
|
666
|
+
)
|
|
667
|
+
```
|
|
668
|
+
|
|
669
|
+
:param table: The table to join with
|
|
670
|
+
:param on: The join condition (e.g., Table1.id == Table2.table1_id)
|
|
671
|
+
:param join_type: The type of join: "INNER", "LEFT", "RIGHT", or "FULL"
|
|
672
|
+
:return: The QueryBuilder instance for method chaining
|
|
673
|
+
|
|
674
|
+
"""
|
|
675
|
+
if not is_comparison(on):
|
|
676
|
+
raise ValueError(
|
|
677
|
+
f"Invalid join condition: {on}, should be MyTable.column == OtherTable.column"
|
|
678
|
+
)
|
|
679
|
+
|
|
680
|
+
# Let the comparison update to handle its current usage in a join
|
|
681
|
+
on_join = on.force_join_constraints()
|
|
682
|
+
|
|
683
|
+
on_left, _ = on_join.left.to_query()
|
|
684
|
+
comparison = QueryLiteral(on_join.comparison.value)
|
|
685
|
+
on_right, _ = on_join.right.to_query()
|
|
686
|
+
|
|
687
|
+
join_sql = f"{join_type} JOIN {sql(table)} ON {on_left} {comparison} {on_right}"
|
|
688
|
+
self._join_clauses.append(join_sql)
|
|
689
|
+
return self
|
|
690
|
+
|
|
691
|
+
@allow_branching
|
|
692
|
+
def set(self, column: T, value: T | None):
|
|
693
|
+
"""
|
|
694
|
+
Sets a column to a specific value in an update query.
|
|
695
|
+
|
|
696
|
+
"""
|
|
697
|
+
if not is_column(column):
|
|
698
|
+
raise ValueError(f"Invalid column for set: {column}")
|
|
699
|
+
|
|
700
|
+
self._update_values.append((column, value))
|
|
701
|
+
return self
|
|
702
|
+
|
|
703
|
+
@allow_branching
|
|
704
|
+
def limit(self, value: int):
|
|
705
|
+
"""
|
|
706
|
+
Limits the number of rows returned by the query.
|
|
707
|
+
|
|
708
|
+
```python {{sticky: True}}
|
|
709
|
+
# Basic limit
|
|
710
|
+
query = (
|
|
711
|
+
QueryBuilder()
|
|
712
|
+
.select(User)
|
|
713
|
+
.limit(10)
|
|
714
|
+
)
|
|
715
|
+
|
|
716
|
+
# Limit with offset for pagination
|
|
717
|
+
query = (
|
|
718
|
+
QueryBuilder()
|
|
719
|
+
.select(User)
|
|
720
|
+
.order_by(User.created_at, "DESC")
|
|
721
|
+
.limit(20)
|
|
722
|
+
.offset(40) # Skip first 40 rows
|
|
723
|
+
)
|
|
724
|
+
```
|
|
725
|
+
|
|
726
|
+
:param value: Maximum number of rows to return
|
|
727
|
+
:return: The QueryBuilder instance for method chaining
|
|
728
|
+
|
|
729
|
+
"""
|
|
730
|
+
self._limit_value = value
|
|
731
|
+
return self
|
|
732
|
+
|
|
733
|
+
@allow_branching
|
|
734
|
+
def offset(self, value: int):
|
|
735
|
+
"""
|
|
736
|
+
Skips the specified number of rows before returning results.
|
|
737
|
+
|
|
738
|
+
```python {{sticky: True}}
|
|
739
|
+
# Basic offset
|
|
740
|
+
query = (
|
|
741
|
+
QueryBuilder()
|
|
742
|
+
.select(User)
|
|
743
|
+
.offset(10)
|
|
744
|
+
)
|
|
745
|
+
|
|
746
|
+
# Implementing pagination
|
|
747
|
+
page_size = 20
|
|
748
|
+
page_number = 3
|
|
749
|
+
query = (
|
|
750
|
+
QueryBuilder()
|
|
751
|
+
.select(User)
|
|
752
|
+
.order_by(User.created_at, "DESC")
|
|
753
|
+
.limit(page_size)
|
|
754
|
+
.offset((page_number - 1) * page_size)
|
|
755
|
+
)
|
|
756
|
+
```
|
|
757
|
+
|
|
758
|
+
:param value: Number of rows to skip
|
|
759
|
+
:return: The QueryBuilder instance for method chaining
|
|
760
|
+
|
|
761
|
+
"""
|
|
762
|
+
self._offset_value = value
|
|
763
|
+
return self
|
|
764
|
+
|
|
765
|
+
@allow_branching
|
|
766
|
+
def group_by(self, *fields: Any):
|
|
767
|
+
"""
|
|
768
|
+
Groups the results by specified fields, typically used with aggregate functions.
|
|
769
|
+
|
|
770
|
+
```python {{sticky: True}}
|
|
771
|
+
# Simple grouping with count
|
|
772
|
+
query = (
|
|
773
|
+
QueryBuilder()
|
|
774
|
+
.select((User.status, func.count(User.id)))
|
|
775
|
+
.group_by(User.status)
|
|
776
|
+
)
|
|
777
|
+
|
|
778
|
+
# Multiple group by fields
|
|
779
|
+
query = (
|
|
780
|
+
QueryBuilder()
|
|
781
|
+
.select((
|
|
782
|
+
User.country,
|
|
783
|
+
User.city,
|
|
784
|
+
func.count(User.id),
|
|
785
|
+
func.avg(User.age)
|
|
786
|
+
))
|
|
787
|
+
.group_by(User.country, User.city)
|
|
788
|
+
)
|
|
789
|
+
|
|
790
|
+
# Group by with having
|
|
791
|
+
query = (
|
|
792
|
+
QueryBuilder()
|
|
793
|
+
.select((User.department, func.count(User.id)))
|
|
794
|
+
.group_by(User.department)
|
|
795
|
+
.having(func.count(User.id) > 5)
|
|
796
|
+
)
|
|
797
|
+
```
|
|
798
|
+
|
|
799
|
+
:param fields: One or more fields to group by
|
|
800
|
+
:return: The QueryBuilder instance for method chaining
|
|
801
|
+
|
|
802
|
+
"""
|
|
803
|
+
|
|
804
|
+
for field in fields:
|
|
805
|
+
if is_column(field):
|
|
806
|
+
field_token, _ = field.to_query()
|
|
807
|
+
elif is_function_metadata(field):
|
|
808
|
+
field_token = field.literal
|
|
809
|
+
else:
|
|
810
|
+
raise ValueError(f"Invalid group by field: {field}")
|
|
811
|
+
|
|
812
|
+
self._group_by_clauses.append(str(field_token))
|
|
813
|
+
|
|
814
|
+
return self
|
|
815
|
+
|
|
816
|
+
@allow_branching
|
|
817
|
+
def having(self, *conditions: bool):
|
|
818
|
+
"""
|
|
819
|
+
Adds HAVING conditions to filter grouped results based on aggregate values.
|
|
820
|
+
|
|
821
|
+
```python {{sticky: True}}
|
|
822
|
+
# Filter groups by count
|
|
823
|
+
query = (
|
|
824
|
+
QueryBuilder()
|
|
825
|
+
.select((User.department, func.count(User.id)))
|
|
826
|
+
.group_by(User.department)
|
|
827
|
+
.having(func.count(User.id) > 10)
|
|
828
|
+
)
|
|
829
|
+
|
|
830
|
+
# Multiple having conditions
|
|
831
|
+
query = (
|
|
832
|
+
QueryBuilder()
|
|
833
|
+
.select((
|
|
834
|
+
User.department,
|
|
835
|
+
func.count(User.id),
|
|
836
|
+
func.avg(User.salary)
|
|
837
|
+
))
|
|
838
|
+
.group_by(User.department)
|
|
839
|
+
.having(
|
|
840
|
+
func.count(User.id) >= 5,
|
|
841
|
+
func.avg(User.salary) > 50000
|
|
842
|
+
)
|
|
843
|
+
)
|
|
844
|
+
```
|
|
845
|
+
|
|
846
|
+
:param conditions: One or more conditions using aggregate functions
|
|
847
|
+
:return: The QueryBuilder instance for method chaining
|
|
848
|
+
|
|
849
|
+
"""
|
|
850
|
+
for condition in conditions:
|
|
851
|
+
if not is_comparison(condition):
|
|
852
|
+
raise ValueError(f"Invalid having condition: {condition}")
|
|
853
|
+
self._having_conditions.append(condition)
|
|
854
|
+
|
|
855
|
+
return self
|
|
856
|
+
|
|
857
|
+
@allow_branching
|
|
858
|
+
def distinct_on(self, *fields: Any):
|
|
859
|
+
"""
|
|
860
|
+
Adds a DISTINCT ON clause to remove duplicate rows based on specified fields.
|
|
861
|
+
|
|
862
|
+
```python {{sticky: True}}
|
|
863
|
+
# Get distinct user names
|
|
864
|
+
query = (
|
|
865
|
+
QueryBuilder()
|
|
866
|
+
.select((User.name, User.email))
|
|
867
|
+
.distinct_on(User.name)
|
|
868
|
+
)
|
|
869
|
+
|
|
870
|
+
# Multiple distinct fields
|
|
871
|
+
query = (
|
|
872
|
+
QueryBuilder()
|
|
873
|
+
.select((User.country, User.city, User.population))
|
|
874
|
+
.distinct_on(User.country, User.city)
|
|
875
|
+
)
|
|
876
|
+
```
|
|
877
|
+
|
|
878
|
+
:param fields: Fields to check for distinctness
|
|
879
|
+
:return: The QueryBuilder instance for method chaining
|
|
880
|
+
|
|
881
|
+
"""
|
|
882
|
+
for field in fields:
|
|
883
|
+
if not is_column(field):
|
|
884
|
+
raise ValueError(f"Invalid field for group by: {field}")
|
|
885
|
+
self._distinct_on_fields.append(sql(field))
|
|
886
|
+
|
|
887
|
+
return self
|
|
888
|
+
|
|
889
|
+
@allow_branching
|
|
890
|
+
def text(self, query: str, *variables: Any):
|
|
891
|
+
"""
|
|
892
|
+
Uses a raw SQL query instead of the query builder.
|
|
893
|
+
|
|
894
|
+
```python {{sticky: True}}
|
|
895
|
+
# Simple raw query
|
|
896
|
+
query = (
|
|
897
|
+
QueryBuilder()
|
|
898
|
+
.text("SELECT * FROM users WHERE age > $1", 18)
|
|
899
|
+
)
|
|
900
|
+
|
|
901
|
+
# Complex raw query with multiple parameters
|
|
902
|
+
query = (
|
|
903
|
+
QueryBuilder()
|
|
904
|
+
.text(
|
|
905
|
+
'''
|
|
906
|
+
SELECT u.name, COUNT(o.id) as order_count
|
|
907
|
+
FROM users u
|
|
908
|
+
LEFT JOIN orders o ON o.user_id = u.id
|
|
909
|
+
WHERE u.created_at > $1
|
|
910
|
+
GROUP BY u.name
|
|
911
|
+
HAVING COUNT(o.id) > $2
|
|
912
|
+
''',
|
|
913
|
+
datetime(2023, 1, 1),
|
|
914
|
+
5
|
|
915
|
+
)
|
|
916
|
+
)
|
|
917
|
+
```
|
|
918
|
+
|
|
919
|
+
:param query: Raw SQL query string with $1, $2, etc. as parameter placeholders
|
|
920
|
+
:param variables: Values for the query parameters
|
|
921
|
+
:return: The QueryBuilder instance for method chaining
|
|
922
|
+
|
|
923
|
+
"""
|
|
924
|
+
self._text_query = query
|
|
925
|
+
self._text_variables = list(variables)
|
|
926
|
+
return self
|
|
927
|
+
|
|
928
|
+
@allow_branching
|
|
929
|
+
def for_update(
|
|
930
|
+
self,
|
|
931
|
+
*,
|
|
932
|
+
nowait: bool = False,
|
|
933
|
+
skip_locked: bool = False,
|
|
934
|
+
of: tuple[Type[TableBase], ...] | None = None,
|
|
935
|
+
) -> QueryBuilder[P, QueryType]:
|
|
936
|
+
"""
|
|
937
|
+
Adds FOR UPDATE clause to the query. This is useful for pessimistic locking.
|
|
938
|
+
Multiple calls will be combined, with the most restrictive options taking precedence.
|
|
939
|
+
|
|
940
|
+
:param nowait: If True, adds NOWAIT option
|
|
941
|
+
:param skip_locked: If True, adds SKIP LOCKED option
|
|
942
|
+
:param of: Optional tuple of models to lock specific tables
|
|
943
|
+
:return: QueryBuilder instance
|
|
944
|
+
"""
|
|
945
|
+
# Combine options, with True taking precedence for flags
|
|
946
|
+
self._for_update_config.nowait |= nowait
|
|
947
|
+
self._for_update_config.skip_locked |= skip_locked
|
|
948
|
+
self._for_update_config.of_tables |= {sql(model) for model in (of or [])}
|
|
949
|
+
|
|
950
|
+
self._for_update_config.conditions_set = True
|
|
951
|
+
return self
|
|
952
|
+
|
|
953
|
+
def build(self) -> tuple[str, list[Any]]:
|
|
954
|
+
"""
|
|
955
|
+
Builds and returns the final SQL query string and parameter values.
|
|
956
|
+
|
|
957
|
+
```python {{sticky: True}}
|
|
958
|
+
# Build a query
|
|
959
|
+
query = (
|
|
960
|
+
QueryBuilder()
|
|
961
|
+
.select(User)
|
|
962
|
+
.where(User.age > 18)
|
|
963
|
+
)
|
|
964
|
+
sql, params = query.build()
|
|
965
|
+
print(sql) # SELECT ... FROM users WHERE age > $1
|
|
966
|
+
print(params) # [18]
|
|
967
|
+
|
|
968
|
+
# Execute the built query
|
|
969
|
+
async with conn.transaction():
|
|
970
|
+
result = await conn.execute(*query.build())
|
|
971
|
+
```
|
|
972
|
+
|
|
973
|
+
:return: A tuple of (query_string, parameter_list)
|
|
974
|
+
|
|
975
|
+
"""
|
|
976
|
+
if self._text_query:
|
|
977
|
+
return self._text_query, self._text_variables
|
|
978
|
+
|
|
979
|
+
query = ""
|
|
980
|
+
variables: list[Any] = []
|
|
981
|
+
|
|
982
|
+
if self._query_type == "SELECT":
|
|
983
|
+
if not self._main_model:
|
|
984
|
+
raise ValueError("No model selected for query")
|
|
985
|
+
|
|
986
|
+
fields = [str(field) for field in self._select_fields]
|
|
987
|
+
query = "SELECT"
|
|
988
|
+
|
|
989
|
+
if self._distinct_on_fields:
|
|
990
|
+
distinct_fields = [
|
|
991
|
+
str(distinct_field) for distinct_field in self._distinct_on_fields
|
|
992
|
+
]
|
|
993
|
+
query += f" DISTINCT ON ({', '.join(distinct_fields)})"
|
|
994
|
+
|
|
995
|
+
query += f" {', '.join(fields)} FROM {sql(self._main_model)}"
|
|
996
|
+
elif self._query_type == "UPDATE":
|
|
997
|
+
if not self._main_model:
|
|
998
|
+
raise ValueError("No model selected for query")
|
|
999
|
+
|
|
1000
|
+
set_components = []
|
|
1001
|
+
for column, value in self._update_values:
|
|
1002
|
+
# Unlike in SELECT commands, we can't specify the table name attached
|
|
1003
|
+
# to columns, since they all need to be tied to the same table.
|
|
1004
|
+
set_components.append(f"{column.key} = ${len(variables) + 1}")
|
|
1005
|
+
variables.append(value)
|
|
1006
|
+
|
|
1007
|
+
set_clause = ", ".join(set_components)
|
|
1008
|
+
query = f"UPDATE {sql(self._main_model)} SET {set_clause}"
|
|
1009
|
+
elif self._query_type == "DELETE":
|
|
1010
|
+
if not self._main_model:
|
|
1011
|
+
raise ValueError("No model selected for query")
|
|
1012
|
+
|
|
1013
|
+
query = f"DELETE FROM {sql(self._main_model)}"
|
|
1014
|
+
|
|
1015
|
+
if self._join_clauses:
|
|
1016
|
+
query += " " + " ".join(self._join_clauses)
|
|
1017
|
+
|
|
1018
|
+
if self._where_conditions:
|
|
1019
|
+
comparison_group = cast(FieldComparisonGroup, and_(*self._where_conditions)) # type: ignore
|
|
1020
|
+
comparison_literal, comparison_variables = comparison_group.to_query(
|
|
1021
|
+
len(variables) + 1
|
|
1022
|
+
)
|
|
1023
|
+
query += f" WHERE {comparison_literal}"
|
|
1024
|
+
variables += comparison_variables
|
|
1025
|
+
|
|
1026
|
+
if self._group_by_clauses:
|
|
1027
|
+
query += " GROUP BY "
|
|
1028
|
+
query += ", ".join(str(field) for field in self._group_by_clauses)
|
|
1029
|
+
|
|
1030
|
+
if self._having_conditions:
|
|
1031
|
+
query += " HAVING "
|
|
1032
|
+
for i, having_condition in enumerate(self._having_conditions):
|
|
1033
|
+
if i > 0:
|
|
1034
|
+
query += " AND "
|
|
1035
|
+
|
|
1036
|
+
having_field = having_condition.left.literal
|
|
1037
|
+
having_value: QueryElementBase
|
|
1038
|
+
if is_function_metadata(having_condition.right):
|
|
1039
|
+
having_value = having_condition.right.literal
|
|
1040
|
+
else:
|
|
1041
|
+
variables.append(having_condition.right)
|
|
1042
|
+
having_value = QueryLiteral("$" + str(len(variables)))
|
|
1043
|
+
|
|
1044
|
+
query += (
|
|
1045
|
+
f"{having_field} {having_condition.comparison.value} {having_value}"
|
|
1046
|
+
)
|
|
1047
|
+
|
|
1048
|
+
if self._order_by_clauses:
|
|
1049
|
+
query += " ORDER BY " + ", ".join(self._order_by_clauses)
|
|
1050
|
+
|
|
1051
|
+
if self._limit_value is not None:
|
|
1052
|
+
query += f" LIMIT {self._limit_value}"
|
|
1053
|
+
|
|
1054
|
+
if self._offset_value is not None:
|
|
1055
|
+
query += f" OFFSET {self._offset_value}"
|
|
1056
|
+
|
|
1057
|
+
if self._for_update_config.conditions_set:
|
|
1058
|
+
query += " FOR UPDATE"
|
|
1059
|
+
if self._for_update_config.of_tables:
|
|
1060
|
+
# Sorting is optional for the query itself but used for test consistency
|
|
1061
|
+
query += f" OF {', '.join([str(table) for table in sorted(self._for_update_config.of_tables)])}"
|
|
1062
|
+
if self._for_update_config.nowait:
|
|
1063
|
+
query += " NOWAIT"
|
|
1064
|
+
elif self._for_update_config.skip_locked:
|
|
1065
|
+
query += " SKIP LOCKED"
|
|
1066
|
+
|
|
1067
|
+
return query, variables
|
|
1068
|
+
|
|
1069
|
+
|
|
1070
|
+
#
|
|
1071
|
+
# Comparison chaining
|
|
1072
|
+
#
|
|
1073
|
+
|
|
1074
|
+
|
|
1075
|
+
def and_(
|
|
1076
|
+
*conditions: bool,
|
|
1077
|
+
) -> bool:
|
|
1078
|
+
"""
|
|
1079
|
+
Combines multiple conditions with logical AND.
|
|
1080
|
+
All conditions must be true for the group to be true.
|
|
1081
|
+
|
|
1082
|
+
```python {{sticky: True}}
|
|
1083
|
+
query = select(User).where(
|
|
1084
|
+
and_(
|
|
1085
|
+
User.age >= 21,
|
|
1086
|
+
User.status == "active",
|
|
1087
|
+
User.role == "member"
|
|
1088
|
+
)
|
|
1089
|
+
)
|
|
1090
|
+
```
|
|
1091
|
+
|
|
1092
|
+
:param conditions: Variable number of conditions to combine
|
|
1093
|
+
:return: A field comparison group object
|
|
1094
|
+
|
|
1095
|
+
"""
|
|
1096
|
+
field_comparisons: list[FieldComparison | FieldComparisonGroup] = []
|
|
1097
|
+
for condition in conditions:
|
|
1098
|
+
if not is_comparison(condition) and not is_comparison_group(condition):
|
|
1099
|
+
raise ValueError(f"Invalid having condition: {condition}")
|
|
1100
|
+
field_comparisons.append(condition)
|
|
1101
|
+
return cast(
|
|
1102
|
+
bool,
|
|
1103
|
+
FieldComparisonGroup(type=ComparisonGroupType.AND, elements=field_comparisons),
|
|
1104
|
+
)
|
|
1105
|
+
|
|
1106
|
+
|
|
1107
|
+
def or_(
|
|
1108
|
+
*conditions: bool,
|
|
1109
|
+
) -> bool:
|
|
1110
|
+
"""
|
|
1111
|
+
Combines multiple conditions with logical OR.
|
|
1112
|
+
At least one condition must be true for the group to be true.
|
|
1113
|
+
|
|
1114
|
+
```python {{sticky: True}}
|
|
1115
|
+
query = select(User).where(
|
|
1116
|
+
or_(
|
|
1117
|
+
User.role == "admin",
|
|
1118
|
+
and_(
|
|
1119
|
+
User.role == "moderator",
|
|
1120
|
+
User.permissions.contains("manage_users")
|
|
1121
|
+
)
|
|
1122
|
+
)
|
|
1123
|
+
)
|
|
1124
|
+
```
|
|
1125
|
+
|
|
1126
|
+
:param conditions: Variable number of conditions to combine
|
|
1127
|
+
:return: A field comparison group object
|
|
1128
|
+
|
|
1129
|
+
"""
|
|
1130
|
+
field_comparisons: list[FieldComparison | FieldComparisonGroup] = []
|
|
1131
|
+
for condition in conditions:
|
|
1132
|
+
if not is_comparison(condition) and not is_comparison_group(condition):
|
|
1133
|
+
raise ValueError(f"Invalid having condition: {condition}")
|
|
1134
|
+
field_comparisons.append(condition)
|
|
1135
|
+
return cast(
|
|
1136
|
+
bool,
|
|
1137
|
+
FieldComparisonGroup(type=ComparisonGroupType.OR, elements=field_comparisons),
|
|
1138
|
+
)
|
|
1139
|
+
|
|
1140
|
+
|
|
1141
|
+
#
|
|
1142
|
+
# Shortcut entrypoints
|
|
1143
|
+
# Instead of having to manually create a QueryBuilder object, these functions
|
|
1144
|
+
# will create one for you and return it.
|
|
1145
|
+
#
|
|
1146
|
+
|
|
1147
|
+
|
|
1148
|
+
@overload
|
|
1149
|
+
def select(fields: T | Type[T]) -> QueryBuilder[T, Literal["SELECT"]]: ...
|
|
1150
|
+
|
|
1151
|
+
|
|
1152
|
+
@overload
|
|
1153
|
+
def select(
|
|
1154
|
+
fields: tuple[T | Type[T]],
|
|
1155
|
+
) -> QueryBuilder[tuple[T], Literal["SELECT"]]: ...
|
|
1156
|
+
|
|
1157
|
+
|
|
1158
|
+
@overload
|
|
1159
|
+
def select(
|
|
1160
|
+
fields: tuple[T | Type[T], T2 | Type[T2]],
|
|
1161
|
+
) -> QueryBuilder[tuple[T, T2], Literal["SELECT"]]: ...
|
|
1162
|
+
|
|
1163
|
+
|
|
1164
|
+
@overload
|
|
1165
|
+
def select(
|
|
1166
|
+
fields: tuple[T | Type[T], T2 | Type[T2], T3 | Type[T3]],
|
|
1167
|
+
) -> QueryBuilder[tuple[T, T2, T3], Literal["SELECT"]]: ...
|
|
1168
|
+
|
|
1169
|
+
|
|
1170
|
+
@overload
|
|
1171
|
+
def select(
|
|
1172
|
+
fields: tuple[T | Type[T], T2 | Type[T2], T3 | Type[T3], T4 | Type[T4]],
|
|
1173
|
+
) -> QueryBuilder[tuple[T, T2, T3, T4], Literal["SELECT"]]: ...
|
|
1174
|
+
|
|
1175
|
+
|
|
1176
|
+
@overload
|
|
1177
|
+
def select(
|
|
1178
|
+
fields: tuple[
|
|
1179
|
+
T | Type[T], T2 | Type[T2], T3 | Type[T3], T4 | Type[T4], T5 | Type[T5]
|
|
1180
|
+
],
|
|
1181
|
+
) -> QueryBuilder[tuple[T, T2, T3, T4, T5], Literal["SELECT"]]: ...
|
|
1182
|
+
|
|
1183
|
+
|
|
1184
|
+
@overload
|
|
1185
|
+
def select(
|
|
1186
|
+
fields: tuple[
|
|
1187
|
+
T | Type[T],
|
|
1188
|
+
T2 | Type[T2],
|
|
1189
|
+
T3 | Type[T3],
|
|
1190
|
+
T4 | Type[T4],
|
|
1191
|
+
T5 | Type[T5],
|
|
1192
|
+
T6 | Type[T6],
|
|
1193
|
+
],
|
|
1194
|
+
) -> QueryBuilder[tuple[T, T2, T3, T4, T5, T6], Literal["SELECT"]]: ...
|
|
1195
|
+
|
|
1196
|
+
|
|
1197
|
+
@overload
|
|
1198
|
+
def select(
|
|
1199
|
+
fields: tuple[
|
|
1200
|
+
T | Type[T],
|
|
1201
|
+
T2 | Type[T2],
|
|
1202
|
+
T3 | Type[T3],
|
|
1203
|
+
T4 | Type[T4],
|
|
1204
|
+
T5 | Type[T5],
|
|
1205
|
+
T6 | Type[T6],
|
|
1206
|
+
T7 | Type[T7],
|
|
1207
|
+
],
|
|
1208
|
+
) -> QueryBuilder[tuple[T, T2, T3, T4, T5, T6, T7], Literal["SELECT"]]: ...
|
|
1209
|
+
|
|
1210
|
+
|
|
1211
|
+
@overload
|
|
1212
|
+
def select(
|
|
1213
|
+
fields: tuple[
|
|
1214
|
+
T | Type[T],
|
|
1215
|
+
T2 | Type[T2],
|
|
1216
|
+
T3 | Type[T3],
|
|
1217
|
+
T4 | Type[T4],
|
|
1218
|
+
T5 | Type[T5],
|
|
1219
|
+
T6 | Type[T6],
|
|
1220
|
+
T7 | Type[T7],
|
|
1221
|
+
T8 | Type[T8],
|
|
1222
|
+
],
|
|
1223
|
+
) -> QueryBuilder[tuple[T, T2, T3, T4, T5, T6, T7, T8], Literal["SELECT"]]: ...
|
|
1224
|
+
|
|
1225
|
+
|
|
1226
|
+
@overload
|
|
1227
|
+
def select(
|
|
1228
|
+
fields: tuple[
|
|
1229
|
+
T | Type[T],
|
|
1230
|
+
T2 | Type[T2],
|
|
1231
|
+
T3 | Type[T3],
|
|
1232
|
+
T4 | Type[T4],
|
|
1233
|
+
T5 | Type[T5],
|
|
1234
|
+
T6 | Type[T6],
|
|
1235
|
+
T7 | Type[T7],
|
|
1236
|
+
T8 | Type[T8],
|
|
1237
|
+
T9 | Type[T9],
|
|
1238
|
+
],
|
|
1239
|
+
) -> QueryBuilder[tuple[T, T2, T3, T4, T5, T6, T7, T8, T9], Literal["SELECT"]]: ...
|
|
1240
|
+
|
|
1241
|
+
|
|
1242
|
+
@overload
|
|
1243
|
+
def select(
|
|
1244
|
+
fields: tuple[
|
|
1245
|
+
T | Type[T],
|
|
1246
|
+
T2 | Type[T2],
|
|
1247
|
+
T3 | Type[T3],
|
|
1248
|
+
T4 | Type[T4],
|
|
1249
|
+
T5 | Type[T5],
|
|
1250
|
+
T6 | Type[T6],
|
|
1251
|
+
T7 | Type[T7],
|
|
1252
|
+
T8 | Type[T8],
|
|
1253
|
+
T9 | Type[T9],
|
|
1254
|
+
T10 | Type[T10],
|
|
1255
|
+
],
|
|
1256
|
+
) -> QueryBuilder[tuple[T, T2, T3, T4, T5, T6, T7, T8, T9, T10], Literal["SELECT"]]: ...
|
|
1257
|
+
|
|
1258
|
+
|
|
1259
|
+
@overload
|
|
1260
|
+
def select(
|
|
1261
|
+
fields: tuple[
|
|
1262
|
+
T | Type[T],
|
|
1263
|
+
T2 | Type[T2],
|
|
1264
|
+
T3 | Type[T3],
|
|
1265
|
+
T4 | Type[T4],
|
|
1266
|
+
T5 | Type[T5],
|
|
1267
|
+
T6 | Type[T6],
|
|
1268
|
+
T7 | Type[T7],
|
|
1269
|
+
T8 | Type[T8],
|
|
1270
|
+
T9 | Type[T9],
|
|
1271
|
+
T10 | Type[T10],
|
|
1272
|
+
*Ts,
|
|
1273
|
+
],
|
|
1274
|
+
) -> QueryBuilder[
|
|
1275
|
+
tuple[T, T2, T3, T4, T5, T6, T7, T8, T9, T10, *Ts], Literal["SELECT"]
|
|
1276
|
+
]: ...
|
|
1277
|
+
|
|
1278
|
+
|
|
1279
|
+
def select(
|
|
1280
|
+
fields: (
|
|
1281
|
+
T
|
|
1282
|
+
| Type[T]
|
|
1283
|
+
| tuple[T | Type[T]]
|
|
1284
|
+
| tuple[T | Type[T], T2 | Type[T2]]
|
|
1285
|
+
| tuple[T | Type[T], T2 | Type[T2], T3 | Type[T3]]
|
|
1286
|
+
| tuple[T | Type[T], T2 | Type[T2], T3 | Type[T3], T4 | Type[T4]]
|
|
1287
|
+
| tuple[T | Type[T], T2 | Type[T2], T3 | Type[T3], T4 | Type[T4], T5 | Type[T5]]
|
|
1288
|
+
| tuple[
|
|
1289
|
+
T | Type[T],
|
|
1290
|
+
T2 | Type[T2],
|
|
1291
|
+
T3 | Type[T3],
|
|
1292
|
+
T4 | Type[T4],
|
|
1293
|
+
T5 | Type[T5],
|
|
1294
|
+
T6 | Type[T6],
|
|
1295
|
+
]
|
|
1296
|
+
| tuple[
|
|
1297
|
+
T | Type[T],
|
|
1298
|
+
T2 | Type[T2],
|
|
1299
|
+
T3 | Type[T3],
|
|
1300
|
+
T4 | Type[T4],
|
|
1301
|
+
T5 | Type[T5],
|
|
1302
|
+
T6 | Type[T6],
|
|
1303
|
+
T7 | Type[T7],
|
|
1304
|
+
]
|
|
1305
|
+
| tuple[
|
|
1306
|
+
T | Type[T],
|
|
1307
|
+
T2 | Type[T2],
|
|
1308
|
+
T3 | Type[T3],
|
|
1309
|
+
T4 | Type[T4],
|
|
1310
|
+
T5 | Type[T5],
|
|
1311
|
+
T6 | Type[T6],
|
|
1312
|
+
T7 | Type[T7],
|
|
1313
|
+
T8 | Type[T8],
|
|
1314
|
+
]
|
|
1315
|
+
| tuple[
|
|
1316
|
+
T | Type[T],
|
|
1317
|
+
T2 | Type[T2],
|
|
1318
|
+
T3 | Type[T3],
|
|
1319
|
+
T4 | Type[T4],
|
|
1320
|
+
T5 | Type[T5],
|
|
1321
|
+
T6 | Type[T6],
|
|
1322
|
+
T7 | Type[T7],
|
|
1323
|
+
T8 | Type[T8],
|
|
1324
|
+
T9 | Type[T9],
|
|
1325
|
+
]
|
|
1326
|
+
| tuple[
|
|
1327
|
+
T | Type[T],
|
|
1328
|
+
T2 | Type[T2],
|
|
1329
|
+
T3 | Type[T3],
|
|
1330
|
+
T4 | Type[T4],
|
|
1331
|
+
T5 | Type[T5],
|
|
1332
|
+
T6 | Type[T6],
|
|
1333
|
+
T7 | Type[T7],
|
|
1334
|
+
T8 | Type[T8],
|
|
1335
|
+
T9 | Type[T9],
|
|
1336
|
+
T10 | Type[T10],
|
|
1337
|
+
]
|
|
1338
|
+
| tuple[
|
|
1339
|
+
T | Type[T],
|
|
1340
|
+
T2 | Type[T2],
|
|
1341
|
+
T3 | Type[T3],
|
|
1342
|
+
T4 | Type[T4],
|
|
1343
|
+
T5 | Type[T5],
|
|
1344
|
+
T6 | Type[T6],
|
|
1345
|
+
T7 | Type[T7],
|
|
1346
|
+
T8 | Type[T8],
|
|
1347
|
+
T9 | Type[T9],
|
|
1348
|
+
T10 | Type[T10],
|
|
1349
|
+
*Ts,
|
|
1350
|
+
]
|
|
1351
|
+
),
|
|
1352
|
+
) -> (
|
|
1353
|
+
QueryBuilder[T, Literal["SELECT"]]
|
|
1354
|
+
| QueryBuilder[tuple[T], Literal["SELECT"]]
|
|
1355
|
+
| QueryBuilder[tuple[T, T2], Literal["SELECT"]]
|
|
1356
|
+
| QueryBuilder[tuple[T, T2, T3], Literal["SELECT"]]
|
|
1357
|
+
| QueryBuilder[tuple[T, T2, T3, T4], Literal["SELECT"]]
|
|
1358
|
+
| QueryBuilder[tuple[T, T2, T3, T4, T5], Literal["SELECT"]]
|
|
1359
|
+
| QueryBuilder[tuple[T, T2, T3, T4, T5, T6], Literal["SELECT"]]
|
|
1360
|
+
| QueryBuilder[tuple[T, T2, T3, T4, T5, T6, T7], Literal["SELECT"]]
|
|
1361
|
+
| QueryBuilder[tuple[T, T2, T3, T4, T5, T6, T7, T8], Literal["SELECT"]]
|
|
1362
|
+
| QueryBuilder[tuple[T, T2, T3, T4, T5, T6, T7, T8, T9], Literal["SELECT"]]
|
|
1363
|
+
| QueryBuilder[tuple[T, T2, T3, T4, T5, T6, T7, T8, T9, T10], Literal["SELECT"]]
|
|
1364
|
+
| QueryBuilder[
|
|
1365
|
+
tuple[T, T2, T3, T4, T5, T6, T7, T8, T9, T10, *Ts], Literal["SELECT"]
|
|
1366
|
+
]
|
|
1367
|
+
):
|
|
1368
|
+
"""
|
|
1369
|
+
Creates a SELECT query to fetch data from the database. This is a shortcut function that creates
|
|
1370
|
+
and returns a new QueryBuilder instance.
|
|
1371
|
+
|
|
1372
|
+
```python {{sticky: True}}
|
|
1373
|
+
# Select all fields from User
|
|
1374
|
+
users = await conn.execute(select(User))
|
|
1375
|
+
|
|
1376
|
+
# Select specific fields
|
|
1377
|
+
results = await conn.execute(select((User.id, User.name)))
|
|
1378
|
+
|
|
1379
|
+
# Select with conditions
|
|
1380
|
+
active_users = await conn.execute(
|
|
1381
|
+
select(User)
|
|
1382
|
+
.where(User.is_active == True)
|
|
1383
|
+
.order_by(User.created_at, "DESC")
|
|
1384
|
+
.limit(10)
|
|
1385
|
+
)
|
|
1386
|
+
```
|
|
1387
|
+
|
|
1388
|
+
:param fields: The fields to select. Can be:
|
|
1389
|
+
- A single field or model class (e.g., User.id or User)
|
|
1390
|
+
- A tuple of fields (e.g., (User.id, User.name))
|
|
1391
|
+
- A tuple of model classes (e.g., (User, Post))
|
|
1392
|
+
:return: A QueryBuilder instance configured for SELECT operations
|
|
1393
|
+
|
|
1394
|
+
"""
|
|
1395
|
+
return QueryBuilder().select(fields)
|
|
1396
|
+
|
|
1397
|
+
|
|
1398
|
+
def update(model: Type[TableBase]) -> QueryBuilder[None, Literal["UPDATE"]]:
|
|
1399
|
+
"""
|
|
1400
|
+
Creates an UPDATE query to modify existing records in the database. This is a shortcut function
|
|
1401
|
+
that creates and returns a new QueryBuilder instance.
|
|
1402
|
+
|
|
1403
|
+
```python {{sticky: True}}
|
|
1404
|
+
# Update all users' status
|
|
1405
|
+
await conn.execute(
|
|
1406
|
+
update(User)
|
|
1407
|
+
.set(User.status, "inactive")
|
|
1408
|
+
.where(User.last_login < datetime.now() - timedelta(days=30))
|
|
1409
|
+
)
|
|
1410
|
+
|
|
1411
|
+
# Update multiple fields with conditions
|
|
1412
|
+
await conn.execute(
|
|
1413
|
+
update(User)
|
|
1414
|
+
.set(User.verified, True)
|
|
1415
|
+
.set(User.verification_date, datetime.now())
|
|
1416
|
+
.where(User.email_confirmed == True)
|
|
1417
|
+
)
|
|
1418
|
+
```
|
|
1419
|
+
|
|
1420
|
+
:param model: The model class representing the table to update
|
|
1421
|
+
:return: A QueryBuilder instance configured for UPDATE operations
|
|
1422
|
+
|
|
1423
|
+
"""
|
|
1424
|
+
return QueryBuilder().update(model)
|
|
1425
|
+
|
|
1426
|
+
|
|
1427
|
+
def delete(model: Type[TableBase]) -> QueryBuilder[None, Literal["DELETE"]]:
|
|
1428
|
+
"""
|
|
1429
|
+
Creates a DELETE query to remove records from the database. This is a shortcut function
|
|
1430
|
+
that creates and returns a new QueryBuilder instance.
|
|
1431
|
+
|
|
1432
|
+
```python {{sticky: True}}
|
|
1433
|
+
# Delete inactive users
|
|
1434
|
+
await conn.execute(
|
|
1435
|
+
delete(User)
|
|
1436
|
+
.where(User.is_active == False)
|
|
1437
|
+
)
|
|
1438
|
+
|
|
1439
|
+
# Delete with complex conditions
|
|
1440
|
+
await conn.execute(
|
|
1441
|
+
delete(User)
|
|
1442
|
+
.where(
|
|
1443
|
+
and_(
|
|
1444
|
+
User.created_at < datetime.now() - timedelta(days=90),
|
|
1445
|
+
User.email_confirmed == False
|
|
1446
|
+
)
|
|
1447
|
+
)
|
|
1448
|
+
)
|
|
1449
|
+
```
|
|
1450
|
+
|
|
1451
|
+
:param model: The model class representing the table to delete from
|
|
1452
|
+
:return: A QueryBuilder instance configured for DELETE operations
|
|
1453
|
+
|
|
1454
|
+
"""
|
|
1455
|
+
return QueryBuilder().delete(model)
|