Skip to content

Commit a7a4b71

Browse files
committed
fix: opencypher not checking for the relationship type but completely ignoring the target node's labels
Fixed issue ArcadeData#3277 The PatternPredicateExpression.checkAnyRelationshipExists() method was checking for the relationship type but completely ignoring the target node's labels.
1 parent abd0e94 commit a7a4b71

2 files changed

Lines changed: 191 additions & 5 deletions

File tree

engine/src/main/java/com/arcadedb/query/opencypher/ast/PatternPredicateExpression.java

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import com.arcadedb.database.Identifiable;
2222
import com.arcadedb.graph.Edge;
2323
import com.arcadedb.graph.Vertex;
24+
import com.arcadedb.query.opencypher.Labels;
2425
import com.arcadedb.query.sql.executor.CommandContext;
2526
import com.arcadedb.query.sql.executor.Result;
2627

@@ -87,13 +88,17 @@ private boolean evaluatePattern(final Result result, final CommandContext contex
8788
final NodePattern endNodePattern = pathPattern.getNode(1);
8889
final Vertex endVertex = getVertexFromPattern(endNodePattern, result);
8990

91+
// Get target node labels (if specified) for filtering
92+
final List<String> targetLabels = endNodePattern != null ? endNodePattern.getLabels() : null;
93+
9094
// Check if the pattern exists
9195
if (endVertex != null) {
9296
// We have a specific end node - check if relationship exists between them
9397
return checkRelationshipExists(startVertex, endVertex, relationshipTypes, isOutgoing, isIncoming);
9498
} else {
9599
// No specific end node - check if any relationship of the specified type exists
96-
return checkAnyRelationshipExists(startVertex, relationshipTypes, isOutgoing, isIncoming);
100+
// Also filter by target node labels if specified
101+
return checkAnyRelationshipExists(startVertex, relationshipTypes, targetLabels, isOutgoing, isIncoming);
97102
}
98103
}
99104

@@ -168,10 +173,12 @@ private boolean checkRelationshipExists(
168173

169174
/**
170175
* Check if any relationship of the specified type exists from the start vertex.
176+
* Also filters by target node labels if specified.
171177
*/
172178
private boolean checkAnyRelationshipExists(
173179
final Vertex startVertex,
174180
final String[] relationshipTypes,
181+
final List<String> targetLabels,
175182
final boolean isOutgoing,
176183
final boolean isIncoming
177184
) {
@@ -184,8 +191,10 @@ private boolean checkAnyRelationshipExists(
184191
outEdges = startVertex.getEdges(Vertex.DIRECTION.OUT).iterator();
185192
}
186193

187-
if (outEdges.hasNext()) {
188-
return true;
194+
while (outEdges.hasNext()) {
195+
final Edge edge = outEdges.next();
196+
if (matchesTargetLabels(edge.getInVertex(), targetLabels))
197+
return true;
189198
}
190199
}
191200

@@ -198,14 +207,32 @@ private boolean checkAnyRelationshipExists(
198207
inEdges = startVertex.getEdges(Vertex.DIRECTION.IN).iterator();
199208
}
200209

201-
if (inEdges.hasNext()) {
202-
return true;
210+
while (inEdges.hasNext()) {
211+
final Edge edge = inEdges.next();
212+
if (matchesTargetLabels(edge.getOutVertex(), targetLabels))
213+
return true;
203214
}
204215
}
205216

206217
return false;
207218
}
208219

220+
/**
221+
* Check if a vertex matches the target labels.
222+
* If no labels are specified, returns true (matches any vertex).
223+
*/
224+
private boolean matchesTargetLabels(final Vertex vertex, final List<String> targetLabels) {
225+
if (targetLabels == null || targetLabels.isEmpty())
226+
return true;
227+
228+
// Check if vertex has ALL the specified labels (AND semantics)
229+
for (final String label : targetLabels) {
230+
if (!Labels.hasLabel(vertex, label))
231+
return false;
232+
}
233+
return true;
234+
}
235+
209236
@Override
210237
public String getText() {
211238
final StringBuilder sb = new StringBuilder();
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
/*
2+
* Copyright © 2021-present Arcade Data Ltd (info@arcadedata.com)
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 (info@arcadedata.com)
17+
* SPDX-License-Identifier: Apache-2.0
18+
*/
19+
package com.arcadedb.query.opencypher;
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+
34+
import static org.assertj.core.api.Assertions.assertThat;
35+
36+
/**
37+
* Tests for pattern predicates in WHERE clauses.
38+
* Regression test for GitHub issue #3277.
39+
*/
40+
public class CypherPatternPredicateTest {
41+
private Database database;
42+
43+
@BeforeEach
44+
void setUp() {
45+
database = new DatabaseFactory("./target/databases/cypher-pattern-predicate").create();
46+
database.getSchema().createVertexType("CHUNK");
47+
database.getSchema().createVertexType("IMAGE");
48+
database.getSchema().createVertexType("DOCUMENT");
49+
database.getSchema().createEdgeType("in");
50+
51+
database.transaction(() -> {
52+
// Create chunks
53+
MutableVertex chunk1 = database.newVertex("CHUNK");
54+
chunk1.set("name", "chunk1");
55+
chunk1.save();
56+
57+
MutableVertex chunk2 = database.newVertex("CHUNK");
58+
chunk2.set("name", "chunk2");
59+
chunk2.save();
60+
61+
MutableVertex chunk3 = database.newVertex("CHUNK");
62+
chunk3.set("name", "chunk3");
63+
chunk3.save();
64+
65+
// Create image and document
66+
MutableVertex image = database.newVertex("IMAGE");
67+
image.set("name", "image1");
68+
image.save();
69+
70+
MutableVertex document = database.newVertex("DOCUMENT");
71+
document.set("name", "doc1");
72+
document.save();
73+
74+
// chunk1 -> in -> IMAGE
75+
chunk1.newEdge("in", image, true, (Object[]) null);
76+
77+
// chunk2 -> in -> DOCUMENT
78+
chunk2.newEdge("in", document, true, (Object[]) null);
79+
80+
// chunk3 has no relationships (orphan)
81+
});
82+
}
83+
84+
@AfterEach
85+
void tearDown() {
86+
if (database != null) {
87+
database.drop();
88+
database = null;
89+
}
90+
}
91+
92+
/**
93+
* Tests the exact query from GitHub issue #3277:
94+
* MATCH (n:CHUNK) WHERE NOT (n)-[:in]-(:IMAGE) AND NOT (n)-[:in]-(:DOCUMENT) return n
95+
* Should return only chunk3 (the orphan chunk with no relationships).
96+
*/
97+
@Test
98+
void testPatternPredicateWithNotAndLabels() {
99+
database.transaction(() -> {
100+
final ResultSet rs = database.query("opencypher",
101+
"MATCH (n:CHUNK) WHERE NOT (n)-[:in]-(:IMAGE) AND NOT (n)-[:in]-(:DOCUMENT) RETURN n");
102+
103+
List<String> names = new ArrayList<>();
104+
while (rs.hasNext()) {
105+
Result result = rs.next();
106+
Vertex vertex = (Vertex) result.toElement();
107+
names.add(vertex.getString("name"));
108+
}
109+
110+
// Should only get chunk3 (no relationships to IMAGE or DOCUMENT)
111+
assertThat(names).hasSize(1);
112+
assertThat(names).containsExactly("chunk3");
113+
});
114+
}
115+
116+
/**
117+
* Tests positive pattern predicate (without NOT).
118+
*/
119+
@Test
120+
void testPatternPredicatePositive() {
121+
database.transaction(() -> {
122+
final ResultSet rs = database.query("opencypher",
123+
"MATCH (n:CHUNK) WHERE (n)-[:in]-(:IMAGE) RETURN n");
124+
125+
List<String> names = new ArrayList<>();
126+
while (rs.hasNext()) {
127+
Result result = rs.next();
128+
Vertex vertex = (Vertex) result.toElement();
129+
names.add(vertex.getString("name"));
130+
}
131+
132+
// Should only get chunk1 (has relationship to IMAGE)
133+
assertThat(names).hasSize(1);
134+
assertThat(names).containsExactly("chunk1");
135+
});
136+
}
137+
138+
/**
139+
* Tests pattern predicate with OR.
140+
*/
141+
@Test
142+
void testPatternPredicateWithOr() {
143+
database.transaction(() -> {
144+
final ResultSet rs = database.query("opencypher",
145+
"MATCH (n:CHUNK) WHERE (n)-[:in]-(:IMAGE) OR (n)-[:in]-(:DOCUMENT) RETURN n");
146+
147+
List<String> names = new ArrayList<>();
148+
while (rs.hasNext()) {
149+
Result result = rs.next();
150+
Vertex vertex = (Vertex) result.toElement();
151+
names.add(vertex.getString("name"));
152+
}
153+
154+
// Should get chunk1 and chunk2 (have relationships)
155+
assertThat(names).hasSize(2);
156+
assertThat(names).containsOnly("chunk1", "chunk2");
157+
});
158+
}
159+
}

0 commit comments

Comments
 (0)