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.

Files changed (75) hide show
  1. iceaxe/__init__.py +20 -0
  2. iceaxe/__tests__/__init__.py +0 -0
  3. iceaxe/__tests__/benchmarks/__init__.py +0 -0
  4. iceaxe/__tests__/benchmarks/test_bulk_insert.py +45 -0
  5. iceaxe/__tests__/benchmarks/test_select.py +114 -0
  6. iceaxe/__tests__/conf_models.py +133 -0
  7. iceaxe/__tests__/conftest.py +204 -0
  8. iceaxe/__tests__/docker_helpers.py +208 -0
  9. iceaxe/__tests__/helpers.py +268 -0
  10. iceaxe/__tests__/migrations/__init__.py +0 -0
  11. iceaxe/__tests__/migrations/conftest.py +36 -0
  12. iceaxe/__tests__/migrations/test_action_sorter.py +237 -0
  13. iceaxe/__tests__/migrations/test_generator.py +140 -0
  14. iceaxe/__tests__/migrations/test_generics.py +91 -0
  15. iceaxe/__tests__/mountaineer/__init__.py +0 -0
  16. iceaxe/__tests__/mountaineer/dependencies/__init__.py +0 -0
  17. iceaxe/__tests__/mountaineer/dependencies/test_core.py +76 -0
  18. iceaxe/__tests__/schemas/__init__.py +0 -0
  19. iceaxe/__tests__/schemas/test_actions.py +1264 -0
  20. iceaxe/__tests__/schemas/test_cli.py +25 -0
  21. iceaxe/__tests__/schemas/test_db_memory_serializer.py +1525 -0
  22. iceaxe/__tests__/schemas/test_db_serializer.py +398 -0
  23. iceaxe/__tests__/schemas/test_db_stubs.py +190 -0
  24. iceaxe/__tests__/test_alias.py +83 -0
  25. iceaxe/__tests__/test_base.py +52 -0
  26. iceaxe/__tests__/test_comparison.py +383 -0
  27. iceaxe/__tests__/test_field.py +11 -0
  28. iceaxe/__tests__/test_helpers.py +9 -0
  29. iceaxe/__tests__/test_modifications.py +151 -0
  30. iceaxe/__tests__/test_queries.py +605 -0
  31. iceaxe/__tests__/test_queries_str.py +173 -0
  32. iceaxe/__tests__/test_session.py +1511 -0
  33. iceaxe/__tests__/test_text_search.py +287 -0
  34. iceaxe/alias_values.py +67 -0
  35. iceaxe/base.py +350 -0
  36. iceaxe/comparison.py +560 -0
  37. iceaxe/field.py +250 -0
  38. iceaxe/functions.py +906 -0
  39. iceaxe/generics.py +140 -0
  40. iceaxe/io.py +107 -0
  41. iceaxe/logging.py +91 -0
  42. iceaxe/migrations/__init__.py +5 -0
  43. iceaxe/migrations/action_sorter.py +98 -0
  44. iceaxe/migrations/cli.py +228 -0
  45. iceaxe/migrations/client_io.py +62 -0
  46. iceaxe/migrations/generator.py +404 -0
  47. iceaxe/migrations/migration.py +86 -0
  48. iceaxe/migrations/migrator.py +101 -0
  49. iceaxe/modifications.py +176 -0
  50. iceaxe/mountaineer/__init__.py +10 -0
  51. iceaxe/mountaineer/cli.py +74 -0
  52. iceaxe/mountaineer/config.py +46 -0
  53. iceaxe/mountaineer/dependencies/__init__.py +6 -0
  54. iceaxe/mountaineer/dependencies/core.py +67 -0
  55. iceaxe/postgres.py +133 -0
  56. iceaxe/py.typed +0 -0
  57. iceaxe/queries.py +1455 -0
  58. iceaxe/queries_str.py +294 -0
  59. iceaxe/schemas/__init__.py +0 -0
  60. iceaxe/schemas/actions.py +864 -0
  61. iceaxe/schemas/cli.py +30 -0
  62. iceaxe/schemas/db_memory_serializer.py +705 -0
  63. iceaxe/schemas/db_serializer.py +346 -0
  64. iceaxe/schemas/db_stubs.py +525 -0
  65. iceaxe/session.py +860 -0
  66. iceaxe/session_optimized.c +12035 -0
  67. iceaxe/session_optimized.cpython-313-darwin.so +0 -0
  68. iceaxe/session_optimized.pyx +212 -0
  69. iceaxe/sql_types.py +148 -0
  70. iceaxe/typing.py +73 -0
  71. iceaxe-0.7.1.dist-info/METADATA +261 -0
  72. iceaxe-0.7.1.dist-info/RECORD +75 -0
  73. iceaxe-0.7.1.dist-info/WHEEL +6 -0
  74. iceaxe-0.7.1.dist-info/licenses/LICENSE +21 -0
  75. 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)