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.
Files changed (92) hide show
  1. async_orm_kit/__init__.py +151 -0
  2. async_orm_kit/_logging.py +58 -0
  3. async_orm_kit/_version.py +1 -0
  4. async_orm_kit/config.py +31 -0
  5. async_orm_kit/exceptions.py +353 -0
  6. async_orm_kit/integrations/__init__.py +15 -0
  7. async_orm_kit/integrations/alembic.py +56 -0
  8. async_orm_kit/integrations/fastapi.py +134 -0
  9. async_orm_kit/integrations/storage.py +213 -0
  10. async_orm_kit/manager/__init__.py +3 -0
  11. async_orm_kit/manager/manager.py +568 -0
  12. async_orm_kit/models/__init__.py +3 -0
  13. async_orm_kit/models/base.py +568 -0
  14. async_orm_kit/models/fields/__init__.py +23 -0
  15. async_orm_kit/models/fields/exceptions.py +10 -0
  16. async_orm_kit/models/fields/typed/__init__.py +11 -0
  17. async_orm_kit/models/fields/typed/base.py +155 -0
  18. async_orm_kit/models/fields/typed/file.py +697 -0
  19. async_orm_kit/models/fields/typed/string.py +38 -0
  20. async_orm_kit/models/fields/validators.py +110 -0
  21. async_orm_kit/models/meta.py +181 -0
  22. async_orm_kit/models/mixins.py +19 -0
  23. async_orm_kit/observers/__init__.py +4 -0
  24. async_orm_kit/observers/mixin.py +26 -0
  25. async_orm_kit/observers/model_observer.py +20 -0
  26. async_orm_kit/observers/queryset_observer.py +38 -0
  27. async_orm_kit/py.typed +0 -0
  28. async_orm_kit/query/__init__.py +0 -0
  29. async_orm_kit/query/aggregations/__init__.py +8 -0
  30. async_orm_kit/query/aggregations/avg.py +71 -0
  31. async_orm_kit/query/aggregations/base.py +317 -0
  32. async_orm_kit/query/aggregations/count.py +115 -0
  33. async_orm_kit/query/aggregations/max.py +76 -0
  34. async_orm_kit/query/aggregations/min.py +76 -0
  35. async_orm_kit/query/aggregations/sum.py +73 -0
  36. async_orm_kit/query/compiler.py +574 -0
  37. async_orm_kit/query/debugger.py +269 -0
  38. async_orm_kit/query/expressions/__init__.py +62 -0
  39. async_orm_kit/query/expressions/analysis.py +56 -0
  40. async_orm_kit/query/expressions/case.py +321 -0
  41. async_orm_kit/query/expressions/collections.py +251 -0
  42. async_orm_kit/query/expressions/f.py +362 -0
  43. async_orm_kit/query/expressions/functions.py +202 -0
  44. async_orm_kit/query/expressions/subquery.py +206 -0
  45. async_orm_kit/query/expressions/value.py +101 -0
  46. async_orm_kit/query/expressions/window.py +95 -0
  47. async_orm_kit/query/lookups/__init__.py +121 -0
  48. async_orm_kit/query/lookups/conditions.py +315 -0
  49. async_orm_kit/query/lookups/correlated.py +86 -0
  50. async_orm_kit/query/lookups/json_paths.py +167 -0
  51. async_orm_kit/query/lookups/parser.py +276 -0
  52. async_orm_kit/query/lookups/resolver/__init__.py +5 -0
  53. async_orm_kit/query/lookups/resolver/context.py +150 -0
  54. async_orm_kit/query/lookups/resolver/resolver.py +572 -0
  55. async_orm_kit/query/lookups/resolver/types.py +150 -0
  56. async_orm_kit/query/mixins/__init__.py +0 -0
  57. async_orm_kit/query/mixins/combinable.py +376 -0
  58. async_orm_kit/query/mixins/orderable.py +208 -0
  59. async_orm_kit/query/observability.py +346 -0
  60. async_orm_kit/query/planning/__init__.py +0 -0
  61. async_orm_kit/query/planning/directives.py +117 -0
  62. async_orm_kit/query/planning/factory.py +40 -0
  63. async_orm_kit/query/planning/passes/__init__.py +15 -0
  64. async_orm_kit/query/planning/passes/base.py +47 -0
  65. async_orm_kit/query/planning/passes/cartesian_detection.py +74 -0
  66. async_orm_kit/query/planning/passes/depth_splitting.py +78 -0
  67. async_orm_kit/query/planning/passes/join_promotion.py +81 -0
  68. async_orm_kit/query/planning/passes/path_conflicts.py +103 -0
  69. async_orm_kit/query/planning/passes/redundant_path.py +67 -0
  70. async_orm_kit/query/planning/passes/strategy_selection.py +49 -0
  71. async_orm_kit/query/planning/planner.py +160 -0
  72. async_orm_kit/query/planning/protocols.py +78 -0
  73. async_orm_kit/query/postgres/__init__.py +0 -0
  74. async_orm_kit/query/postgres/optimizer.py +189 -0
  75. async_orm_kit/query/q.py +596 -0
  76. async_orm_kit/query/relations/__init__.py +0 -0
  77. async_orm_kit/query/relations/enums.py +16 -0
  78. async_orm_kit/query/relations/graph.py +908 -0
  79. async_orm_kit/query/relations/node.py +421 -0
  80. async_orm_kit/queryset/__init__.py +4 -0
  81. async_orm_kit/queryset/builder.py +1259 -0
  82. async_orm_kit/queryset/compound.py +379 -0
  83. async_orm_kit/queryset/executor.py +718 -0
  84. async_orm_kit/queryset/queryset.py +1726 -0
  85. async_orm_kit/queryset/state.py +164 -0
  86. async_orm_kit/queryset/validators.py +361 -0
  87. async_orm_kit/queryset/writer.py +755 -0
  88. async_orm_kit/types.py +23 -0
  89. async_orm_kit-1.0.0.dist-info/METADATA +318 -0
  90. async_orm_kit-1.0.0.dist-info/RECORD +92 -0
  91. async_orm_kit-1.0.0.dist-info/WHEEL +4 -0
  92. async_orm_kit-1.0.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,251 @@
1
+ """Typed JSON and PostgreSQL ARRAY expressions.
2
+
3
+ These expressions resolve mapped columns through the normal field resolver; JSON
4
+ path selectors use SQLAlchemy's dialect-aware JSON operators (not string-built
5
+ SQL). PostgreSQL-only operators require PostgreSQL column types and compile with
6
+ bound parameters. Unsupported combinations raise instead of falling back to
7
+ LIKE comparisons or incorrect indexes.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from typing import Any, Literal
13
+
14
+ from sqlalchemy import Integer, func
15
+ from sqlalchemy.dialects.postgresql import ARRAY, JSONB
16
+ from sqlalchemy.exc import CompileError
17
+ from sqlalchemy.ext.compiler import compiles
18
+ from sqlalchemy.sql.elements import ColumnElement
19
+ from sqlalchemy.sql.sqltypes import JSON
20
+ from sqlalchemy.sql.visitors import InternalTraversal
21
+
22
+ from async_orm_kit.query.expressions.functions import _operand
23
+ from async_orm_kit.query.lookups.resolver import ResolverContext
24
+ from async_orm_kit.query.mixins.combinable import CombinableMixin
25
+ from async_orm_kit.query.mixins.orderable import OrderableMixin
26
+
27
+
28
+ class _PostgreSQLOnly(ColumnElement):
29
+ """Prevent PostgreSQL-only predicates from silently compiling elsewhere."""
30
+
31
+ inherit_cache = True
32
+ _traverse_internals = [("_wrapped", InternalTraversal.dp_clauseelement)]
33
+
34
+ def __init__(self, expression):
35
+ self._wrapped = expression
36
+ self.type = expression.type
37
+
38
+
39
+ @compiles(_PostgreSQLOnly, "postgresql")
40
+ def _compile_postgresql(element, compiler, **kwargs):
41
+ return compiler.process(element._wrapped, **kwargs)
42
+
43
+
44
+ @compiles(_PostgreSQLOnly)
45
+ def _reject_other_dialects(element, compiler, **kwargs):
46
+ raise CompileError("This JSONB/ARRAY operation is supported only on PostgreSQL")
47
+
48
+
49
+ class _TypedExpression(CombinableMixin, OrderableMixin):
50
+ __slots__ = ()
51
+
52
+
53
+ def _json_source(expression: Any, resolver: Any, context: ResolverContext):
54
+ source = _operand(expression, resolver, context)
55
+ if not isinstance(source.type, JSON):
56
+ raise TypeError("JSON expressions require a SQLAlchemy JSON/JSONB column")
57
+ return source
58
+
59
+
60
+ def _postgres_array(expression: Any, resolver: Any, context: ResolverContext):
61
+ source = _operand(expression, resolver, context)
62
+ if not isinstance(source.type, ARRAY):
63
+ raise TypeError("ARRAY expressions require a PostgreSQL ARRAY column")
64
+ return source
65
+
66
+
67
+ class JSONPath(_TypedExpression):
68
+ """Extract a JSON path, retaining an explicit result type.
69
+
70
+ ``JSONPath(F('payload'), 'customer', 'id', as_type='integer')`` compiles
71
+ with the JSON operators of the active SQLAlchemy dialect. The path is a
72
+ SQLAlchemy JSON index parameter, not interpolated SQL text.
73
+
74
+ ``as_type`` can be ``json``, ``string``, ``integer``, ``float`` or
75
+ ``boolean``. JSON null and SQL NULL have distinct semantics and should be
76
+ tested for each intended dialect.
77
+ """
78
+
79
+ __slots__ = ("expression", "path", "as_type")
80
+ _types = frozenset({"json", "string", "integer", "float", "boolean"})
81
+
82
+ def __init__(
83
+ self,
84
+ expression: Any,
85
+ *path: str | int,
86
+ as_type: Literal["json", "string", "integer", "float", "boolean"] = "json",
87
+ ):
88
+ if not path:
89
+ raise ValueError("JSONPath requires at least one path segment")
90
+ if any(
91
+ isinstance(part, bool)
92
+ or not isinstance(part, (str, int))
93
+ or (isinstance(part, str) and not part)
94
+ or (isinstance(part, int) and part < 0)
95
+ for part in path
96
+ ):
97
+ raise ValueError(
98
+ "JSONPath segments must be non-empty keys or nonnegative indexes"
99
+ )
100
+ if as_type not in self._types:
101
+ raise ValueError(f"Unsupported JSON result type: {as_type!r}")
102
+ self.expression = expression
103
+ self.path = tuple(path)
104
+ self.as_type = as_type
105
+
106
+ def resolve(self, field_resolver, context: ResolverContext = ResolverContext()):
107
+ source = _json_source(self.expression, field_resolver, context)
108
+ # Multi-segment JSON paths compile to #> for PostgreSQL and
109
+ # json_extract/JSON_EXTRACT for SQLite/MySQL.
110
+ selected = source[self.path[0] if len(self.path) == 1 else self.path]
111
+ if self.as_type == "json":
112
+ return selected
113
+ return getattr(selected, f"as_{self.as_type}")()
114
+
115
+
116
+ class JSONHasKey(_TypedExpression):
117
+ """PostgreSQL JSONB top-level key-existence operator ``?``.
118
+
119
+ The JSONB comparator is required to prevent SQLAlchemy's generic string
120
+ containment behavior from being used on SQLite or MySQL JSON values.
121
+ """
122
+
123
+ __slots__ = ("expression", "key")
124
+
125
+ def __init__(self, expression: Any, key: str):
126
+ if not isinstance(key, str) or not key:
127
+ raise ValueError("JSONHasKey requires a non-empty key")
128
+ self.expression = expression
129
+ self.key = key
130
+
131
+ def resolve(self, field_resolver, context: ResolverContext = ResolverContext()):
132
+ source = _json_source(self.expression, field_resolver, context)
133
+ if not isinstance(source.type, JSONB):
134
+ raise TypeError("JSONHasKey requires a PostgreSQL JSONB column")
135
+ return _PostgreSQLOnly(source.has_key(self.key)) # noqa: W601
136
+
137
+
138
+ class JSONContains(_TypedExpression):
139
+ """PostgreSQL JSONB containment ``@>`` with a bound JSON value."""
140
+
141
+ __slots__ = ("expression", "value")
142
+
143
+ def __init__(self, expression: Any, value: dict | list):
144
+ if not isinstance(value, (dict, list)):
145
+ raise TypeError("JSONContains expects a JSON object or array")
146
+ self.expression = expression
147
+ self.value = value
148
+
149
+ def resolve(self, field_resolver, context: ResolverContext = ResolverContext()):
150
+ source = _json_source(self.expression, field_resolver, context)
151
+ if not isinstance(source.type, JSONB):
152
+ raise TypeError("JSONContains requires a PostgreSQL JSONB column")
153
+ return _PostgreSQLOnly(source.contains(self.value))
154
+
155
+
156
+ class ArrayIndex(_TypedExpression):
157
+ """Index a PostgreSQL ARRAY using the SQLAlchemy column's index convention.
158
+
159
+ Normally PostgreSQL arrays are 1-indexed. If the mapped SQLAlchemy ARRAY
160
+ has ``zero_indexes=True``, SQLAlchemy adjusts indexes automatically.
161
+ """
162
+
163
+ __slots__ = ("expression", "index")
164
+
165
+ def __init__(self, expression: Any, index: int):
166
+ if isinstance(index, bool) or not isinstance(index, int) or index < 0:
167
+ raise ValueError("ArrayIndex requires a nonnegative integer")
168
+ self.expression = expression
169
+ self.index = index
170
+
171
+ def resolve(self, field_resolver, context: ResolverContext = ResolverContext()):
172
+ source = _postgres_array(self.expression, field_resolver, context)
173
+ if self.index == 0 and not source.type.zero_indexes:
174
+ raise ValueError(
175
+ "PostgreSQL ARRAY indexes start at 1 unless zero_indexes=True"
176
+ )
177
+ return _PostgreSQLOnly(source[self.index])
178
+
179
+
180
+ class ArrayContains(_TypedExpression):
181
+ """PostgreSQL ARRAY containment ``@>``."""
182
+
183
+ __slots__ = ("expression", "values")
184
+
185
+ def __init__(self, expression: Any, values: list | tuple):
186
+ if not isinstance(values, (list, tuple)):
187
+ raise TypeError("ArrayContains expects a list or tuple")
188
+ self.expression = expression
189
+ self.values = list(values)
190
+
191
+ def resolve(self, field_resolver, context: ResolverContext = ResolverContext()):
192
+ return _PostgreSQLOnly(
193
+ _postgres_array(self.expression, field_resolver, context).contains(
194
+ self.values
195
+ )
196
+ )
197
+
198
+
199
+ class ArrayOverlap(_TypedExpression):
200
+ """PostgreSQL ARRAY overlap ``&&``."""
201
+
202
+ __slots__ = ("expression", "values")
203
+
204
+ def __init__(self, expression: Any, values: list | tuple):
205
+ if not isinstance(values, (list, tuple)):
206
+ raise TypeError("ArrayOverlap expects a list or tuple")
207
+ self.expression = expression
208
+ self.values = list(values)
209
+
210
+ def resolve(self, field_resolver, context: ResolverContext = ResolverContext()):
211
+ return _PostgreSQLOnly(
212
+ _postgres_array(self.expression, field_resolver, context).overlap(
213
+ self.values
214
+ )
215
+ )
216
+
217
+
218
+ class ArrayLength(_TypedExpression):
219
+ """PostgreSQL ``array_length(array, dimension)``."""
220
+
221
+ __slots__ = ("expression", "dimension")
222
+
223
+ def __init__(self, expression: Any, dimension: int = 1):
224
+ if (
225
+ isinstance(dimension, bool)
226
+ or not isinstance(dimension, int)
227
+ or dimension < 1
228
+ ):
229
+ raise ValueError("ArrayLength dimension must be a positive integer")
230
+ self.expression = expression
231
+ self.dimension = dimension
232
+
233
+ def resolve(self, field_resolver, context: ResolverContext = ResolverContext()):
234
+ return _PostgreSQLOnly(
235
+ func.array_length(
236
+ _postgres_array(self.expression, field_resolver, context),
237
+ self.dimension,
238
+ type_=Integer(),
239
+ )
240
+ )
241
+
242
+
243
+ __all__ = [
244
+ "ArrayContains",
245
+ "ArrayIndex",
246
+ "ArrayLength",
247
+ "ArrayOverlap",
248
+ "JSONContains",
249
+ "JSONHasKey",
250
+ "JSONPath",
251
+ ]
@@ -0,0 +1,362 @@
1
+ import operator
2
+ from typing import Any
3
+
4
+ from sqlalchemy import func
5
+ from sqlalchemy.sql.elements import ClauseElement, literal
6
+
7
+ from async_orm_kit.query.expressions.case import Case
8
+ from async_orm_kit.query.expressions.value import Value
9
+ from async_orm_kit.query.lookups.resolver import ResolverContext
10
+ from async_orm_kit.query.mixins.combinable import CombinableMixin
11
+ from async_orm_kit.query.mixins.orderable import OrderableMixin
12
+ from async_orm_kit.types import SAFE_SCALAR_TYPES
13
+
14
+
15
+ class F(CombinableMixin, OrderableMixin):
16
+ """Reference to a model field for database-level operations and expressions.
17
+
18
+ F objects enable database-side computation by referencing field names that get
19
+ resolved to actual columns at query time. They support arithmetic operations,
20
+ comparisons, and can traverse relationships using double-underscore syntax.
21
+ Operations on F objects create FExpression instances that resolve to SQLAlchemy
22
+ expressions, allowing efficient database-side calculations without loading data
23
+ into Python.
24
+
25
+ Common use cases include updating fields based on their current values, computing
26
+ derived fields in annotations, and building complex filter conditions that compare
27
+ fields to each other rather than to literal values.
28
+
29
+ Args:
30
+ name: Field name or relationship path using double-underscore syntax.
31
+ Examples: "price", "quantity", "order__customer__name"
32
+
33
+ Note:
34
+ F objects are lazy - they don't execute queries until the QuerySet is evaluated.
35
+ Operations create expression trees that resolve to SQL at query compilation time.
36
+ Relationship traversal automatically registers joins with the QueryBuilder,
37
+ ensuring proper JOIN clauses in the generated SQL.
38
+ """
39
+
40
+ _name: str
41
+ __slots__ = ("_name",)
42
+
43
+ @property
44
+ def name(self) -> str:
45
+ return self._name
46
+
47
+ def __setattr__(self, name: str, value: Any) -> None:
48
+ raise AttributeError("F expressions are immutable.")
49
+
50
+ def __init__(self, name: str) -> None:
51
+ """Initialize F object with field name or relationship path.
52
+
53
+ Args:
54
+ name: Field name or relationship path.
55
+ Use double-underscore for relationships: "field_name" or "relation__field_name"
56
+
57
+ Example:
58
+ >>> F("price")
59
+ >>> F("order__customer__email")
60
+ """
61
+ if not isinstance(name, str) or not name:
62
+ raise ValueError("F() requires a non-empty field name.")
63
+ object.__setattr__(self, "_name", name)
64
+
65
+ def resolve(self, field_resolver, context: ResolverContext = ResolverContext()):
66
+ """Resolve field name to SQLAlchemy column expression.
67
+
68
+ Uses FieldResolver to handle relationship traversal and join registration.
69
+ The resolver maintains join state across the entire query, ensuring F objects
70
+ in different parts of the query (filters, annotations, order_by) share the
71
+ same join strategy.
72
+
73
+ Args:
74
+ field_resolver: FieldResolver instance for column resolution and join tracking.
75
+ context: ResolverContext for OR context and filter marking propagation.
76
+
77
+ Returns:
78
+ SQLAlchemy Column object representing the resolved field.
79
+
80
+ Example:
81
+ >>> # Internal usage during query compilation
82
+ >>> f = F("order__customer__name")
83
+ >>> column = f.resolve(field_resolver, context)
84
+ >>> # Returns Customer.name column with joins registered
85
+ """
86
+ resolution = field_resolver.resolve_to_column(key=self.name, context=context)
87
+ return resolution.column
88
+
89
+ def __pow__(self, other):
90
+ """Power operation: F("x") ** 2 creates exponentiation expression."""
91
+ return FExpression(self, "pow", other)
92
+
93
+ def __rpow__(self, other):
94
+ """Reverse power: 2 ** F("x") creates exponentiation expression."""
95
+ return FExpression(other, "pow", self)
96
+
97
+ def __eq__(self, other):
98
+ """Equality comparison: F("x") == value creates comparison expression."""
99
+ return FExpression(self, operator.eq, other)
100
+
101
+ def __ne__(self, other):
102
+ """Inequality comparison: F("x") != value creates comparison expression."""
103
+ return FExpression(self, operator.ne, other)
104
+
105
+ def __lt__(self, other):
106
+ """Less than: F("x") < value creates comparison expression."""
107
+ return FExpression(self, operator.lt, other)
108
+
109
+ def __le__(self, other):
110
+ """Less than or equal: F("x") <= value creates comparison expression."""
111
+ return FExpression(self, operator.le, other)
112
+
113
+ def __gt__(self, other):
114
+ """Greater than: F("x") > value creates comparison expression."""
115
+ return FExpression(self, operator.gt, other)
116
+
117
+ def __ge__(self, other):
118
+ """Greater than or equal: F("x") >= value creates comparison expression."""
119
+ return FExpression(self, operator.ge, other)
120
+
121
+ def __pos__(self):
122
+ """Unary plus: +F("x") returns self unchanged."""
123
+ return self
124
+
125
+ def __repr__(self) -> str:
126
+ """Return string representation for debugging."""
127
+ return f"F('{self.name}')"
128
+
129
+ def __hash__(self) -> int:
130
+ """Return hash based on field name for use in sets and dicts."""
131
+ return hash(self.name)
132
+
133
+
134
+ class FExpression(CombinableMixin, OrderableMixin):
135
+ """Complex expression tree built from F objects and operations.
136
+
137
+ FExpression represents the result of operations on F objects, creating an
138
+ expression tree that resolves to SQLAlchemy expressions at query time. Supports
139
+ arithmetic operations (``+``, ``-``, ``*``, ``/``, ``//``, ``%``, ``**``),
140
+ comparisons (``==``, ``!=``, ``<``, ``<=``, ``>``, ``>=``),
141
+ bitwise operations (``&``, ``|``, ``^``), and unary operations (-, ~, abs).
142
+
143
+ Expressions can be nested arbitrarily deep and combined with literals, other F
144
+ objects, Value() wrappers, Case() conditionals, and Aggregate functions. The
145
+ expression tree is evaluated lazily during query compilation, generating efficient
146
+ SQL that performs all computation database-side.
147
+
148
+ Args:
149
+ left: Left operand - can be F, FExpression, Value, Case, Aggregate, scalar,
150
+ or None for unary operations.
151
+ op: Operator function from operator module or special string like "pow", "abs".
152
+ right: Right operand - same types as left.
153
+
154
+ Note:
155
+ Expressions are immutable - each operation creates a new FExpression instance.
156
+ This enables safe reuse of expression components across multiple queries.
157
+
158
+ Resolution is recursive - complex nested expressions resolve from leaves to
159
+ root, with each operand resolving independently before the operation is applied.
160
+ """
161
+
162
+ _left: Any
163
+ _operator: Any
164
+ _right: Any
165
+ __slots__ = ("_left", "_operator", "_right")
166
+
167
+ @property
168
+ def left(self):
169
+ return self._left
170
+
171
+ @property
172
+ def operator(self):
173
+ return self._operator
174
+
175
+ @property
176
+ def right(self):
177
+ return self._right
178
+
179
+ def __setattr__(self, name: str, value: Any) -> None:
180
+ raise AttributeError("FExpression objects are immutable.")
181
+
182
+ def __init__(self, left, op, right):
183
+ """Initialize expression with operands and operator.
184
+
185
+ Args:
186
+ left: Left operand or None for unary operations.
187
+ op: Operator function or special string ("pow", "abs").
188
+ right: Right operand.
189
+ """
190
+ allowed_operators = {
191
+ operator.add,
192
+ operator.sub,
193
+ operator.mul,
194
+ operator.truediv,
195
+ operator.mod,
196
+ operator.neg,
197
+ operator.eq,
198
+ operator.ne,
199
+ operator.lt,
200
+ operator.le,
201
+ operator.gt,
202
+ operator.ge,
203
+ "pow",
204
+ "abs",
205
+ "floordiv",
206
+ "bit_and",
207
+ "bit_or",
208
+ "bit_not",
209
+ }
210
+ if op not in allowed_operators:
211
+ raise ValueError(f"Unsupported F expression operator: {op!r}")
212
+ object.__setattr__(self, "_left", left)
213
+ object.__setattr__(self, "_operator", op)
214
+ object.__setattr__(self, "_right", right)
215
+
216
+ def resolve(
217
+ self,
218
+ field_resolver,
219
+ context: ResolverContext = ResolverContext(),
220
+ ):
221
+ """Resolve expression tree to SQLAlchemy expression.
222
+
223
+ Recursively resolves operands then applies the operator.
224
+ Handles special function-based operations (abs, pow), unary operations (left=None), and
225
+ standard binary operations.
226
+
227
+ Args:
228
+ field_resolver: FieldResolver instance for column resolution and join tracking.
229
+ context: ResolverContext for OR context and filter marking propagation.
230
+
231
+ Returns:
232
+ SQLAlchemy expression representing the resolved operation.
233
+
234
+ Example:
235
+ >>> # Internal usage during query compilation
236
+ >>> expr = F("price") * F("quantity")
237
+ >>> sql_expr = expr.resolve(field_resolver, context)
238
+ >>> # Returns: products.price * products.quantity
239
+ """
240
+ left_val = self._resolve_operand(self.left, field_resolver, context)
241
+ right_val = self._resolve_operand(self.right, field_resolver, context)
242
+
243
+ # Handle special function-based operations
244
+ if self.operator == "abs":
245
+ return func.abs(right_val)
246
+ if self.operator == "floordiv":
247
+ return func.floor(left_val / right_val)
248
+ if self.operator == "bit_and":
249
+ return left_val.bitwise_and(right_val)
250
+ if self.operator == "bit_or":
251
+ return left_val.bitwise_or(right_val)
252
+ if self.operator == "bit_not":
253
+ return right_val.bitwise_not()
254
+ elif self.operator == "pow":
255
+ return func.power(left_val, right_val)
256
+
257
+ # Handle unary operations
258
+ if self.left is None:
259
+ return self.operator(right_val)
260
+
261
+ # Handle binary operations
262
+ return self.operator(left_val, right_val)
263
+
264
+ def _resolve_operand(
265
+ self, operand, field_resolver, context: ResolverContext = ResolverContext()
266
+ ):
267
+ """Resolve operand to SQLAlchemy expression or literal.
268
+
269
+ Handles multiple operand types with priority-based resolution:
270
+ 1. None - Returns None for unary operations
271
+ 2. F/FExpression - Resolves to column/expression
272
+ 3. Value/Case - Resolves to literal/conditional
273
+ 4. Aggregate - Resolves to aggregate function
274
+ 5. ClauseElement - SQLAlchemy expression passed through
275
+ 6. Safe scalars - Wrapped in literal()
276
+ 7. Other types - Raises TypeError
277
+
278
+ Args:
279
+ operand: Value to resolve.
280
+ field_resolver: FieldResolver for column resolution.
281
+ context: ResolverContext for propagation.
282
+
283
+ Returns:
284
+ SQLAlchemy expression or literal value.
285
+
286
+ Raises:
287
+ TypeError: If operand type is unsupported.
288
+ """
289
+ from async_orm_kit.query.aggregations.base import Aggregate
290
+
291
+ if operand is None:
292
+ return None
293
+
294
+ if isinstance(operand, (F, FExpression)):
295
+ return operand.resolve(field_resolver, context)
296
+
297
+ if isinstance(operand, (Value, Case)):
298
+ return operand.resolve(field_resolver, context)
299
+
300
+ if isinstance(operand, Aggregate):
301
+ return operand.resolve(field_resolver=field_resolver, context=context)
302
+
303
+ if isinstance(operand, CombinableMixin) and hasattr(operand, "resolve"):
304
+ return operand.resolve(field_resolver=field_resolver, context=context)
305
+
306
+ if isinstance(operand, ClauseElement):
307
+ return operand
308
+
309
+ if isinstance(operand, SAFE_SCALAR_TYPES):
310
+ return literal(operand)
311
+
312
+ raise TypeError(
313
+ f"Unsupported operand type in F expression: {type(operand).__name__!r}. "
314
+ f"Use F(), Value(), Case(), or a scalar (int, float, str, etc.)."
315
+ )
316
+
317
+ # ── Operations NOT provided by CombinableMixin ──
318
+
319
+ def __pow__(self, other):
320
+ """Power operation: expr ** 2 creates nested exponentiation."""
321
+ return FExpression(self, "pow", other)
322
+
323
+ def __rpow__(self, other):
324
+ """Reverse power: 2 ** expr creates nested exponentiation."""
325
+ return FExpression(other, "pow", self)
326
+
327
+ def __eq__(self, other):
328
+ """Equality comparison: expr == value creates comparison."""
329
+ return FExpression(self, operator.eq, other)
330
+
331
+ def __ne__(self, other):
332
+ """Inequality comparison: expr != value creates comparison."""
333
+ return FExpression(self, operator.ne, other)
334
+
335
+ def __lt__(self, other):
336
+ """Less than: expr < value creates comparison."""
337
+ return FExpression(self, operator.lt, other)
338
+
339
+ def __le__(self, other):
340
+ """Less than or equal: expr <= value creates comparison."""
341
+ return FExpression(self, operator.le, other)
342
+
343
+ def __gt__(self, other):
344
+ """Greater than: expr > value creates comparison."""
345
+ return FExpression(self, operator.gt, other)
346
+
347
+ def __ge__(self, other):
348
+ """Greater than or equal: expr >= value creates comparison."""
349
+ return FExpression(self, operator.ge, other)
350
+
351
+ def __hash__(self) -> int:
352
+ """Return hash based on operand identities for use in sets and dicts."""
353
+ return hash((id(self.left), id(self.operator), id(self.right)))
354
+
355
+ def __repr__(self) -> str:
356
+ """Return string representation showing expression tree structure."""
357
+ op_name = (
358
+ self.operator.__name__
359
+ if hasattr(self.operator, "__name__")
360
+ else str(self.operator)
361
+ )
362
+ return f"FExpression({self.left!r}, {op_name}, {self.right!r})"