async-orm-kit 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- async_orm_kit/__init__.py +151 -0
- async_orm_kit/_logging.py +58 -0
- async_orm_kit/_version.py +1 -0
- async_orm_kit/config.py +31 -0
- async_orm_kit/exceptions.py +353 -0
- async_orm_kit/integrations/__init__.py +15 -0
- async_orm_kit/integrations/alembic.py +56 -0
- async_orm_kit/integrations/fastapi.py +134 -0
- async_orm_kit/integrations/storage.py +213 -0
- async_orm_kit/manager/__init__.py +3 -0
- async_orm_kit/manager/manager.py +568 -0
- async_orm_kit/models/__init__.py +3 -0
- async_orm_kit/models/base.py +568 -0
- async_orm_kit/models/fields/__init__.py +23 -0
- async_orm_kit/models/fields/exceptions.py +10 -0
- async_orm_kit/models/fields/typed/__init__.py +11 -0
- async_orm_kit/models/fields/typed/base.py +155 -0
- async_orm_kit/models/fields/typed/file.py +697 -0
- async_orm_kit/models/fields/typed/string.py +38 -0
- async_orm_kit/models/fields/validators.py +110 -0
- async_orm_kit/models/meta.py +181 -0
- async_orm_kit/models/mixins.py +19 -0
- async_orm_kit/observers/__init__.py +4 -0
- async_orm_kit/observers/mixin.py +26 -0
- async_orm_kit/observers/model_observer.py +20 -0
- async_orm_kit/observers/queryset_observer.py +38 -0
- async_orm_kit/py.typed +0 -0
- async_orm_kit/query/__init__.py +0 -0
- async_orm_kit/query/aggregations/__init__.py +8 -0
- async_orm_kit/query/aggregations/avg.py +71 -0
- async_orm_kit/query/aggregations/base.py +317 -0
- async_orm_kit/query/aggregations/count.py +115 -0
- async_orm_kit/query/aggregations/max.py +76 -0
- async_orm_kit/query/aggregations/min.py +76 -0
- async_orm_kit/query/aggregations/sum.py +73 -0
- async_orm_kit/query/compiler.py +574 -0
- async_orm_kit/query/debugger.py +269 -0
- async_orm_kit/query/expressions/__init__.py +62 -0
- async_orm_kit/query/expressions/analysis.py +56 -0
- async_orm_kit/query/expressions/case.py +321 -0
- async_orm_kit/query/expressions/collections.py +251 -0
- async_orm_kit/query/expressions/f.py +362 -0
- async_orm_kit/query/expressions/functions.py +202 -0
- async_orm_kit/query/expressions/subquery.py +206 -0
- async_orm_kit/query/expressions/value.py +101 -0
- async_orm_kit/query/expressions/window.py +95 -0
- async_orm_kit/query/lookups/__init__.py +121 -0
- async_orm_kit/query/lookups/conditions.py +315 -0
- async_orm_kit/query/lookups/correlated.py +86 -0
- async_orm_kit/query/lookups/json_paths.py +167 -0
- async_orm_kit/query/lookups/parser.py +276 -0
- async_orm_kit/query/lookups/resolver/__init__.py +5 -0
- async_orm_kit/query/lookups/resolver/context.py +150 -0
- async_orm_kit/query/lookups/resolver/resolver.py +572 -0
- async_orm_kit/query/lookups/resolver/types.py +150 -0
- async_orm_kit/query/mixins/__init__.py +0 -0
- async_orm_kit/query/mixins/combinable.py +376 -0
- async_orm_kit/query/mixins/orderable.py +208 -0
- async_orm_kit/query/observability.py +346 -0
- async_orm_kit/query/planning/__init__.py +0 -0
- async_orm_kit/query/planning/directives.py +117 -0
- async_orm_kit/query/planning/factory.py +40 -0
- async_orm_kit/query/planning/passes/__init__.py +15 -0
- async_orm_kit/query/planning/passes/base.py +47 -0
- async_orm_kit/query/planning/passes/cartesian_detection.py +74 -0
- async_orm_kit/query/planning/passes/depth_splitting.py +78 -0
- async_orm_kit/query/planning/passes/join_promotion.py +81 -0
- async_orm_kit/query/planning/passes/path_conflicts.py +103 -0
- async_orm_kit/query/planning/passes/redundant_path.py +67 -0
- async_orm_kit/query/planning/passes/strategy_selection.py +49 -0
- async_orm_kit/query/planning/planner.py +160 -0
- async_orm_kit/query/planning/protocols.py +78 -0
- async_orm_kit/query/postgres/__init__.py +0 -0
- async_orm_kit/query/postgres/optimizer.py +189 -0
- async_orm_kit/query/q.py +596 -0
- async_orm_kit/query/relations/__init__.py +0 -0
- async_orm_kit/query/relations/enums.py +16 -0
- async_orm_kit/query/relations/graph.py +908 -0
- async_orm_kit/query/relations/node.py +421 -0
- async_orm_kit/queryset/__init__.py +4 -0
- async_orm_kit/queryset/builder.py +1259 -0
- async_orm_kit/queryset/compound.py +379 -0
- async_orm_kit/queryset/executor.py +718 -0
- async_orm_kit/queryset/queryset.py +1726 -0
- async_orm_kit/queryset/state.py +164 -0
- async_orm_kit/queryset/validators.py +361 -0
- async_orm_kit/queryset/writer.py +755 -0
- async_orm_kit/types.py +23 -0
- async_orm_kit-1.0.0.dist-info/METADATA +318 -0
- async_orm_kit-1.0.0.dist-info/RECORD +92 -0
- async_orm_kit-1.0.0.dist-info/WHEEL +4 -0
- async_orm_kit-1.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import TYPE_CHECKING, Any
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from async_orm_kit.query.relations.node import JoinNode
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(slots=True)
|
|
11
|
+
class FieldResolution:
|
|
12
|
+
"""Result of field path resolution containing column and JOIN metadata.
|
|
13
|
+
|
|
14
|
+
FieldResolution encapsulates the complete result of resolving a Django-style
|
|
15
|
+
field path to a SQLAlchemy column. It includes the resolved column, the lookup
|
|
16
|
+
operator to apply, and optional JOIN metadata if relationship traversal occurred.
|
|
17
|
+
|
|
18
|
+
This data class serves as the contract between FieldResolver and ConditionBuilder,
|
|
19
|
+
providing all information needed to build a filter condition without exposing
|
|
20
|
+
internal resolution details.
|
|
21
|
+
|
|
22
|
+
Attributes:
|
|
23
|
+
field_path: Full relationship path traversed, empty string for root model
|
|
24
|
+
fields. Examples: "", "building", "user__books".
|
|
25
|
+
field_name: Terminal field name resolved, empty string for direct relation
|
|
26
|
+
filters. Examples: "name", "title", "".
|
|
27
|
+
lookup: Lookup operator to apply. Examples: "exact", "gte", "icontains".
|
|
28
|
+
column: SQLAlchemy column or alias to use in condition. Can be a Column,
|
|
29
|
+
InstrumentedAttribute, or AliasedClass for direct relation filters.
|
|
30
|
+
join_node: Optional JoinNode reference if relationship traversal occurred.
|
|
31
|
+
None for root model fields. Used for JOIN graph optimization.
|
|
32
|
+
|
|
33
|
+
Example:
|
|
34
|
+
>>> # Simple field resolution (no JOINs)
|
|
35
|
+
>>> FieldResolution(
|
|
36
|
+
... field_path="",
|
|
37
|
+
... field_name="status",
|
|
38
|
+
... lookup="exact",
|
|
39
|
+
... column=Product.status,
|
|
40
|
+
... join_node=None
|
|
41
|
+
... )
|
|
42
|
+
>>>
|
|
43
|
+
>>> # Relationship field resolution (with JOIN)
|
|
44
|
+
>>> FieldResolution(
|
|
45
|
+
... field_path="category",
|
|
46
|
+
... field_name="name",
|
|
47
|
+
... lookup="icontains",
|
|
48
|
+
... column=category_alias.name,
|
|
49
|
+
... join_node=JoinNode(path="category", ...)
|
|
50
|
+
... )
|
|
51
|
+
>>>
|
|
52
|
+
>>> # Direct relation filter (no terminal field)
|
|
53
|
+
>>> FieldResolution(
|
|
54
|
+
... field_path="building",
|
|
55
|
+
... field_name="",
|
|
56
|
+
... lookup="exact",
|
|
57
|
+
... column=building_alias,
|
|
58
|
+
... join_node=JoinNode(path="building", ...)
|
|
59
|
+
... )
|
|
60
|
+
|
|
61
|
+
Note:
|
|
62
|
+
The join_node reference enables ConditionBuilder to update JOIN metadata
|
|
63
|
+
based on the lookup operator (e.g., marking has_nullable_check for isnull
|
|
64
|
+
lookups). This bidirectional communication ensures optimal JOIN type
|
|
65
|
+
selection.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
field_path: str # "building" or "books__author"
|
|
69
|
+
field_name: str # "name" or "title" or "" for direct relation
|
|
70
|
+
lookup: str # "exact", "gte", "icontains", etc.
|
|
71
|
+
column: Any # SQLAlchemy column or alias
|
|
72
|
+
join_node: JoinNode | None = None # Optional JoinNode reference
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@dataclass(slots=True)
|
|
76
|
+
class ParsedFieldKey:
|
|
77
|
+
"""Result of field key parsing containing relationship and field components.
|
|
78
|
+
|
|
79
|
+
ParsedFieldKey represents the structural decomposition of a Django-style field
|
|
80
|
+
key into its constituent parts: relationship traversal path, terminal field name,
|
|
81
|
+
and lookup operator. This is a pure data structure with no behavior, serving as
|
|
82
|
+
the intermediate result between key parsing and column resolution.
|
|
83
|
+
|
|
84
|
+
The parsing process uses model introspection to distinguish relationships from
|
|
85
|
+
fields and lookups, but doesn't create JOINs or resolve columns - that's
|
|
86
|
+
FieldResolver's job.
|
|
87
|
+
|
|
88
|
+
Attributes:
|
|
89
|
+
relationship_parts: List of relationship names to traverse in order.
|
|
90
|
+
Empty list for root model fields. Examples: [], ["building"], ["user", "books"].
|
|
91
|
+
field_name: Terminal field name to resolve on the final model.
|
|
92
|
+
Empty string for direct relation filters.
|
|
93
|
+
Examples: "name", "title", "".
|
|
94
|
+
lookup: Lookup operator extracted from the key.
|
|
95
|
+
Defaults to "exact" if not specified.
|
|
96
|
+
Examples: "exact", "gte", "icontains".
|
|
97
|
+
is_simple: True if no relationship traversal required (optimization flag).
|
|
98
|
+
False if relationship_parts is non-empty.
|
|
99
|
+
|
|
100
|
+
Example:
|
|
101
|
+
>>> # Simple field: "status"
|
|
102
|
+
>>> ParsedFieldKey(
|
|
103
|
+
... relationship_parts=[],
|
|
104
|
+
... field_name="status",
|
|
105
|
+
... lookup="exact",
|
|
106
|
+
... is_simple=True
|
|
107
|
+
... )
|
|
108
|
+
>>>
|
|
109
|
+
>>> # Field with lookup: "price__gte"
|
|
110
|
+
>>> ParsedFieldKey(
|
|
111
|
+
... relationship_parts=[],
|
|
112
|
+
... field_name="price",
|
|
113
|
+
... lookup="gte",
|
|
114
|
+
... is_simple=True
|
|
115
|
+
... )
|
|
116
|
+
>>>
|
|
117
|
+
>>> # Single relationship: "building__name"
|
|
118
|
+
>>> ParsedFieldKey(
|
|
119
|
+
... relationship_parts=["building"],
|
|
120
|
+
... field_name="name",
|
|
121
|
+
... lookup="exact",
|
|
122
|
+
... is_simple=False
|
|
123
|
+
... )
|
|
124
|
+
>>>
|
|
125
|
+
>>> # Nested relationships: "user__books__title__icontains"
|
|
126
|
+
>>> ParsedFieldKey(
|
|
127
|
+
... relationship_parts=["user", "books"],
|
|
128
|
+
... field_name="title",
|
|
129
|
+
... lookup="icontains",
|
|
130
|
+
... is_simple=False
|
|
131
|
+
... )
|
|
132
|
+
>>>
|
|
133
|
+
>>> # Direct relation filter: "building"
|
|
134
|
+
>>> ParsedFieldKey(
|
|
135
|
+
... relationship_parts=["building"],
|
|
136
|
+
... field_name="",
|
|
137
|
+
... lookup="exact",
|
|
138
|
+
... is_simple=False
|
|
139
|
+
... )
|
|
140
|
+
|
|
141
|
+
Note:
|
|
142
|
+
The is_simple flag enables fast-path optimization in FieldResolver - simple
|
|
143
|
+
fields can skip JOIN graph interaction entirely. This is a significant
|
|
144
|
+
performance win for queries without relationship traversal.
|
|
145
|
+
"""
|
|
146
|
+
|
|
147
|
+
relationship_parts: list[str] # ["building"] or ["user", "books"]
|
|
148
|
+
field_name: str # "name" or "title" or ""
|
|
149
|
+
lookup: str # "icontains", "exact", etc.
|
|
150
|
+
is_simple: bool # True if no relationship traversal
|
|
File without changes
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import operator
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from async_orm_kit.query.expressions.f import FExpression
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CombinableMixin:
|
|
12
|
+
"""Mixin that adds arithmetic operators to ORM expression classes.
|
|
13
|
+
|
|
14
|
+
Enables any ORM expression with a ``resolve(field_resolver)`` method to
|
|
15
|
+
participate in database-level arithmetic through Python's operator
|
|
16
|
+
overloading. All operations produce :class:`FExpression` instances that
|
|
17
|
+
defer actual SQL generation until query compilation time.
|
|
18
|
+
|
|
19
|
+
This mixin implements both forward (``self OP other``) and reflected
|
|
20
|
+
(``other OP self``) variants for all binary operators, ensuring correct
|
|
21
|
+
behavior regardless of operand order. Unary operators (negation, absolute
|
|
22
|
+
value) are also supported.
|
|
23
|
+
|
|
24
|
+
Supported Operations:
|
|
25
|
+
- **Addition**: ``expr + other``, ``other + expr``
|
|
26
|
+
- **Subtraction**: ``expr - other``, ``other - expr``
|
|
27
|
+
- **Multiplication**: ``expr * other``, ``other * expr``
|
|
28
|
+
- **Division**: ``expr / other``, ``other / expr``
|
|
29
|
+
- **Modulo**: ``expr % other``, ``other % expr``
|
|
30
|
+
- **Negation**: ``-expr``
|
|
31
|
+
- **Absolute Value**: ``abs(expr)``
|
|
32
|
+
|
|
33
|
+
Classes Using This Mixin:
|
|
34
|
+
- :class:`F` — field references (``F("price") * 1.1``)
|
|
35
|
+
- :class:`Value` — literal values (``Value(100) - F("discount")``)
|
|
36
|
+
- :class:`Aggregate` — aggregate results (``Sum("qty") * F("price")``)
|
|
37
|
+
- :class:`FExpression` — nested expressions (composable trees)
|
|
38
|
+
|
|
39
|
+
Example:
|
|
40
|
+
>>> from async_orm_kit.query.expressions import F, Value
|
|
41
|
+
>>>
|
|
42
|
+
>>> # Simple field arithmetic — produces FExpression tree
|
|
43
|
+
>>> discount_price = F("price") * Value(0.9)
|
|
44
|
+
>>>
|
|
45
|
+
>>> # Mixed operand types — reflected operators handle int/float LHS
|
|
46
|
+
>>> adjusted = 100 - F("discount") # calls F.__rsub__, wraps 100 as LHS
|
|
47
|
+
>>>
|
|
48
|
+
>>> # Deep composition — arbitrary nesting
|
|
49
|
+
>>> margin = (F("revenue") - F("cost")) / F("revenue") * Value(100)
|
|
50
|
+
>>>
|
|
51
|
+
>>> # Unary operators
|
|
52
|
+
>>> negated = -F("balance") # FExpression(None, operator.neg, F("balance"))
|
|
53
|
+
>>> absolute = abs(F("balance")) # FExpression(None, "abs", F("balance"))
|
|
54
|
+
|
|
55
|
+
Note:
|
|
56
|
+
- All operators return new :class:`FExpression` instances; the original
|
|
57
|
+
expression objects are **never mutated** (immutable pattern).
|
|
58
|
+
- The ``other`` operand can be any type: another ``CombinableMixin``
|
|
59
|
+
subclass, a raw Python scalar (int, float, Decimal), or any object.
|
|
60
|
+
Non-expression operands are wrapped in :class:`Value` during
|
|
61
|
+
resolution by :class:`FExpression`.
|
|
62
|
+
- Reflected operators (``__radd__``, ``__rsub__``, etc.) are invoked by
|
|
63
|
+
Python when the left operand doesn't support the operation, enabling
|
|
64
|
+
expressions like ``2 * F("price")`` where ``int.__mul__`` returns
|
|
65
|
+
``NotImplemented``.
|
|
66
|
+
|
|
67
|
+
See Also:
|
|
68
|
+
- :class:`FExpression` for the expression tree node structure and
|
|
69
|
+
resolution logic.
|
|
70
|
+
- :class:`F` for field reference construction.
|
|
71
|
+
- :class:`Value` for literal value wrapping.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
def _make_expression(
|
|
75
|
+
self,
|
|
76
|
+
op: Callable,
|
|
77
|
+
other: Any,
|
|
78
|
+
reverse: bool = False,
|
|
79
|
+
) -> FExpression:
|
|
80
|
+
"""Create a binary :class:`FExpression` node from this expression and another operand.
|
|
81
|
+
|
|
82
|
+
Factory method used by all arithmetic dunder methods to construct
|
|
83
|
+
expression tree nodes. Handles operand ordering for both forward
|
|
84
|
+
and reflected operations.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
op: Python operator function from the :mod:`operator` module
|
|
88
|
+
(e.g., ``operator.add``, ``operator.mul``). Stored in the
|
|
89
|
+
resulting :class:`FExpression` and applied during resolution.
|
|
90
|
+
other: Right-hand operand for forward operations, or left-hand
|
|
91
|
+
operand for reflected operations. Can be any type — non-expression
|
|
92
|
+
values are wrapped in :class:`Value` during FExpression resolution.
|
|
93
|
+
reverse: If ``True``, places ``other`` as the left operand and
|
|
94
|
+
``self`` as the right operand. Used by reflected operators
|
|
95
|
+
(``__radd__``, ``__rsub__``, etc.) to preserve mathematical
|
|
96
|
+
correctness for non-commutative operations.
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
FExpression: New expression tree node representing ``self OP other``
|
|
100
|
+
(forward) or ``other OP self`` (reflected).
|
|
101
|
+
|
|
102
|
+
Example:
|
|
103
|
+
>>> expr = F("price")
|
|
104
|
+
>>> # Forward: F("price") + 10 → FExpression(F("price"), add, 10)
|
|
105
|
+
>>> result = expr._make_expression(operator.add, 10)
|
|
106
|
+
>>>
|
|
107
|
+
>>> # Reflected: 10 - F("price") → FExpression(10, sub, F("price"))
|
|
108
|
+
>>> result = expr._make_expression(operator.sub, 10, reverse=True)
|
|
109
|
+
|
|
110
|
+
Note:
|
|
111
|
+
Uses a deferred import for :class:`FExpression` to avoid circular
|
|
112
|
+
dependencies, since FExpression itself inherits CombinableMixin.
|
|
113
|
+
"""
|
|
114
|
+
from async_orm_kit.query.expressions.f import FExpression
|
|
115
|
+
|
|
116
|
+
if reverse:
|
|
117
|
+
return FExpression(other, op, self)
|
|
118
|
+
return FExpression(self, op, other)
|
|
119
|
+
|
|
120
|
+
def __add__(self, other: Any) -> FExpression:
|
|
121
|
+
"""Create addition expression: ``self + other``.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
other: Right-hand operand. Can be another expression or a scalar.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
FExpression representing ``self + other``.
|
|
128
|
+
|
|
129
|
+
Example:
|
|
130
|
+
>>> F("base_price") + F("tax")
|
|
131
|
+
>>> F("score") + 10
|
|
132
|
+
"""
|
|
133
|
+
return self._make_expression(operator.add, other)
|
|
134
|
+
|
|
135
|
+
def __sub__(self, other: Any) -> FExpression:
|
|
136
|
+
"""Create subtraction expression: ``self - other``.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
other: Right-hand operand (subtrahend).
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
FExpression representing ``self - other``.
|
|
143
|
+
|
|
144
|
+
Example:
|
|
145
|
+
>>> F("revenue") - F("cost")
|
|
146
|
+
>>> F("stock") - 1
|
|
147
|
+
"""
|
|
148
|
+
return self._make_expression(operator.sub, other)
|
|
149
|
+
|
|
150
|
+
def __mul__(self, other: Any) -> FExpression:
|
|
151
|
+
"""Create multiplication expression: ``self * other``.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
other: Right-hand operand (multiplier).
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
FExpression representing ``self * other``.
|
|
158
|
+
|
|
159
|
+
Example:
|
|
160
|
+
>>> F("price") * F("quantity")
|
|
161
|
+
>>> F("amount") * Value(Decimal("0.09")) # tax calculation
|
|
162
|
+
"""
|
|
163
|
+
return self._make_expression(operator.mul, other)
|
|
164
|
+
|
|
165
|
+
def __truediv__(self, other: Any) -> FExpression:
|
|
166
|
+
"""Create true division expression: ``self / other``.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
other: Right-hand operand (divisor).
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
FExpression representing ``self / other``.
|
|
173
|
+
|
|
174
|
+
Example:
|
|
175
|
+
>>> F("total") / F("count") # average
|
|
176
|
+
>>> F("profit") / F("revenue") * 100 # profit margin %
|
|
177
|
+
|
|
178
|
+
Note:
|
|
179
|
+
Database behavior for division by zero varies by backend:
|
|
180
|
+
|
|
181
|
+
- **PostgreSQL**: Raises ``division_by_zero`` error.
|
|
182
|
+
- **SQLite**: Returns ``NULL``.
|
|
183
|
+
- **MySQL**: Returns ``NULL``.
|
|
184
|
+
|
|
185
|
+
Consider using ``Case/When`` guards for safe division.
|
|
186
|
+
"""
|
|
187
|
+
return self._make_expression(operator.truediv, other)
|
|
188
|
+
|
|
189
|
+
def __mod__(self, other: Any) -> FExpression:
|
|
190
|
+
"""Create modulo expression: ``self % other``.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
other: Right-hand operand (divisor for remainder).
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
FExpression representing ``self % other``.
|
|
197
|
+
|
|
198
|
+
Example:
|
|
199
|
+
>>> F("row_number") % 2 # odd/even check
|
|
200
|
+
>>> F("seconds") % 60 # remaining seconds
|
|
201
|
+
"""
|
|
202
|
+
return self._make_expression(operator.mod, other)
|
|
203
|
+
|
|
204
|
+
# ── Binary Arithmetic (Reflected) ───────────────────────────────
|
|
205
|
+
|
|
206
|
+
def __radd__(self, other: Any) -> FExpression:
|
|
207
|
+
"""Create reflected addition: ``other + self``.
|
|
208
|
+
|
|
209
|
+
Called when the left operand's ``__add__`` returns ``NotImplemented``.
|
|
210
|
+
Since addition is commutative, the result is mathematically equivalent
|
|
211
|
+
to ``self + other``, but operand order is preserved for SQL clarity.
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
other: Left-hand operand (typically a Python scalar).
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
FExpression representing ``other + self``.
|
|
218
|
+
|
|
219
|
+
Example:
|
|
220
|
+
>>> 100 + F("bonus") # int.__add__(F) → NotImplemented → F.__radd__(100)
|
|
221
|
+
"""
|
|
222
|
+
return self._make_expression(operator.add, other, reverse=True)
|
|
223
|
+
|
|
224
|
+
def __rsub__(self, other: Any) -> FExpression:
|
|
225
|
+
"""Create reflected subtraction: ``other - self``.
|
|
226
|
+
|
|
227
|
+
Called when the left operand's ``__sub__`` returns ``NotImplemented``.
|
|
228
|
+
Operand order matters here since subtraction is **not** commutative.
|
|
229
|
+
|
|
230
|
+
Args:
|
|
231
|
+
other: Left-hand operand (minuend).
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
FExpression representing ``other - self``.
|
|
235
|
+
|
|
236
|
+
Example:
|
|
237
|
+
>>> 1000 - F("spent") # remaining budget
|
|
238
|
+
>>> # Generates: 1000 - "spent" column (NOT "spent" - 1000)
|
|
239
|
+
"""
|
|
240
|
+
return self._make_expression(operator.sub, other, reverse=True)
|
|
241
|
+
|
|
242
|
+
def __rmul__(self, other: Any) -> FExpression:
|
|
243
|
+
"""Create reflected multiplication: ``other * self``.
|
|
244
|
+
|
|
245
|
+
Called when the left operand's ``__mul__`` returns ``NotImplemented``.
|
|
246
|
+
Since multiplication is commutative, result is mathematically equivalent
|
|
247
|
+
to ``self * other``.
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
other: Left-hand operand (typically a Python scalar).
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
FExpression representing ``other * self``.
|
|
254
|
+
|
|
255
|
+
Example:
|
|
256
|
+
>>> 2 * F("quantity") # double the quantity
|
|
257
|
+
"""
|
|
258
|
+
return self._make_expression(operator.mul, other, reverse=True)
|
|
259
|
+
|
|
260
|
+
def __rtruediv__(self, other: Any) -> FExpression:
|
|
261
|
+
"""Create reflected division: ``other / self``.
|
|
262
|
+
|
|
263
|
+
Called when the left operand's ``__truediv__`` returns ``NotImplemented``.
|
|
264
|
+
Operand order matters since division is **not** commutative.
|
|
265
|
+
|
|
266
|
+
Args:
|
|
267
|
+
other: Left-hand operand (dividend).
|
|
268
|
+
|
|
269
|
+
Returns:
|
|
270
|
+
FExpression representing ``other / self``.
|
|
271
|
+
|
|
272
|
+
Example:
|
|
273
|
+
>>> 1 / F("rate") # inverse rate
|
|
274
|
+
>>> # Generates: 1 / "rate" column (NOT "rate" / 1)
|
|
275
|
+
"""
|
|
276
|
+
return self._make_expression(operator.truediv, other, reverse=True)
|
|
277
|
+
|
|
278
|
+
def __rmod__(self, other: Any) -> FExpression:
|
|
279
|
+
"""Create reflected modulo: ``other % self``.
|
|
280
|
+
|
|
281
|
+
Called when the left operand's ``__mod__`` returns ``NotImplemented``.
|
|
282
|
+
Operand order matters since modulo is **not** commutative.
|
|
283
|
+
|
|
284
|
+
Args:
|
|
285
|
+
other: Left-hand operand (dividend for remainder).
|
|
286
|
+
|
|
287
|
+
Returns:
|
|
288
|
+
FExpression representing ``other % self``.
|
|
289
|
+
|
|
290
|
+
Example:
|
|
291
|
+
>>> 100 % F("divisor")
|
|
292
|
+
"""
|
|
293
|
+
return self._make_expression(operator.mod, other, reverse=True)
|
|
294
|
+
|
|
295
|
+
# ── Unary Operators ─────────────────────────────────────────────
|
|
296
|
+
|
|
297
|
+
def __neg__(self) -> FExpression:
|
|
298
|
+
"""Create negation expression: ``-self``.
|
|
299
|
+
|
|
300
|
+
Produces a unary :class:`FExpression` with ``None`` as the left operand
|
|
301
|
+
and ``operator.neg`` as the operation. During resolution, FExpression
|
|
302
|
+
recognizes the ``None`` left operand and applies unary negation.
|
|
303
|
+
|
|
304
|
+
Returns:
|
|
305
|
+
FExpression representing ``-self``.
|
|
306
|
+
|
|
307
|
+
Example:
|
|
308
|
+
>>> -F("balance") # negate balance
|
|
309
|
+
>>> -Sum("adjustments") # negate aggregate result
|
|
310
|
+
>>>
|
|
311
|
+
>>> # Equivalent SQL: -"balance", -SUM("adjustments")
|
|
312
|
+
"""
|
|
313
|
+
from async_orm_kit.query.expressions.f import FExpression
|
|
314
|
+
|
|
315
|
+
return FExpression(None, operator.neg, self)
|
|
316
|
+
|
|
317
|
+
def __abs__(self) -> FExpression:
|
|
318
|
+
"""Create absolute value expression: ``abs(self)``.
|
|
319
|
+
|
|
320
|
+
Produces a unary :class:`FExpression` with ``None`` as the left operand
|
|
321
|
+
and the string ``"abs"`` as the operation identifier. During resolution,
|
|
322
|
+
FExpression maps ``"abs"`` to the ``func.abs()`` SQL function.
|
|
323
|
+
|
|
324
|
+
Returns:
|
|
325
|
+
FExpression representing ``ABS(self)``.
|
|
326
|
+
|
|
327
|
+
Example:
|
|
328
|
+
>>> abs(F("profit_loss")) # absolute value of column
|
|
329
|
+
>>> abs(F("actual") - F("expected")) # absolute deviation
|
|
330
|
+
>>>
|
|
331
|
+
>>> # Equivalent SQL: ABS("profit_loss"), ABS("actual" - "expected")
|
|
332
|
+
|
|
333
|
+
Note:
|
|
334
|
+
Uses the string ``"abs"`` instead of ``operator.abs`` because
|
|
335
|
+
Python's :mod:`operator` module does not provide an ``abs``
|
|
336
|
+
function. The :class:`FExpression` resolver handles this string
|
|
337
|
+
specially by delegating to ``sqlalchemy.func.abs()``.
|
|
338
|
+
"""
|
|
339
|
+
from async_orm_kit.query.expressions.f import FExpression
|
|
340
|
+
|
|
341
|
+
return FExpression(None, "abs", self)
|
|
342
|
+
|
|
343
|
+
def __floordiv__(self, other):
|
|
344
|
+
"""Portable floor division using SQL FLOOR(lhs / rhs)."""
|
|
345
|
+
return self._make_expression("floordiv", other)
|
|
346
|
+
|
|
347
|
+
def __rfloordiv__(self, other):
|
|
348
|
+
return self._make_expression("floordiv", other, reverse=True)
|
|
349
|
+
|
|
350
|
+
def __and__(self, other):
|
|
351
|
+
"""Bitwise AND (SQLAlchemy's dialect-aware operator)."""
|
|
352
|
+
return self._make_expression("bit_and", other)
|
|
353
|
+
|
|
354
|
+
def __rand__(self, other):
|
|
355
|
+
return self._make_expression("bit_and", other, reverse=True)
|
|
356
|
+
|
|
357
|
+
def __or__(self, other):
|
|
358
|
+
"""Bitwise OR; Q expressions use their own logical OR."""
|
|
359
|
+
return self._make_expression("bit_or", other)
|
|
360
|
+
|
|
361
|
+
def __ror__(self, other):
|
|
362
|
+
return self._make_expression("bit_or", other, reverse=True)
|
|
363
|
+
|
|
364
|
+
def __invert__(self):
|
|
365
|
+
"""Bitwise NOT; Q expressions use their own logical negation."""
|
|
366
|
+
from async_orm_kit.query.expressions.f import FExpression
|
|
367
|
+
|
|
368
|
+
return FExpression(None, "bit_not", self)
|
|
369
|
+
|
|
370
|
+
def eq(self, other):
|
|
371
|
+
"""Explicit SQL equality, even for Value objects with structural __eq__."""
|
|
372
|
+
return self._make_expression(operator.eq, other)
|
|
373
|
+
|
|
374
|
+
def ne(self, other):
|
|
375
|
+
"""Explicit SQL inequality, independent of Python object equality."""
|
|
376
|
+
return self._make_expression(operator.ne, other)
|