diff --git a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/networkGraph/NetworkGraphOpDesc.scala b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/networkGraph/NetworkGraphOpDesc.scala index ccbf51c7377..789a6b5bb3c 100644 --- a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/networkGraph/NetworkGraphOpDesc.scala +++ b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/visualization/networkGraph/NetworkGraphOpDesc.scala @@ -98,7 +98,9 @@ class NetworkGraphOpDesc extends PythonOperatorDescriptor { | if not table.empty: | sources = table[$source] | destinations = table[$destination] - | nodes = set(sources + destinations) + | # Union of the two columns, in first-appearance order. Adding the + | # Series pairs them off element-wise; a set reorders per run. + | nodes = list(dict.fromkeys(pd.concat([sources, destinations]).tolist())) | G = nx.Graph() | for node in nodes: | G.add_node(node) diff --git a/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/networkGraph/NetworkGraphOpDescSpec.scala b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/networkGraph/NetworkGraphOpDescSpec.scala index 840b62b6431..fc666c61abd 100644 --- a/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/networkGraph/NetworkGraphOpDescSpec.scala +++ b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/visualization/networkGraph/NetworkGraphOpDescSpec.scala @@ -85,4 +85,19 @@ class NetworkGraphOpDescSpec extends AnyFlatSpec with BeforeAndAfter with Matche assert(carries(code, "My Graph")) code should include("class ProcessTableOperator(UDFTableOperator)") } + + it should "build the node set as a union rather than by adding the two columns" in { + opDesc.source = "from_node" + opDesc.destination = "to_node" + val code = opDesc.generatePythonCode() + + // `sources + destinations` is element-wise on two Series, so it glued each + // source to its destination and those strings entered the graph as nodes. + code should not include "set(sources + destinations)" + code should include("pd.concat([sources, destinations])") + + // Ordered de-duplication, not a set: a set iterates strings in an order that + // varies between processes, which would move the nodes from run to run. + code should include("dict.fromkeys") + } }