Skip to content

Commit 9f01ab6

Browse files
committed
fix: cypher Dijkstra return
Fixed Issue #4042
1 parent 82808c2 commit 9f01ab6

2 files changed

Lines changed: 175 additions & 11 deletions

File tree

engine/src/main/java/com/arcadedb/query/opencypher/procedures/algo/AlgoDijkstra.java

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
*/
1919
package com.arcadedb.query.opencypher.procedures.algo;
2020

21+
import com.arcadedb.database.Database;
2122
import com.arcadedb.database.RID;
2223
import com.arcadedb.graph.Edge;
2324
import com.arcadedb.graph.Vertex;
@@ -26,6 +27,7 @@
2627
import com.arcadedb.query.sql.executor.ResultInternal;
2728
import com.arcadedb.function.sql.graph.SQLFunctionAstar;
2829

30+
import java.util.ArrayList;
2931
import java.util.HashMap;
3032
import java.util.LinkedList;
3133
import java.util.List;
@@ -102,23 +104,43 @@ public Stream<Result> execute(final Object[] args, final Result inputRow, final
102104
if (pathRids == null || pathRids.isEmpty())
103105
return Stream.empty();
104106

105-
// Calculate total weight
107+
// The A* implementation returns vertices only. Traverse the edges between consecutive
108+
// vertices to reconstruct the path's total weight and to expose the relationships.
109+
final Database db = context.getDatabase();
110+
final Vertex.DIRECTION dir = parseDirection(direction);
111+
final String[] edgeTypeFilter = relType != null && !relType.isEmpty() ? new String[] { relType } : null;
112+
113+
final List<RID> pathWithEdges = new ArrayList<>(pathRids.size() * 2 - 1);
114+
pathWithEdges.add(pathRids.get(0));
115+
106116
double totalWeight = 0.0;
107117
for (int i = 0; i < pathRids.size() - 1; i++) {
108-
final RID current = pathRids.get(i);
109-
final var currentDoc = context.getDatabase().lookupByRID(current, true);
110-
111-
// If this is an edge, get its weight
112-
if (currentDoc.getRecord() instanceof Edge edge) {
113-
final Object weight = edge.get(weightProperty);
114-
if (weight instanceof Number num) {
115-
totalWeight += num.doubleValue();
118+
final Vertex from = pathRids.get(i).asVertex();
119+
final RID toRid = pathRids.get(i + 1);
120+
121+
Edge bestEdge = null;
122+
double bestWeight = Double.POSITIVE_INFINITY;
123+
for (final Edge edge : edgeTypeFilter != null ? from.getEdges(dir, edgeTypeFilter) : from.getEdges(dir)) {
124+
final RID otherRid = edge.getOut().equals(from.getIdentity()) ? edge.getIn() : edge.getOut();
125+
if (!toRid.equals(otherRid))
126+
continue;
127+
final Object w = edge.get(weightProperty);
128+
final double edgeWeight = w instanceof Number num ? num.doubleValue() : 0.0;
129+
if (edgeWeight < bestWeight) {
130+
bestWeight = edgeWeight;
131+
bestEdge = edge;
116132
}
117133
}
134+
135+
if (bestEdge != null) {
136+
totalWeight += bestWeight;
137+
pathWithEdges.add(bestEdge.getIdentity());
138+
}
139+
pathWithEdges.add(toRid);
118140
}
119141

120-
// Build path representation
121-
final Map<String, Object> path = buildPath(pathRids, context.getDatabase());
142+
// Build path representation including edges
143+
final Map<String, Object> path = buildPath(pathWithEdges, db);
122144

123145
final ResultInternal result = new ResultInternal();
124146
result.setProperty("path", path);
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
/*
2+
* Copyright © 2021-present Arcade Data Ltd ([email protected])
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
* SPDX-FileCopyrightText: 2021-present Arcade Data Ltd ([email protected])
17+
* SPDX-License-Identifier: Apache-2.0
18+
*/
19+
package com.arcadedb.query.opencypher.procedures.algo;
20+
21+
import com.arcadedb.database.Database;
22+
import com.arcadedb.database.DatabaseFactory;
23+
import com.arcadedb.graph.MutableVertex;
24+
import com.arcadedb.graph.Vertex;
25+
import com.arcadedb.query.sql.executor.Result;
26+
import com.arcadedb.query.sql.executor.ResultSet;
27+
import org.junit.jupiter.api.AfterEach;
28+
import org.junit.jupiter.api.BeforeEach;
29+
import org.junit.jupiter.api.Test;
30+
31+
import java.util.ArrayList;
32+
import java.util.List;
33+
import java.util.Map;
34+
35+
import static org.assertj.core.api.Assertions.assertThat;
36+
37+
/**
38+
* Tests for the algo.dijkstra Cypher procedure.
39+
*
40+
* Regression for issue #4042: yielded weight was always 0 because the path returned by the
41+
* underlying A* implementation only contains vertex RIDs, not edges. Weight must be reconstructed
42+
* by traversing the edges between consecutive vertices on the path.
43+
*
44+
* @author Luca Garulli ([email protected])
45+
*/
46+
class AlgoDijkstraTest {
47+
private Database database;
48+
49+
@BeforeEach
50+
void setup() {
51+
final DatabaseFactory factory = new DatabaseFactory("./target/databases/test-algo-dijkstra");
52+
if (factory.exists())
53+
factory.open().drop();
54+
database = factory.create();
55+
database.getSchema().createVertexType("Node");
56+
database.getSchema().createEdgeType("Road");
57+
58+
// Start --2--> Middle --3--> Finish
59+
database.transaction(() -> {
60+
final MutableVertex start = database.newVertex("Node").set("name", "Start").save();
61+
final MutableVertex middle = database.newVertex("Node").set("name", "Middle").save();
62+
final MutableVertex finish = database.newVertex("Node").set("name", "Finish").save();
63+
start.newEdge("Road", middle, true, new Object[] { "distance", 2 }).save();
64+
middle.newEdge("Road", finish, true, new Object[] { "distance", 3 }).save();
65+
});
66+
}
67+
68+
@AfterEach
69+
void teardown() {
70+
if (database != null)
71+
database.drop();
72+
}
73+
74+
@Test
75+
void dijkstraComputesCorrectWeight() {
76+
final ResultSet rs = database.query("opencypher",
77+
"""
78+
MATCH (src:Node {name:'Start'}),(dst:Node {name:'Finish'}) \
79+
CALL algo.dijkstra(src, dst, 'Road', 'distance') \
80+
YIELD path, weight RETURN weight""");
81+
82+
assertThat(rs.hasNext()).isTrue();
83+
final Result result = rs.next();
84+
final Object weight = result.getProperty("weight");
85+
assertThat(weight).isInstanceOf(Number.class);
86+
assertThat(((Number) weight).doubleValue()).isEqualTo(5.0);
87+
}
88+
89+
@Test
90+
void dijkstraPathIncludesEdges() {
91+
final ResultSet rs = database.query("opencypher",
92+
"""
93+
MATCH (src:Node {name:'Start'}),(dst:Node {name:'Finish'}) \
94+
CALL algo.dijkstra(src, dst, 'Road', 'distance') \
95+
YIELD path, weight RETURN path, weight""");
96+
97+
assertThat(rs.hasNext()).isTrue();
98+
final Result result = rs.next();
99+
@SuppressWarnings("unchecked")
100+
final Map<String, Object> path = (Map<String, Object>) result.getProperty("path");
101+
assertThat(path).isNotNull();
102+
103+
final List<?> nodes = (List<?>) path.get("nodes");
104+
final List<?> relationships = (List<?>) path.get("relationships");
105+
assertThat(nodes).hasSize(3);
106+
assertThat(relationships).hasSize(2);
107+
}
108+
109+
@Test
110+
void dijkstraStartEqualsEnd() {
111+
final ResultSet rs = database.query("opencypher",
112+
"""
113+
MATCH (src:Node {name:'Start'}) \
114+
CALL algo.dijkstra(src, src, 'Road', 'distance') \
115+
YIELD path, weight RETURN weight""");
116+
117+
assertThat(rs.hasNext()).isTrue();
118+
final Result result = rs.next();
119+
assertThat(((Number) result.getProperty("weight")).doubleValue()).isEqualTo(0.0);
120+
}
121+
122+
@Test
123+
void dijkstraMultiHopAccumulatesWeight() {
124+
// Add a fourth hop: Finish --4--> End
125+
database.transaction(() -> {
126+
final Vertex finish = database.query("sql",
127+
"SELECT FROM Node WHERE name='Finish'").next().getElement().get().asVertex();
128+
final MutableVertex end = database.newVertex("Node").set("name", "End").save();
129+
finish.modify().newEdge("Road", end, true, new Object[] { "distance", 4 }).save();
130+
});
131+
132+
final ResultSet rs = database.query("opencypher",
133+
"""
134+
MATCH (src:Node {name:'Start'}),(dst:Node {name:'End'}) \
135+
CALL algo.dijkstra(src, dst, 'Road', 'distance') \
136+
YIELD path, weight RETURN weight""");
137+
138+
assertThat(rs.hasNext()).isTrue();
139+
final Result result = rs.next();
140+
assertThat(((Number) result.getProperty("weight")).doubleValue()).isEqualTo(9.0);
141+
}
142+
}

0 commit comments

Comments
 (0)