Skip to content

Commit 58da446

Browse files
committed
fix: OpenCypher functions
Fixed issues: #3445 #3446 #3447 #3448 #3449
1 parent 7503e30 commit 58da446

File tree

5 files changed

+220
-2
lines changed

5 files changed

+220
-2
lines changed

engine/src/main/java/com/arcadedb/function/coll/CollIndexOf.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ public Object execute(final Object[] args, final CommandContext context) {
5454
if (args.length != 2)
5555
throw new CommandExecutionException("coll.indexOf() requires exactly 2 arguments");
5656
final List<Object> list = asList(args[0]);
57-
if (list == null)
57+
if (list == null || args[1] == null)
5858
return null;
5959
return (long) list.indexOf(args[1]);
6060
}

engine/src/main/java/com/arcadedb/function/coll/CollRemove.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ public Object execute(final Object[] args, final CommandContext context) {
6060
return null;
6161

6262
final int index = ((Number) args[1]).intValue();
63+
if (index < 0 || index >= list.size())
64+
throw new CommandExecutionException("coll.remove() index " + index + " is out of range for list of size " + list.size());
6365
final int count = args.length > 2 ? ((Number) args[2]).intValue() : 1;
6466
final List<Object> result = new ArrayList<>(list);
6567
for (int i = 0; i < count && index < result.size(); i++)

engine/src/main/java/com/arcadedb/function/coll/SizeFunction.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,16 @@ public Object execute(final Object[] args, final CommandContext context) {
4141
return null;
4242
if (args[0] instanceof List)
4343
return (long) ((List<?>) args[0]).size();
44-
else if (args[0] instanceof String)
44+
if (args[0] instanceof String)
4545
return (long) ((String) args[0]).length();
46+
if (args[0] instanceof float[])
47+
return (long) ((float[]) args[0]).length;
48+
if (args[0] instanceof double[])
49+
return (long) ((double[]) args[0]).length;
50+
if (args[0] instanceof int[])
51+
return (long) ((int[]) args[0]).length;
52+
if (args[0] instanceof long[])
53+
return (long) ((long[]) args[0]).length;
4654
return null;
4755
}
4856
}

engine/src/main/java/com/arcadedb/query/opencypher/parser/CypherASTBuilder.java

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -832,6 +832,34 @@ public String getText() {
832832
return createFallbackComparison(ctx);
833833
}
834834

835+
// Check if the expression is a function call used as a predicate (e.g., isEmpty(x), exists(x))
836+
final Cypher25Parser.FunctionInvocationContext funcCtx = expressionBuilder.findFunctionInvocationRecursive(expr6);
837+
if (funcCtx != null) {
838+
final Expression funcExpr = expressionBuilder.parseExpressionFromText(expr6);
839+
return new BooleanExpression() {
840+
@Override
841+
public boolean evaluate(final Result result, final CommandContext context) {
842+
final Object value = funcExpr.evaluate(result, context);
843+
return value instanceof Boolean && (Boolean) value;
844+
}
845+
846+
@Override
847+
public Object evaluateTernary(final Result result, final CommandContext context) {
848+
final Object value = funcExpr.evaluate(result, context);
849+
if (value == null)
850+
return null;
851+
if (value instanceof Boolean)
852+
return value;
853+
return Boolean.TRUE;
854+
}
855+
856+
@Override
857+
public String getText() {
858+
return funcExpr.getText();
859+
}
860+
};
861+
}
862+
835863
// If no special comparison, treat as a simple expression that should evaluate to boolean
836864
// This is a fallback for cases we haven't handled yet
837865
return createFallbackComparison(ctx);
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
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;
20+
21+
import com.arcadedb.TestHelper;
22+
import com.arcadedb.exception.CommandExecutionException;
23+
import com.arcadedb.query.sql.executor.Result;
24+
import com.arcadedb.query.sql.executor.ResultSet;
25+
import org.junit.jupiter.api.Test;
26+
27+
import java.util.ArrayList;
28+
import java.util.List;
29+
30+
import static org.assertj.core.api.Assertions.assertThat;
31+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
32+
33+
/**
34+
* Regression tests for GitHub issues #3445-#3449.
35+
*
36+
* @author Luca Garulli ([email protected])
37+
*/
38+
class OpenCypherBugFixesTest extends TestHelper {
39+
40+
// ===================== #3445: coll.indexOf with null should return null =====================
41+
42+
@Test
43+
void testCollIndexOfWithNullValueReturnsNull() {
44+
// Issue #3445: coll.indexOf(['a'], null) should return null, not -1
45+
try (final ResultSet rs = database.command("opencypher", "RETURN coll.indexOf(['a'], null) AS result")) {
46+
assertThat(rs.hasNext()).isTrue();
47+
assertThat((Object) rs.next().getProperty("result")).isNull();
48+
}
49+
}
50+
51+
@Test
52+
void testCollIndexOfWithNullListReturnsNull() {
53+
try (final ResultSet rs = database.command("opencypher", "RETURN coll.indexOf(null, 'a') AS result")) {
54+
assertThat(rs.hasNext()).isTrue();
55+
assertThat((Object) rs.next().getProperty("result")).isNull();
56+
}
57+
}
58+
59+
@Test
60+
void testCollIndexOfNormalBehavior() {
61+
try (final ResultSet rs = database.command("opencypher", "RETURN coll.indexOf(['a', 'b', 'c'], 'b') AS result")) {
62+
assertThat(rs.hasNext()).isTrue();
63+
assertThat(((Number) rs.next().getProperty("result")).longValue()).isEqualTo(1L);
64+
}
65+
}
66+
67+
// ===================== #3446: coll.remove with out-of-bounds index should throw =====================
68+
69+
@Test
70+
void testCollRemoveOutOfBoundsThrows() {
71+
// Issue #3446: coll.remove([1, 2], 10) should throw an error
72+
assertThatThrownBy(() -> {
73+
try (final ResultSet rs = database.command("opencypher", "RETURN coll.remove([1, 2], 10) AS result")) {
74+
rs.next();
75+
}
76+
}).isInstanceOf(CommandExecutionException.class);
77+
}
78+
79+
@Test
80+
void testCollRemoveNegativeIndexThrows() {
81+
assertThatThrownBy(() -> {
82+
try (final ResultSet rs = database.command("opencypher", "RETURN coll.remove([1, 2], -1) AS result")) {
83+
rs.next();
84+
}
85+
}).isInstanceOf(CommandExecutionException.class);
86+
}
87+
88+
@Test
89+
void testCollRemoveNormalBehavior() {
90+
try (final ResultSet rs = database.command("opencypher", "RETURN coll.remove([1, 2, 3], 1) AS result")) {
91+
assertThat(rs.hasNext()).isTrue();
92+
@SuppressWarnings("unchecked")
93+
final List<Object> result = rs.next().getProperty("result");
94+
assertThat(result).hasSize(2);
95+
assertThat(((Number) result.get(0)).longValue()).isEqualTo(1L);
96+
assertThat(((Number) result.get(1)).longValue()).isEqualTo(3L);
97+
}
98+
}
99+
100+
// ===================== #3447: isEmpty should not treat null as empty =====================
101+
102+
@Test
103+
void testIsEmptyWithNullProperty() {
104+
// Issue #3447: isEmpty(p.address) where p.address is null should NOT return true
105+
database.transaction(() -> {
106+
database.getSchema().createVertexType("PersonTest");
107+
database.command("opencypher",
108+
"CREATE (p:PersonTest {name:'Jessica', address:''}), (q:PersonTest {name:'Keanu'})");
109+
});
110+
111+
try (final ResultSet rs = database.command("opencypher",
112+
"MATCH (p:PersonTest) WHERE isEmpty(p.address) RETURN p.name AS result ORDER BY result")) {
113+
final List<String> names = new ArrayList<>();
114+
while (rs.hasNext())
115+
names.add(rs.next().getProperty("result"));
116+
// Only Jessica has address='' (empty string), Keanu has no address property (null)
117+
assertThat(names).containsExactly("Jessica");
118+
}
119+
}
120+
121+
@Test
122+
void testIsEmptyWithEmptyString() {
123+
try (final ResultSet rs = database.command("opencypher", "RETURN isEmpty('') AS result")) {
124+
assertThat(rs.hasNext()).isTrue();
125+
assertThat(rs.next().<Boolean>getProperty("result")).isTrue();
126+
}
127+
}
128+
129+
@Test
130+
void testIsEmptyWithNonEmptyString() {
131+
try (final ResultSet rs = database.command("opencypher", "RETURN isEmpty('hello') AS result")) {
132+
assertThat(rs.hasNext()).isTrue();
133+
assertThat(rs.next().<Boolean>getProperty("result")).isFalse();
134+
}
135+
}
136+
137+
@Test
138+
void testIsEmptyWithNull() {
139+
try (final ResultSet rs = database.command("opencypher", "RETURN isEmpty(null) AS result")) {
140+
assertThat(rs.hasNext()).isTrue();
141+
assertThat((Object) rs.next().getProperty("result")).isNull();
142+
}
143+
}
144+
145+
// ===================== #3448: toBooleanOrNull(1.5) should return null =====================
146+
147+
@Test
148+
void testToBooleanOrNullWithFloat() {
149+
// Issue #3448: toBooleanOrNull(1.5) should return null, not true
150+
try (final ResultSet rs = database.command("opencypher",
151+
"RETURN toBooleanOrNull('not a boolean') AS str, toBooleanOrNull(1.5) AS float, toBooleanOrNull([]) AS array")) {
152+
assertThat(rs.hasNext()).isTrue();
153+
final Result row = rs.next();
154+
assertThat((Object) row.getProperty("str")).isNull();
155+
assertThat((Object) row.getProperty("float")).isNull();
156+
assertThat((Object) row.getProperty("array")).isNull();
157+
}
158+
}
159+
160+
// ===================== #3449: size on vector should return vector length =====================
161+
162+
@Test
163+
void testSizeOnVector() {
164+
// Issue #3449: size(vector([1, 2, 3, 4, 5], 5, INTEGER)) should return 5
165+
try (final ResultSet rs = database.command("opencypher",
166+
"RETURN size(vector([1, 2, 3, 4, 5], 5, INTEGER)) AS sizeResult")) {
167+
assertThat(rs.hasNext()).isTrue();
168+
assertThat(((Number) rs.next().getProperty("sizeResult")).longValue()).isEqualTo(5L);
169+
}
170+
}
171+
172+
@Test
173+
void testSizeOnVectorThreeElements() {
174+
try (final ResultSet rs = database.command("opencypher",
175+
"RETURN size(vector([1.0, 2.0, 3.0], 3, FLOAT)) AS sizeResult")) {
176+
assertThat(rs.hasNext()).isTrue();
177+
assertThat(((Number) rs.next().getProperty("sizeResult")).longValue()).isEqualTo(3L);
178+
}
179+
}
180+
}

0 commit comments

Comments
 (0)