oxyde-postgres 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. oxyde/__init__.py +151 -0
  2. oxyde/_msgpack.py +3 -0
  3. oxyde/cli/__init__.py +7 -0
  4. oxyde/cli/app.py +107 -0
  5. oxyde/cli/codegen.py +66 -0
  6. oxyde/cli/config.py +84 -0
  7. oxyde/cli/migrations.py +536 -0
  8. oxyde/codegen/__init__.py +15 -0
  9. oxyde/codegen/stub_generator.py +1229 -0
  10. oxyde/core/__init__.py +90 -0
  11. oxyde/core/column_types.py +367 -0
  12. oxyde/core/ir.py +380 -0
  13. oxyde/core/types.py +69 -0
  14. oxyde/core/wrapper.py +242 -0
  15. oxyde/db/__init__.py +230 -0
  16. oxyde/db/pool.py +305 -0
  17. oxyde/db/registry.py +113 -0
  18. oxyde/db/schema.py +51 -0
  19. oxyde/db/transaction.py +392 -0
  20. oxyde/exceptions.py +89 -0
  21. oxyde/migrations/__init__.py +49 -0
  22. oxyde/migrations/config.py +187 -0
  23. oxyde/migrations/context.py +598 -0
  24. oxyde/migrations/executor.py +456 -0
  25. oxyde/migrations/extract.py +316 -0
  26. oxyde/migrations/generator.py +534 -0
  27. oxyde/migrations/replay.py +367 -0
  28. oxyde/migrations/squash.py +109 -0
  29. oxyde/migrations/tracker.py +162 -0
  30. oxyde/migrations/utils.py +145 -0
  31. oxyde/models/__init__.py +83 -0
  32. oxyde/models/base.py +871 -0
  33. oxyde/models/decorators.py +111 -0
  34. oxyde/models/field.py +204 -0
  35. oxyde/models/lookups.py +667 -0
  36. oxyde/models/metadata.py +174 -0
  37. oxyde/models/registry.py +170 -0
  38. oxyde/models/serializers.py +144 -0
  39. oxyde/models/utils.py +116 -0
  40. oxyde/py.typed +0 -0
  41. oxyde/queries/__init__.py +79 -0
  42. oxyde/queries/aggregates.py +145 -0
  43. oxyde/queries/base.py +249 -0
  44. oxyde/queries/conditions.py +92 -0
  45. oxyde/queries/expressions.py +171 -0
  46. oxyde/queries/insert.py +124 -0
  47. oxyde/queries/joins.py +66 -0
  48. oxyde/queries/manager.py +409 -0
  49. oxyde/queries/mixins/__init__.py +68 -0
  50. oxyde/queries/mixins/aggregation.py +335 -0
  51. oxyde/queries/mixins/debug.py +154 -0
  52. oxyde/queries/mixins/execution.py +701 -0
  53. oxyde/queries/mixins/filtering.py +144 -0
  54. oxyde/queries/mixins/joining.py +160 -0
  55. oxyde/queries/mixins/mutation.py +508 -0
  56. oxyde/queries/mixins/pagination.py +145 -0
  57. oxyde/queries/q.py +250 -0
  58. oxyde/queries/raw.py +114 -0
  59. oxyde/queries/select.py +345 -0
  60. oxyde/queries/typed.py +213 -0
  61. oxyde_postgres-0.1.0.dist-info/METADATA +141 -0
  62. oxyde_postgres-0.1.0.dist-info/RECORD +66 -0
  63. oxyde_postgres-0.1.0.dist-info/WHEEL +5 -0
  64. oxyde_postgres-0.1.0.dist-info/entry_points.txt +2 -0
  65. oxyde_postgres-0.1.0.dist-info/licenses/LICENSE +21 -0
  66. oxyde_postgres-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,124 @@
1
+ """INSERT query builder for single and bulk inserts (internal).
2
+
3
+ InsertQuery is used internally by QueryManager for create() and bulk_create().
4
+ Use Model.objects.create() and Model.objects.bulk_create() instead.
5
+
6
+ Example (via Manager):
7
+ user = await User.objects.create(name="Alice", age=30)
8
+ users = await User.objects.bulk_create([
9
+ User(name="Alice", age=30),
10
+ User(name="Bob", age=25),
11
+ ])
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from typing import TYPE_CHECKING, Any
17
+
18
+ from oxyde._msgpack import msgpack
19
+ from oxyde.core import ir
20
+ from oxyde.queries.base import (
21
+ SupportsExecute,
22
+ _build_column_types,
23
+ _map_values_to_columns,
24
+ _model_key,
25
+ _primary_key_meta,
26
+ )
27
+ from oxyde.queries.expressions import _serialize_value_for_ir
28
+
29
+ if TYPE_CHECKING:
30
+ from oxyde.models.base import Model
31
+
32
+
33
+ class InsertQuery:
34
+ """INSERT query builder."""
35
+
36
+ def __init__(self, model_class: type[Model]):
37
+ self.model_class = model_class
38
+ self._values: dict[str, Any] = {}
39
+ self._bulk_values: list[dict[str, Any]] | None = None
40
+
41
+ def _clone(self) -> InsertQuery:
42
+ clone = InsertQuery(self.model_class)
43
+ clone._values = dict(self._values)
44
+ clone._bulk_values = list(self._bulk_values) if self._bulk_values else None
45
+ return clone
46
+
47
+ def values(self, **kwargs: Any) -> InsertQuery:
48
+ """Set values to insert."""
49
+ clone = self._clone()
50
+ clone._values = dict(kwargs)
51
+ clone._bulk_values = None
52
+ return clone
53
+
54
+ def bulk_values(self, values: list[dict[str, Any]]) -> InsertQuery:
55
+ """Set bulk values for batch insert."""
56
+ clone = self._clone()
57
+ clone._bulk_values = list(values)
58
+ clone._values = {}
59
+ return clone
60
+
61
+ def _get_pk_column(self) -> str | None:
62
+ """Get the database column name for the primary key field."""
63
+ try:
64
+ pk_meta = _primary_key_meta(self.model_class)
65
+ return pk_meta.db_column
66
+ except Exception:
67
+ return None
68
+
69
+ def to_ir(self) -> dict[str, Any]:
70
+ """Convert to IR format."""
71
+ table_name = self.model_class.get_table_name()
72
+ pk_column = self._get_pk_column()
73
+ column_types = _build_column_types(self.model_class)
74
+
75
+ if self._bulk_values is not None:
76
+ if not self._bulk_values:
77
+ raise ValueError("Bulk INSERT requires at least one row")
78
+
79
+ # Serialize all rows
80
+ serialized_bulk = []
81
+ for row_values in self._bulk_values:
82
+ mapped = _map_values_to_columns(self.model_class, row_values)
83
+ serialized = {
84
+ key: _serialize_value_for_ir(value) for key, value in mapped.items()
85
+ }
86
+ serialized_bulk.append(serialized)
87
+
88
+ # Bulk insert: only return PKs for efficiency
89
+ return ir.build_insert_ir(
90
+ table=table_name,
91
+ bulk_values=serialized_bulk,
92
+ column_types=column_types,
93
+ model=_model_key(self.model_class),
94
+ pk_column=pk_column,
95
+ )
96
+ else:
97
+ if not self._values:
98
+ raise ValueError("INSERT query requires at least one column/value pair")
99
+
100
+ mapped_values = _map_values_to_columns(self.model_class, self._values)
101
+ serialized_values = {
102
+ key: _serialize_value_for_ir(value)
103
+ for key, value in mapped_values.items()
104
+ }
105
+
106
+ # Single insert: use RETURNING * to get all fields (including db defaults)
107
+ return ir.build_insert_ir(
108
+ table=table_name,
109
+ values=serialized_values,
110
+ column_types=column_types,
111
+ model=_model_key(self.model_class),
112
+ pk_column=pk_column,
113
+ returning=True,
114
+ )
115
+
116
+ async def execute(self, client: SupportsExecute) -> dict[str, Any]:
117
+ """Execute insert query."""
118
+ ir = self.to_ir()
119
+ result_bytes = await client.execute(ir)
120
+ result: dict[str, Any] = msgpack.unpackb(result_bytes, raw=False)
121
+ return result
122
+
123
+
124
+ __all__ = ["InsertQuery"]
oxyde/queries/joins.py ADDED
@@ -0,0 +1,66 @@
1
+ """Internal descriptor for JOIN operations in queries.
2
+
3
+ _JoinDescriptor holds all metadata needed to generate a JOIN clause
4
+ and map results back to nested model instances.
5
+
6
+ Attributes:
7
+ path: Dot-separated relation path (e.g., "author", "author.profile")
8
+ alias: SQL table alias for this join (e.g., "t1", "t2")
9
+ result_prefix: Prefix for result columns (e.g., "author__")
10
+ parent_path: Path to parent join (None for direct joins)
11
+ parent_alias: SQL alias of parent table
12
+ attr_name: Attribute name on parent model (e.g., "author")
13
+ source_column: FK column on source table (e.g., "author_id")
14
+ target_column: PK column on target table (e.g., "id")
15
+ columns: List of (field_name, db_column) for SELECT
16
+ target_model: Model class being joined
17
+ parent_model: Model class of the parent
18
+ nullable: Whether this is a LEFT JOIN (True) or INNER JOIN (False)
19
+
20
+ Join Resolution:
21
+ JoiningMixin.join("author") creates _JoinDescriptor:
22
+ - Finds FK field on current model pointing to Author
23
+ - Resolves source_column (author_id) and target_column (id)
24
+ - Generates unique alias
25
+ - Collects columns from target model
26
+
27
+ Nested Joins:
28
+ join("author.profile") creates two descriptors:
29
+ 1. Post → Author (path="author")
30
+ 2. Author → Profile (path="author.profile", parent_path="author")
31
+
32
+ Usage:
33
+ # Query uses these internally
34
+ query = Post.objects.join("author", "category")
35
+ for desc in query._join_specs:
36
+ print(desc.path, desc.alias) # "author" "t1", "category" "t2"
37
+ """
38
+
39
+ from __future__ import annotations
40
+
41
+ from dataclasses import dataclass
42
+ from typing import TYPE_CHECKING
43
+
44
+ if TYPE_CHECKING:
45
+ from oxyde.models.base import Model
46
+
47
+
48
+ @dataclass(slots=True)
49
+ class _JoinDescriptor:
50
+ """Descriptor for a join operation."""
51
+
52
+ path: str
53
+ alias: str
54
+ result_prefix: str
55
+ parent_path: str | None
56
+ parent_alias: str | None
57
+ attr_name: str
58
+ source_column: str
59
+ target_column: str
60
+ columns: list[tuple[str, str]]
61
+ target_model: type[Model]
62
+ parent_model: type[Model]
63
+ nullable: bool
64
+
65
+
66
+ __all__ = ["_JoinDescriptor"]
@@ -0,0 +1,409 @@
1
+ """Django-style QueryManager for Model.objects interface.
2
+
3
+ This module provides QueryManager - the class behind Model.objects that
4
+ enables Django-like query syntax. Each Model gets a QueryManager
5
+ instance automatically assigned to its `objects` attribute.
6
+
7
+ Design:
8
+ QueryManager is a thin proxy that creates Query instances.
9
+ All actual functionality lives in Query and its mixins.
10
+ Manager methods just return Query(model) with the appropriate method called.
11
+
12
+ Example:
13
+ # Manager is auto-attached to models
14
+ class User(Model):
15
+ ...
16
+
17
+ # All methods return Query objects or execute queries
18
+ users = await User.objects.all()
19
+ user = await User.objects.filter(id=1).get()
20
+ new_user = await User.objects.create(name="Alice")
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ from collections.abc import Coroutine, Iterable
26
+ from typing import TYPE_CHECKING, Any, Generic, Literal, TypeVar, overload
27
+
28
+ from oxyde.exceptions import IntegrityError, ManagerError, NotFoundError
29
+ from oxyde.models.serializers import _derive_create_data
30
+ from oxyde.queries.select import Query
31
+
32
+ if TYPE_CHECKING:
33
+ from oxyde.models.base import Model
34
+ from oxyde.queries.base import SupportsExecute
35
+ from oxyde.queries.typed import (
36
+ FlatValuesListQuery,
37
+ ValuesListQuery,
38
+ ValuesQuery,
39
+ )
40
+
41
+
42
+ TModel = TypeVar("TModel", bound="Model")
43
+
44
+
45
+ class QueryManager(Generic[TModel]):
46
+ """Manager that provides Query access for a model.
47
+
48
+ All methods delegate to Query - this is just a factory/proxy.
49
+ """
50
+
51
+ def __init__(self, model_class: type[TModel]) -> None:
52
+ self.model_class = model_class
53
+
54
+ def _query(self) -> Query[TModel]:
55
+ """Create a new Query for this model."""
56
+ return Query(self.model_class)
57
+
58
+ # --- Query building methods (return Query) ---
59
+
60
+ def query(self) -> Query[TModel]:
61
+ """Return a Query builder for this model."""
62
+ return self._query()
63
+
64
+ def filter(self, *args: Any, **kwargs: Any) -> Query[TModel]:
65
+ """Filter by Q-expressions or field lookups."""
66
+ return self._query().filter(*args, **kwargs)
67
+
68
+ def exclude(self, *args: Any, **kwargs: Any) -> Query[TModel]:
69
+ """Exclude rows matching conditions (NOT filter)."""
70
+ return self._query().exclude(*args, **kwargs)
71
+
72
+ def values(self, *fields: str) -> ValuesQuery[TModel]:
73
+ """Return dicts instead of models."""
74
+ return self._query().values(*fields)
75
+
76
+ @overload
77
+ def values_list(
78
+ self, field: str, /, *, flat: Literal[True]
79
+ ) -> FlatValuesListQuery[TModel]: ...
80
+
81
+ @overload
82
+ def values_list(
83
+ self, *fields: str, flat: Literal[False] = ...
84
+ ) -> ValuesListQuery[TModel]: ...
85
+
86
+ @overload
87
+ def values_list(
88
+ self, *fields: str, flat: bool
89
+ ) -> ValuesListQuery[TModel] | FlatValuesListQuery[TModel]: ...
90
+
91
+ def values_list(self, *fields: str, flat: bool = False) -> Any:
92
+ """Return tuples/lists instead of models."""
93
+ return self._query().values_list(*fields, flat=flat)
94
+
95
+ def distinct(self, distinct: bool = True) -> Query[TModel]:
96
+ """Add DISTINCT to query."""
97
+ return self._query().distinct(distinct)
98
+
99
+ def join(self, *paths: str) -> Query[TModel]:
100
+ """Eager load relations via JOIN."""
101
+ return self._query().join(*paths)
102
+
103
+ def prefetch(self, *paths: str) -> Query[TModel]:
104
+ """Prefetch relations in separate queries."""
105
+ return self._query().prefetch(*paths)
106
+
107
+ def for_update(self) -> Query[TModel]:
108
+ """Add FOR UPDATE lock."""
109
+ return self._query().for_update()
110
+
111
+ def for_share(self) -> Query[TModel]:
112
+ """Add FOR SHARE lock."""
113
+ return self._query().for_share()
114
+
115
+ def order_by(self, *fields: str) -> Query[TModel]:
116
+ """Order results by fields. Prefix with '-' for descending."""
117
+ return self._query().order_by(*fields)
118
+
119
+ def limit(self, value: int) -> Query[TModel]:
120
+ """Limit number of results."""
121
+ return self._query().limit(value)
122
+
123
+ def offset(self, value: int) -> Query[TModel]:
124
+ """Skip first N results."""
125
+ return self._query().offset(value)
126
+
127
+ def annotate(self, **annotations: Any) -> Query[TModel]:
128
+ """Add aggregate annotations."""
129
+ return self._query().annotate(**annotations)
130
+
131
+ # --- Execution methods (delegate to Query) ---
132
+
133
+ @overload
134
+ def all(
135
+ self,
136
+ *,
137
+ using: str | None = ...,
138
+ client: SupportsExecute | None = ...,
139
+ mode: Literal["models"] = ...,
140
+ ) -> Coroutine[Any, Any, list[TModel]]: ...
141
+
142
+ @overload
143
+ def all(
144
+ self,
145
+ *,
146
+ using: str | None = ...,
147
+ client: SupportsExecute | None = ...,
148
+ mode: str,
149
+ ) -> Coroutine[Any, Any, Any]: ...
150
+
151
+ def all(
152
+ self,
153
+ *,
154
+ using: str | None = None,
155
+ client: SupportsExecute | None = None,
156
+ mode: str = "models",
157
+ ) -> Coroutine[Any, Any, Any]:
158
+ """Execute query and return all results.
159
+
160
+ Args:
161
+ using: Database alias
162
+ client: Optional database client
163
+ mode: Result mode ("models", "dict", "list", "msgpack")
164
+ """
165
+ q: Query[TModel] = self._query()
166
+ if mode == "dict":
167
+ q = q.values()
168
+ elif mode == "list":
169
+ q = q.values_list()
170
+ elif mode == "msgpack":
171
+ q._result_mode = "msgpack"
172
+ return q.all(using=using, client=client)
173
+
174
+ async def first(
175
+ self,
176
+ *,
177
+ using: str | None = None,
178
+ client: SupportsExecute | None = None,
179
+ ) -> TModel | None:
180
+ """Return first result or None."""
181
+ return await self._query().first(using=using, client=client)
182
+
183
+ async def last(
184
+ self,
185
+ *,
186
+ using: str | None = None,
187
+ client: SupportsExecute | None = None,
188
+ ) -> TModel | None:
189
+ """Return last result or None."""
190
+ return await self._query().last(using=using, client=client)
191
+
192
+ async def get(
193
+ self,
194
+ *,
195
+ using: str | None = None,
196
+ client: SupportsExecute | None = None,
197
+ **filters: Any,
198
+ ) -> TModel:
199
+ """Return exactly one result. Raises NotFoundError/MultipleObjectsReturned."""
200
+ q = self._query()
201
+ if filters:
202
+ q = q.filter(**filters)
203
+ return await q.get(using=using, client=client)
204
+
205
+ async def get_or_none(
206
+ self,
207
+ *,
208
+ using: str | None = None,
209
+ client: SupportsExecute | None = None,
210
+ **filters: Any,
211
+ ) -> TModel | None:
212
+ """Return one result or None. Raises MultipleObjectsReturned if many."""
213
+ q = self._query()
214
+ if filters:
215
+ q = q.filter(**filters)
216
+ return await q.get_or_none(using=using, client=client)
217
+
218
+ async def get_or_create(
219
+ self,
220
+ *,
221
+ defaults: dict[str, Any] | None = None,
222
+ using: str | None = None,
223
+ client: SupportsExecute | None = None,
224
+ **filters: Any,
225
+ ) -> tuple[TModel, bool]:
226
+ """Get existing object or create a new one.
227
+
228
+ Args:
229
+ defaults: Field values to use when creating (not for lookup)
230
+ using: Database alias
231
+ client: Optional database client
232
+ **filters: Lookup conditions for finding existing object
233
+
234
+ Returns:
235
+ Tuple of (instance, created) where created is True if new object was made
236
+ """
237
+ try:
238
+ obj = await self.get(using=using, client=client, **filters)
239
+ return obj, False
240
+ except NotFoundError:
241
+ create_data = _derive_create_data(filters, defaults)
242
+ try:
243
+ obj = await self.create(using=using, client=client, **create_data)
244
+ return obj, True
245
+ except IntegrityError:
246
+ obj = await self.get(using=using, client=client, **filters)
247
+ return obj, False
248
+
249
+ async def exists(
250
+ self,
251
+ *,
252
+ using: str | None = None,
253
+ client: SupportsExecute | None = None,
254
+ ) -> bool:
255
+ """Check if any records match."""
256
+ return await self._query().exists(using=using, client=client)
257
+
258
+ async def count(
259
+ self,
260
+ *,
261
+ using: str | None = None,
262
+ client: SupportsExecute | None = None,
263
+ ) -> int:
264
+ """Count all records using SQL COUNT(*)."""
265
+ return await self._query().count(using=using, client=client)
266
+
267
+ async def sum(
268
+ self,
269
+ field: str,
270
+ *,
271
+ using: str | None = None,
272
+ client: SupportsExecute | None = None,
273
+ ) -> Any:
274
+ """Calculate sum of field values."""
275
+ return await self._query().sum(field, using=using, client=client)
276
+
277
+ async def avg(
278
+ self,
279
+ field: str,
280
+ *,
281
+ using: str | None = None,
282
+ client: SupportsExecute | None = None,
283
+ ) -> Any:
284
+ """Calculate average of field values."""
285
+ return await self._query().avg(field, using=using, client=client)
286
+
287
+ async def max(
288
+ self,
289
+ field: str,
290
+ *,
291
+ using: str | None = None,
292
+ client: SupportsExecute | None = None,
293
+ ) -> Any:
294
+ """Get maximum field value."""
295
+ return await self._query().max(field, using=using, client=client)
296
+
297
+ async def min(
298
+ self,
299
+ field: str,
300
+ *,
301
+ using: str | None = None,
302
+ client: SupportsExecute | None = None,
303
+ ) -> Any:
304
+ """Get minimum field value."""
305
+ return await self._query().min(field, using=using, client=client)
306
+
307
+ # --- Mutation methods (delegate to Query) ---
308
+
309
+ async def create(
310
+ self,
311
+ *,
312
+ instance: TModel | None = None,
313
+ using: str | None = None,
314
+ client: SupportsExecute | None = None,
315
+ _skip_hooks: bool = False,
316
+ **data: Any,
317
+ ) -> TModel:
318
+ """Create a new record in the database."""
319
+ return await self._query().create(
320
+ instance=instance,
321
+ using=using,
322
+ client=client,
323
+ _skip_hooks=_skip_hooks,
324
+ **data,
325
+ )
326
+
327
+ async def bulk_create(
328
+ self,
329
+ objects: Iterable[TModel | dict[str, Any]],
330
+ *,
331
+ using: str | None = None,
332
+ client: SupportsExecute | None = None,
333
+ batch_size: int | None = None,
334
+ ) -> list[TModel]:
335
+ """Bulk insert multiple objects efficiently."""
336
+ return await self._query().bulk_create(
337
+ objects,
338
+ using=using,
339
+ client=client,
340
+ batch_size=batch_size,
341
+ )
342
+
343
+ async def bulk_update(
344
+ self,
345
+ objects: Iterable[TModel],
346
+ fields: Iterable[str],
347
+ *,
348
+ using: str | None = None,
349
+ client: SupportsExecute | None = None,
350
+ ) -> int:
351
+ """Bulk update multiple objects with CASE WHEN."""
352
+ return await self._query().bulk_update(
353
+ objects,
354
+ fields,
355
+ using=using,
356
+ client=client,
357
+ )
358
+
359
+ async def update_or_create(
360
+ self,
361
+ *,
362
+ defaults: dict[str, Any] | None = None,
363
+ using: str | None = None,
364
+ client: SupportsExecute | None = None,
365
+ **filters: Any,
366
+ ) -> tuple[TModel, bool]:
367
+ """Get existing object and update it, or create it if it does not exist.
368
+
369
+ Args:
370
+ defaults: Field values to use when creating or updating
371
+ using: Database alias
372
+ client: Optional database client
373
+ **filters: Lookup conditions for finding existing object
374
+
375
+ Returns:
376
+ Tuple of (instance, created) where created is True if a new
377
+ object was made.
378
+ """
379
+ try:
380
+ obj = await self.get(using=using, client=client, **filters)
381
+ except NotFoundError:
382
+ create_data = _derive_create_data(filters, defaults)
383
+ try:
384
+ obj = await self.create(using=using, client=client, **create_data)
385
+ return obj, True
386
+ except IntegrityError:
387
+ obj = await self.get(using=using, client=client, **filters)
388
+
389
+ if not defaults:
390
+ return obj, False
391
+
392
+ for key, value in defaults.items():
393
+ setattr(obj, key, value)
394
+
395
+ saved_obj = await obj.save(
396
+ client=client,
397
+ using=using,
398
+ update_fields=defaults.keys(),
399
+ )
400
+ return saved_obj, False
401
+
402
+ async def upsert(self, *args: Any, **kwargs: Any) -> Any:
403
+ """Not implemented yet."""
404
+ raise ManagerError("upsert() is not implemented yet")
405
+
406
+
407
+ __all__ = [
408
+ "QueryManager",
409
+ ]
@@ -0,0 +1,68 @@
1
+ """Query mixins providing modular functionality for Query class.
2
+
3
+ The Query class inherits from all these mixins, each providing a specific
4
+ category of query operations. This keeps the code organized and testable.
5
+
6
+ Mixins:
7
+ FilteringMixin:
8
+ filter(), exclude() - WHERE clause building
9
+
10
+ PaginationMixin:
11
+ select() - choose specific columns
12
+ limit(), offset() - result pagination
13
+ order_by() - ORDER BY clause
14
+ distinct() - DISTINCT modifier
15
+ values(), values_list() - return dicts/tuples instead of models
16
+ __getitem__ - slice syntax for limit/offset
17
+
18
+ JoiningMixin:
19
+ join() - eager loading with SQL JOINs
20
+ prefetch() - prefetch related objects (separate queries)
21
+
22
+ AggregationMixin:
23
+ annotate() - add computed columns (COUNT, SUM, etc.)
24
+ group_by() - GROUP BY clause
25
+ having() - HAVING clause
26
+ count(), sum(), avg(), max(), min() - terminal aggregates
27
+
28
+ ExecutionMixin:
29
+ fetch_all(), fetch_one() - raw execution returning dicts
30
+ fetch_rows() - raw execution returning list of tuples
31
+ fetch_models() - execution returning model instances
32
+ fetch_msgpack() - execution returning raw MessagePack bytes
33
+ all(), first(), last(), exists() - convenience methods
34
+
35
+ MutationMixin:
36
+ create(), bulk_create() - INSERT operations
37
+ update(), bulk_update() - UPDATE operations
38
+ delete() - DELETE from SELECT query
39
+ increment() - atomic increment via F()
40
+
41
+ DebugMixin:
42
+ sql() - get generated SQL string
43
+ query - property to inspect IR dict
44
+ explain() - get query execution plan
45
+ union(), union_all() - combine queries
46
+
47
+ Design:
48
+ Each mixin expects certain attributes on self (from Query.__init__).
49
+ Mixins use _clone() to maintain immutability.
50
+ """
51
+
52
+ from oxyde.queries.mixins.aggregation import AggregationMixin
53
+ from oxyde.queries.mixins.debug import DebugMixin
54
+ from oxyde.queries.mixins.execution import ExecutionMixin
55
+ from oxyde.queries.mixins.filtering import FilteringMixin
56
+ from oxyde.queries.mixins.joining import JoiningMixin
57
+ from oxyde.queries.mixins.mutation import MutationMixin
58
+ from oxyde.queries.mixins.pagination import PaginationMixin
59
+
60
+ __all__ = [
61
+ "FilteringMixin",
62
+ "PaginationMixin",
63
+ "JoiningMixin",
64
+ "AggregationMixin",
65
+ "ExecutionMixin",
66
+ "MutationMixin",
67
+ "DebugMixin",
68
+ ]