Skip to content
Open
Show file tree
Hide file tree
Changes from 45 commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
f363428
added neo4j functions tests
Feb 15, 2026
2376022
fixed factory
Feb 15, 2026
cdec6bb
Merge branch 'ArcadeData:main' into more_tests
ExtReMLapin Feb 15, 2026
44d0c39
Merge branch 'ArcadeData:main' into more_tests
ExtReMLapin Feb 15, 2026
be84308
added elementid() (uses ID())
Feb 15, 2026
1ba12c7
Revert "added elementid() (uses ID())"
Feb 15, 2026
1d0342e
Merge branch 'ArcadeData:main' into more_tests
ExtReMLapin Feb 15, 2026
6488c38
fixed some tests (expect 64bit, not 32)
Feb 15, 2026
e8375db
more 64bit fixes
Feb 15, 2026
a41ff64
fixed some errors
Feb 15, 2026
ca38e62
more errors fixes
Feb 15, 2026
703eba0
Merge branch 'ArcadeData:main' into more_tests
ExtReMLapin Feb 16, 2026
a8f9dcd
Update engine/src/test/java/com/arcadedb/query/opencypher/functions/O…
ExtReMLapin Feb 16, 2026
ddc6dbf
Update engine/src/test/java/com/arcadedb/query/opencypher/functions/O…
ExtReMLapin Feb 16, 2026
99aa587
Merge branch 'ArcadeData:main' into more_tests
ExtReMLapin Feb 16, 2026
c454c1c
fixed compilation error
Feb 18, 2026
c826fe5
Merge branch 'ArcadeData:main' into more_tests
ExtReMLapin Mar 19, 2026
16af5f2
fixed exception not being detected
Mar 19, 2026
00afff9
fixed missing exceptions
Mar 19, 2026
64e2abd
updated tests
Mar 19, 2026
8280281
updated tests
Mar 20, 2026
3b6fd79
Coll insert/remove force second arg
Mar 20, 2026
6a23fdf
point.distance cypher
Mar 20, 2026
9dff309
cypher point use map like cypher, not like sql
Mar 20, 2026
0762c40
geo utils support map xy or longitude latitude
Mar 20, 2026
91407e5
left support two args error and neg len error
Mar 20, 2026
2c5f0f0
two args and neg len
Mar 20, 2026
6275cb2
force 3 args for Replace function
Mar 20, 2026
69e265d
lTrim support two args
Mar 20, 2026
50ec604
rtrim two args
Mar 20, 2026
28f6a6e
substring better errors handling
Mar 20, 2026
87e67e6
sync
Mar 20, 2026
2f591d6
Round added precision and mode
Mar 20, 2026
3266b9b
added timezones
Mar 20, 2026
7e9e0c6
updated vectors
Mar 20, 2026
ce445f6
sync
Mar 20, 2026
f7b0fb0
SQLFunctionStandardDeviation now returns 0.0 instead of null
Mar 20, 2026
34c5150
Update engine/src/main/java/com/arcadedb/function/geo/CypherPointFunc…
ExtReMLapin Mar 20, 2026
ef55e43
updated old test
Mar 20, 2026
14fa415
Merge branch 'ArcadeData:main' into more_tests
ExtReMLapin Apr 18, 2026
8e79172
sync
Apr 18, 2026
f8f942c
sync
Apr 18, 2026
867ccca
support for parallel tests running + isolation
Apr 18, 2026
f58e1ad
fixed config print
Apr 18, 2026
b09a93f
fixed codacy being a crybaby
Apr 18, 2026
136f775
Merge branch 'main' into more_test_fast_tests
ExtReMLapin Apr 22, 2026
05758f2
removed whitespace
ExtReMLapin Apr 22, 2026
24af8f3
removed whitespace
ExtReMLapin Apr 22, 2026
2ea2ee0
removed whitespace
ExtReMLapin Apr 22, 2026
3b6117c
removed whitespace
ExtReMLapin Apr 22, 2026
7705b9e
removed whitespace
ExtReMLapin Apr 22, 2026
15b72ba
removed whitespace
ExtReMLapin Apr 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,10 @@ cd package
### Testing Commands
- **Run specific test class**: `mvn test -Dtest=ClassName`
- **Run tests with specific pattern**: `mvn test -Dtest="*Pattern*"`
- **Run without slow/benchmark tests**: `mvn test -pl engine -DskipITs -Dgroups='!slow,!benchmark'`
- **Override fork count** (e.g. all cores): `mvn test -Dforkcount=1C`
- **Performance tests**: Located in `src/test/java/performance/` packages
- **Parallel execution**: enabled by default - classes run concurrently across JVM forks; methods within a class run sequentially. Tests that mutate `GlobalConfiguration` are annotated `@ResourceLock("GlobalConfiguration")` to serialize with each other.

## Codebase Navigation Map

Expand Down
20 changes: 20 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,26 @@ Build and run default tests:
$ mvn clean install
```

Tests run in parallel by default: classes execute concurrently across multiple JVM forks (`forkCount=0.5C`, i.e. half the available CPU cores). No extra flags needed.

Run only unit tests for the engine module, skipping slow/benchmark tests:

```shell
$ mvn test -pl engine -DskipITs -Dgroups='!slow,!benchmark'
```

Override the number of parallel JVM forks (e.g. use all cores):

```shell
$ mvn test -pl engine -DskipITs -Dforkcount=1C
```

Run a single test class:

```shell
$ mvn test -pl engine -Dtest=MyTestClass -DskipITs
```

To run additional integration test locally use:

```shell
Expand Down
25 changes: 12 additions & 13 deletions engine/src/main/java/com/arcadedb/GlobalConfiguration.java
Original file line number Diff line number Diff line change
Expand Up @@ -42,20 +42,19 @@ public enum GlobalConfiguration {
// ENVIRONMENT
DUMP_CONFIG_AT_STARTUP("arcadedb.dumpConfigAtStartup", SCOPE.JVM, "Dumps the configuration at startup", Boolean.class, false,
value -> {
//dumpConfiguration(System.out);

try {
final ByteArrayOutputStream buffer = new ByteArrayOutputStream();
dumpConfiguration(new PrintStream(buffer));
if (LogManager.instance() != null)
LogManager.instance().log(buffer, Level.WARNING, new String(buffer.toByteArray()));
else
System.out.println(new String(buffer.toByteArray()));
if (Boolean.TRUE.equals(value))
try {
final ByteArrayOutputStream buffer = new ByteArrayOutputStream();
dumpConfiguration(new PrintStream(buffer));
if (LogManager.instance() != null)
LogManager.instance().log(buffer, Level.WARNING, new String(buffer.toByteArray()));
else
System.out.println(new String(buffer.toByteArray()));

buffer.close();
} catch (IOException e) {
System.out.println("Error on printing initial configuration to log (error=" + e + ")");
}
buffer.close();
} catch (IOException e) {
System.out.println("Error on printing initial configuration to log (error=" + e + ")");
}

return value;
}),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,13 @@ public Object execute(final Object[] args, final CommandContext context) {
final List<Object> list = asList(args[0]);
if (list == null)
return null;
if (args[1] == null)
return null;
final int index = ((Number) args[1]).intValue();
if (index < 0)
throw new CommandExecutionException("coll.insert() does not support negative index: " + index);
if (index > list.size())
throw new CommandExecutionException("coll.insert() index " + index + " is out of range for list of size " + list.size());
final List<Object> result = new ArrayList<>(list);
result.add(index, args[2]);
return result;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,13 @@ public Object execute(final Object[] args, final CommandContext context) {
final List<Object> list = asList(args[0]);
if (list == null)
return null;
if (args[1] == null)
return null;

final int index = ((Number) args[1]).intValue();
if (index < 0 || index >= list.size())
if (index < 0)
throw new CommandExecutionException("coll.remove() does not support negative index: " + index);
if (index >= list.size())
throw new CommandExecutionException("coll.remove() index " + index + " is out of range for list of size " + list.size());
final int count = args.length > 2 ? ((Number) args[2]).intValue() : 1;
final List<Object> result = new ArrayList<>(list);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*
* Copyright © 2021-present Arcade Data Ltd ([email protected])
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-FileCopyrightText: 2021-present Arcade Data Ltd ([email protected])
* SPDX-License-Identifier: Apache-2.0
*/
package com.arcadedb.function.geo;

import com.arcadedb.exception.CommandExecutionException;
import com.arcadedb.function.StatelessFunction;
import com.arcadedb.query.sql.executor.CommandContext;

import java.util.Map;

/**
* Cypher {@code point.distance(point1, point2)} function.
*
* <p>Computes the distance between two points. Uses Haversine formula for WGS-84
* geographic points (result in meters), and Euclidean distance for Cartesian points.</p>
*/
public class CypherPointDistanceFunction implements StatelessFunction {
private static final double EARTH_RADIUS_M = 6371000.0;

@Override
public String getName() {
return "point.distance";
}

@Override
public Object execute(final Object[] args, final CommandContext context) {
if (args == null || args.length != 2)
throw new CommandExecutionException("point.distance() requires exactly 2 arguments");
if (args[0] == null || args[1] == null)
return null;
if (!(args[0] instanceof Map) || !(args[1] instanceof Map))
throw new CommandExecutionException("point.distance() arguments must be point values (maps)");
final Map<?, ?> p1 = (Map<?, ?>) args[0];
final Map<?, ?> p2 = (Map<?, ?>) args[1];

// WGS-84: use Haversine formula
if (p1.containsKey("longitude") && p1.containsKey("latitude") &&
p2.containsKey("longitude") && p2.containsKey("latitude"))
return haversineDistance(
((Number) p1.get("latitude")).doubleValue(), ((Number) p1.get("longitude")).doubleValue(),
((Number) p2.get("latitude")).doubleValue(), ((Number) p2.get("longitude")).doubleValue());
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This code is unsafe as it assumes the map values are non-null and of type Number. If a coordinate is null in the map (which is possible in Cypher), this will throw a NullPointerException. If it's a String, it will throw a ClassCastException. It is safer to use GeoUtils.getDoubleValue() after verifying the values are not null.


// Cartesian: use Euclidean distance
final Number x1n = (Number) p1.get("x");
final Number y1n = (Number) p1.get("y");
final Number x2n = (Number) p2.get("x");
final Number y2n = (Number) p2.get("y");
if (x1n == null || y1n == null || x2n == null || y2n == null)
return null;
final double dx = x2n.doubleValue() - x1n.doubleValue();
final double dy = y2n.doubleValue() - y1n.doubleValue();
double sumSq = dx * dx + dy * dy;
final Number z1n = (Number) p1.get("z");
final Number z2n = (Number) p2.get("z");
if (z1n != null && z2n != null) {
final double dz = z2n.doubleValue() - z1n.doubleValue();
sumSq += dz * dz;
}
return Math.sqrt(sumSq);
}

private double haversineDistance(final double lat1, final double lon1, final double lat2, final double lon2) {
final double dLat = Math.toRadians(lat2 - lat1);
final double dLon = Math.toRadians(lon2 - lon1);
final double a = Math.pow(Math.sin(dLat / 2), 2)
+ Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2))
* Math.pow(Math.sin(dLon / 2), 2);
return 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)) * EARTH_RADIUS_M;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,23 @@

import com.arcadedb.exception.CommandExecutionException;
import com.arcadedb.function.StatelessFunction;
import com.arcadedb.function.sql.geo.GeoUtils;
import com.arcadedb.function.sql.geo.LightweightPoint;
import com.arcadedb.query.sql.executor.CommandContext;

import java.util.LinkedHashMap;
import java.util.Map;

/**
* Cypher {@code point(lat, lon)} function.
*
* <p>Constructs a spatial point from latitude and longitude. Following Cypher/Neo4j convention,
* the first argument is latitude and the second is longitude. The point is stored internally
* using the spatial4j convention (x=longitude, y=latitude) so that spatial distance functions
* such as {@code geo.distance} operate correctly.</p>
* Cypher {@code point(map)} function.
*
* <p>Usage: {@code point(<latitude>, <longitude>)}</p>
* <p>Constructs a point from a map of coordinate properties. Supports:</p>
* <ul>
* <li>WGS-84 2D: {@code point({longitude: x, latitude: y})}</li>
* <li>WGS-84 3D: {@code point({longitude: x, latitude: y, height: z})}</li>
* <li>Cartesian 2D: {@code point({x: a, y: b})}</li>
* <li>Cartesian 3D: {@code point({x: a, y: b, z: c})}</li>
* </ul>
* <p>The returned map contains the coordinate keys and a {@code crs} field indicating
* the coordinate reference system.</p>
*/
public class CypherPointFunction implements StatelessFunction {
@Override
Expand All @@ -42,13 +46,84 @@ public String getName() {

@Override
public Object execute(final Object[] args, final CommandContext context) {
if (args == null || args.length < 2)
throw new CommandExecutionException("point() requires latitude and longitude as parameters");
if (args[0] == null || args[1] == null)
if (args == null || args.length == 0 || args.length > 2)
throw new CommandExecutionException("point() requires either one map argument (point({...})) or two numeric arguments (point(latitude, longitude))");

// 2-arg positional form: point(latitude, longitude) → WGS-84 2D
if (args.length == 2) {
if (args[0] == null || args[1] == null)
return null;
if (!(args[0] instanceof Number) || !(args[1] instanceof Number))
throw new CommandExecutionException("point() with two arguments requires numeric latitude and longitude");
final double lat = ((Number) args[0]).doubleValue();
final double lon = ((Number) args[1]).doubleValue();
Comment on lines +58 to +59
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This change reduces robustness compared to the previous implementation. The previous code used GeoUtils.getDoubleValue(), which safely handles both Number and String inputs. The direct cast to Number will throw a ClassCastException if the input is a string representation of a coordinate.

Suggested change
final double lat = ((Number) args[0]).doubleValue();
final double lon = ((Number) args[1]).doubleValue();
final double lat = com.arcadedb.function.sql.geo.GeoUtils.getDoubleValue(args[0]);
final double lon = com.arcadedb.function.sql.geo.GeoUtils.getDoubleValue(args[1]);

final Map<String, Object> result = new LinkedHashMap<>();
result.put("latitude", lat);
result.put("longitude", lon);
result.put("x", lon);
result.put("y", lat);
result.put("crs", "WGS-84");
result.put("srid", 4326);
return result;
}

if (args[0] == null)
return null;
final double lat = GeoUtils.getDoubleValue(args[0]);
final double lon = GeoUtils.getDoubleValue(args[1]);
// Store as LightweightPoint(x=longitude, y=latitude) per spatial4j convention
return new LightweightPoint(lon, lat);
if (!(args[0] instanceof Map))
throw new CommandExecutionException("point() argument must be a map with coordinate properties");
final Map<?, ?> map = (Map<?, ?>) args[0];

final Map<String, Object> result = new LinkedHashMap<>();

if (map.containsKey("longitude") || map.containsKey("latitude")) {
// WGS-84 coordinate system
final Object lon = map.get("longitude");
final Object lat = map.get("latitude");
if (lon == null || lat == null)
return null;
final double x = ((Number) lon).doubleValue();
final double y = ((Number) lat).doubleValue();
result.put("longitude", x);
result.put("latitude", y);
result.put("x", x);
result.put("y", y);
addOptionalZ(result, map);
result.put("crs", result.containsKey("z") ? "WGS-84-3D" : "WGS-84");
result.put("srid", result.containsKey("z") ? 4979 : 4326);
} else if (map.containsKey("x") || map.containsKey("y")) {
// Cartesian coordinate system
final Object xv = map.get("x");
final Object yv = map.get("y");
if (xv == null || yv == null)
return null;
final double x = ((Number) xv).doubleValue();
final double y = ((Number) yv).doubleValue();
result.put("x", x);
result.put("y", y);
addOptionalZ(result, map);
final Object crsObj = map.get("crs");
if (crsObj != null)
result.put("crs", crsObj.toString());
else
result.put("crs", result.containsKey("z") ? "cartesian-3D" : "cartesian");
if (map.containsKey("srid"))
result.put("srid", ((Number) map.get("srid")).intValue());
} else {
throw new CommandExecutionException("point() map must contain x/y or longitude/latitude properties");
}

return result;
}

private void addOptionalZ(final Map<String, Object> result, final Map<?, ?> map) {
if (map.containsKey("z")) {
final Object zv = map.get("z");
if (zv != null)
result.put("z", ((Number) zv).doubleValue());
} else if (map.containsKey("height")) {
final Object hv = map.get("height");
if (hv != null)
result.put("z", ((Number) hv).doubleValue());
}
}
}
21 changes: 18 additions & 3 deletions engine/src/main/java/com/arcadedb/function/math/RoundFunction.java
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public String getName() {

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

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

// round(value, precision)
// round(value, precision) or round(value, precision, mode)
if (args[1] == null)
return null;

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

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

final BigDecimal bd = BigDecimal.valueOf(value).setScale(precision, RoundingMode.HALF_UP);
RoundingMode mode = RoundingMode.HALF_UP;
if (args.length == 3 && args[2] != null) {
final String modeStr = args[2].toString().toUpperCase().replace(" ", "_");
mode = switch (modeStr) {
case "UP" -> RoundingMode.UP;
case "DOWN" -> RoundingMode.DOWN;
case "CEILING" -> RoundingMode.CEILING;
case "FLOOR" -> RoundingMode.FLOOR;
case "HALF_UP" -> RoundingMode.HALF_UP;
case "HALF_DOWN" -> RoundingMode.HALF_DOWN;
case "HALF_EVEN" -> RoundingMode.HALF_EVEN;
default -> throw new CommandExecutionException("round() unknown rounding mode: " + args[2]);
};
}

final BigDecimal bd = BigDecimal.valueOf(value).setScale(precision, mode);
return bd.doubleValue();
}
}
16 changes: 16 additions & 0 deletions engine/src/main/java/com/arcadedb/function/sql/geo/GeoUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import org.locationtech.spatial4j.shape.jts.JtsGeometry;

import java.util.Locale;
import java.util.Map;

/**
* Geospatial utility class.
Expand Down Expand Up @@ -69,6 +70,21 @@ public static Shape parseGeometry(final Object value) {
return null;
if (value instanceof Shape shape)
return shape;
// Cypher point() returns a Map with x/y or longitude/latitude keys
if (value instanceof Map<?, ?> map) {
double x;
double y;
if (map.containsKey("x") && map.containsKey("y")) {
x = ((Number) map.get("x")).doubleValue();
y = ((Number) map.get("y")).doubleValue();
} else if (map.containsKey("longitude") && map.containsKey("latitude")) {
x = ((Number) map.get("longitude")).doubleValue();
y = ((Number) map.get("latitude")).doubleValue();
} else {
throw new IllegalArgumentException("Cannot parse geometry from map: missing x/y or longitude/latitude keys");
}
return SPATIAL_CONTEXT.getShapeFactory().pointXY(x, y);
}
final String wkt = value.toString().trim();
if (wkt.isEmpty())
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,6 @@ public Object getResult() {
final Object variance = super.getResult();
if (variance != null)
return Math.sqrt((Double) variance);
return null;
return 0.0;
}
}
Loading
Loading