Preserve dataflow layout across SQL edits

This commit is contained in:
2026-07-28 18:36:09 +02:00
parent 74f1210a32
commit e54225a86e
8 changed files with 331 additions and 19 deletions

View File

@@ -13,8 +13,10 @@ from govoplan_dataflow.backend.schemas import (
GraphEdge,
GraphNode,
GraphPosition,
PipelineDraftRequest,
PipelineGraph,
)
from govoplan_dataflow.backend.service import compile_sql_draft
from govoplan_dataflow.backend.sql_compiler import SqlCompilationError, compile_sql, render_sql
@@ -301,6 +303,14 @@ class DataflowGraphAndSqlTests(unittest.TestCase):
["source.inline", "source.inline", "combine.union", "output"],
[node.type for node in roundtrip.nodes],
)
self.assertEqual(
[60, 60, 300, 540],
[node.position.x for node in roundtrip.nodes],
)
self.assertEqual(
120,
roundtrip.nodes[1].position.y - roundtrip.nodes[0].position.y,
)
self.assertEqual(result.rows, execute_preview(roundtrip, row_limit=100).rows)
def test_legacy_composite_edge_ids_remain_readable(self) -> None:
@@ -310,6 +320,110 @@ class DataflowGraphAndSqlTests(unittest.TestCase):
self.assertEqual(edge_id, edge.id)
def test_sql_roundtrip_preserves_layout_for_compatible_topology(self) -> None:
compiled, sql_text, _ = compile_sql(
"""
SELECT *
FROM monthly_files
WHERE status = 'open'
ORDER BY amount DESC
""",
source_nodes=[inline_source()],
)
id_map = {
node.id: f"custom-node-{index}"
for index, node in enumerate(compiled.nodes, start=1)
}
reference = compiled.model_copy(
update={
"nodes": [
node.model_copy(
update={
"id": id_map[node.id],
"label": f"Custom {index}",
"position": GraphPosition(
x=111 + index * 137,
y=73 + index * 41,
),
},
deep=True,
)
for index, node in enumerate(compiled.nodes, start=1)
],
"edges": [
edge.model_copy(
update={
"id": f"custom-edge-{index}",
"source": id_map[edge.source],
"target": id_map[edge.target],
},
deep=True,
)
for index, edge in enumerate(compiled.edges, start=1)
],
},
deep=True,
)
reference = reference.model_copy(
update={
"nodes": list(reversed(reference.nodes)),
"edges": list(reversed(reference.edges)),
},
deep=True,
)
response = compile_sql_draft(
PipelineDraftRequest(
graph=reference,
sql_text=sql_text.replace("'open'", "'closed'"),
)
)
self.assertTrue(response.valid)
self.assertIsNotNone(response.graph)
assert response.graph is not None
self.assertEqual(
[
(node.id, node.label, node.position)
for node in reference.nodes
],
[
(node.id, node.label, node.position)
for node in response.graph.nodes
],
)
self.assertEqual(
[edge.id for edge in reference.edges],
[edge.id for edge in response.graph.edges],
)
filter_node = next(
node
for node in response.graph.nodes
if node.type == "filter"
)
self.assertEqual("closed", filter_node.config["value"])
def test_changed_sql_topology_uses_compact_layer_layout(self) -> None:
reference, _, _ = compile_sql(
"SELECT * FROM monthly_files WHERE status = 'open'",
source_nodes=[inline_source()],
)
response = compile_sql_draft(
PipelineDraftRequest(
graph=reference,
sql_text="SELECT * FROM monthly_files",
)
)
self.assertTrue(response.valid)
self.assertIsNotNone(response.graph)
assert response.graph is not None
self.assertEqual(
[60, 300],
[node.position.x for node in response.graph.nodes],
)
def test_connector_source_uses_bounded_resolver_and_records_lineage(self) -> None:
source = GraphNode(
id="connector",