@@ -604,3 +604,171 @@ def test_callback_has_no_parent_span_id_when_no_component(self):
604604 assert callback is not None
605605 assert isinstance (callback , NativeCallbackHandler )
606606 assert callback .parent_span_id is None
607+
608+
609+ # ---------------------------------------------------------------------------
610+ # _topological_sort_spans
611+ # ---------------------------------------------------------------------------
612+
613+
614+ class TestTopologicalSortSpans :
615+ """Verify that spans are sorted so parents appear before children.
616+
617+ This is critical for PostgreSQL which enforces FK constraints at INSERT time.
618+ """
619+
620+ @staticmethod
621+ def _make_span (span_id , parent_id = None ) -> tuple [dict , UUID , UUID | None ]:
622+ """Helper to create a resolved (span_data, span_uuid, parent_uuid) tuple."""
623+ span_data = {"id" : str (span_id ), "name" : f"span-{ span_id } " }
624+ return (span_data , span_id , parent_id )
625+
626+ def test_no_parents (self ):
627+ a , b , c = uuid4 (), uuid4 (), uuid4 ()
628+ items = [self ._make_span (a ), self ._make_span (b ), self ._make_span (c )]
629+ result = NativeTracer ._topological_sort_spans (items )
630+ assert [r [1 ] for r in result ] == [a , b , c ]
631+
632+ def test_child_after_parent (self ):
633+ parent_id = uuid4 ()
634+ child_id = uuid4 ()
635+ # Child comes first in the input list
636+ items = [self ._make_span (child_id , parent_id ), self ._make_span (parent_id )]
637+ result = NativeTracer ._topological_sort_spans (items )
638+ uuids = [r [1 ] for r in result ]
639+ assert uuids .index (parent_id ) < uuids .index (child_id )
640+
641+ def test_deep_nesting (self ):
642+ root = uuid4 ()
643+ mid = uuid4 ()
644+ leaf = uuid4 ()
645+ # Reverse order: leaf, mid, root
646+ items = [
647+ self ._make_span (leaf , mid ),
648+ self ._make_span (mid , root ),
649+ self ._make_span (root ),
650+ ]
651+ result = NativeTracer ._topological_sort_spans (items )
652+ uuids = [r [1 ] for r in result ]
653+ assert uuids .index (root ) < uuids .index (mid ) < uuids .index (leaf )
654+
655+ def test_parent_outside_batch (self ):
656+ """Spans referencing a parent not in the batch are treated as roots."""
657+ external_parent = uuid4 ()
658+ child_id = uuid4 ()
659+ items = [self ._make_span (child_id , external_parent )]
660+ result = NativeTracer ._topological_sort_spans (items )
661+ assert len (result ) == 1
662+ assert result [0 ][1 ] == child_id
663+
664+ def test_mixed_roots_and_children (self ):
665+ root_a = uuid4 ()
666+ child_a = uuid4 ()
667+ root_b = uuid4 ()
668+ items = [
669+ self ._make_span (child_a , root_a ),
670+ self ._make_span (root_b ),
671+ self ._make_span (root_a ),
672+ ]
673+ result = NativeTracer ._topological_sort_spans (items )
674+ uuids = [r [1 ] for r in result ]
675+ assert uuids .index (root_a ) < uuids .index (child_a )
676+
677+ def test_cycle_two_node (self ):
678+ """Spans forming a 2-node cycle should not cause errors or drop spans."""
679+ a = uuid4 ()
680+ b = uuid4 ()
681+ items = [
682+ self ._make_span (a , b ),
683+ self ._make_span (b , a ),
684+ ]
685+ result = NativeTracer ._topological_sort_spans (items )
686+ # Ensure all spans are present and no infinite loop / exception occurs.
687+ uuids = [r [1 ] for r in result ]
688+ assert len (uuids ) == 2
689+ assert set (uuids ) == {a , b }
690+
691+ def test_self_parent_span (self ):
692+ """A span that lists itself as its own parent should still be returned."""
693+ span_id = uuid4 ()
694+ items = [
695+ self ._make_span (span_id , span_id ),
696+ ]
697+ result = NativeTracer ._topological_sort_spans (items )
698+ uuids = [r [1 ] for r in result ]
699+ assert len (uuids ) == 1
700+ assert uuids [0 ] == span_id
701+
702+ def test_empty_input (self ):
703+ result = NativeTracer ._topological_sort_spans ([])
704+ assert result == []
705+
706+ # ---------------------------------------------------------------------------
707+ # _flush_to_database with parent/child spans
708+ # ---------------------------------------------------------------------------
709+
710+
711+ class TestFlushParentChildOrder :
712+ @pytest .mark .asyncio
713+ async def test_flush_inserts_parent_before_child (self ):
714+ """Verify that the DB merge order respects parent -> child ordering."""
715+ flow_id = str (uuid4 ())
716+ tracer = _make_tracer (flow_id = flow_id )
717+
718+ parent_uuid = uuid4 ()
719+ child_uuid = uuid4 ()
720+
721+ # Deliberately add child first to simulate the problematic ordering
722+ tracer .completed_spans = [
723+ {
724+ "id" : str (child_uuid ),
725+ "name" : "Child Span" ,
726+ "span_type" : SpanType .CHAIN ,
727+ "inputs" : {},
728+ "outputs" : None ,
729+ "start_time" : datetime .now (tz = timezone .utc ),
730+ "end_time" : datetime .now (tz = timezone .utc ),
731+ "latency_ms" : 5 ,
732+ "status" : SpanStatus .OK ,
733+ "error" : None ,
734+ "attributes" : {},
735+ "span_source" : "component" ,
736+ "parent_span_id" : parent_uuid ,
737+ },
738+ {
739+ "id" : str (parent_uuid ),
740+ "name" : "Parent Span" ,
741+ "span_type" : SpanType .CHAIN ,
742+ "inputs" : {},
743+ "outputs" : None ,
744+ "start_time" : datetime .now (tz = timezone .utc ),
745+ "end_time" : datetime .now (tz = timezone .utc ),
746+ "latency_ms" : 10 ,
747+ "status" : SpanStatus .OK ,
748+ "error" : None ,
749+ "attributes" : {},
750+ "span_source" : "component" ,
751+ },
752+ ]
753+
754+ merged_objects = []
755+ mock_session = AsyncMock ()
756+ mock_session .__aenter__ = AsyncMock (return_value = mock_session )
757+ mock_session .__aexit__ = AsyncMock (return_value = False )
758+
759+ async def capture_merge (obj ):
760+ merged_objects .append (obj )
761+
762+ mock_session .merge = capture_merge
763+
764+ with patch ("lfx.services.deps.session_scope" , return_value = mock_session ):
765+ await tracer ._flush_to_database ()
766+
767+ from langflow .services .database .models .traces .model import SpanTable
768+
769+ span_objects = [o for o in merged_objects if isinstance (o , SpanTable )]
770+ assert len (span_objects ) == 2
771+ # Parent (no parent_span_id) must come before child
772+ assert span_objects [0 ].id == parent_uuid
773+ assert span_objects [1 ].id == child_uuid
774+ assert span_objects [1 ].parent_span_id == parent_uuid
0 commit comments