11import json
22import logging
3+ from collections import defaultdict , deque
34from typing import Generator , Iterable , Iterator , List , Optional , Type
45
56from django .core .serializers .json import DjangoJSONEncoder
2526class SerializeTask (object ):
2627 """Carrier class for providing context through the pipeline"""
2728
28- __slots__ = ("model" , "obj" , "store" , "counter" )
29+ __slots__ = (
30+ "model" ,
31+ "obj" ,
32+ "store" ,
33+ "counter" ,
34+ "_self_ref_fk_value" ,
35+ "_self_ref_order" ,
36+ )
2937
3038 def __init__ (self , model : Type [SyncableModel ], obj : SyncableModel ):
3139 self .model = model
3240 self .obj = obj
3341 self .store : Optional [Store ] = None
3442 self .counter : Optional [RecordMaxCounter ] = None
43+ self ._self_ref_fk_value : Optional [str ] = None
44+ self ._self_ref_order : Optional [int ] = None
3545
3646 @property
3747 def is_store_update (self ):
@@ -51,6 +61,20 @@ def self_referential_fk(self) -> Optional[str]:
5161 """Return the attname of the self-referential FK on *model*, or ``None``."""
5262 return self_referential_fk (self .model )
5363
64+ @property
65+ def self_ref_fk_value (self ) -> Optional [str ]:
66+ return self ._self_ref_fk_value
67+
68+ @property
69+ def self_ref_order (self ) -> Optional [int ]:
70+ return self ._self_ref_order
71+
72+ def set_self_ref_fk_value (self , value : Optional [str ]):
73+ self ._self_ref_fk_value = value
74+
75+ def set_self_ref_order (self , value : Optional [int ]):
76+ self ._self_ref_order = value
77+
5478
5579class AppModelSource (Source [SerializeTask ]):
5680 """
@@ -138,6 +162,84 @@ def transform(self, tasks: List[SerializeTask]) -> List[SerializeTask]:
138162 return tasks
139163
140164
165+ class SelfRefOrderLookup (Transform [List [SerializeTask ]]):
166+ """
167+ Computes self-referential metadata for a buffered batch of tasks.
168+
169+ Resolution order is cache-first, then DB fallback:
170+ - roots (`_self_ref_fk == ""`) get order 0 immediately
171+ - child order is `parent_order + 1` when parent order is known
172+ - unresolved/missing parents keep order as ``None``
173+ """
174+
175+ def __init__ (self ):
176+ # Carries resolved parent orders across buffered chunks in the same pipeline run.
177+ self .known_order_by_id = {}
178+
179+ def transform (self , tasks : List [SerializeTask ]) -> List [SerializeTask ]:
180+ # Cache of resolved order values keyed by record id.
181+ known_order_by_id = self .known_order_by_id
182+ # Self-ref tasks present in the current buffered batch.
183+ tasks_by_id = {}
184+ # Adjacency list for the in-batch self-ref graph: parent_id -> [child tasks].
185+ children_by_parent = defaultdict (list )
186+
187+ # First pass:
188+ # - identify roots (order = 0)
189+ # - build lookup maps:
190+ # tasks_by_id → tasks present in this batch
191+ # children_by_parent → {parent_id → list of child tasks}
192+ for task in tasks :
193+ self_ref_fk = task .self_referential_fk ()
194+ if not self_ref_fk :
195+ task .set_self_ref_fk_value (None )
196+ task .set_self_ref_order (None )
197+ continue
198+
199+ self_ref_fk_value = getattr (task .obj , self_ref_fk ) or ""
200+ task .set_self_ref_fk_value (self_ref_fk_value )
201+ tasks_by_id [task .obj .id ] = task
202+
203+ if not self_ref_fk_value :
204+ task .set_self_ref_order (0 )
205+ known_order_by_id [task .obj .id ] = 0
206+ continue
207+
208+
209+ task .set_self_ref_order (None )
210+ children_by_parent [self_ref_fk_value ].append (task )
211+
212+ # Parent IDs that are referenced by this batch but not present in this batch.
213+ # These must be looked up from existing Store rows.
214+ external_parent_ids = set (children_by_parent .keys ()) - set (tasks_by_id .keys ())
215+ if external_parent_ids :
216+ for parent_id , parent_order in Store .objects .filter (
217+ id__in = external_parent_ids
218+ ).values_list ("id" , "_self_ref_order" ):
219+ if parent_order is not None :
220+ known_order_by_id [parent_id ] = parent_order
221+
222+ # Breadth-first propagation from all known parents (roots + DB-resolved parents).
223+ # Each resolved parent unlocks its children as `parent + 1`.
224+ queue = deque (known_order_by_id .keys ())
225+ while queue :
226+ parent_id = queue .popleft ()
227+ parent_order = known_order_by_id .get (parent_id )
228+ if parent_order is None :
229+ continue
230+
231+ for task in children_by_parent .get (parent_id , []):
232+ if task .self_ref_order is not None :
233+ continue
234+ child_order = parent_order + 1
235+ task .set_self_ref_order (child_order )
236+ child_id = task .obj .id
237+ known_order_by_id [child_id ] = child_order
238+ queue .append (child_id )
239+
240+ return tasks
241+
242+
141243class StoreUpdate (Transform [SerializeTask ]):
142244 """Processes the updates to the Morango store and record counters."""
143245
@@ -188,10 +290,12 @@ def _handle_store_update(self, task: SerializeTask):
188290
189291 self_ref_fk = task .self_referential_fk ()
190292 if self_ref_fk :
191- new_fk_value = getattr (task .obj , self_ref_fk ) or ""
293+ new_fk_value = task .self_ref_fk_value
294+ if new_fk_value is None :
295+ new_fk_value = getattr (task .obj , self_ref_fk ) or ""
192296 if new_fk_value != task .store ._self_ref_fk :
193297 task .store ._self_ref_fk = new_fk_value
194- task .store ._self_ref_order = self . _compute_self_ref_order ( new_fk_value )
298+ task .store ._self_ref_order = task . self_ref_order
195299
196300 def _handle_store_create (self , task : SerializeTask ):
197301 kwargs = {
@@ -207,29 +311,11 @@ def _handle_store_create(self, task: SerializeTask):
207311
208312 self_ref_fk = task .self_referential_fk ()
209313 if self_ref_fk :
210- self_ref_fk_value = getattr (task .obj , self_ref_fk ) or ""
211- kwargs ["_self_ref_fk" ] = self_ref_fk_value
212- kwargs ["_self_ref_order" ] = self ._compute_self_ref_order (self_ref_fk_value )
314+ kwargs ["_self_ref_fk" ] = task .self_ref_fk_value
315+ kwargs ["_self_ref_order" ] = task .self_ref_order
213316
214317 task .set_store (Store (** kwargs ))
215318
216- @staticmethod
217- def _compute_self_ref_order (self_ref_fk_value ):
218- """
219- Compute ``_self_ref_order`` for a self-referential store record.
220-
221- Returns ``0`` when the record has no parent (root), otherwise queries
222- the parent ``Store`` row and returns the next order value.
223- """
224- if not self_ref_fk_value :
225- return 0
226- parent_order = (
227- Store .objects .filter (id = self_ref_fk_value )
228- .values_list ("_self_ref_order" , flat = True )
229- .first ()
230- )
231- return parent_order + 1 if parent_order is not None else None
232-
233319
234320class ModelPartitionBuffer (Buffer [List [SerializeTask ]]):
235321 """Buffers tasks into chunks that have the same model class."""
@@ -419,6 +505,7 @@ def serialize_into_store(
419505 AppModelSource (profile , sync_filter = sync_filter , dirty_only = dirty_only )
420506 .pipe (Buffer (size = 500 ))
421507 .pipe (StoreLookup (current_id ))
508+ .pipe (SelfRefOrderLookup ())
422509 .pipe (Unbuffer ())
423510 .pipe (StoreUpdate (current_id ))
424511 .pipe (ModelPartitionBuffer (size = 500 ))
0 commit comments