Skip to content

Commit 582167a

Browse files
robfrankPierre FExtReMLapinCNE Pierre FICHEPOIL
authored
added neo4j functions tests (#3949)
Co-authored-by: Pierre F <[email protected]> Co-authored-by: ExtReMLapin <[email protected]> Co-authored-by: CNE Pierre FICHEPOIL <[email protected]>
1 parent 4cb5061 commit 582167a

45 files changed

Lines changed: 7694 additions & 111 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,13 @@ public Object execute(final Object[] args, final CommandContext context) {
5757
final List<Object> list = asList(args[0]);
5858
if (list == null)
5959
return null;
60+
if (args[1] == null)
61+
return null;
6062
final int index = ((Number) args[1]).intValue();
63+
if (index < 0)
64+
throw new CommandExecutionException("coll.insert() does not support negative index: " + index);
65+
if (index > list.size())
66+
throw new CommandExecutionException("coll.insert() index " + index + " is out of range for list of size " + list.size());
6167
final List<Object> result = new ArrayList<>(list);
6268
result.add(index, args[2]);
6369
return result;

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,13 @@ public Object execute(final Object[] args, final CommandContext context) {
5858
final List<Object> list = asList(args[0]);
5959
if (list == null)
6060
return null;
61+
if (args[1] == null)
62+
return null;
6163

6264
final int index = ((Number) args[1]).intValue();
63-
if (index < 0 || index >= list.size())
65+
if (index < 0)
66+
throw new CommandExecutionException("coll.remove() does not support negative index: " + index);
67+
if (index >= list.size())
6468
throw new CommandExecutionException("coll.remove() index " + index + " is out of range for list of size " + list.size());
6569
final int count = args.length > 2 ? ((Number) args[2]).intValue() : 1;
6670
final List<Object> result = new ArrayList<>(list);
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
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.function.geo;
20+
21+
import com.arcadedb.exception.CommandExecutionException;
22+
import com.arcadedb.function.StatelessFunction;
23+
import com.arcadedb.query.sql.executor.CommandContext;
24+
25+
import java.util.Map;
26+
27+
/**
28+
* Cypher {@code point.distance(point1, point2)} function.
29+
*
30+
* <p>Computes the distance between two points. Uses Haversine formula for WGS-84
31+
* geographic points (result in meters), and Euclidean distance for Cartesian points.</p>
32+
*/
33+
public class CypherPointDistanceFunction implements StatelessFunction {
34+
private static final double EARTH_RADIUS_M = 6371000.0;
35+
36+
@Override
37+
public String getName() {
38+
return "point.distance";
39+
}
40+
41+
@Override
42+
public Object execute(final Object[] args, final CommandContext context) {
43+
if (args == null || args.length != 2)
44+
throw new CommandExecutionException("point.distance() requires exactly 2 arguments");
45+
if (args[0] == null || args[1] == null)
46+
return null;
47+
if (!(args[0] instanceof Map) || !(args[1] instanceof Map))
48+
throw new CommandExecutionException("point.distance() arguments must be point values (maps)");
49+
final Map<?, ?> p1 = (Map<?, ?>) args[0];
50+
final Map<?, ?> p2 = (Map<?, ?>) args[1];
51+
52+
// WGS-84: use Haversine formula
53+
if (p1.containsKey("longitude") && p1.containsKey("latitude") &&
54+
p2.containsKey("longitude") && p2.containsKey("latitude")) {
55+
final Number lat1n = (Number) p1.get("latitude");
56+
final Number lon1n = (Number) p1.get("longitude");
57+
final Number lat2n = (Number) p2.get("latitude");
58+
final Number lon2n = (Number) p2.get("longitude");
59+
if (lat1n == null || lon1n == null || lat2n == null || lon2n == null)
60+
return null;
61+
return haversineDistance(lat1n.doubleValue(), lon1n.doubleValue(), lat2n.doubleValue(), lon2n.doubleValue());
62+
}
63+
64+
// Cartesian: use Euclidean distance
65+
final Number x1n = (Number) p1.get("x");
66+
final Number y1n = (Number) p1.get("y");
67+
final Number x2n = (Number) p2.get("x");
68+
final Number y2n = (Number) p2.get("y");
69+
if (x1n == null || y1n == null || x2n == null || y2n == null)
70+
return null;
71+
final double dx = x2n.doubleValue() - x1n.doubleValue();
72+
final double dy = y2n.doubleValue() - y1n.doubleValue();
73+
double sumSq = dx * dx + dy * dy;
74+
final Number z1n = (Number) p1.get("z");
75+
final Number z2n = (Number) p2.get("z");
76+
if ((z1n == null) != (z2n == null))
77+
return null;
78+
if (z1n != null) {
79+
final double dz = z2n.doubleValue() - z1n.doubleValue();
80+
sumSq += dz * dz;
81+
}
82+
return Math.sqrt(sumSq);
83+
}
84+
85+
private double haversineDistance(final double lat1, final double lon1, final double lat2, final double lon2) {
86+
final double dLat = Math.toRadians(lat2 - lat1);
87+
final double dLon = Math.toRadians(lon2 - lon1);
88+
final double a = Math.pow(Math.sin(dLat / 2), 2)
89+
+ Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2))
90+
* Math.pow(Math.sin(dLon / 2), 2);
91+
return 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)) * EARTH_RADIUS_M;
92+
}
93+
}

engine/src/main/java/com/arcadedb/function/geo/CypherPointFunction.java

Lines changed: 91 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -20,19 +20,23 @@
2020

2121
import com.arcadedb.exception.CommandExecutionException;
2222
import com.arcadedb.function.StatelessFunction;
23-
import com.arcadedb.function.sql.geo.GeoUtils;
24-
import com.arcadedb.function.sql.geo.LightweightPoint;
2523
import com.arcadedb.query.sql.executor.CommandContext;
2624

25+
import java.util.LinkedHashMap;
26+
import java.util.Map;
27+
2728
/**
28-
* Cypher {@code point(lat, lon)} function.
29-
*
30-
* <p>Constructs a spatial point from latitude and longitude. Following Cypher/Neo4j convention,
31-
* the first argument is latitude and the second is longitude. The point is stored internally
32-
* using the spatial4j convention (x=longitude, y=latitude) so that spatial distance functions
33-
* such as {@code geo.distance} operate correctly.</p>
29+
* Cypher {@code point(map)} function.
3430
*
35-
* <p>Usage: {@code point(<latitude>, <longitude>)}</p>
31+
* <p>Constructs a point from a map of coordinate properties. Supports:</p>
32+
* <ul>
33+
* <li>WGS-84 2D: {@code point({longitude: x, latitude: y})}</li>
34+
* <li>WGS-84 3D: {@code point({longitude: x, latitude: y, height: z})}</li>
35+
* <li>Cartesian 2D: {@code point({x: a, y: b})}</li>
36+
* <li>Cartesian 3D: {@code point({x: a, y: b, z: c})}</li>
37+
* </ul>
38+
* <p>The returned map contains the coordinate keys and a {@code crs} field indicating
39+
* the coordinate reference system.</p>
3640
*/
3741
public class CypherPointFunction implements StatelessFunction {
3842
@Override
@@ -42,13 +46,84 @@ public String getName() {
4246

4347
@Override
4448
public Object execute(final Object[] args, final CommandContext context) {
45-
if (args == null || args.length < 2)
46-
throw new CommandExecutionException("point() requires latitude and longitude as parameters");
47-
if (args[0] == null || args[1] == null)
49+
if (args == null || args.length == 0 || args.length > 2)
50+
throw new CommandExecutionException("point() requires either one map argument (point({...})) or two numeric arguments (point(latitude, longitude))");
51+
52+
// 2-arg positional form: point(latitude, longitude) → WGS-84 2D
53+
if (args.length == 2) {
54+
if (args[0] == null || args[1] == null)
55+
return null;
56+
if (!(args[0] instanceof Number) || !(args[1] instanceof Number))
57+
throw new CommandExecutionException("point() with two arguments requires numeric latitude and longitude");
58+
final double lat = ((Number) args[0]).doubleValue();
59+
final double lon = ((Number) args[1]).doubleValue();
60+
final Map<String, Object> result = new LinkedHashMap<>();
61+
result.put("latitude", lat);
62+
result.put("longitude", lon);
63+
result.put("x", lon);
64+
result.put("y", lat);
65+
result.put("crs", "WGS-84");
66+
result.put("srid", 4326);
67+
return result;
68+
}
69+
70+
if (args[0] == null)
4871
return null;
49-
final double lat = GeoUtils.getDoubleValue(args[0]);
50-
final double lon = GeoUtils.getDoubleValue(args[1]);
51-
// Store as LightweightPoint(x=longitude, y=latitude) per spatial4j convention
52-
return new LightweightPoint(lon, lat);
72+
if (!(args[0] instanceof Map))
73+
throw new CommandExecutionException("point() argument must be a map with coordinate properties");
74+
final Map<?, ?> map = (Map<?, ?>) args[0];
75+
76+
final Map<String, Object> result = new LinkedHashMap<>();
77+
78+
if (map.containsKey("longitude") || map.containsKey("latitude")) {
79+
// WGS-84 coordinate system
80+
final Object lon = map.get("longitude");
81+
final Object lat = map.get("latitude");
82+
if (lon == null || lat == null)
83+
return null;
84+
final double x = ((Number) lon).doubleValue();
85+
final double y = ((Number) lat).doubleValue();
86+
result.put("longitude", x);
87+
result.put("latitude", y);
88+
result.put("x", x);
89+
result.put("y", y);
90+
addOptionalZ(result, map);
91+
result.put("crs", result.containsKey("z") ? "WGS-84-3D" : "WGS-84");
92+
result.put("srid", result.containsKey("z") ? 4979 : 4326);
93+
} else if (map.containsKey("x") || map.containsKey("y")) {
94+
// Cartesian coordinate system
95+
final Object xv = map.get("x");
96+
final Object yv = map.get("y");
97+
if (xv == null || yv == null)
98+
return null;
99+
final double x = ((Number) xv).doubleValue();
100+
final double y = ((Number) yv).doubleValue();
101+
result.put("x", x);
102+
result.put("y", y);
103+
addOptionalZ(result, map);
104+
final Object crsObj = map.get("crs");
105+
if (crsObj != null)
106+
result.put("crs", crsObj.toString());
107+
else
108+
result.put("crs", result.containsKey("z") ? "cartesian-3D" : "cartesian");
109+
if (map.containsKey("srid"))
110+
result.put("srid", ((Number) map.get("srid")).intValue());
111+
} else {
112+
throw new CommandExecutionException("point() map must contain x/y or longitude/latitude properties");
113+
}
114+
115+
return result;
116+
}
117+
118+
private void addOptionalZ(final Map<String, Object> result, final Map<?, ?> map) {
119+
if (map.containsKey("z")) {
120+
final Object zv = map.get("z");
121+
if (zv != null)
122+
result.put("z", ((Number) zv).doubleValue());
123+
} else if (map.containsKey("height")) {
124+
final Object hv = map.get("height");
125+
if (hv != null)
126+
result.put("z", ((Number) hv).doubleValue());
127+
}
53128
}
54129
}

engine/src/main/java/com/arcadedb/function/math/RoundFunction.java

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ public String getName() {
4242

4343
@Override
4444
public Object execute(final Object[] args, final CommandContext context) {
45-
if (args.length < 1 || args.length > 2)
45+
if (args.length < 1 || args.length > 3)
4646
throw new CommandExecutionException("round() requires one or two arguments");
4747

4848
if (args[0] == null)
@@ -61,7 +61,7 @@ public Object execute(final Object[] args, final CommandContext context) {
6161
return (double) Math.round(value);
6262
}
6363

64-
// round(value, precision)
64+
// round(value, precision) or round(value, precision, mode)
6565
if (args[1] == null)
6666
return null;
6767

@@ -70,7 +70,22 @@ public Object execute(final Object[] args, final CommandContext context) {
7070

7171
final int precision = ((Number) args[1]).intValue();
7272

73-
final BigDecimal bd = BigDecimal.valueOf(value).setScale(precision, RoundingMode.HALF_UP);
73+
RoundingMode mode = RoundingMode.HALF_UP;
74+
if (args.length == 3 && args[2] != null) {
75+
final String modeStr = args[2].toString().toUpperCase().replace(" ", "_");
76+
mode = switch (modeStr) {
77+
case "UP" -> RoundingMode.UP;
78+
case "DOWN" -> RoundingMode.DOWN;
79+
case "CEILING" -> RoundingMode.CEILING;
80+
case "FLOOR" -> RoundingMode.FLOOR;
81+
case "HALF_UP" -> RoundingMode.HALF_UP;
82+
case "HALF_DOWN" -> RoundingMode.HALF_DOWN;
83+
case "HALF_EVEN" -> RoundingMode.HALF_EVEN;
84+
default -> throw new CommandExecutionException("round() unknown rounding mode: " + args[2]);
85+
};
86+
}
87+
88+
final BigDecimal bd = BigDecimal.valueOf(value).setScale(precision, mode);
7489
return bd.doubleValue();
7590
}
7691
}

engine/src/main/java/com/arcadedb/function/sql/geo/GeoUtils.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import org.locationtech.spatial4j.shape.jts.JtsGeometry;
3333

3434
import java.util.Locale;
35+
import java.util.Map;
3536

3637
/**
3738
* Geospatial utility class.
@@ -69,6 +70,21 @@ public static Shape parseGeometry(final Object value) {
6970
return null;
7071
if (value instanceof Shape shape)
7172
return shape;
73+
// Cypher point() returns a Map with x/y or longitude/latitude keys
74+
if (value instanceof Map<?, ?> map) {
75+
double x;
76+
double y;
77+
if (map.containsKey("x") && map.containsKey("y")) {
78+
x = ((Number) map.get("x")).doubleValue();
79+
y = ((Number) map.get("y")).doubleValue();
80+
} else if (map.containsKey("longitude") && map.containsKey("latitude")) {
81+
x = ((Number) map.get("longitude")).doubleValue();
82+
y = ((Number) map.get("latitude")).doubleValue();
83+
} else {
84+
throw new IllegalArgumentException("Cannot parse geometry from map: missing x/y or longitude/latitude keys");
85+
}
86+
return SPATIAL_CONTEXT.getShapeFactory().pointXY(x, y);
87+
}
7288
final String wkt = value.toString().trim();
7389
if (wkt.isEmpty())
7490
return null;

engine/src/main/java/com/arcadedb/function/sql/math/SQLFunctionStandardDeviation.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,6 @@ public Object getResult() {
4040
final Object variance = super.getResult();
4141
if (variance != null)
4242
return Math.sqrt((Double) variance);
43-
return null;
43+
return 0.0;
4444
}
4545
}

engine/src/main/java/com/arcadedb/function/temporal/DateConstructorFunction.java

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030

3131
import java.time.LocalDate;
3232
import java.time.LocalDateTime;
33+
import java.time.ZoneId;
3334
import java.util.Map;
3435

3536
/**
@@ -48,8 +49,18 @@ public Object execute(final Object[] args, final CommandContext context) {
4849
return CypherFunctionHelper.getStatementTime(context).get("date");
4950
if (args[0] == null)
5051
return null;
51-
if (args[0] instanceof String)
52-
return CypherDate.parse((String) args[0]);
52+
if (args[0] instanceof String) {
53+
final String str = (String) args[0];
54+
try {
55+
return CypherDate.parse(str);
56+
} catch (final Exception e) {
57+
try {
58+
return new CypherDate(LocalDate.now(ZoneId.of(str)));
59+
} catch (final Exception e2) {
60+
throw new CommandExecutionException("date() cannot parse '" + str + "' as a date or timezone");
61+
}
62+
}
63+
}
5364
if (args[0] instanceof Map)
5465
return CypherDate.fromMap((Map<String, Object>) args[0]);
5566
if (args[0] instanceof CypherDate)

engine/src/main/java/com/arcadedb/function/temporal/DateTimeConstructorFunction.java

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,9 @@
2929

3030
import java.time.LocalDate;
3131
import java.time.LocalDateTime;
32+
import java.time.ZoneId;
3233
import java.time.ZoneOffset;
34+
import java.time.ZonedDateTime;
3335
import java.util.Map;
3436

3537
/**
@@ -48,8 +50,18 @@ public Object execute(final Object[] args, final CommandContext context) {
4850
return CypherFunctionHelper.getStatementTime(context).get("datetime");
4951
if (args[0] == null)
5052
return null;
51-
if (args[0] instanceof String)
52-
return CypherDateTime.parse((String) args[0]);
53+
if (args[0] instanceof String) {
54+
final String str = (String) args[0];
55+
try {
56+
return CypherDateTime.parse(str);
57+
} catch (final Exception e) {
58+
try {
59+
return new CypherDateTime(ZonedDateTime.now(ZoneId.of(str)));
60+
} catch (final Exception e2) {
61+
throw new CommandExecutionException("datetime() cannot parse '" + str + "' as a datetime or timezone");
62+
}
63+
}
64+
}
5365
if (args[0] instanceof Map)
5466
return CypherDateTime.fromMap((Map<String, Object>) args[0]);
5567
if (args[0] instanceof CypherDateTime)

0 commit comments

Comments
 (0)