This is an automated email from the ASF dual-hosted git repository.

pierrejeambrun pushed a commit to branch v3-1-test
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/v3-1-test by this push:
     new 4436ef91d19 [v3-1-test] grid merge node dict storage (#61656) (#61789)
4436ef91d19 is described below

commit 4436ef91d199bee0fc93a267c9b6a74f2dde31ee
Author: github-actions[bot] 
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Thu Feb 12 10:54:28 2026 +0100

    [v3-1-test] grid merge node dict storage (#61656) (#61789)
    
    (cherry picked from commit 9da4a153b22854df7f9dd98cda093d33abd2488c)
    
    Co-authored-by: Steve Ahn <[email protected]>
---
 .../api_fastapi/core_api/services/ui/grid.py       | 17 ++---
 .../api_fastapi/core_api/services/ui/__init__.py   | 16 +++++
 .../api_fastapi/core_api/services/ui/test_grid.py  | 72 ++++++++++++++++++++++
 3 files changed, 94 insertions(+), 11 deletions(-)

diff --git a/airflow-core/src/airflow/api_fastapi/core_api/services/ui/grid.py 
b/airflow-core/src/airflow/api_fastapi/core_api/services/ui/grid.py
index 68520371116..b2e9b01e248 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/services/ui/grid.py
+++ b/airflow-core/src/airflow/api_fastapi/core_api/services/ui/grid.py
@@ -33,22 +33,17 @@ log = structlog.get_logger(logger_name=__name__)
 
 
 def _merge_node_dicts(current, new) -> None:
-    current_ids = {node["id"] for node in current}
+    current_nodes_by_id = {node["id"]: node for node in current}
     for node in new:
-        if node["id"] in current_ids:
-            current_node = _get_node_by_id(current, node["id"])
+        node_id = node["id"]
+        current_node = current_nodes_by_id.get(node_id)
+        if current_node is not None:
             # if we have children, merge those as well
             if current_node.get("children"):
-                _merge_node_dicts(current_node["children"], node["children"])
+                _merge_node_dicts(current_node["children"], 
node.get("children", []))
         else:
             current.append(node)
-
-
-def _get_node_by_id(nodes, node_id):
-    for node in nodes:
-        if node["id"] == node_id:
-            return node
-    return {}
+            current_nodes_by_id[node_id] = node
 
 
 def agg_state(states):
diff --git 
a/airflow-core/tests/unit/api_fastapi/core_api/services/ui/__init__.py 
b/airflow-core/tests/unit/api_fastapi/core_api/services/ui/__init__.py
new file mode 100644
index 00000000000..13a83393a91
--- /dev/null
+++ b/airflow-core/tests/unit/api_fastapi/core_api/services/ui/__init__.py
@@ -0,0 +1,16 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
diff --git 
a/airflow-core/tests/unit/api_fastapi/core_api/services/ui/test_grid.py 
b/airflow-core/tests/unit/api_fastapi/core_api/services/ui/test_grid.py
new file mode 100644
index 00000000000..55c3532c171
--- /dev/null
+++ b/airflow-core/tests/unit/api_fastapi/core_api/services/ui/test_grid.py
@@ -0,0 +1,72 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from __future__ import annotations
+
+from airflow.api_fastapi.core_api.services.ui.grid import _merge_node_dicts
+
+
+def test_merge_node_dicts_merges_children_and_appends_new_nodes():
+    current = [
+        {
+            "id": "group",
+            "label": "group",
+            "children": [{"id": "group.task_a", "label": "task_a"}],
+        },
+        {"id": "task", "label": "task"},
+    ]
+    new = [
+        {
+            "id": "group",
+            "label": "group",
+            "children": [{"id": "group.task_b", "label": "task_b"}],
+        },
+        {"id": "new_task", "label": "new_task"},
+    ]
+
+    _merge_node_dicts(current, new)
+
+    assert [node["id"] for node in current] == ["group", "task", "new_task"]
+    group_children = {child["id"] for child in current[0]["children"]}
+    assert group_children == {"group.task_a", "group.task_b"}
+
+
+def test_merge_node_dicts_preserves_existing_non_group_node_shape():
+    current = [{"id": "task", "label": "task"}]
+    new = [{"id": "task", "label": "task", "children": [{"id": "task.subtask", 
"label": "subtask"}]}]
+
+    _merge_node_dicts(current, new)
+
+    assert current == [{"id": "task", "label": "task"}]
+
+
+def test_merge_node_dicts_large_merge_keeps_unique_nodes():
+    current = [{"id": f"group_{i}", "children": [{"id": 
f"group_{i}.old_task"}]} for i in range(400)]
+    new = [{"id": f"group_{i}", "children": [{"id": f"group_{i}.new_task"}]} 
for i in range(400)]
+    new.extend({"id": f"new_task_{i}"} for i in range(400))
+
+    _merge_node_dicts(current, new)
+
+    assert len(current) == 800
+    assert {child["id"] for child in current[0]["children"]} == {
+        "group_0.old_task",
+        "group_0.new_task",
+    }
+    assert {child["id"] for child in current[-401]["children"]} == {
+        "group_399.old_task",
+        "group_399.new_task",
+    }

Reply via email to