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