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,421 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import TYPE_CHECKING, Any
|
|
5
|
+
|
|
6
|
+
from .enums import JoinType
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from sqlalchemy.orm import RelationshipProperty
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(slots=True)
|
|
13
|
+
class JoinNode:
|
|
14
|
+
"""Represents a single JOIN operation in the query's JOIN graph.
|
|
15
|
+
|
|
16
|
+
JoinNode is a tree node that tracks metadata about a relationship traversal
|
|
17
|
+
in a Django-style query. Each node corresponds to one relationship in a path
|
|
18
|
+
like "building__city__country", forming a tree structure where the root
|
|
19
|
+
represents the base model and children represent joined relationships.
|
|
20
|
+
|
|
21
|
+
The node stores usage flags that influence JOIN type optimization during
|
|
22
|
+
query compilation. For example, a node marked with used_in_filter=True may
|
|
23
|
+
be promoted from LEFT JOIN to INNER JOIN if it doesn't appear in OR/NOT
|
|
24
|
+
contexts or nullable checks.
|
|
25
|
+
|
|
26
|
+
This is a pure data container with minimal logic - JOIN type optimization
|
|
27
|
+
and SQL compilation are handled by JoinGraph and QueryCompiler respectively.
|
|
28
|
+
|
|
29
|
+
Attributes:
|
|
30
|
+
path: Django-style relationship path from root model using double
|
|
31
|
+
underscores. Examples: "", "building", "building__city__country".
|
|
32
|
+
Empty string for synthetic root node.
|
|
33
|
+
alias: SQLAlchemy aliased class for this joined table. Populated during
|
|
34
|
+
SQL compilation by QueryCompiler, not during graph construction.
|
|
35
|
+
join_type: Current JOIN type (INNER or LEFT). May be mutated by
|
|
36
|
+
JoinGraph optimization passes based on usage flags.
|
|
37
|
+
relationship: SQLAlchemy RelationshipProperty defining the foreign key
|
|
38
|
+
relationship. None only for the synthetic root node.
|
|
39
|
+
parent: Parent node in the JOIN tree. None for root node.
|
|
40
|
+
children: Direct child nodes representing relationships joined from this
|
|
41
|
+
node's model.
|
|
42
|
+
used_in_filter: True if WHERE clause references columns through this
|
|
43
|
+
JOIN. Enables LEFT → INNER promotion unless other flags prevent it.
|
|
44
|
+
used_in_or_context: True if filter using this JOIN is inside OR clause.
|
|
45
|
+
Prevents LEFT → INNER promotion to preserve NULL rows.
|
|
46
|
+
has_nullable_check: True if filter is IS NULL or IS NOT NULL check.
|
|
47
|
+
Prevents LEFT → INNER promotion as it requires NULL rows.
|
|
48
|
+
used_in_negation: True if filter using this JOIN is inside NOT/exclude().
|
|
49
|
+
Prevents LEFT → INNER promotion to preserve NULL rows.
|
|
50
|
+
used_in_order_by: True if ORDER BY references columns through this JOIN.
|
|
51
|
+
Keeps JOIN in query even if not used elsewhere.
|
|
52
|
+
used_in_annotation: True if annotate() references columns through this
|
|
53
|
+
JOIN. Keeps JOIN in query even if not used elsewhere.
|
|
54
|
+
|
|
55
|
+
Example:
|
|
56
|
+
>>> # Building JOIN graph for: Product.objects.filter(
|
|
57
|
+
>>> # category__parent__name="Electronics"
|
|
58
|
+
>>> # )
|
|
59
|
+
>>> root = JoinNode(path="", alias=Product)
|
|
60
|
+
>>> category_node = JoinNode(
|
|
61
|
+
... path="category",
|
|
62
|
+
... relationship=Product.category.property,
|
|
63
|
+
... used_in_filter=True,
|
|
64
|
+
... parent=root
|
|
65
|
+
... )
|
|
66
|
+
>>> parent_node = JoinNode(
|
|
67
|
+
... path="category__parent",
|
|
68
|
+
... relationship=Category.parent.property,
|
|
69
|
+
... used_in_filter=True,
|
|
70
|
+
... parent=category_node
|
|
71
|
+
... )
|
|
72
|
+
>>> print(parent_node.depth) # 2
|
|
73
|
+
>>> print(parent_node.segment) # "parent"
|
|
74
|
+
>>> print(parent_node.ancestors()) # [category_node]
|
|
75
|
+
>>>
|
|
76
|
+
>>> # OR context prevents promotion
|
|
77
|
+
>>> or_node = JoinNode(
|
|
78
|
+
... path="supplier",
|
|
79
|
+
... used_in_filter=True,
|
|
80
|
+
... used_in_or_context=True
|
|
81
|
+
... )
|
|
82
|
+
>>> print(or_node.is_left) # True (stays LEFT JOIN)
|
|
83
|
+
|
|
84
|
+
Note:
|
|
85
|
+
Performance: Property access is O(1) except ancestors() which is O(d)
|
|
86
|
+
where d is depth. Tree operations like add_child() are O(1).
|
|
87
|
+
|
|
88
|
+
The node is immutable after creation except for join_type which may be
|
|
89
|
+
updated by optimization passes in planning. Usage flags should be set during initial
|
|
90
|
+
construction or via JoinGraph.add_node() which handles flag merging.
|
|
91
|
+
|
|
92
|
+
Hash and equality are based solely on path, allowing nodes to be used
|
|
93
|
+
in sets and dicts for deduplication during graph construction.
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
path: str
|
|
97
|
+
alias: Any
|
|
98
|
+
join_type: JoinType = JoinType.LEFT
|
|
99
|
+
relationship: RelationshipProperty | None = None
|
|
100
|
+
parent: JoinNode | None = None
|
|
101
|
+
children: list[JoinNode] = field(default_factory=list)
|
|
102
|
+
|
|
103
|
+
# ── Usage flags (set by the query builder) ──
|
|
104
|
+
used_in_filter: bool = False
|
|
105
|
+
used_in_or_context: bool = False
|
|
106
|
+
has_nullable_check: bool = False
|
|
107
|
+
used_in_negation: bool = False
|
|
108
|
+
used_in_order_by: bool = False
|
|
109
|
+
used_in_annotation: bool = False
|
|
110
|
+
|
|
111
|
+
# ── Internal ──
|
|
112
|
+
_depth: int | None = field(default=None, repr=False)
|
|
113
|
+
|
|
114
|
+
@property
|
|
115
|
+
def depth(self) -> int:
|
|
116
|
+
"""Calculate depth in JOIN tree with caching.
|
|
117
|
+
|
|
118
|
+
Depth represents the number of relationship traversals from root model.
|
|
119
|
+
Root node has depth 0, direct relationships have depth 1, etc. The value
|
|
120
|
+
is computed once on first access and cached for subsequent calls.
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
Integer depth where 0 is root, 1 is direct relationship, 2 is nested
|
|
124
|
+
relationship, etc. Computed by counting "__" separators in path.
|
|
125
|
+
|
|
126
|
+
Example:
|
|
127
|
+
>>> root = JoinNode(path="", alias=Product)
|
|
128
|
+
>>> root.depth # 0
|
|
129
|
+
>>> category = JoinNode(path="category", parent=root)
|
|
130
|
+
>>> category.depth # 1
|
|
131
|
+
>>> parent = JoinNode(path="category__parent", parent=category)
|
|
132
|
+
>>> parent.depth # 2
|
|
133
|
+
|
|
134
|
+
Note:
|
|
135
|
+
Performance: O(n) on first call where n is path length, O(1) on
|
|
136
|
+
subsequent calls due to caching. Cache is stored in _depth field.
|
|
137
|
+
"""
|
|
138
|
+
if self._depth is None:
|
|
139
|
+
self._depth = self.path.count("__") + 1 if self.path else 0
|
|
140
|
+
return self._depth
|
|
141
|
+
|
|
142
|
+
@property
|
|
143
|
+
def is_root(self) -> bool:
|
|
144
|
+
"""Check if this is the synthetic root node.
|
|
145
|
+
|
|
146
|
+
Root node represents the base model being queried and has no parent or
|
|
147
|
+
path. All relationship JOINs are descendants of the root.
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
True if this is the root node (empty path and no parent), False
|
|
151
|
+
otherwise.
|
|
152
|
+
|
|
153
|
+
Example:
|
|
154
|
+
>>> root = JoinNode(path="", alias=Product)
|
|
155
|
+
>>> root.is_root # True
|
|
156
|
+
>>> child = JoinNode(path="category", parent=root)
|
|
157
|
+
>>> child.is_root # False
|
|
158
|
+
"""
|
|
159
|
+
return self.path == "" and self.parent is None
|
|
160
|
+
|
|
161
|
+
@property
|
|
162
|
+
def segment(self) -> str:
|
|
163
|
+
"""Extract the final relationship name from the path.
|
|
164
|
+
|
|
165
|
+
Returns the last component of the double-underscore separated path,
|
|
166
|
+
representing the relationship name on the parent model. For root node
|
|
167
|
+
or single-level paths, returns the entire path.
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
Final path segment (relationship name). Empty string for root node.
|
|
171
|
+
|
|
172
|
+
Example:
|
|
173
|
+
>>> node = JoinNode(path="building__city__country")
|
|
174
|
+
>>> node.segment # "country"
|
|
175
|
+
>>> node = JoinNode(path="category")
|
|
176
|
+
>>> node.segment # "category"
|
|
177
|
+
>>> root = JoinNode(path="")
|
|
178
|
+
>>> root.segment # ""
|
|
179
|
+
"""
|
|
180
|
+
return self.path.rsplit("__", 1)[-1] if self.path else ""
|
|
181
|
+
|
|
182
|
+
@property
|
|
183
|
+
def parent_path(self) -> str:
|
|
184
|
+
"""Extract the parent node's path from this node's path.
|
|
185
|
+
|
|
186
|
+
Removes the final segment from the path to get the parent's path. Used
|
|
187
|
+
during graph construction to locate parent nodes when building the tree.
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
Path of parent node. Empty string for direct children of root.
|
|
191
|
+
|
|
192
|
+
Example:
|
|
193
|
+
>>> node = JoinNode(path="building__city__country")
|
|
194
|
+
>>> node.parent_path # "building__city"
|
|
195
|
+
>>> node = JoinNode(path="category")
|
|
196
|
+
>>> node.parent_path # "">>> root = JoinNode(path="")
|
|
197
|
+
>>> root.parent_path # ""
|
|
198
|
+
"""
|
|
199
|
+
parts = self.path.rsplit("__", 1)
|
|
200
|
+
return parts[0] if len(parts) > 1 else ""
|
|
201
|
+
|
|
202
|
+
@property
|
|
203
|
+
def is_inner(self) -> bool:
|
|
204
|
+
"""Check if this node uses INNER JOIN.
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
True if join_type is INNER, False otherwise.
|
|
208
|
+
|
|
209
|
+
Example:
|
|
210
|
+
>>> node = JoinNode(path="category", join_type=JoinType.INNER)
|
|
211
|
+
>>> node.is_inner # True
|
|
212
|
+
>>> node.is_left # False
|
|
213
|
+
"""
|
|
214
|
+
return self.join_type == JoinType.INNER
|
|
215
|
+
|
|
216
|
+
@property
|
|
217
|
+
def is_left(self) -> bool:
|
|
218
|
+
"""Check if this node uses LEFT JOIN.
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
True if join_type is LEFT, False otherwise.
|
|
222
|
+
|
|
223
|
+
Example:
|
|
224
|
+
>>> node = JoinNode(path="category", join_type=JoinType.LEFT)
|
|
225
|
+
>>> node.is_left # True
|
|
226
|
+
>>> node.is_inner # False
|
|
227
|
+
"""
|
|
228
|
+
return self.join_type == JoinType.LEFT
|
|
229
|
+
|
|
230
|
+
def add_child(self, child: JoinNode) -> None:
|
|
231
|
+
"""Attach a direct child while preserving the graph's tree invariants."""
|
|
232
|
+
if not isinstance(child, JoinNode):
|
|
233
|
+
raise TypeError("child must be a JoinNode instance.")
|
|
234
|
+
if child is self:
|
|
235
|
+
raise ValueError("A join node cannot be its own child.")
|
|
236
|
+
if child.parent is not None and child.parent is not self:
|
|
237
|
+
raise ValueError(
|
|
238
|
+
f"Join node {child.path!r} already belongs to another parent."
|
|
239
|
+
)
|
|
240
|
+
if child.parent_path != self.path:
|
|
241
|
+
raise ValueError(
|
|
242
|
+
f"Join node {child.path!r} is not a direct child of {self.path!r}."
|
|
243
|
+
)
|
|
244
|
+
if any(
|
|
245
|
+
existing is child or existing.path == child.path
|
|
246
|
+
for existing in self.children
|
|
247
|
+
):
|
|
248
|
+
raise ValueError(f"Duplicate child path {child.path!r}.")
|
|
249
|
+
|
|
250
|
+
current: JoinNode | None = self
|
|
251
|
+
while current is not None:
|
|
252
|
+
if current is child:
|
|
253
|
+
raise ValueError("Attaching this node would create a cycle.")
|
|
254
|
+
current = current.parent
|
|
255
|
+
|
|
256
|
+
child.parent = self
|
|
257
|
+
self.children.append(child)
|
|
258
|
+
|
|
259
|
+
def ancestors(self) -> list[JoinNode]:
|
|
260
|
+
"""Return all ancestor nodes from parent to root (excluding root).
|
|
261
|
+
|
|
262
|
+
Walks up the parent chain collecting nodes until reaching the root node.
|
|
263
|
+
Useful for JOIN dependency analysis and path validation. The root node
|
|
264
|
+
itself is excluded from the result.
|
|
265
|
+
|
|
266
|
+
Returns:
|
|
267
|
+
List of ancestor nodes ordered from immediate parent to most distant
|
|
268
|
+
ancestor (closest to root). Empty list if this is root or direct
|
|
269
|
+
child of root.
|
|
270
|
+
|
|
271
|
+
Example:
|
|
272
|
+
>>> # Tree: root -> building -> city -> country
|
|
273
|
+
>>> country.ancestors()
|
|
274
|
+
>>> # Returns: [city, building]>>>
|
|
275
|
+
>>> building.ancestors()
|
|
276
|
+
>>> # Returns: [] (direct child of root)
|
|
277
|
+
>>>
|
|
278
|
+
>>> root.ancestors()
|
|
279
|
+
>>> # Returns: [] (is root)
|
|
280
|
+
|
|
281
|
+
Note:
|
|
282
|
+
Performance: O(d) where d is depth. Allocates a new list on each
|
|
283
|
+
call - cache result if calling multiple times.
|
|
284
|
+
"""
|
|
285
|
+
result: list[JoinNode] = []
|
|
286
|
+
current = self.parent
|
|
287
|
+
while current is not None and not current.is_root:
|
|
288
|
+
result.append(current)
|
|
289
|
+
current = current.parent
|
|
290
|
+
return result
|
|
291
|
+
|
|
292
|
+
def __hash__(self) -> int:
|
|
293
|
+
"""Hash based on path for use in sets and dicts.
|
|
294
|
+
|
|
295
|
+
Allows JoinNode instances to be used as dictionary keys and set members
|
|
296
|
+
for deduplication during graph construction. Only path is used for hash
|
|
297
|
+
to ensure nodes with same path are considered identical.
|
|
298
|
+
|
|
299
|
+
Returns:
|
|
300
|
+
Hash value based on path string.
|
|
301
|
+
|
|
302
|
+
Example:
|
|
303
|
+
>>> node1 = JoinNode(path="category", used_in_filter=True)
|
|
304
|
+
>>> node2 = JoinNode(path="category", used_in_filter=False)
|
|
305
|
+
>>> hash(node1) == hash(node2) # True
|
|
306
|
+
>>> {node1, node2} # Set with one element
|
|
307
|
+
"""
|
|
308
|
+
return hash(self.path)
|
|
309
|
+
|
|
310
|
+
def __eq__(self, other: object) -> bool:
|
|
311
|
+
"""Compare nodes by path for equality.
|
|
312
|
+
|
|
313
|
+
Two nodes are equal if they have the same path, regardless of other
|
|
314
|
+
attributes. This enables deduplication in sets and dicts during graph
|
|
315
|
+
construction.
|
|
316
|
+
|
|
317
|
+
Args:
|
|
318
|
+
other: Object to compare with. Must be JoinNode instance.
|
|
319
|
+
|
|
320
|
+
Returns:
|
|
321
|
+
True if other is JoinNode with same path, False otherwise.
|
|
322
|
+
|
|
323
|
+
Example:
|
|
324
|
+
>>> node1 = JoinNode(path="category", join_type=JoinType.INNER)
|
|
325
|
+
>>> node2 = JoinNode(path="category", join_type=JoinType.LEFT)
|
|
326
|
+
>>> node1 == node2 # True (same path)
|
|
327
|
+
>>> node3 = JoinNode(path="supplier")
|
|
328
|
+
>>> node1 == node3 # False (different path)
|
|
329
|
+
"""
|
|
330
|
+
if not isinstance(other, JoinNode):
|
|
331
|
+
return NotImplemented
|
|
332
|
+
return self.path == other.path
|
|
333
|
+
|
|
334
|
+
def __repr__(self) -> str:
|
|
335
|
+
"""Generate debug representation with path, type, and active flags.
|
|
336
|
+
|
|
337
|
+
Creates a compact string showing the node's path, JOIN type, and any
|
|
338
|
+
active usage flags. Flags are abbreviated: F=filter, OR=or_context,
|
|
339
|
+
NULL=nullable_check, NEG=negation, ORD=order_by, ANN=annotation.
|
|
340
|
+
|
|
341
|
+
Returns:
|
|
342
|
+
String representation suitable for debugging and logging.
|
|
343
|
+
|
|
344
|
+
Example:
|
|
345
|
+
>>> node = JoinNode(
|
|
346
|
+
... path="category",
|
|
347
|
+
... join_type=JoinType.INNER,
|
|
348
|
+
... used_in_filter=True,
|
|
349
|
+
... used_in_or_context=True
|
|
350
|
+
... )
|
|
351
|
+
>>> repr(node)
|
|
352
|
+
"JoinNode('category', inner [F,OR])"
|
|
353
|
+
>>>
|
|
354
|
+
>>> root = JoinNode(path="", alias=Product)
|
|
355
|
+
>>> repr(root)
|
|
356
|
+
"JoinNode('', left)"
|
|
357
|
+
"""
|
|
358
|
+
flags = []
|
|
359
|
+
if self.used_in_filter:
|
|
360
|
+
flags.append("F")
|
|
361
|
+
if self.used_in_or_context:
|
|
362
|
+
flags.append("OR")
|
|
363
|
+
if self.has_nullable_check:
|
|
364
|
+
flags.append("NULL")
|
|
365
|
+
if self.used_in_negation:
|
|
366
|
+
flags.append("NEG")
|
|
367
|
+
if self.used_in_order_by:
|
|
368
|
+
flags.append("ORD")
|
|
369
|
+
if self.used_in_annotation:
|
|
370
|
+
flags.append("ANN")
|
|
371
|
+
flag_str = f" [{','.join(flags)}]" if flags else ""
|
|
372
|
+
return f"JoinNode({self.path!r}, {self.join_type.value}{flag_str})"
|
|
373
|
+
|
|
374
|
+
def _clone(self) -> JoinNode:
|
|
375
|
+
"""Create shallow copy of this node without parent/child relationships.
|
|
376
|
+
|
|
377
|
+
Copies all attributes except parent and children, which are reset to
|
|
378
|
+
None and empty list respectively. Used by JoinGraph.clone() to duplicate
|
|
379
|
+
the graph structure while preserving node state.
|
|
380
|
+
|
|
381
|
+
Returns:
|
|
382
|
+
New JoinNode with same attributes but no tree relationships.
|
|
383
|
+
|
|
384
|
+
Example:
|
|
385
|
+
>>> original = JoinNode(
|
|
386
|
+
... path="category",
|
|
387
|
+
... used_in_filter=True,
|
|
388
|
+
... parent=root
|
|
389
|
+
... )
|
|
390
|
+
>>> clone = original._clone()
|
|
391
|
+
>>> clone.path # "category"
|
|
392
|
+
>>> clone.used_in_filter # True
|
|
393
|
+
>>> clone.parent # None (not copied)
|
|
394
|
+
>>> clone is original # False
|
|
395
|
+
|
|
396
|
+
Note:
|
|
397
|
+
Performance: O(1) operation. Uses __new__ to bypass dataclass
|
|
398
|
+
__init__ for maximum speed. Parent/child relationships must be
|
|
399
|
+
reconstructed by caller (typically JoinGraph.clone()).
|
|
400
|
+
"""
|
|
401
|
+
node = JoinNode.__new__(JoinNode) # skip dataclass __init__
|
|
402
|
+
|
|
403
|
+
# Copy all fields manually (fastest)
|
|
404
|
+
node.path = self.path
|
|
405
|
+
node.alias = self.alias
|
|
406
|
+
node.join_type = self.join_type
|
|
407
|
+
node.relationship = self.relationship
|
|
408
|
+
|
|
409
|
+
node.parent = None # filled later
|
|
410
|
+
node.children = [] # filled later
|
|
411
|
+
|
|
412
|
+
node.used_in_filter = self.used_in_filter
|
|
413
|
+
node.used_in_or_context = self.used_in_or_context
|
|
414
|
+
node.has_nullable_check = self.has_nullable_check
|
|
415
|
+
node.used_in_negation = self.used_in_negation
|
|
416
|
+
node.used_in_order_by = self.used_in_order_by
|
|
417
|
+
node.used_in_annotation = self.used_in_annotation
|
|
418
|
+
|
|
419
|
+
node._depth = self._depth
|
|
420
|
+
|
|
421
|
+
return node
|