diff --git a/engine/src/main/java/com/arcadedb/function/coll/CollInsert.java b/engine/src/main/java/com/arcadedb/function/coll/CollInsert.java index 1ade5e1eb2..33fd6ee303 100644 --- a/engine/src/main/java/com/arcadedb/function/coll/CollInsert.java +++ b/engine/src/main/java/com/arcadedb/function/coll/CollInsert.java @@ -57,7 +57,13 @@ public Object execute(final Object[] args, final CommandContext context) { final List 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 result = new ArrayList<>(list); result.add(index, args[2]); return result; diff --git a/engine/src/main/java/com/arcadedb/function/coll/CollRemove.java b/engine/src/main/java/com/arcadedb/function/coll/CollRemove.java index bfe5330eff..9616bbfc18 100644 --- a/engine/src/main/java/com/arcadedb/function/coll/CollRemove.java +++ b/engine/src/main/java/com/arcadedb/function/coll/CollRemove.java @@ -58,9 +58,13 @@ public Object execute(final Object[] args, final CommandContext context) { final List 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 result = new ArrayList<>(list); diff --git a/engine/src/main/java/com/arcadedb/function/geo/CypherPointDistanceFunction.java b/engine/src/main/java/com/arcadedb/function/geo/CypherPointDistanceFunction.java new file mode 100644 index 0000000000..81fdd97261 --- /dev/null +++ b/engine/src/main/java/com/arcadedb/function/geo/CypherPointDistanceFunction.java @@ -0,0 +1,93 @@ +/* + * Copyright © 2021-present Arcade Data Ltd (info@arcadedata.com) + * + * 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 (info@arcadedata.com) + * 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. + * + *

Computes the distance between two points. Uses Haversine formula for WGS-84 + * geographic points (result in meters), and Euclidean distance for Cartesian points.

+ */ +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")) { + final Number lat1n = (Number) p1.get("latitude"); + final Number lon1n = (Number) p1.get("longitude"); + final Number lat2n = (Number) p2.get("latitude"); + final Number lon2n = (Number) p2.get("longitude"); + if (lat1n == null || lon1n == null || lat2n == null || lon2n == null) + return null; + return haversineDistance(lat1n.doubleValue(), lon1n.doubleValue(), lat2n.doubleValue(), lon2n.doubleValue()); + } + + // 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)) + return null; + if (z1n != 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; + } +} diff --git a/engine/src/main/java/com/arcadedb/function/geo/CypherPointFunction.java b/engine/src/main/java/com/arcadedb/function/geo/CypherPointFunction.java index 8e727710f8..3630c7e325 100644 --- a/engine/src/main/java/com/arcadedb/function/geo/CypherPointFunction.java +++ b/engine/src/main/java/com/arcadedb/function/geo/CypherPointFunction.java @@ -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. - * - *

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.

+ * Cypher {@code point(map)} function. * - *

Usage: {@code point(, )}

+ *

Constructs a point from a map of coordinate properties. Supports:

+ *
    + *
  • WGS-84 2D: {@code point({longitude: x, latitude: y})}
  • + *
  • WGS-84 3D: {@code point({longitude: x, latitude: y, height: z})}
  • + *
  • Cartesian 2D: {@code point({x: a, y: b})}
  • + *
  • Cartesian 3D: {@code point({x: a, y: b, z: c})}
  • + *
+ *

The returned map contains the coordinate keys and a {@code crs} field indicating + * the coordinate reference system.

*/ public class CypherPointFunction implements StatelessFunction { @Override @@ -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(); + final Map 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 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 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()); + } } } diff --git a/engine/src/main/java/com/arcadedb/function/math/RoundFunction.java b/engine/src/main/java/com/arcadedb/function/math/RoundFunction.java index 4ec1f22221..98b7c15b6c 100644 --- a/engine/src/main/java/com/arcadedb/function/math/RoundFunction.java +++ b/engine/src/main/java/com/arcadedb/function/math/RoundFunction.java @@ -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) @@ -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; @@ -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(); } } diff --git a/engine/src/main/java/com/arcadedb/function/sql/geo/GeoUtils.java b/engine/src/main/java/com/arcadedb/function/sql/geo/GeoUtils.java index 44cbf4a038..c05c73bcc6 100644 --- a/engine/src/main/java/com/arcadedb/function/sql/geo/GeoUtils.java +++ b/engine/src/main/java/com/arcadedb/function/sql/geo/GeoUtils.java @@ -32,6 +32,7 @@ import org.locationtech.spatial4j.shape.jts.JtsGeometry; import java.util.Locale; +import java.util.Map; /** * Geospatial utility class. @@ -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; diff --git a/engine/src/main/java/com/arcadedb/function/sql/math/SQLFunctionStandardDeviation.java b/engine/src/main/java/com/arcadedb/function/sql/math/SQLFunctionStandardDeviation.java index a4c8f28d45..1142fa4539 100644 --- a/engine/src/main/java/com/arcadedb/function/sql/math/SQLFunctionStandardDeviation.java +++ b/engine/src/main/java/com/arcadedb/function/sql/math/SQLFunctionStandardDeviation.java @@ -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; } } diff --git a/engine/src/main/java/com/arcadedb/function/temporal/DateConstructorFunction.java b/engine/src/main/java/com/arcadedb/function/temporal/DateConstructorFunction.java index f37661dc27..3f5318ba31 100644 --- a/engine/src/main/java/com/arcadedb/function/temporal/DateConstructorFunction.java +++ b/engine/src/main/java/com/arcadedb/function/temporal/DateConstructorFunction.java @@ -30,6 +30,7 @@ import java.time.LocalDate; import java.time.LocalDateTime; +import java.time.ZoneId; import java.util.Map; /** @@ -48,8 +49,18 @@ public Object execute(final Object[] args, final CommandContext context) { return CypherFunctionHelper.getStatementTime(context).get("date"); if (args[0] == null) return null; - if (args[0] instanceof String) - return CypherDate.parse((String) args[0]); + if (args[0] instanceof String) { + final String str = (String) args[0]; + try { + return CypherDate.parse(str); + } catch (final Exception e) { + try { + return new CypherDate(LocalDate.now(ZoneId.of(str))); + } catch (final Exception e2) { + throw new CommandExecutionException("date() cannot parse '" + str + "' as a date or timezone"); + } + } + } if (args[0] instanceof Map) return CypherDate.fromMap((Map) args[0]); if (args[0] instanceof CypherDate) diff --git a/engine/src/main/java/com/arcadedb/function/temporal/DateTimeConstructorFunction.java b/engine/src/main/java/com/arcadedb/function/temporal/DateTimeConstructorFunction.java index 7640bb8433..0870402281 100644 --- a/engine/src/main/java/com/arcadedb/function/temporal/DateTimeConstructorFunction.java +++ b/engine/src/main/java/com/arcadedb/function/temporal/DateTimeConstructorFunction.java @@ -29,7 +29,9 @@ import java.time.LocalDate; import java.time.LocalDateTime; +import java.time.ZoneId; import java.time.ZoneOffset; +import java.time.ZonedDateTime; import java.util.Map; /** @@ -48,8 +50,18 @@ public Object execute(final Object[] args, final CommandContext context) { return CypherFunctionHelper.getStatementTime(context).get("datetime"); if (args[0] == null) return null; - if (args[0] instanceof String) - return CypherDateTime.parse((String) args[0]); + if (args[0] instanceof String) { + final String str = (String) args[0]; + try { + return CypherDateTime.parse(str); + } catch (final Exception e) { + try { + return new CypherDateTime(ZonedDateTime.now(ZoneId.of(str))); + } catch (final Exception e2) { + throw new CommandExecutionException("datetime() cannot parse '" + str + "' as a datetime or timezone"); + } + } + } if (args[0] instanceof Map) return CypherDateTime.fromMap((Map) args[0]); if (args[0] instanceof CypherDateTime) diff --git a/engine/src/main/java/com/arcadedb/function/temporal/LocalDateTimeConstructorFunction.java b/engine/src/main/java/com/arcadedb/function/temporal/LocalDateTimeConstructorFunction.java index fa5b415010..ef99adbbfa 100644 --- a/engine/src/main/java/com/arcadedb/function/temporal/LocalDateTimeConstructorFunction.java +++ b/engine/src/main/java/com/arcadedb/function/temporal/LocalDateTimeConstructorFunction.java @@ -30,6 +30,7 @@ import java.time.LocalDate; import java.time.LocalDateTime; +import java.time.ZoneId; import java.util.Map; /** @@ -48,8 +49,18 @@ public Object execute(final Object[] args, final CommandContext context) { return CypherFunctionHelper.getStatementTime(context).get("localdatetime"); if (args[0] == null) return null; - if (args[0] instanceof String) - return CypherLocalDateTime.parse((String) args[0]); + if (args[0] instanceof String) { + final String str = (String) args[0]; + try { + return CypherLocalDateTime.parse(str); + } catch (final Exception e) { + try { + return new CypherLocalDateTime(LocalDateTime.now(ZoneId.of(str))); + } catch (final Exception e2) { + throw new CommandExecutionException("localdatetime() cannot parse '" + str + "' as a local datetime or timezone"); + } + } + } if (args[0] instanceof Map) return CypherLocalDateTime.fromMap((Map) args[0]); if (args[0] instanceof CypherLocalDateTime) diff --git a/engine/src/main/java/com/arcadedb/function/temporal/LocalTimeConstructorFunction.java b/engine/src/main/java/com/arcadedb/function/temporal/LocalTimeConstructorFunction.java index f5189931ce..ed331ba963 100644 --- a/engine/src/main/java/com/arcadedb/function/temporal/LocalTimeConstructorFunction.java +++ b/engine/src/main/java/com/arcadedb/function/temporal/LocalTimeConstructorFunction.java @@ -29,6 +29,8 @@ import com.arcadedb.query.sql.executor.CommandContext; import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.ZoneId; import java.util.Map; /** @@ -47,8 +49,18 @@ public Object execute(final Object[] args, final CommandContext context) { return CypherFunctionHelper.getStatementTime(context).get("localtime"); if (args[0] == null) return null; - if (args[0] instanceof String) - return CypherLocalTime.parse((String) args[0]); + if (args[0] instanceof String) { + final String str = (String) args[0]; + try { + return CypherLocalTime.parse(str); + } catch (final Exception e) { + try { + return new CypherLocalTime(LocalTime.now(ZoneId.of(str))); + } catch (final Exception e2) { + throw new CommandExecutionException("localtime() cannot parse '" + str + "' as a local time or timezone"); + } + } + } if (args[0] instanceof Map) return CypherLocalTime.fromMap((Map) args[0]); if (args[0] instanceof CypherLocalTime) diff --git a/engine/src/main/java/com/arcadedb/function/temporal/TimeConstructorFunction.java b/engine/src/main/java/com/arcadedb/function/temporal/TimeConstructorFunction.java index 54cfa05a5f..dd9739a111 100644 --- a/engine/src/main/java/com/arcadedb/function/temporal/TimeConstructorFunction.java +++ b/engine/src/main/java/com/arcadedb/function/temporal/TimeConstructorFunction.java @@ -28,6 +28,8 @@ import com.arcadedb.query.opencypher.temporal.CypherTime; import com.arcadedb.query.sql.executor.CommandContext; +import java.time.OffsetTime; +import java.time.ZoneId; import java.time.ZoneOffset; import java.util.Map; @@ -47,8 +49,18 @@ public Object execute(final Object[] args, final CommandContext context) { return CypherFunctionHelper.getStatementTime(context).get("time"); if (args[0] == null) return null; - if (args[0] instanceof String) - return CypherTime.parse((String) args[0]); + if (args[0] instanceof String) { + final String str = (String) args[0]; + try { + return CypherTime.parse(str); + } catch (final Exception e) { + try { + return new CypherTime(OffsetTime.now(ZoneId.of(str))); + } catch (final Exception e2) { + throw new CommandExecutionException("time() cannot parse '" + str + "' as a time or timezone"); + } + } + } if (args[0] instanceof Map) return CypherTime.fromMap((Map) args[0]); if (args[0] instanceof CypherTime) diff --git a/engine/src/main/java/com/arcadedb/function/text/LTrimFunction.java b/engine/src/main/java/com/arcadedb/function/text/LTrimFunction.java index 43256fd167..00946fbd42 100644 --- a/engine/src/main/java/com/arcadedb/function/text/LTrimFunction.java +++ b/engine/src/main/java/com/arcadedb/function/text/LTrimFunction.java @@ -33,10 +33,24 @@ public String getName() { @Override public Object execute(final Object[] args, final CommandContext context) { - if (args.length != 1) - throw new CommandExecutionException("lTrim() requires exactly one argument"); - if (args[0] == null) - return null; - return args[0].toString().stripLeading(); + if (args.length == 1) { + if (args[0] == null) + return null; + return args[0].toString().stripLeading(); + } + if (args.length == 2) { + if (args[0] == null || args[1] == null) + return null; + final String source = args[0].toString(); + final String trimChar = args[1].toString(); + if (trimChar.isEmpty()) + return source.stripLeading(); + return stripLeading(source, trimChar); + } + throw new CommandExecutionException("lTrim() requires 1 or 2 arguments"); + } + + private static String stripLeading(final String source, final String trimChars) { + return TrimFunction.stripLeading(source, trimChars); } } diff --git a/engine/src/main/java/com/arcadedb/function/text/LeftFunction.java b/engine/src/main/java/com/arcadedb/function/text/LeftFunction.java index 8695b34763..6d3f8f3f7e 100644 --- a/engine/src/main/java/com/arcadedb/function/text/LeftFunction.java +++ b/engine/src/main/java/com/arcadedb/function/text/LeftFunction.java @@ -35,10 +35,12 @@ public String getName() { public Object execute(final Object[] args, final CommandContext context) { if (args.length != 2) throw new CommandExecutionException("left() requires exactly 2 arguments: left(string, length)"); - if (args[0] == null) + if (args[0] == null || args[1] == null) return null; final String str = args[0].toString(); final int length = ((Number) args[1]).intValue(); + if (length < 0) + throw new CommandExecutionException("left(): negative length is not supported: " + length); return str.substring(0, Math.min(length, str.length())); } } diff --git a/engine/src/main/java/com/arcadedb/function/text/RTrimFunction.java b/engine/src/main/java/com/arcadedb/function/text/RTrimFunction.java index 4d81984092..0ab71b55dd 100644 --- a/engine/src/main/java/com/arcadedb/function/text/RTrimFunction.java +++ b/engine/src/main/java/com/arcadedb/function/text/RTrimFunction.java @@ -33,10 +33,24 @@ public String getName() { @Override public Object execute(final Object[] args, final CommandContext context) { - if (args.length != 1) - throw new CommandExecutionException("rTrim() requires exactly one argument"); - if (args[0] == null) - return null; - return args[0].toString().stripTrailing(); + if (args.length == 1) { + if (args[0] == null) + return null; + return args[0].toString().stripTrailing(); + } + if (args.length == 2) { + if (args[0] == null || args[1] == null) + return null; + final String source = args[0].toString(); + final String trimChar = args[1].toString(); + if (trimChar.isEmpty()) + return source.stripTrailing(); + return stripTrailing(source, trimChar); + } + throw new CommandExecutionException("rTrim() requires 1 or 2 arguments"); + } + + private static String stripTrailing(final String source, final String trimChars) { + return TrimFunction.stripTrailing(source, trimChars); } } diff --git a/engine/src/main/java/com/arcadedb/function/text/ReplaceFunction.java b/engine/src/main/java/com/arcadedb/function/text/ReplaceFunction.java index bb48fef19c..c1f03b26b1 100644 --- a/engine/src/main/java/com/arcadedb/function/text/ReplaceFunction.java +++ b/engine/src/main/java/com/arcadedb/function/text/ReplaceFunction.java @@ -38,7 +38,7 @@ public String getName() { public Object execute(final Object[] args, final CommandContext context) { if (args.length != 3) throw new CommandExecutionException("replace() requires exactly 3 arguments: replace(original, search, replace)"); - if (args[0] == null) + if (args[0] == null || args[1] == null || args[2] == null) return null; return args[0].toString().replace(args[1].toString(), args[2].toString()); } diff --git a/engine/src/main/java/com/arcadedb/function/text/RightFunction.java b/engine/src/main/java/com/arcadedb/function/text/RightFunction.java index d4821eaa35..ef470ff6f6 100644 --- a/engine/src/main/java/com/arcadedb/function/text/RightFunction.java +++ b/engine/src/main/java/com/arcadedb/function/text/RightFunction.java @@ -35,10 +35,12 @@ public String getName() { public Object execute(final Object[] args, final CommandContext context) { if (args.length != 2) throw new CommandExecutionException("right() requires exactly 2 arguments: right(string, length)"); - if (args[0] == null) + if (args[0] == null || args[1] == null) return null; final String str = args[0].toString(); final int length = ((Number) args[1]).intValue(); + if (length < 0) + throw new CommandExecutionException("right(): negative length is not supported: " + length); return str.substring(Math.max(0, str.length() - length)); } } diff --git a/engine/src/main/java/com/arcadedb/function/text/SubstringFunction.java b/engine/src/main/java/com/arcadedb/function/text/SubstringFunction.java index 910e159c7b..c359caba55 100644 --- a/engine/src/main/java/com/arcadedb/function/text/SubstringFunction.java +++ b/engine/src/main/java/com/arcadedb/function/text/SubstringFunction.java @@ -35,7 +35,7 @@ public String getName() { public Object execute(final Object[] args, final CommandContext context) { if (args.length < 2 || args.length > 3) throw new CommandExecutionException("substring() requires 2 or 3 arguments: substring(string, start[, length])"); - if (args[0] == null) + if (args[0] == null || args[1] == null) return null; final String str = args[0].toString(); final int start = ((Number) args[1]).intValue(); @@ -43,6 +43,8 @@ public Object execute(final Object[] args, final CommandContext context) { return ""; if (args.length == 3 && args[2] != null) { final int length = ((Number) args[2]).intValue(); + if (length < 0) + throw new CommandExecutionException("substring(): negative length is not supported: " + length); return str.substring(start, Math.min(start + length, str.length())); } return str.substring(start); diff --git a/engine/src/main/java/com/arcadedb/function/text/TrimFunction.java b/engine/src/main/java/com/arcadedb/function/text/TrimFunction.java index 623a37fa19..236b5eb495 100644 --- a/engine/src/main/java/com/arcadedb/function/text/TrimFunction.java +++ b/engine/src/main/java/com/arcadedb/function/text/TrimFunction.java @@ -74,17 +74,17 @@ public Object execute(final Object[] args, final CommandContext context) { throw new CommandExecutionException("trim() requires 1 or 3 arguments"); } - private static String stripLeading(final String source, final String trimChar) { + static String stripLeading(final String source, final String trimChars) { int start = 0; - while (start < source.length() && source.startsWith(trimChar, start)) - start += trimChar.length(); + while (start < source.length() && trimChars.indexOf(source.charAt(start)) >= 0) + start++; return source.substring(start); } - private static String stripTrailing(final String source, final String trimChar) { + static String stripTrailing(final String source, final String trimChars) { int end = source.length(); - while (end >= trimChar.length() && source.startsWith(trimChar, end - trimChar.length())) - end -= trimChar.length(); + while (end > 0 && trimChars.indexOf(source.charAt(end - 1)) >= 0) + end--; return source.substring(0, end); } } diff --git a/engine/src/main/java/com/arcadedb/function/vector/VectorCreateFunction.java b/engine/src/main/java/com/arcadedb/function/vector/VectorCreateFunction.java index 9122defab1..8642d87402 100644 --- a/engine/src/main/java/com/arcadedb/function/vector/VectorCreateFunction.java +++ b/engine/src/main/java/com/arcadedb/function/vector/VectorCreateFunction.java @@ -42,6 +42,10 @@ public Object execute(final Object[] args, final CommandContext context) { if (args[0] == null) return null; + // Null propagation: if dimension is explicitly null, return null + if (args.length >= 2 && args[1] == null) + return null; + final float[] result = VectorUtils.toFloatArray(args[0]); // Validate dimension if provided diff --git a/engine/src/main/java/com/arcadedb/function/vector/VectorDistanceFunction.java b/engine/src/main/java/com/arcadedb/function/vector/VectorDistanceFunction.java index fefcf51127..b6ae52d312 100644 --- a/engine/src/main/java/com/arcadedb/function/vector/VectorDistanceFunction.java +++ b/engine/src/main/java/com/arcadedb/function/vector/VectorDistanceFunction.java @@ -51,10 +51,25 @@ public Object execute(final Object[] args, final CommandContext context) { return switch (metric) { case "EUCLIDEAN" -> (double) VectorUtils.l2Distance(a, b); + case "EUCLIDEAN_SQUARED" -> { + double sum = 0.0; + for (int i = 0; i < a.length; i++) { + final double diff = a[i] - b[i]; + sum += diff * diff; + } + yield sum; + } case "COSINE" -> 1.0 - (double) VectorUtils.cosineSimilarity(a, b); case "MANHATTAN" -> (double) VectorUtils.manhattanDistance(a, b); + case "HAMMING" -> { + int count = 0; + for (int i = 0; i < a.length; i++) + if (a[i] != b[i]) + count++; + yield (double) count; + } default -> throw new CommandExecutionException("vector_distance(): unsupported metric: " + metric - + ". Supported metrics: EUCLIDEAN, COSINE, MANHATTAN"); + + ". Supported metrics: EUCLIDEAN, EUCLIDEAN_SQUARED, COSINE, MANHATTAN, HAMMING"); }; } } diff --git a/engine/src/main/java/com/arcadedb/index/vector/VectorUtils.java b/engine/src/main/java/com/arcadedb/index/vector/VectorUtils.java index 196ba6f0a7..78a62987b8 100644 --- a/engine/src/main/java/com/arcadedb/index/vector/VectorUtils.java +++ b/engine/src/main/java/com/arcadedb/index/vector/VectorUtils.java @@ -90,6 +90,17 @@ public static float[] toFloatArray(final Object vectorObj) { } return result; } + if (vectorObj instanceof String s) { + final String trimmed = s.trim(); + final String inner = trimmed.startsWith("[") && trimmed.endsWith("]") ? trimmed.substring(1, trimmed.length() - 1) : trimmed; + if (inner.isEmpty()) + return new float[0]; + final String[] parts = inner.split(","); + final float[] result = new float[parts.length]; + for (int i = 0; i < parts.length; i++) + result[i] = Float.parseFloat(parts[i].trim()); + return result; + } throw new IllegalArgumentException("Vector must be an array or list, found: " + vectorObj.getClass().getSimpleName()); } diff --git a/engine/src/main/java/com/arcadedb/query/opencypher/ast/FunctionCallExpression.java b/engine/src/main/java/com/arcadedb/query/opencypher/ast/FunctionCallExpression.java index a275c3698d..5980d77505 100644 --- a/engine/src/main/java/com/arcadedb/query/opencypher/ast/FunctionCallExpression.java +++ b/engine/src/main/java/com/arcadedb/query/opencypher/ast/FunctionCallExpression.java @@ -131,7 +131,7 @@ public boolean isDistinct() { */ private static boolean isAggregationFunction(final String functionName) { return switch (functionName) { - case "count", "sum", "avg", "min", "max", "collect", "stdev", "stdevp", "percentilecont", "percentiledisc" -> true; + case "count", "sum", "avg", "min", "max", "collect", "collect_list", "stdev", "stdev_samp", "stdevp", "stdev_pop", "percentilecont", "percentile_cont", "percentiledisc", "percentile_disc" -> true; default -> false; }; } diff --git a/engine/src/main/java/com/arcadedb/query/opencypher/executor/CypherFunctionFactory.java b/engine/src/main/java/com/arcadedb/query/opencypher/executor/CypherFunctionFactory.java index 3af7aeae74..1b11fee232 100644 --- a/engine/src/main/java/com/arcadedb/query/opencypher/executor/CypherFunctionFactory.java +++ b/engine/src/main/java/com/arcadedb/query/opencypher/executor/CypherFunctionFactory.java @@ -47,6 +47,7 @@ import com.arcadedb.function.cypher.LoadCSVFileFunction; import com.arcadedb.function.cypher.LoadCSVLineNumberFunction; import com.arcadedb.function.cypher.SQLFunctionBridge; +import com.arcadedb.function.geo.CypherPointDistanceFunction; import com.arcadedb.function.geo.CypherPointFunction; import com.arcadedb.function.geo.PointWithinBBoxFunction; import com.arcadedb.function.graph.ElementIdFunction; @@ -102,11 +103,8 @@ import com.arcadedb.function.text.RTrimFunction; import com.arcadedb.function.text.ReplaceFunction; import com.arcadedb.function.text.RightFunction; -import com.arcadedb.function.text.SplitFunction; -import com.arcadedb.function.text.SubstringFunction; import com.arcadedb.function.text.ToLowerFunction; import com.arcadedb.function.text.ToUpperFunction; -import com.arcadedb.function.text.TrimFunction; import com.arcadedb.function.vector.VectorCreateFunction; import com.arcadedb.function.vector.VectorDimensionCountFunction; import com.arcadedb.function.vector.VectorDistanceCosineFunction; @@ -169,6 +167,9 @@ private Map createFunctionMapping() { // min/max handled as Cypher-specific to support mixed-type comparison mapping.put("stdev", "stddev"); mapping.put("stdevp", "stddevp"); + // Aliases for stdev/stdevp + mapping.put("stdev_samp", "stddev"); + mapping.put("stdev_pop", "stddevp"); // String functions - need to check if SQL has these mapping.put("toupper", "upper"); @@ -300,12 +301,12 @@ private boolean isCypherSpecificFunction(final String functionName) { // Graph functions case "id", "elementid", "labels", "type", "keys", "properties", "startnode", "endnode" -> true; // Path functions - case "nodes", "relationships", "length" -> true; + case "nodes", "relationships", "length", "path_length" -> true; // Math functions - case "rand", "sign", "ceil", "floor", "abs", "sqrt", "round", "isnan", + case "rand", "sign", "ceil", "ceiling", "floor", "abs", "sqrt", "round", "isnan", "cosh", "sinh", "tanh", "cot", "coth", "pi", "e", "randomuuid", "acos", "asin", "atan", "atan2", "cos", "sin", "tan", - "degrees", "radians", "haversin", "exp", "log", "log10" -> true; + "degrees", "radians", "haversin", "exp", "log", "ln", "log10" -> true; // General functions case "coalesce" -> true; // Predicate functions @@ -316,7 +317,7 @@ private boolean isCypherSpecificFunction(final String functionName) { case "left", "right", "reverse", "split", "substring", "tolower", "toupper", "lower", "upper", "ltrim", "rtrim", "btrim" -> true; // String functions (additional) - case "trim", "replace", "char.length", "character.length", "normalize" -> true; + case "trim", "replace", "char.length", "character.length", "char_length", "character_length", "normalize" -> true; // Type conversion functions case "tostring", "tointeger", "tofloat", "toboolean", "tostringornull", "tointegerornull", "tofloatornull", "tobooleanornull", @@ -324,11 +325,13 @@ private boolean isCypherSpecificFunction(final String functionName) { // Scalar functions case "nullif", "valuetype" -> true; // Aggregation functions - case "collect", "percentiledisc", "percentilecont", "min", "max", "avg" -> true; + case "collect", "collect_list", "percentiledisc", "percentile_disc", "percentilecont", "percentile_cont", "min", "max", + "avg" -> true; // Temporal functions case "timestamp" -> true; // Temporal constructor functions - case "date", "localtime", "time", "localdatetime", "datetime", "duration" -> true; + case "date", "localtime", "local_time", "time", "zoned_time", "localdatetime", "local_datetime", "datetime", "zoned_datetime", + "duration", "duration_between" -> true; // Temporal truncation functions case "date.truncate", "localtime.truncate", "time.truncate", "localdatetime.truncate", "datetime.truncate" -> true; // Temporal epoch functions @@ -345,13 +348,13 @@ private boolean isCypherSpecificFunction(final String functionName) { // Note: vector_norm and vector_distance with EUCLIDEAN/DOT metrics delegate to SQL functions // (vector.magnitude, vector.l1Norm, vector.l2Distance, vector.dotProduct) via the SQL bridge case "vector.create", "vector.distance.manhattan", "vector.distance.cosine", - "vector", "vector.dimension.count", "vector.distance" -> true; + "vector", "vector.dimension.count", "vector_dimension_count", "vector.distance" -> true; // Vector distance functions case "vector.distance.euclidean" -> true; // Vector norm function case "vector.norm" -> true; // Geo-spatial functions - case "point", "distance", "point.withinbbox" -> true; + case "point", "distance", "point.withinbbox", "point.distance" -> true; // Temporal clock functions (realtime/statement/transaction are aliases for current instant) case "date.realtime", "date.statement", "date.transaction" -> true; case "localtime.realtime", "localtime.statement", "localtime.transaction" -> true; @@ -375,6 +378,7 @@ private StatelessFunction createCypherSpecificExecutor(final String functionName case "randomuuid" -> new RandomUuidFunction(); case "sign" -> new SignFunction(); case "ceil" -> new MathUnaryFunction("ceil", Math::ceil); + case "ceiling" -> new MathUnaryFunction("ceiling", Math::ceil); case "floor" -> new MathUnaryFunction("floor", Math::floor); case "abs" -> new MathUnaryFunction("abs", Math::abs); case "sqrt" -> new MathUnaryFunction("sqrt", Math::sqrt); @@ -400,7 +404,7 @@ private StatelessFunction createCypherSpecificExecutor(final String functionName case "haversin" -> new MathUnaryFunction("haversin", v -> (1.0 - Math.cos(v)) / 2.0); // Logarithmic functions case "exp" -> new MathUnaryFunction("exp", Math::exp); - case "log" -> new MathUnaryFunction("log", Math::log); + case "log", "ln" -> new MathUnaryFunction("log", Math::log); case "log10" -> new MathUnaryFunction("log10", Math::log10); // General functions case "coalesce" -> new CoalesceFunction(); @@ -416,7 +420,7 @@ private StatelessFunction createCypherSpecificExecutor(final String functionName // Path functions case "nodes" -> new NodesFunction(); case "relationships" -> new RelationshipsFunction(); - case "length" -> new LengthFunction(); + case "length", "path_length" -> new LengthFunction(); // List functions case "size" -> new SizeFunction(); case "head" -> new HeadFunction(); @@ -430,15 +434,15 @@ private StatelessFunction createCypherSpecificExecutor(final String functionName case "left" -> new LeftFunction(); case "right" -> new RightFunction(); case "reverse" -> new ReverseFunction(); - case "split" -> new SplitFunction(); - case "substring" -> new SubstringFunction(); + case "split" -> new CypherSplitFunction(); + case "substring" -> new CypherSubstringFunction(); case "tolower", "lower" -> new ToLowerFunction(); case "toupper", "upper" -> new ToUpperFunction(); case "ltrim" -> new LTrimFunction(); case "rtrim" -> new RTrimFunction(); - case "trim", "btrim" -> new TrimFunction(); + case "trim", "btrim" -> new CypherTrimFunction(); case "replace" -> new ReplaceFunction(); - case "char.length", "character.length" -> new CharLengthFunction(); + case "char.length", "character.length", "char_length", "character_length" -> new CharLengthFunction(); case "normalize" -> new NormalizeFunction(); // Type conversion functions case "tostring" -> new ToStringFunction(); @@ -459,11 +463,12 @@ private StatelessFunction createCypherSpecificExecutor(final String functionName case "valuetype" -> new ValueTypeFunction(); // Aggregation functions case "avg" -> distinct ? new DistinctAggregationWrapper(new CypherAvgFunction()) : new CypherAvgFunction(); - case "collect" -> distinct ? new CollectDistinctFunction() : new CollectFunction(); + case "collect", "collect_list" -> distinct ? new CollectDistinctFunction() : new CollectFunction(); case "min" -> distinct ? new DistinctAggregationWrapper(new CypherMinFunction()) : new CypherMinFunction(); case "max" -> distinct ? new DistinctAggregationWrapper(new CypherMaxFunction()) : new CypherMaxFunction(); - case "percentiledisc" -> new PercentileDiscFunction(); - case "percentilecont" -> new PercentileContFunction(); + case "percentiledisc", "percentile_disc" -> new PercentileDiscFunction(); + case "percentilecont", "percentile_cont" -> new PercentileContFunction(); + // stdev/stdevp are SQL functions, not Cypher-specific // Temporal functions case "timestamp" -> new TimestampFunction(); // Temporal format function @@ -478,7 +483,7 @@ private StatelessFunction createCypherSpecificExecutor(final String functionName case "vector", "vector.create" -> new VectorCreateFunction(); case "vector.distance.manhattan" -> new VectorDistanceManhattanFunction(); case "vector.distance.cosine" -> new VectorDistanceCosineFunction(); - case "vector.dimension.count" -> new VectorDimensionCountFunction(); + case "vector.dimension.count", "vector_dimension_count" -> new VectorDimensionCountFunction(); case "vector.distance" -> new VectorDistanceFunction(); // Vector distance functions case "vector.distance.euclidean" -> new VectorDistanceEuclideanFunction(); @@ -488,12 +493,13 @@ private StatelessFunction createCypherSpecificExecutor(final String functionName case "point" -> new CypherPointFunction(); case "distance" -> new SQLFunctionBridge(sqlFunctionFactory.getFunctionInstance(SQLFunctionGeoDistance.NAME), "distance"); case "point.withinbbox" -> new PointWithinBBoxFunction(); + case "point.distance" -> new CypherPointDistanceFunction(); // Temporal constructor functions case "date" -> new DateConstructorFunction(); - case "localtime" -> new LocalTimeConstructorFunction(); - case "time" -> new TimeConstructorFunction(); - case "localdatetime" -> new LocalDateTimeConstructorFunction(); - case "datetime" -> new DateTimeConstructorFunction(); + case "localtime", "local_time" -> new LocalTimeConstructorFunction(); + case "time", "zoned_time" -> new TimeConstructorFunction(); + case "localdatetime", "local_datetime" -> new LocalDateTimeConstructorFunction(); + case "datetime", "zoned_datetime" -> new DateTimeConstructorFunction(); case "duration" -> new DurationConstructorFunction(); // Temporal truncation functions case "date.truncate" -> new DateTruncateFunction(); @@ -505,7 +511,7 @@ private StatelessFunction createCypherSpecificExecutor(final String functionName case "datetime.fromepoch" -> new DateTimeFromEpochFunction(); case "datetime.fromepochmillis" -> new DateTimeFromEpochMillisFunction(); // Duration calculation functions - case "duration.between" -> new DurationBetweenFunction(); + case "duration.between", "duration_between" -> new DurationBetweenFunction(); case "duration.inmonths" -> new DurationInMonthsFunction(); case "duration.indays" -> new DurationInDaysFunction(); case "duration.inseconds" -> new DurationInSecondsFunction(); diff --git a/engine/src/main/java/com/arcadedb/query/opencypher/executor/CypherSplitFunction.java b/engine/src/main/java/com/arcadedb/query/opencypher/executor/CypherSplitFunction.java new file mode 100644 index 0000000000..826d738318 --- /dev/null +++ b/engine/src/main/java/com/arcadedb/query/opencypher/executor/CypherSplitFunction.java @@ -0,0 +1,48 @@ +/* + * Copyright © 2021-present Arcade Data Ltd (info@arcadedata.com) + * + * 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 (info@arcadedata.com) + * SPDX-License-Identifier: Apache-2.0 + */ +package com.arcadedb.query.opencypher.executor; + +import com.arcadedb.exception.CommandExecutionException; +import com.arcadedb.function.StatelessFunction; +import com.arcadedb.query.sql.executor.CommandContext; + +import java.util.List; +import java.util.regex.Pattern; + +/** + * Cypher split() function - splits a string by a delimiter. + * Returns null if either string or delimiter is null (Cypher behavior). + */ +public class CypherSplitFunction implements StatelessFunction { + @Override + public String getName() { + return "split"; + } + + @Override + public Object execute(final Object[] args, final CommandContext context) { + if (args.length != 2) + throw new CommandExecutionException("split() requires exactly 2 arguments: split(string, delimiter)"); + if (args[0] == null || args[1] == null) + return null; + final String str = args[0].toString(); + final String delimiter = args[1].toString(); + return List.of(str.split(Pattern.quote(delimiter), -1)); + } +} diff --git a/engine/src/main/java/com/arcadedb/query/opencypher/executor/CypherSubstringFunction.java b/engine/src/main/java/com/arcadedb/query/opencypher/executor/CypherSubstringFunction.java new file mode 100644 index 0000000000..6a0a1b67dc --- /dev/null +++ b/engine/src/main/java/com/arcadedb/query/opencypher/executor/CypherSubstringFunction.java @@ -0,0 +1,55 @@ +/* + * Copyright © 2021-present Arcade Data Ltd (info@arcadedata.com) + * + * 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 (info@arcadedata.com) + * SPDX-License-Identifier: Apache-2.0 + */ +package com.arcadedb.query.opencypher.executor; + +import com.arcadedb.exception.CommandExecutionException; +import com.arcadedb.function.StatelessFunction; +import com.arcadedb.query.sql.executor.CommandContext; + +/** + * Cypher substring() function - returns a substring of the original string. + * Cypher uses 0-based indexing and raises error for negative start/length. + */ +public class CypherSubstringFunction implements StatelessFunction { + @Override + public String getName() { + return "substring"; + } + + @Override + public Object execute(final Object[] args, final CommandContext context) { + if (args.length < 2 || args.length > 3) + throw new CommandExecutionException("substring() requires 2 or 3 arguments: substring(string, start[, length])"); + if (args[0] == null || args[1] == null) + return null; + final String str = args[0].toString(); + final int start = ((Number) args[1]).intValue(); + if (start < 0) + throw new CommandExecutionException("Cannot handle negative start index nor negative length"); + if (start >= str.length()) + return ""; + if (args.length == 3 && args[2] != null) { + final int length = ((Number) args[2]).intValue(); + if (length < 0) + throw new CommandExecutionException("Cannot handle negative start index nor negative length"); + return str.substring(start, Math.min(start + length, str.length())); + } + return str.substring(start); + } +} diff --git a/engine/src/main/java/com/arcadedb/query/opencypher/executor/CypherTrimFunction.java b/engine/src/main/java/com/arcadedb/query/opencypher/executor/CypherTrimFunction.java new file mode 100644 index 0000000000..993161713e --- /dev/null +++ b/engine/src/main/java/com/arcadedb/query/opencypher/executor/CypherTrimFunction.java @@ -0,0 +1,100 @@ +/* + * Copyright © 2021-present Arcade Data Ltd (info@arcadedata.com) + * + * 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 (info@arcadedata.com) + * SPDX-License-Identifier: Apache-2.0 + */ +package com.arcadedb.query.opencypher.executor; + +import com.arcadedb.exception.CommandExecutionException; +import com.arcadedb.function.StatelessFunction; +import com.arcadedb.query.sql.executor.CommandContext; + +/** + * Cypher trim() and btrim() functions. + * Supports multiple forms: + * - trim(source) / btrim(source) - strips leading and trailing whitespace + * - btrim(source, trimCharacter) - strips specified character from both sides + * - trim(BOTH/LEADING/TRAILING char FROM string) - SQL-style trim syntax + * Returns null if any argument is null (Cypher behavior). + */ +public class CypherTrimFunction implements StatelessFunction { + @Override + public String getName() { + return "trim"; + } + + @Override + public Object execute(final Object[] args, final CommandContext context) { + if (args.length == 1) { + // Simple form: trim(source) or btrim(source) + if (args[0] == null) + return null; + return args[0].toString().strip(); + } + + if (args.length == 2) { + // 2-arg form: btrim(source, trimCharacter) + if (args[0] == null || args[1] == null) + return null; + final String source = args[0].toString(); + final String trimChar = args[1].toString(); + if (trimChar.isEmpty()) + return source.strip(); + return stripLeading(stripTrailing(source, trimChar), trimChar); + } + + if (args.length == 3) { + // SQL-style: trim(BOTH/LEADING/TRAILING char FROM string) + final String mode = args[0] != null ? args[0].toString() : null; + final String trimChar = args[1] != null ? args[1].toString() : null; + final String source = args[2] != null ? args[2].toString() : null; + + if (source == null) + return null; + if (trimChar == null) + return null; + if (trimChar.isEmpty()) { + return switch (mode) { + case "LEADING" -> source.stripLeading(); + case "TRAILING" -> source.stripTrailing(); + default -> source.strip(); + }; + } + + return switch (mode) { + case "LEADING" -> stripLeading(source, trimChar); + case "TRAILING" -> stripTrailing(source, trimChar); + default -> stripLeading(stripTrailing(source, trimChar), trimChar); + }; + } + + throw new CommandExecutionException("trim() and btrim() require 1, 2, or 3 arguments"); + } + + private static String stripLeading(final String source, final String trimChars) { + int start = 0; + while (start < source.length() && trimChars.indexOf(source.charAt(start)) >= 0) + start++; + return source.substring(start); + } + + private static String stripTrailing(final String source, final String trimChars) { + int end = source.length(); + while (end > 0 && trimChars.indexOf(source.charAt(end - 1)) >= 0) + end--; + return source.substring(0, end); + } +} diff --git a/engine/src/main/java/com/arcadedb/query/opencypher/parser/CypherExpressionBuilder.java b/engine/src/main/java/com/arcadedb/query/opencypher/parser/CypherExpressionBuilder.java index 7df4aa96da..56180077f5 100644 --- a/engine/src/main/java/com/arcadedb/query/opencypher/parser/CypherExpressionBuilder.java +++ b/engine/src/main/java/com/arcadedb/query/opencypher/parser/CypherExpressionBuilder.java @@ -389,6 +389,10 @@ Expression parseExpressionFromText(final ParseTree node) { return parseVectorNormFunction(e1.vectorNormFunction()); if (e1.vectorDistanceFunction() != null) return parseVectorDistanceFunction(e1.vectorDistanceFunction()); + if (e1.trimFunction() != null) + return parseTrimFunction(e1.trimFunction()); + if (e1.normalizeFunction() != null) + return parseNormalizeFunction(e1.normalizeFunction()); // Check for map literal final Cypher25Parser.MapContext e1MapCtx = findMapRecursive(e1); if (e1MapCtx != null) @@ -811,7 +815,7 @@ private Cypher25Parser.TrimFunctionContext findTrimFunctionRecursive(final Parse * Parse a trimFunction context into a FunctionCallExpression. * Handles both simple trim(source) and extended trim(LEADING/TRAILING/BOTH trimChar FROM source). */ - private Expression parseTrimFunction(final Cypher25Parser.TrimFunctionContext ctx) { + Expression parseTrimFunction(final Cypher25Parser.TrimFunctionContext ctx) { final List args = new ArrayList<>(); if (ctx.FROM() != null) { @@ -824,11 +828,13 @@ else if (ctx.TRAILING() != null) args.add(new LiteralExpression(mode, mode)); - // trimCharacterString may be null (e.g., trim(LEADING FROM source)) + // trimCharacterString may be absent (e.g., trim(LEADING FROM source) - trim whitespace) + // vs explicitly null (e.g., trim(null FROM source) - return null per Cypher spec). + // Use empty string to signal "default whitespace" when no character is provided. if (ctx.trimCharacterString != null) args.add(parseExpression(ctx.trimCharacterString)); else - args.add(new LiteralExpression(null, "null")); + args.add(new LiteralExpression("", "''")); args.add(parseExpression(ctx.trimSource)); } else { @@ -839,6 +845,19 @@ else if (ctx.TRAILING() != null) return new FunctionCallExpression("trim", args, false); } + /** + * Parse a normalizeFunction context into a FunctionCallExpression. + * Handles normalize(string) and normalize(string, normalForm). + * The normalForm keyword (NFC/NFD/NFKC/NFKD) is passed as a string literal argument. + */ + Expression parseNormalizeFunction(final Cypher25Parser.NormalizeFunctionContext ctx) { + final List args = new ArrayList<>(); + args.add(parseExpression(ctx.expression())); + if (ctx.normalForm() != null) + args.add(new LiteralExpression(ctx.normalForm().getText(), ctx.normalForm().getText())); + return new FunctionCallExpression("normalize", args, false); + } + /** * Recursively find EXISTS expression in the parse tree. */ @@ -2535,17 +2554,23 @@ Expression parseVectorNormFunction(final Cypher25Parser.VectorNormFunctionContex */ Expression parseVectorDistanceFunction(final Cypher25Parser.VectorDistanceFunctionContext ctx) { final String metric = ctx.vectorDistanceMetric().getText().toUpperCase(); + + final List args = new ArrayList<>(); + args.add(parseExpression(ctx.vector1)); + args.add(parseExpression(ctx.vector2)); + final String functionName = switch (metric) { case "EUCLIDEAN" -> "vector.l2Distance"; case "MANHATTAN" -> "vector.distance.manhattan"; case "COSINE" -> "vector.distance.cosine"; case "DOT" -> "vector.dotProduct"; + case "EUCLIDEAN_SQUARED", "HAMMING" -> { + args.add(new LiteralExpression(metric, "'" + metric + "'")); + yield "vector.distance"; + } default -> throw new CommandParsingException("Unsupported vector_distance metric: " + metric); }; - final List args = new ArrayList<>(); - args.add(parseExpression(ctx.vector1)); - args.add(parseExpression(ctx.vector2)); return new FunctionCallExpression(functionName, args, false); } } diff --git a/engine/src/main/java/com/arcadedb/query/opencypher/parser/ExpressionTypeDetector.java b/engine/src/main/java/com/arcadedb/query/opencypher/parser/ExpressionTypeDetector.java index ca635cf5a2..ede9c00720 100644 --- a/engine/src/main/java/com/arcadedb/query/opencypher/parser/ExpressionTypeDetector.java +++ b/engine/src/main/java/com/arcadedb/query/opencypher/parser/ExpressionTypeDetector.java @@ -224,6 +224,14 @@ Expression tryParsePrimary(final Cypher25Parser.ExpressionContext ctx) { return builder.parseVectorNormFunction(vecExpr1.vectorNormFunction()); if (vecExpr1.vectorDistanceFunction() != null) return builder.parseVectorDistanceFunction(vecExpr1.vectorDistanceFunction()); + // trimFunction and normalizeFunction are special grammar rules in expression1. + // Check them BEFORE findFunctionInvocationRecursive, which would otherwise find + // an inner function (e.g., toLower inside trim(toLower(...))) and return it + // instead of the outer trim/normalize. + if (vecExpr1.trimFunction() != null) + return builder.parseTrimFunction(vecExpr1.trimFunction()); + if (vecExpr1.normalizeFunction() != null) + return builder.parseNormalizeFunction(vecExpr1.normalizeFunction()); } // Check for top-level list literals BEFORE function invocations. diff --git a/engine/src/main/java/com/arcadedb/query/opencypher/parser/FunctionValidator.java b/engine/src/main/java/com/arcadedb/query/opencypher/parser/FunctionValidator.java index 5a5b8d6740..855a43d114 100644 --- a/engine/src/main/java/com/arcadedb/query/opencypher/parser/FunctionValidator.java +++ b/engine/src/main/java/com/arcadedb/query/opencypher/parser/FunctionValidator.java @@ -108,6 +108,7 @@ public String getExpectedArgsDescription() { registerFunction("properties", 1, 1, "All properties of entity", false); registerFunction("size", 1, 1, "Size of list/string", false); registerFunction("length", 1, 1, "Length of path", false); + registerFunction("path_length", 1, 1, "Length of path (alias for length)", false); registerFunction("reverse", 1, 1, "Reverse list/string", false); // String functions @@ -115,8 +116,8 @@ public String getExpectedArgsDescription() { registerFunction("tolower", 1, 1, "Convert to lowercase", false); registerFunction("toupper", 1, 1, "Convert to uppercase", false); registerFunction("trim", 1, 3, "Trim whitespace", false); - registerFunction("ltrim", 1, 1, "Trim left whitespace", false); - registerFunction("rtrim", 1, 1, "Trim right whitespace", false); + registerFunction("ltrim", 1, 2, "Trim left whitespace", false); + registerFunction("rtrim", 1, 2, "Trim right whitespace", false); registerFunction("substring", 2, 3, "Extract substring", false); registerFunction("replace", 3, 3, "Replace string", false); registerFunction("split", 2, 2, "Split string", false); @@ -126,12 +127,14 @@ public String getExpectedArgsDescription() { // Math functions registerFunction("abs", 1, 1, "Absolute value", false); registerFunction("ceil", 1, 1, "Ceiling", false); + registerFunction("ceiling", 1, 1, "Ceiling (alias for ceil)", false); registerFunction("floor", 1, 1, "Floor", false); - registerFunction("round", 1, 2, "Round to integer or precision", false); + registerFunction("round", 1, 3, "Round to integer or precision", false); registerFunction("sign", 1, 1, "Sign of number", false); registerFunction("rand", 0, 0, "Random number", false); registerFunction("sqrt", 1, 1, "Square root", false); registerFunction("log", 1, 1, "Natural logarithm", false); + registerFunction("ln", 1, 1, "Natural logarithm (alias for log)", false); registerFunction("log10", 1, 1, "Base-10 logarithm", false); registerFunction("exp", 1, 1, "Exponential", false); registerFunction("sin", 1, 1, "Sine", false); @@ -161,6 +164,13 @@ public String getExpectedArgsDescription() { registerFunction("upper", 1, 1, "Convert to uppercase (alias for toUpper)", false); registerFunction("btrim", 1, 3, "Trim both sides (alias for trim)", false); + // Aggregation function aliases (GQL conformance) + registerFunction("collect_list", 1, 1, "Collect values into list (alias for collect)", true); + registerFunction("stdev_samp", 1, 1, "Sample standard deviation (alias for stdev)", true); + registerFunction("stdev_pop", 1, 1, "Population standard deviation (alias for stdevp)", true); + registerFunction("percentile_cont", 2, 2, "Continuous percentile (alias for percentilecont)", true); + registerFunction("percentile_disc", 2, 2, "Discrete percentile (alias for percentiledisc)", true); + // List conversion functions registerFunction("tobooleanlist", 1, 1, "Convert list elements to booleans", false); registerFunction("tofloatlist", 1, 1, "Convert list elements to floats", false); @@ -170,6 +180,7 @@ public String getExpectedArgsDescription() { // Vector functions registerFunction("vector", 1, 2, "Create vector from list (alias for vector.create)", false); registerFunction("vector.dimension.count", 1, 1, "Return dimension count of vector", false); + registerFunction("vector_dimension_count", 1, 1, "Return dimension count of vector", false); registerFunction("vector.distance", 2, 3, "Calculate distance between vectors", false); registerFunction("vector.distance.euclidean", 2, 2, "Euclidean distance between vectors", false); registerFunction("vector.norm", 1, 1, "L2 norm (magnitude) of vector", false); @@ -180,11 +191,16 @@ public String getExpectedArgsDescription() { // Temporal functions registerFunction("timestamp", 0, 0, "Current timestamp", false); registerFunction("datetime", 0, 1, "Current or parsed datetime", false); + registerFunction("zoned_datetime", 0, 1, "Current or parsed datetime (alias for datetime)", false); registerFunction("date", 0, 1, "Current or parsed date", false); registerFunction("time", 0, 1, "Current or parsed time", false); + registerFunction("zoned_time", 0, 1, "Current or parsed time (alias for time)", false); registerFunction("localtime", 0, 1, "Current or parsed local time", false); + registerFunction("local_time", 0, 1, "Current or parsed local time (alias for localtime)", false); registerFunction("localdatetime", 0, 1, "Current or parsed local datetime", false); + registerFunction("local_datetime", 0, 1, "Current or parsed local datetime (alias for localdatetime)", false); registerFunction("duration", 1, 1, "Duration value", false); + registerFunction("duration_between", 2, 2, "Duration between two temporal values (alias for duration.between)", false); // Temporal format function registerFunction("format", 1, 2, "Format temporal value as string", false); @@ -200,6 +216,7 @@ public String getExpectedArgsDescription() { // Geo-spatial functions registerFunction("point.withinbbox", 3, 3, "Check if point is within bounding box", false); + registerFunction("point.distance", 2, 2, "Distance between two points", false); // Path/Graph functions registerFunction("nodes", 1, 1, "Nodes in path", false); @@ -217,7 +234,7 @@ public String getExpectedArgsDescription() { registerFunction("randomuuid", 0, 0, "Random UUID", false); registerFunction("pi", 0, 0, "Pi constant", false); registerFunction("e", 0, 0, "Euler's number", false); - registerFunction("point", 1, 1, "Create point", false); + registerFunction("point", 1, 2, "Create point (map or latitude,longitude)", false); registerFunction("distance", 2, 2, "Distance between points", false); registerFunction("degrees", 1, 1, "Radians to degrees", false); registerFunction("radians", 1, 1, "Degrees to radians", false); @@ -228,6 +245,8 @@ public String getExpectedArgsDescription() { registerFunction("cot", 1, 1, "Cotangent", false); registerFunction("coth", 1, 1, "Hyperbolic cotangent", false); registerFunction("charlength", 1, 1, "Character length", false); + registerFunction("char_length", 1, 1, "Character length", false); + registerFunction("character_length", 1, 1, "Character length", false); registerFunction("charat", 2, 2, "Character at position", false); registerFunction("normalize", 1, 2, "Normalize string", false); registerFunction("isnormalized", 1, 2, "Check if normalized", false); diff --git a/engine/src/test/java/com/arcadedb/function/text/TextStatelessFunctionsTest.java b/engine/src/test/java/com/arcadedb/function/text/TextStatelessFunctionsTest.java index 8fc1f08db4..f354178c07 100644 --- a/engine/src/test/java/com/arcadedb/function/text/TextStatelessFunctionsTest.java +++ b/engine/src/test/java/com/arcadedb/function/text/TextStatelessFunctionsTest.java @@ -167,7 +167,7 @@ void lTrimNullReturnsNull() { void lTrimWrongArgCount() { final LTrimFunction fn = new LTrimFunction(); - assertThatThrownBy(() -> fn.execute(new Object[]{"a", "b"}, null)) + assertThatThrownBy(() -> fn.execute(new Object[]{"a", "b", "c"}, null)) .isInstanceOf(CommandExecutionException.class); } @@ -192,7 +192,7 @@ void rTrimNullReturnsNull() { void rTrimWrongArgCount() { final RTrimFunction fn = new RTrimFunction(); - assertThatThrownBy(() -> fn.execute(new Object[]{"a", "b"}, null)) + assertThatThrownBy(() -> fn.execute(new Object[]{"a", "b", "c"}, null)) .isInstanceOf(CommandExecutionException.class); } diff --git a/engine/src/test/java/com/arcadedb/index/LSMTreeIndexTest.java b/engine/src/test/java/com/arcadedb/index/LSMTreeIndexTest.java index f970ffdbdb..05ae2a9159 100644 --- a/engine/src/test/java/com/arcadedb/index/LSMTreeIndexTest.java +++ b/engine/src/test/java/com/arcadedb/index/LSMTreeIndexTest.java @@ -18,25 +18,6 @@ */ package com.arcadedb.index; -import com.arcadedb.TestHelper; -import com.arcadedb.database.Document; -import com.arcadedb.database.Identifiable; -import com.arcadedb.database.MutableDocument; -import com.arcadedb.database.RID; -import com.arcadedb.exception.DuplicatedKeyException; -import com.arcadedb.exception.NeedRetryException; -import com.arcadedb.log.DefaultLogger; -import com.arcadedb.log.LogManager; -import com.arcadedb.log.Logger; -import com.arcadedb.query.sql.executor.Result; -import com.arcadedb.query.sql.executor.ResultSet; -import com.arcadedb.schema.DocumentType; -import com.arcadedb.schema.Schema; -import org.assertj.core.api.Assertions; -import org.awaitility.Awaitility; -import org.junit.jupiter.api.Tag; -import org.junit.jupiter.api.Test; - import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -44,7 +25,6 @@ import java.util.Iterator; import java.util.List; import java.util.Map; -import java.util.Random; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -55,8 +35,27 @@ import java.util.concurrent.atomic.AtomicLong; import java.util.logging.Level; +import org.assertj.core.api.Assertions; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import org.awaitility.Awaitility; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import com.arcadedb.TestHelper; +import com.arcadedb.database.Document; +import com.arcadedb.database.Identifiable; +import com.arcadedb.database.MutableDocument; +import com.arcadedb.database.RID; +import com.arcadedb.exception.DuplicatedKeyException; +import com.arcadedb.exception.NeedRetryException; +import com.arcadedb.log.DefaultLogger; +import com.arcadedb.log.LogManager; +import com.arcadedb.log.Logger; +import com.arcadedb.query.sql.executor.Result; +import com.arcadedb.query.sql.executor.ResultSet; +import com.arcadedb.schema.DocumentType; +import com.arcadedb.schema.Schema; @Tag("slow") class LSMTreeIndexTest extends TestHelper { diff --git a/engine/src/test/java/com/arcadedb/query/opencypher/functions/OpenCypherAggregatingFunctionsComprehensiveTest.java b/engine/src/test/java/com/arcadedb/query/opencypher/functions/OpenCypherAggregatingFunctionsComprehensiveTest.java new file mode 100644 index 0000000000..c683fe7cae --- /dev/null +++ b/engine/src/test/java/com/arcadedb/query/opencypher/functions/OpenCypherAggregatingFunctionsComprehensiveTest.java @@ -0,0 +1,564 @@ +/* + * Copyright 2021-present Arcade Data Ltd (info@arcadedata.com) + * + * 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 (info@arcadedata.com) + * SPDX-License-Identifier: Apache-2.0 + */ +package com.arcadedb.query.opencypher.functions; + +import com.arcadedb.database.Database; +import com.arcadedb.database.DatabaseFactory; +import com.arcadedb.query.sql.executor.ResultSet; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import org.assertj.core.api.Assertions; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.within; + +/** + * Comprehensive tests for OpenCypher Aggregating functions based on Neo4j Cypher documentation. + * Tests cover: avg(), collect(), count(), max(), min(), percentileCont(), percentileDisc(), stDev(), stDevP(), sum() + */ +class OpenCypherAggregatingFunctionsComprehensiveTest { + private Database database; + + @BeforeEach + void setUp() { + final DatabaseFactory factory = new DatabaseFactory("./databases/test-cypher-aggregating-functions"); + if (factory.exists()) + factory.open().drop(); + database = factory.create(); + + // Create test graph matching Neo4j documentation + database.getSchema().createVertexType("Person"); + database.getSchema().createVertexType("Movie"); + database.getSchema().createEdgeType("ACTED_IN"); + database.getSchema().createEdgeType("KNOWS"); + + database.command("opencypher", + "CREATE " + + "(keanu:Person {name: 'Keanu Reeves', age: 58}), " + + "(liam:Person {name: 'Liam Neeson', age: 70}), " + + "(carrie:Person {name: 'Carrie Anne Moss', age: 55}), " + + "(guy:Person {name: 'Guy Pearce', age: 55}), " + + "(kathryn:Person {name: 'Kathryn Bigelow', age: 71}), " + + "(speed:Movie {title: 'Speed'}), " + + "(keanu)-[:ACTED_IN]->(speed), " + + "(keanu)-[:KNOWS]->(carrie), " + + "(keanu)-[:KNOWS]->(liam), " + + "(keanu)-[:KNOWS]->(kathryn), " + + "(carrie)-[:KNOWS]->(guy), " + + "(liam)-[:KNOWS]->(guy)"); + } + + @AfterEach + void tearDown() { + if (database != null) + database.drop(); + } + + // ==================== avg() Tests ==================== + + @Test + void avgBasic() { + final ResultSet result = database.command("opencypher", + "MATCH (p:Person) RETURN avg(p.age) AS result"); + Assertions.assertThat(result.hasNext()).isTrue(); + assertThat(((Number) result.next().getProperty("result")).doubleValue()).isCloseTo(61.8, within(0.1)); + } + + @Test + void avgWithNulls() { + final ResultSet result = database.command("opencypher", + "UNWIND [1, 2, 3, null, 4] AS val RETURN avg(val) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat(((Number) result.next().getProperty("result")).doubleValue()).isCloseTo(2.5, within(0.1)); + } + + @Test + void avgNull() { + final ResultSet result = database.command("opencypher", + "UNWIND [null, null] AS val RETURN avg(val) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + // ==================== collect() Tests ==================== + + @Test + void collectBasic() { + final ResultSet result = database.command("opencypher", + "MATCH (p:Person) RETURN collect(p.age) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + @SuppressWarnings("unchecked") + final List ages = (List) result.next().getProperty("result"); + assertThat(ages).hasSize(5); + assertThat(ages).contains(58, 70, 55, 71); + } + + @Test + void collectWithNulls() { + final ResultSet result = database.command("opencypher", + "UNWIND [1, 2, null, 3, null, 4] AS val RETURN collect(val) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + @SuppressWarnings("unchecked") + final List collected = (List) result.next().getProperty("result"); + assertThat(collected).containsExactly(1L, 2L, 3L, 4L); + } + + @Test + void collectNull() { + final ResultSet result = database.command("opencypher", + "UNWIND [null, null] AS val RETURN collect(val) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + @SuppressWarnings("unchecked") + final List collected = (List) result.next().getProperty("result"); + assertThat(collected).isEmpty(); + } + + // ==================== count() Tests ==================== + + @Test + void countStar() { + final ResultSet result = database.command("opencypher", + "MATCH (p:Person {name: 'Keanu Reeves'})-->(x) RETURN count(*) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat(((Number) result.next().getProperty("result")).intValue()).isEqualTo(4); + } + + @Test + void countExpression() { + final ResultSet result = database.command("opencypher", + "MATCH (p:Person) RETURN count(p.age) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat(((Number) result.next().getProperty("result")).intValue()).isEqualTo(5); + } + + @Test + void countWithNulls() { + final ResultSet result = database.command("opencypher", + "UNWIND [1, 2, null, 3, null] AS val RETURN count(val) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat(((Number) result.next().getProperty("result")).intValue()).isEqualTo(3); + } + + @Test + void countNull() { + final ResultSet result = database.command("opencypher", + "RETURN count(null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat(((Number) result.next().getProperty("result")).intValue()).isEqualTo(0); + } + + @Test + void countDistinct() { + final ResultSet result = database.command("opencypher", + "UNWIND [1, 2, 2, 3, 3, 3] AS val RETURN count(DISTINCT val) AS distinct, count(val) AS all"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final var row = result.next(); + assertThat(((Number) row.getProperty("distinct")).intValue()).isEqualTo(3); + assertThat(((Number) row.getProperty("all")).intValue()).isEqualTo(6); + } + + @Test + void countGroupByRelationshipType() { + final ResultSet result = database.command("opencypher", + "MATCH (p:Person {name: 'Keanu Reeves'})-[r]->() RETURN type(r) AS relType, count(*) AS count ORDER BY relType"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + int totalCount = 0; + while (result.hasNext()) { + final var row = result.next(); + final String relType = (String) row.getProperty("relType"); + final int count = ((Number) row.getProperty("count")).intValue(); + totalCount += count; + assertThat(relType).isIn("ACTED_IN", "KNOWS"); + } + assertThat(totalCount).isEqualTo(4); + } + + // ==================== max() Tests ==================== + + @Test + void maxBasic() { + final ResultSet result = database.command("opencypher", + "MATCH (p:Person) RETURN max(p.age) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat(((Number) result.next().getProperty("result")).intValue()).isEqualTo(71); + } + + @Test + void maxMixedTypes() { + final ResultSet result = database.command("opencypher", + "UNWIND [1, 'a', null, 0.2, 'b', '1', '99'] AS val RETURN max(val) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + // Numeric values are higher than strings + assertThat(((Number) result.next().getProperty("result")).intValue()).isEqualTo(1); + } + + @Test + void maxLists() { + final ResultSet result = database.command("opencypher", + "UNWIND [[1, 'a', 89], [1, 2]] AS val RETURN max(val) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + @SuppressWarnings("unchecked") + final List maxList = (List) result.next().getProperty("result"); + assertThat(maxList).hasSize(2); + assertThat(((Number) maxList.get(0)).intValue()).isEqualTo(1); + assertThat(((Number) maxList.get(1)).intValue()).isEqualTo(2); + } + + @Test + void maxNull() { + final ResultSet result = database.command("opencypher", + "UNWIND [null, null] AS val RETURN max(val) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + // ==================== min() Tests ==================== + + @Test + void minBasic() { + final ResultSet result = database.command("opencypher", + "MATCH (p:Person) RETURN min(p.age) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat(((Number) result.next().getProperty("result")).intValue()).isEqualTo(55); + } + + @Test + void minMixedTypes() { + final ResultSet result = database.command("opencypher", + "UNWIND [1, 'a', null, 0.2, 'b', '1', '99'] AS val RETURN min(val) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + // Strings are lower than numeric values + assertThat((String) result.next().getProperty("result")).isEqualTo("1"); + } + + @Test + void minLists() { + final ResultSet result = database.command("opencypher", + "UNWIND ['d', [1, 2], ['a', 'c', 23]] AS val RETURN min(val) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + @SuppressWarnings("unchecked") + final List minList = (List) result.next().getProperty("result"); + assertThat(minList).hasSize(3); + assertThat((String) minList.get(0)).isEqualTo("a"); + assertThat((String) minList.get(1)).isEqualTo("c"); + assertThat(((Number) minList.get(2)).intValue()).isEqualTo(23); + } + + @Test + void minNull() { + final ResultSet result = database.command("opencypher", + "UNWIND [null, null] AS val RETURN min(val) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + // ==================== percentileCont() Tests ==================== + + @Test + void percentileContBasic() { + final ResultSet result = database.command("opencypher", + "MATCH (p:Person) RETURN percentileCont(p.age, 0.4) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + // Should use linear interpolation + assertThat(((Number) result.next().getProperty("result")).doubleValue()).isCloseTo(56.8, within(1.0)); + } + + @Test + void percentileContMedian() { + final ResultSet result = database.command("opencypher", + "MATCH (p:Person) RETURN percentileCont(p.age, 0.5) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat(((Number) result.next().getProperty("result")).doubleValue()).isGreaterThan(50.0); + } + + @Test + void percentileContNull() { + final ResultSet result = database.command("opencypher", + "UNWIND [null, null] AS val RETURN percentileCont(val, 0.5) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + // ==================== percentileDisc() Tests ==================== + + @Test + void percentileDiscBasic() { + final ResultSet result = database.command("opencypher", + "MATCH (p:Person) RETURN percentileDisc(p.age, 0.5) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + // Should return an actual value from the set + final int percentile = ((Number) result.next().getProperty("result")).intValue(); + assertThat(percentile).isIn(55, 58, 70, 71); + } + + @Test + void percentileDiscLowPercentile() { + final ResultSet result = database.command("opencypher", + "MATCH (p:Person) RETURN percentileDisc(p.age, 0.0) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat(((Number) result.next().getProperty("result")).intValue()).isEqualTo(55); + } + + @Test + void percentileDiscNull() { + final ResultSet result = database.command("opencypher", + "UNWIND [null, null] AS val RETURN percentileDisc(val, 0.5) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + // ==================== stDev() Tests ==================== + + @Test + void stDevBasic() { + final ResultSet result = database.command("opencypher", + "MATCH (p:Person) WHERE p.name IN ['Keanu Reeves', 'Liam Neeson', 'Carrie Anne Moss'] " + + "RETURN stDev(p.age) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + // Sample standard deviation for ages 58, 70, 55 + assertThat(((Number) result.next().getProperty("result")).doubleValue()).isCloseTo(7.937, within(0.01)); + } + + @Test + void stDevNull() { + final ResultSet result = database.command("opencypher", + "UNWIND [null, null] AS val RETURN stDev(val) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat(((Number) result.next().getProperty("result")).doubleValue()).isEqualTo(0.0); + } + + // ==================== stDevP() Tests ==================== + + @Test + void stDevPBasic() { + final ResultSet result = database.command("opencypher", + "MATCH (p:Person) WHERE p.name IN ['Keanu Reeves', 'Liam Neeson', 'Carrie Anne Moss'] " + + "RETURN stDevP(p.age) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + // Population standard deviation for ages 58, 70, 55 + assertThat(((Number) result.next().getProperty("result")).doubleValue()).isCloseTo(6.481, within(0.01)); + } + + @Test + void stDevPNull() { + final ResultSet result = database.command("opencypher", + "UNWIND [null, null] AS val RETURN stDevP(val) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat(((Number) result.next().getProperty("result")).doubleValue()).isEqualTo(0.0); + } + + // ==================== sum() Tests ==================== + + @Test + void sumBasic() { + final ResultSet result = database.command("opencypher", + "MATCH (p:Person) RETURN sum(p.age) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat(((Number) result.next().getProperty("result")).intValue()).isEqualTo(309); + } + + @Test + void sumWithNulls() { + final ResultSet result = database.command("opencypher", + "UNWIND [1, 2, null, 3, null, 4] AS val RETURN sum(val) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat(((Number) result.next().getProperty("result")).intValue()).isEqualTo(10); + } + + @Test + void sumNull() { + final ResultSet result = database.command("opencypher", + "UNWIND [null, null] AS val RETURN sum(val) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat(((Number) result.next().getProperty("result")).intValue()).isEqualTo(0); + } + + // ==================== Combined/Integration Tests ==================== + + @Test + void aggregationsCombined() { + final ResultSet result = database.command("opencypher", + "MATCH (p:Person) " + + "RETURN count(p) AS count, avg(p.age) AS avgAge, min(p.age) AS minAge, max(p.age) AS maxAge, sum(p.age) AS sumAge"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final var row = result.next(); + assertThat(((Number) row.getProperty("count")).intValue()).isEqualTo(5); + assertThat(((Number) row.getProperty("avgAge")).doubleValue()).isCloseTo(61.8, within(0.1)); + assertThat(((Number) row.getProperty("minAge")).intValue()).isEqualTo(55); + assertThat(((Number) row.getProperty("maxAge")).intValue()).isEqualTo(71); + assertThat(((Number) row.getProperty("sumAge")).intValue()).isEqualTo(309); + } + + @Test + void aggregationWithGrouping() { + final ResultSet result = database.command("opencypher", + "MATCH (p:Person {name: 'Keanu Reeves'})-[:KNOWS]-(f:Person) " + + "RETURN p.name AS person, count(f) AS friendCount, avg(f.age) AS avgFriendAge"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final var row = result.next(); + assertThat((String) row.getProperty("person")).isEqualTo("Keanu Reeves"); + assertThat(((Number) row.getProperty("friendCount")).intValue()).isEqualTo(3); + assertThat(((Number) row.getProperty("avgFriendAge")).doubleValue()).isGreaterThan(50.0); + } + + @Test + void collectAndCount() { + final ResultSet result = database.command("opencypher", + "MATCH (p:Person) " + + "RETURN collect(p.name) AS names, count(p.name) AS nameCount"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final var row = result.next(); + @SuppressWarnings("unchecked") + final List names = (List) row.getProperty("names"); + final int count = ((Number) row.getProperty("nameCount")).intValue(); + assertThat(names).hasSize(count); + assertThat(count).isEqualTo(5); + } + + // ==================== collect_list() Tests (alias of collect()) ==================== + + @Test + void collectListBasic() { + final ResultSet result = database.command("opencypher", + "MATCH (p:Person) RETURN collect_list(p.age) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + @SuppressWarnings("unchecked") + final List ages = (List) result.next().getProperty("result"); + assertThat(ages).hasSize(5); + assertThat(ages).contains(58, 70, 55, 71); + } + + @Test + void collectListWithNulls() { + final ResultSet result = database.command("opencypher", + "UNWIND [1, 2, null, 3, null, 4] AS val RETURN collect_list(val) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + @SuppressWarnings("unchecked") + final List collected = (List) result.next().getProperty("result"); + assertThat(collected).containsExactly(1L, 2L, 3L, 4L); + } + + @Test + void collectListNull() { + final ResultSet result = database.command("opencypher", + "UNWIND [null, null] AS val RETURN collect_list(val) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + @SuppressWarnings("unchecked") + final List collected = (List) result.next().getProperty("result"); + assertThat(collected).isEmpty(); + } + + // ==================== percentile_cont() Tests (alias of percentileCont()) ==================== + + @Test + void percentileContAliasBasic() { + final ResultSet result = database.command("opencypher", + "MATCH (p:Person) RETURN percentile_cont(p.age, 0.4) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + // Should use linear interpolation + assertThat(((Number) result.next().getProperty("result")).doubleValue()).isCloseTo(56.8, within(1.0)); + } + + @Test + void percentileContAliasMedian() { + final ResultSet result = database.command("opencypher", + "MATCH (p:Person) RETURN percentile_cont(p.age, 0.5) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat(((Number) result.next().getProperty("result")).doubleValue()).isGreaterThan(50.0); + } + + @Test + void percentileContAliasNull() { + final ResultSet result = database.command("opencypher", + "UNWIND [null, null] AS val RETURN percentile_cont(val, 0.5) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + // ==================== percentile_disc() Tests (alias of percentileDisc()) ==================== + + @Test + void percentileDiscAliasBasic() { + final ResultSet result = database.command("opencypher", + "MATCH (p:Person) RETURN percentile_disc(p.age, 0.5) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + // Should return an actual value from the set + final int percentile = ((Number) result.next().getProperty("result")).intValue(); + assertThat(percentile).isIn(55, 58, 70, 71); + } + + @Test + void percentileDiscAliasLowPercentile() { + final ResultSet result = database.command("opencypher", + "MATCH (p:Person) RETURN percentile_disc(p.age, 0.0) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat(((Number) result.next().getProperty("result")).intValue()).isEqualTo(55); + } + + @Test + void percentileDiscAliasNull() { + final ResultSet result = database.command("opencypher", + "UNWIND [null, null] AS val RETURN percentile_disc(val, 0.5) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + // ==================== stdev_samp() Tests (alias of stDev()) ==================== + + @Test + void stdevSampBasic() { + final ResultSet result = database.command("opencypher", + "MATCH (p:Person) WHERE p.name IN ['Keanu Reeves', 'Liam Neeson', 'Carrie Anne Moss'] " + + "RETURN stdev_samp(p.age) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + // Sample standard deviation for ages 58, 70, 55 + assertThat(((Number) result.next().getProperty("result")).doubleValue()).isCloseTo(7.937, within(0.01)); + } + + @Test + void stdevSampNull() { + final ResultSet result = database.command("opencypher", + "UNWIND [null, null] AS val RETURN stdev_samp(val) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat(((Number) result.next().getProperty("result")).doubleValue()).isEqualTo(0.0); + } + + // ==================== stdev_pop() Tests (alias of stDevP()) ==================== + + @Test + void stdevPopBasic() { + final ResultSet result = database.command("opencypher", + "MATCH (p:Person) WHERE p.name IN ['Keanu Reeves', 'Liam Neeson', 'Carrie Anne Moss'] " + + "RETURN stdev_pop(p.age) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + // Population standard deviation for ages 58, 70, 55 + assertThat(((Number) result.next().getProperty("result")).doubleValue()).isCloseTo(6.481, within(0.01)); + } + + @Test + void stdevPopNull() { + final ResultSet result = database.command("opencypher", + "UNWIND [null, null] AS val RETURN stdev_pop(val) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat(((Number) result.next().getProperty("result")).doubleValue()).isEqualTo(0.0); + } +} diff --git a/engine/src/test/java/com/arcadedb/query/opencypher/functions/OpenCypherListFunctionsComprehensiveTest.java b/engine/src/test/java/com/arcadedb/query/opencypher/functions/OpenCypherListFunctionsComprehensiveTest.java new file mode 100644 index 0000000000..0b1a0b96a1 --- /dev/null +++ b/engine/src/test/java/com/arcadedb/query/opencypher/functions/OpenCypherListFunctionsComprehensiveTest.java @@ -0,0 +1,798 @@ +/* + * Copyright 2021-present Arcade Data Ltd (info@arcadedata.com) + * + * 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 (info@arcadedata.com) + * SPDX-License-Identifier: Apache-2.0 + */ +package com.arcadedb.query.opencypher.functions; + +import com.arcadedb.database.Database; +import com.arcadedb.database.DatabaseFactory; +import com.arcadedb.query.sql.executor.ResultSet; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import org.assertj.core.api.Assertions; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * Comprehensive tests for OpenCypher List functions based on Neo4j Cypher documentation. + * Tests cover all 21 list functions including coll.* functions, keys(), labels(), nodes(), range(), reduce(), relationships(), reverse(), tail(), and to*List() functions. + */ +class OpenCypherListFunctionsComprehensiveTest { + private Database database; + + @BeforeEach + void setUp() { + final DatabaseFactory factory = new DatabaseFactory("./databases/test-cypher-list-functions"); + if (factory.exists()) + factory.open().drop(); + database = factory.create(); + + // Create test graph matching Neo4j documentation + database.getSchema().createVertexType("Developer"); + database.getSchema().createVertexType("Administrator"); + database.getSchema().createVertexType("Designer"); + database.getSchema().createEdgeType("KNOWS"); + database.getSchema().createEdgeType("MARRIED"); + + database.command("opencypher", + "CREATE " + + "(alice:Developer {name:'Alice', age: 38, eyes: 'Brown'}), " + + "(bob:Administrator {name: 'Bob', age: 25, eyes: 'Blue'}), " + + "(charlie:Administrator {name: 'Charlie', age: 53, eyes: 'Green'}), " + + "(daniel:Administrator {name: 'Daniel', age: 54, eyes: 'Brown'}), " + + "(eskil:Designer {name: 'Eskil', age: 41, eyes: 'blue', likedColors: ['Pink', 'Yellow', 'Black']}), " + + "(alice)-[:KNOWS]->(bob), " + + "(alice)-[:KNOWS]->(charlie), " + + "(bob)-[:KNOWS]->(daniel), " + + "(charlie)-[:KNOWS]->(daniel), " + + "(bob)-[:MARRIED]->(eskil)"); + } + + @AfterEach + void tearDown() { + if (database != null) + database.drop(); + } + + // ==================== coll.distinct() Tests ==================== + + @Test + void collDistinctBasic() { + final ResultSet result = database.command("opencypher", + "RETURN coll.distinct([1, 3, 2, 4, 2, 3, 1]) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + @SuppressWarnings("unchecked") + final List distinct = (List) result.next().getProperty("result"); + assertThat(distinct).containsExactly(1L, 3L, 2L, 4L); + } + + @Test + void collDistinctMixedTypes() { + final ResultSet result = database.command("opencypher", + "RETURN coll.distinct([1, true, true, null, 'a', false, true, 1, null]) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + @SuppressWarnings("unchecked") + final List distinct = (List) result.next().getProperty("result"); + assertThat(distinct).hasSize(5); + assertThat(distinct.get(0)).isEqualTo(1L); + assertThat(distinct.get(1)).isEqualTo(true); + assertThat(distinct.get(2)).isNull(); + assertThat(distinct.get(3)).isEqualTo("a"); + assertThat(distinct.get(4)).isEqualTo(false); + } + + @Test + void collDistinctEmptyList() { + final ResultSet result = database.command("opencypher", "RETURN coll.distinct([]) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + @SuppressWarnings("unchecked") + final List distinct = (List) result.next().getProperty("result"); + assertThat(distinct).isEmpty(); + } + + @Test + void collDistinctNull() { + final ResultSet result = database.command("opencypher", "RETURN coll.distinct(null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + // ==================== coll.flatten() Tests ==================== + + @Test + void collFlattenDefaultDepth() { + final ResultSet result = database.command("opencypher", + "RETURN coll.flatten(['a', ['b', ['c']]]) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + @SuppressWarnings("unchecked") + final List flattened = (List) result.next().getProperty("result"); + assertThat(flattened).hasSize(3); + assertThat(flattened.get(0)).isEqualTo("a"); + assertThat(flattened.get(1)).isEqualTo("b"); + // Third element should still be a list since default depth is 1 + assertThat(flattened.get(2)).isInstanceOf(List.class); + } + + @Test + void collFlattenWithDepth() { + final ResultSet result = database.command("opencypher", + "RETURN coll.flatten(['a', ['b', ['c']]], 2) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + @SuppressWarnings("unchecked") + final List flattened = (List) result.next().getProperty("result"); + assertThat(flattened).containsExactly("a", "b", "c"); + } + + @Test + void collFlattenDepthZero() { + final ResultSet result = database.command("opencypher", + "RETURN coll.flatten(['a', ['b']], 0) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + @SuppressWarnings("unchecked") + final List flattened = (List) result.next().getProperty("result"); + assertThat(flattened).hasSize(2); + assertThat(flattened.get(0)).isEqualTo("a"); + assertThat(flattened.get(1)).isInstanceOf(List.class); + } + + @Test + void collFlattenNull() { + ResultSet result = database.command("opencypher", "RETURN coll.flatten(null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + + result = database.command("opencypher", "RETURN coll.flatten(['a'], null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + // ==================== coll.indexOf() Tests ==================== + + @Test + void collIndexOfBasic() { + final ResultSet result = database.command("opencypher", + "RETURN coll.indexOf(['a', 'b', 'c', 'c'], 'c') AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat(((Number) result.next().getProperty("result")).longValue()).isEqualTo(2L); + } + + @Test + void collIndexOfNotFound() { + final ResultSet result = database.command("opencypher", + "RETURN coll.indexOf([1, 'b', false], 4.3) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat(((Number) result.next().getProperty("result")).longValue()).isEqualTo(-1L); + } + + @Test + void collIndexOfFirstMatch() { + final ResultSet result = database.command("opencypher", + "RETURN coll.indexOf([1, 2, 3, 2], 2) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat(((Number) result.next().getProperty("result")).longValue()).isEqualTo(1L); + } + + @Test + void collIndexOfNull() { + ResultSet result = database.command("opencypher", "RETURN coll.indexOf(null, 'a') AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + + result = database.command("opencypher", "RETURN coll.indexOf(['a'], null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + // ==================== coll.insert() Tests ==================== + + @Test + void collInsertBasic() { + final ResultSet result = database.command("opencypher", + "RETURN coll.insert([true, 'a', 1, 5.4], 1, false) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + @SuppressWarnings("unchecked") + final List inserted = (List) result.next().getProperty("result"); + assertThat(inserted).hasSize(5); + assertThat(inserted.get(0)).isEqualTo(true); + assertThat(inserted.get(1)).isEqualTo(false); + assertThat(inserted.get(2)).isEqualTo("a"); + } + + @Test + void collInsertAtStart() { + final ResultSet result = database.command("opencypher", + "RETURN coll.insert([1, 2, 3], 0, 0) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + @SuppressWarnings("unchecked") + final List inserted = (List) result.next().getProperty("result"); + assertThat(inserted).containsExactly(0L, 1L, 2L, 3L); + } + + @Test + void collInsertAtEnd() { + final ResultSet result = database.command("opencypher", + "RETURN coll.insert([1, 2, 3], 3, 4) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + @SuppressWarnings("unchecked") + final List inserted = (List) result.next().getProperty("result"); + assertThat(inserted).containsExactly(1L, 2L, 3L, 4L); + } + + @Test + void collInsertNegativeIndexRaisesError() { + assertThatThrownBy(() -> database.command("opencypher", "RETURN coll.insert([1, 2], -1, 0) AS result").hasNext()) + .hasMessageContaining("negative"); + } + + @Test + void collInsertIndexTooLargeRaisesError() { + assertThatThrownBy(() -> database.command("opencypher", "RETURN coll.insert([1, 2], 10, 0) AS result").hasNext()) + .hasMessageContaining("index"); + } + + @Test + void collInsertNull() { + ResultSet result = database.command("opencypher", "RETURN coll.insert(null, 0, 'a') AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + + result = database.command("opencypher", "RETURN coll.insert([1], null, 'a') AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + // ==================== coll.max() Tests ==================== + + @Test + void collMaxBasic() { + final ResultSet result = database.command("opencypher", + "RETURN coll.max([true, 'a', 1, 5.4]) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat(((Number) result.next().getProperty("result")).doubleValue()).isEqualTo(5.4); + } + + @Test + void collMaxNumbers() { + final ResultSet result = database.command("opencypher", + "RETURN coll.max([1, 5, 3, 9, 2]) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat(((Number) result.next().getProperty("result")).intValue()).isEqualTo(9); + } + + @Test + void collMaxEmptyList() { + final ResultSet result = database.command("opencypher", "RETURN coll.max([]) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + @Test + void collMaxNull() { + final ResultSet result = database.command("opencypher", "RETURN coll.max(null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + // ==================== coll.min() Tests ==================== + + @Test + void collMinBasic() { + // coll.min uses cypherTypeRank: Map=0, Vertex=1, Edge=2, List=3, other=4, String=5, Boolean=6, Number=7 + // So String 'a' (rank 5) < Boolean true (rank 6) < Number 1 (rank 7) + final ResultSet result = database.command("opencypher", + "RETURN coll.min([true, 'a', 1, 5.4]) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat((String) result.next().getProperty("result")).isEqualTo("a"); + } + + @Test + void collMinNumbers() { + final ResultSet result = database.command("opencypher", + "RETURN coll.min([5, 1, 9, 2, 3]) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat(((Number) result.next().getProperty("result")).intValue()).isEqualTo(1); + } + + @Test + void collMinEmptyList() { + final ResultSet result = database.command("opencypher", "RETURN coll.min([]) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + @Test + void collMinNull() { + final ResultSet result = database.command("opencypher", "RETURN coll.min(null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + // ==================== coll.remove() Tests ==================== + + @Test + void collRemoveBasic() { + final ResultSet result = database.command("opencypher", + "RETURN coll.remove([true, 'a', 1, 5.4], 1) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + @SuppressWarnings("unchecked") + final List removed = (List) result.next().getProperty("result"); + assertThat(removed).containsExactly(true, 1L, 5.4); + } + + @Test + void collRemoveFirst() { + final ResultSet result = database.command("opencypher", + "RETURN coll.remove([1, 2, 3], 0) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + @SuppressWarnings("unchecked") + final List removed = (List) result.next().getProperty("result"); + assertThat(removed).containsExactly(2L, 3L); + } + + @Test + void collRemoveLast() { + final ResultSet result = database.command("opencypher", + "RETURN coll.remove([1, 2, 3], 2) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + @SuppressWarnings("unchecked") + final List removed = (List) result.next().getProperty("result"); + assertThat(removed).containsExactly(1L, 2L); + } + + @Test + void collRemoveNegativeIndexRaisesError() { + assertThatThrownBy(() -> database.command("opencypher", "RETURN coll.remove([1, 2], -1) AS result").hasNext()) + .hasMessageContaining("negative"); + } + + @Test + void collRemoveIndexTooLargeRaisesError() { + assertThatThrownBy(() -> database.command("opencypher", "RETURN coll.remove([1, 2], 10) AS result").hasNext()) + .hasMessageContaining("index"); + } + + @Test + void collRemoveNull() { + ResultSet result = database.command("opencypher", "RETURN coll.remove(null, 0) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + + result = database.command("opencypher", "RETURN coll.remove([1], null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + // ==================== coll.sort() Tests ==================== + + @Test + void collSortBasic() { + final ResultSet result = database.command("opencypher", + "RETURN coll.sort([true, 'a', 1, 2]) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + @SuppressWarnings("unchecked") + final List sorted = (List) result.next().getProperty("result"); + assertThat(sorted).hasSize(4); + // Cypher ordering: strings < booleans < numbers + assertThat(sorted.get(0)).isEqualTo("a"); + assertThat(sorted.get(1)).isEqualTo(true); + } + + @Test + void collSortNumbers() { + final ResultSet result = database.command("opencypher", + "RETURN coll.sort([5, 1, 9, 2, 3]) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + @SuppressWarnings("unchecked") + final List sorted = (List) result.next().getProperty("result"); + assertThat(sorted).containsExactly(1L, 2L, 3L, 5L, 9L); + } + + @Test + void collSortStrings() { + final ResultSet result = database.command("opencypher", + "RETURN coll.sort(['zebra', 'apple', 'banana']) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + @SuppressWarnings("unchecked") + final List sorted = (List) result.next().getProperty("result"); + assertThat(sorted).containsExactly("apple", "banana", "zebra"); + } + + @Test + void collSortNull() { + final ResultSet result = database.command("opencypher", "RETURN coll.sort(null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + // ==================== keys() Tests ==================== + + @Test + void keysFromNode() { + final ResultSet result = database.command("opencypher", + "MATCH (a) WHERE a.name = 'Alice' RETURN keys(a) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + @SuppressWarnings("unchecked") + final List keys = (List) result.next().getProperty("result"); + assertThat(keys).containsExactlyInAnyOrder("name", "age", "eyes"); + } + + @Test + void keysFromMap() { + final ResultSet result = database.command("opencypher", + "RETURN keys({name: 'Alice', age: 38}) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + @SuppressWarnings("unchecked") + final List keys = (List) result.next().getProperty("result"); + assertThat(keys).containsExactlyInAnyOrder("name", "age"); + } + + @Test + void keysNull() { + final ResultSet result = database.command("opencypher", "RETURN keys(null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + // ==================== labels() Tests ==================== + + @Test + void labelsBasic() { + final ResultSet result = database.command("opencypher", + "MATCH (a) WHERE a.name = 'Alice' RETURN labels(a) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + @SuppressWarnings("unchecked") + final List labels = (List) result.next().getProperty("result"); + assertThat(labels).contains("Developer"); + } + + @Test + void labelsNull() { + final ResultSet result = database.command("opencypher", "RETURN labels(null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + // ==================== nodes() Tests ==================== + + @Test + void nodesFromPath() { + final ResultSet result = database.command("opencypher", + "MATCH p = (a)-->(b)-->(c) WHERE a.name = 'Alice' AND c.name = 'Eskil' " + + "RETURN size(nodes(p)) AS nodeCount"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat(((Number) result.next().getProperty("nodeCount")).intValue()).isEqualTo(3); + } + + @Test + void nodesNull() { + final ResultSet result = database.command("opencypher", "RETURN nodes(null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + // ==================== range() Tests ==================== + + @Test + void rangeBasic() { + final ResultSet result = database.command("opencypher", + "RETURN range(0, 10) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + @SuppressWarnings("unchecked") + final List range = (List) result.next().getProperty("result"); + assertThat(range).hasSize(11); + assertThat(((Number) range.get(0)).intValue()).isEqualTo(0); + assertThat(((Number) range.get(10)).intValue()).isEqualTo(10); + } + + @Test + void rangeWithStep() { + final ResultSet result = database.command("opencypher", + "RETURN range(2, 18, 3) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + @SuppressWarnings("unchecked") + final List range = (List) result.next().getProperty("result"); + assertThat(range).hasSize(6); + assertThat(((Number) range.get(0)).intValue()).isEqualTo(2); + assertThat(((Number) range.get(1)).intValue()).isEqualTo(5); + assertThat(((Number) range.get(5)).intValue()).isEqualTo(17); + } + + @Test + void rangeDecreasing() { + final ResultSet result = database.command("opencypher", + "RETURN range(10, 0, -2) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + @SuppressWarnings("unchecked") + final List range = (List) result.next().getProperty("result"); + assertThat(range).hasSize(6); + assertThat(((Number) range.get(0)).intValue()).isEqualTo(10); + assertThat(((Number) range.get(5)).intValue()).isEqualTo(0); + } + + @Test + void rangeEmpty() { + // Positive start, positive end, negative step = empty range + final ResultSet result = database.command("opencypher", + "RETURN range(0, 5, -1) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + @SuppressWarnings("unchecked") + final List range = (List) result.next().getProperty("result"); + assertThat(range).isEmpty(); + } + + // ==================== reduce() Tests ==================== + + @Test + void reduceBasic() { + final ResultSet result = database.command("opencypher", + "MATCH p = (a)-->(b)-->(c) " + + "WHERE a.name = 'Alice' AND b.name = 'Bob' AND c.name = 'Daniel' " + + "RETURN reduce(totalAge = 0, n IN nodes(p) | totalAge + n.age) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat(((Number) result.next().getProperty("result")).intValue()).isEqualTo(117); // 38 + 25 + 54 + } + + @Test + void reduceSimpleList() { + final ResultSet result = database.command("opencypher", + "RETURN reduce(sum = 0, x IN [1, 2, 3, 4, 5] | sum + x) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat(((Number) result.next().getProperty("result")).intValue()).isEqualTo(15); + } + + @Test + void reduceMultiply() { + final ResultSet result = database.command("opencypher", + "RETURN reduce(product = 1, x IN [2, 3, 4] | product * x) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat(((Number) result.next().getProperty("result")).intValue()).isEqualTo(24); + } + + // ==================== relationships() Tests ==================== + + @Test + void relationshipsFromPath() { + final ResultSet result = database.command("opencypher", + "MATCH p = (a)-->(b)-->(c) WHERE a.name = 'Alice' AND c.name = 'Eskil' " + + "RETURN size(relationships(p)) AS relCount"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat(((Number) result.next().getProperty("relCount")).intValue()).isEqualTo(2); + } + + @Test + void relationshipsNull() { + final ResultSet result = database.command("opencypher", "RETURN relationships(null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + // ==================== reverse() Tests ==================== + + @Test + void reverseList() { + final ResultSet result = database.command("opencypher", + "RETURN reverse([4923, 'abc', 521, null, 487]) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + @SuppressWarnings("unchecked") + final List reversed = (List) result.next().getProperty("result"); + assertThat(reversed).hasSize(5); + assertThat(((Number) reversed.get(0)).intValue()).isEqualTo(487); + assertThat(reversed.get(1)).isNull(); + assertThat(((Number) reversed.get(2)).intValue()).isEqualTo(521); + assertThat(reversed.get(3)).isEqualTo("abc"); + assertThat(((Number) reversed.get(4)).intValue()).isEqualTo(4923); + } + + @Test + void reverseEmpty() { + final ResultSet result = database.command("opencypher", "RETURN reverse([]) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + @SuppressWarnings("unchecked") + final List reversed = (List) result.next().getProperty("result"); + assertThat(reversed).isEmpty(); + } + + @Test + void reverseNull() { + final ResultSet result = database.command("opencypher", "RETURN reverse(null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + // ==================== tail() Tests ==================== + + @Test + void tailBasic() { + final ResultSet result = database.command("opencypher", + "MATCH (a) WHERE a.name = 'Eskil' RETURN tail(a.likedColors) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + @SuppressWarnings("unchecked") + final List tail = (List) result.next().getProperty("result"); + assertThat(tail).containsExactly("Yellow", "Black"); + } + + @Test + void tailSimpleList() { + final ResultSet result = database.command("opencypher", + "RETURN tail([1, 2, 3, 4, 5]) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + @SuppressWarnings("unchecked") + final List tail = (List) result.next().getProperty("result"); + assertThat(tail).hasSize(4); + assertThat(((Number) tail.get(0)).intValue()).isEqualTo(2); + assertThat(((Number) tail.get(3)).intValue()).isEqualTo(5); + } + + @Test + void tailSingleElement() { + final ResultSet result = database.command("opencypher", "RETURN tail([1]) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + @SuppressWarnings("unchecked") + final List tail = (List) result.next().getProperty("result"); + assertThat(tail).isEmpty(); + } + + @Test + void tailEmpty() { + final ResultSet result = database.command("opencypher", "RETURN tail([]) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + @SuppressWarnings("unchecked") + final List tail = (List) result.next().getProperty("result"); + assertThat(tail).isEmpty(); + } + + // ==================== toBooleanList() Tests ==================== + + @Test + void toBooleanListBasic() { + final ResultSet result = database.command("opencypher", + "RETURN toBooleanList(['a string', true, 'false', null, ['A','B']]) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + @SuppressWarnings("unchecked") + final List boolList = (List) result.next().getProperty("result"); + assertThat(boolList).hasSize(5); + assertThat(boolList.get(0)).isNull(); // 'a string' not convertible + assertThat(boolList.get(1)).isEqualTo(true); + assertThat(boolList.get(2)).isEqualTo(false); // 'false' string converts to false + assertThat(boolList.get(3)).isNull(); + assertThat(boolList.get(4)).isNull(); // list not convertible + } + + @Test + void toBooleanListNull() { + final ResultSet result = database.command("opencypher", + "RETURN toBooleanList(null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + @Test + void toBooleanListNullsInList() { + final ResultSet result = database.command("opencypher", + "RETURN toBooleanList([null, null]) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + @SuppressWarnings("unchecked") + final List boolList = (List) result.next().getProperty("result"); + assertThat(boolList).containsExactly(null, null); + } + + // ==================== toFloatList() Tests ==================== + + @Test + void toFloatListBasic() { + final ResultSet result = database.command("opencypher", + "RETURN toFloatList(['a string', 2.5, '3.14159', null, ['A','B']]) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + @SuppressWarnings("unchecked") + final List floatList = (List) result.next().getProperty("result"); + assertThat(floatList).hasSize(5); + assertThat(floatList.get(0)).isNull(); + assertThat(((Number) floatList.get(1)).doubleValue()).isEqualTo(2.5); + assertThat(((Number) floatList.get(2)).doubleValue()).isEqualTo(3.14159); + assertThat(floatList.get(3)).isNull(); + assertThat(floatList.get(4)).isNull(); + } + + @Test + void toFloatListNull() { + final ResultSet result = database.command("opencypher", + "RETURN toFloatList(null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + // ==================== toIntegerList() Tests ==================== + + @Test + void toIntegerListBasic() { + final ResultSet result = database.command("opencypher", + "RETURN toIntegerList(['a string', 2, '5', null, ['A','B']]) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + @SuppressWarnings("unchecked") + final List intList = (List) result.next().getProperty("result"); + assertThat(intList).hasSize(5); + assertThat(intList.get(0)).isNull(); + assertThat(((Number) intList.get(1)).intValue()).isEqualTo(2); + assertThat(((Number) intList.get(2)).intValue()).isEqualTo(5); + assertThat(intList.get(3)).isNull(); + assertThat(intList.get(4)).isNull(); + } + + @Test + void toIntegerListNull() { + final ResultSet result = database.command("opencypher", + "RETURN toIntegerList(null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + // ==================== toStringList() Tests ==================== + + @Test + void toStringListBasic() { + final ResultSet result = database.command("opencypher", + "RETURN toStringList(['already a string', 2, null, ['A','B']]) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + @SuppressWarnings("unchecked") + final List strList = (List) result.next().getProperty("result"); + assertThat(strList).hasSize(4); + assertThat(strList.get(0)).isEqualTo("already a string"); + assertThat(strList.get(1)).isEqualTo("2"); + assertThat(strList.get(2)).isNull(); + assertThat(strList.get(3)).isNull(); // list not convertible + } + + @Test + void toStringListNull() { + final ResultSet result = database.command("opencypher", + "RETURN toStringList(null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + // ==================== Combined/Integration Tests ==================== + + @Test + void listFunctionsCombined() { + final ResultSet result = database.command("opencypher", + "RETURN tail(coll.sort([5, 1, 3, 9, 2])) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + @SuppressWarnings("unchecked") + final List combined = (List) result.next().getProperty("result"); + assertThat(combined).containsExactly(2L, 3L, 5L, 9L); // sorted, then tail + } + + @Test + void listFunctionsWithReduce() { + final ResultSet result = database.command("opencypher", + "RETURN reduce(s = '', x IN ['hello', 'world'] | s + x + ' ') AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat((String) result.next().getProperty("result")).isEqualTo("hello world "); + } + + @Test + void rangeWithReduce() { + final ResultSet result = database.command("opencypher", + "RETURN reduce(sum = 0, x IN range(1, 10) | sum + x) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat(((Number) result.next().getProperty("result")).intValue()).isEqualTo(55); + } +} diff --git a/engine/src/test/java/com/arcadedb/query/opencypher/functions/OpenCypherLoadCsvFunctionsComprehensiveTest.java b/engine/src/test/java/com/arcadedb/query/opencypher/functions/OpenCypherLoadCsvFunctionsComprehensiveTest.java new file mode 100644 index 0000000000..c0071747ad --- /dev/null +++ b/engine/src/test/java/com/arcadedb/query/opencypher/functions/OpenCypherLoadCsvFunctionsComprehensiveTest.java @@ -0,0 +1,323 @@ +/* + * Copyright 2021-present Arcade Data Ltd (info@arcadedata.com) + * + * 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 (info@arcadedata.com) + * SPDX-License-Identifier: Apache-2.0 + */ +package com.arcadedb.query.opencypher.functions; + +import com.arcadedb.database.Database; +import com.arcadedb.database.DatabaseFactory; +import com.arcadedb.query.sql.executor.ResultSet; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.BufferedWriter; +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * Comprehensive tests for OpenCypher LOAD CSV functions based on Neo4j Cypher documentation. + * Tests cover: file(), linenumber() + * + * These functions are only useful when run on a query that uses LOAD CSV. + * In all other contexts they always return null. + */ +class OpenCypherLoadCsvFunctionsComprehensiveTest { + + private Database database; + private Path testCsvFile; + + @BeforeEach + void setUp() throws Exception { + final DatabaseFactory factory = new DatabaseFactory("./databases/test-cypher-load-csv-functions"); + if (factory.exists()) + factory.open().drop(); + database = factory.create(); + + // Create test CSV file with headers + testCsvFile = Files.createTempFile("test-load-csv", ".csv"); + try (BufferedWriter writer = Files.newBufferedWriter(testCsvFile)) { + writer.write("name,age,city"); + writer.newLine(); + writer.write("Alice,30,Paris"); + writer.newLine(); + writer.write("Bob,25,London"); + writer.newLine(); + writer.write("Charlie,35,Berlin"); + writer.newLine(); + } + } + + @AfterEach + void tearDown() { + if (database != null) + database.drop(); + if (testCsvFile != null) { + try { + Files.deleteIfExists(testCsvFile); + } catch (Exception e) { + // Ignore cleanup errors + } + } + } + + // ==================== file() Tests ==================== + + @Test + void fileReturnsFileName() { + final ResultSet result = database.command("opencypher", + "LOAD CSV FROM '" + testCsvFile.toAbsolutePath() + "' AS row\n" + + "RETURN file() AS filename"); + assertThat(result.hasNext()).isTrue(); + final String filename = (String) result.next().getProperty("filename"); + assertThat(filename).isNotNull(); + assertThat(filename).isEqualTo(testCsvFile.toAbsolutePath().toString()); + } + + @Test + void fileReturnsSameNameForAllRows() { + final ResultSet result = database.command("opencypher", + "LOAD CSV FROM '" + testCsvFile.toAbsolutePath() + "' AS row\n" + + "RETURN file() AS filename"); + assertThat(result.hasNext()).isTrue(); + final String expectedFilename = testCsvFile.toAbsolutePath().toString(); + while (result.hasNext()) { + final String filename = (String) result.next().getProperty("filename"); + assertThat(filename).isEqualTo(expectedFilename); + } + } + + @Test + void fileReturnsNullOutsideLoadCsvContext() { + final ResultSet result = database.command("opencypher", "RETURN file() AS filename"); + assertThat(result.hasNext()).isTrue(); + assertThat(result.next().getProperty("filename") == null).isTrue(); + } + + @Test + void fileWithHeaderRow() { + final ResultSet result = database.command("opencypher", + "LOAD CSV WITH HEADERS FROM '" + testCsvFile.toAbsolutePath() + "' AS row\n" + + "RETURN file() AS filename LIMIT 1"); + assertThat(result.hasNext()).isTrue(); + final String filename = (String) result.next().getProperty("filename"); + assertThat(filename).isNotNull(); + assertThat(filename).isEqualTo(testCsvFile.toAbsolutePath().toString()); + } + + @Test + void fileWithoutHeaderRow() { + final ResultSet result = database.command("opencypher", + "LOAD CSV FROM '" + testCsvFile.toAbsolutePath() + "' AS row\n" + + "RETURN file() AS filename LIMIT 1"); + assertThat(result.hasNext()).isTrue(); + final String filename = (String) result.next().getProperty("filename"); + assertThat(filename).isNotNull(); + assertThat(filename).isEqualTo(testCsvFile.toAbsolutePath().toString()); + } + + // ==================== linenumber() Tests ==================== + + @Test + void linenumberReturnsLineNumber() { + final ResultSet result = database.command("opencypher", + "LOAD CSV FROM '" + testCsvFile.toAbsolutePath() + "' AS row\n" + + "RETURN linenumber() AS line"); + assertThat(result.hasNext()).isTrue(); + int expectedLine = 1; + while (result.hasNext()) { + final Integer line = ((Number) result.next().getProperty("line")).intValue(); + assertThat(line).isEqualTo(expectedLine); + expectedLine++; + } + } + + @Test + void linenumberWithHeadersStartsAt1() { + // When CSV has headers, the header row is line 1, first data row is line 2 + final ResultSet result = database.command("opencypher", + "LOAD CSV WITH HEADERS FROM '" + testCsvFile.toAbsolutePath() + "' AS row\n" + + "RETURN linenumber() AS line"); + assertThat(result.hasNext()).isTrue(); + int expectedLine = 2; // First data row is line 2 (line 1 is header) + while (result.hasNext()) { + final Integer line = ((Number) result.next().getProperty("line")).intValue(); + assertThat(line).isEqualTo(expectedLine); + expectedLine++; + } + } + + @Test + void linenumberWithoutHeadersStartsAt1() { + // When CSV has no headers, first row is line 1 + final ResultSet result = database.command("opencypher", + "LOAD CSV FROM '" + testCsvFile.toAbsolutePath() + "' AS row\n" + + "RETURN linenumber() AS line"); + assertThat(result.hasNext()).isTrue(); + int expectedLine = 1; // First row is line 1 + while (result.hasNext()) { + final Integer line = ((Number) result.next().getProperty("line")).intValue(); + assertThat(line).isEqualTo(expectedLine); + expectedLine++; + } + } + + @Test + void linenumberReturnsNullOutsideLoadCsvContext() { + final ResultSet result = database.command("opencypher", "RETURN linenumber() AS line"); + assertThat(result.hasNext()).isTrue(); + assertThat(result.next().getProperty("line") == null).isTrue(); + } + + @Test + void linenumberIncrementsPerRow() { + final ResultSet result = database.command("opencypher", + "LOAD CSV FROM '" + testCsvFile.toAbsolutePath() + "' AS row\n" + + "RETURN linenumber() AS line ORDER BY line"); + assertThat(result.hasNext()).isTrue(); + int previousLine = 0; + while (result.hasNext()) { + final Integer line = ((Number) result.next().getProperty("line")).intValue(); + assertThat(line).isGreaterThan(previousLine); + previousLine = line; + } + } + + @Test + void linenumberWithFileCombined() { + final ResultSet result = database.command("opencypher", + "LOAD CSV FROM '" + testCsvFile.toAbsolutePath() + "' AS row\n" + + "RETURN file() AS filename, linenumber() AS line"); + assertThat(result.hasNext()).isTrue(); + final String expectedFilename = testCsvFile.toAbsolutePath().toString(); + int expectedLine = 1; + while (result.hasNext()) { + final var row = result.next(); + final String filename = (String) row.getProperty("filename"); + final Integer line = ((Number) row.getProperty("line")).intValue(); + assertThat(filename).isEqualTo(expectedFilename); + assertThat(line).isEqualTo(expectedLine); + expectedLine++; + } + } + + @Test + void linenumberWithSelectClause() { + final ResultSet result = database.command("opencypher", + "LOAD CSV FROM '" + testCsvFile.toAbsolutePath() + "' AS row\n" + + "WITH row, linenumber() AS line\n" + + "WHERE line > 1\n" + + "RETURN line"); + assertThat(result.hasNext()).isTrue(); + while (result.hasNext()) { + final Integer line = ((Number) result.next().getProperty("line")).intValue(); + assertThat(line).isGreaterThan(1); + } + } + + @Test + void linenumberWithCreate() { + database.getSchema().createVertexType("Person"); + final ResultSet result = database.command("opencypher", + "LOAD CSV WITH HEADERS FROM '" + testCsvFile.toAbsolutePath() + "' AS row\n" + + "CREATE (p:Person {name: row.name, age: toInteger(row.age), lineNumber: linenumber()})\n" + + "RETURN count(p) AS count"); + assertThat(result.hasNext()).isTrue(); + final Integer count = ((Number) result.next().getProperty("count")).intValue(); + assertThat(count).isEqualTo(3); // 3 data rows (excluding header) + + // Verify line numbers were created correctly + final ResultSet verifyResult = database.command("opencypher", + "MATCH (p:Person) RETURN p.lineNumber AS line ORDER BY line"); + assertThat(verifyResult.hasNext()).isTrue(); + int expectedLine = 2; // First data row is line 2 (line 1 is header) + while (verifyResult.hasNext()) { + final Integer line = ((Number) verifyResult.next().getProperty("line")).intValue(); + assertThat(line).isEqualTo(expectedLine); + expectedLine++; + } + } + + @Test + void fileAndLinenumberWithMultipleLoadCsv() throws Exception { + // Create a second CSV file + Path secondCsvFile = null; + try { + secondCsvFile = Files.createTempFile("test-load-csv-2", ".csv"); + try (BufferedWriter writer = Files.newBufferedWriter(secondCsvFile)) { + writer.write("value"); + writer.newLine(); + writer.write("100"); + writer.newLine(); + writer.write("200"); + writer.newLine(); + } + + // When there are multiple LOAD CSV clauses, functions return info about + // the most recently executed LOAD CSV clause + final ResultSet result = database.command("opencypher", + "LOAD CSV FROM '" + testCsvFile.toAbsolutePath() + "' AS row1\n" + + "LOAD CSV FROM '" + secondCsvFile.toAbsolutePath() + "' AS row2\n" + + "RETURN file() AS filename, linenumber() AS line"); + assertThat(result.hasNext()).isTrue(); + while (result.hasNext()) { + final var row = result.next(); + final String filename = (String) row.getProperty("filename"); + assertThat(filename).isEqualTo(secondCsvFile.toAbsolutePath().toString()); + } + } finally { + if (secondCsvFile != null) { + Files.deleteIfExists(secondCsvFile); + } + } + } + + @Test + void linenumberWithSkipRows() { + final ResultSet result = database.command("opencypher", + "LOAD CSV FROM '" + testCsvFile.toAbsolutePath() + "' AS row\n" + + "SKIP 1\n" + + "RETURN linenumber() AS line"); + assertThat(result.hasNext()).isTrue(); + // Even with SKIP, linenumber should reflect the actual line number in the file + int expectedLine = 2; // First row after skip is line 2 + while (result.hasNext()) { + final Integer line = ((Number) result.next().getProperty("line")).intValue(); + assertThat(line).isEqualTo(expectedLine); + expectedLine++; + } + } + + @Test + void linenumberWithLimit() { + final ResultSet result = database.command("opencypher", + "LOAD CSV FROM '" + testCsvFile.toAbsolutePath() + "' AS row\n" + + "LIMIT 2\n" + + "RETURN linenumber() AS line"); + assertThat(result.hasNext()).isTrue(); + int count = 0; + while (result.hasNext()) { + final Integer line = ((Number) result.next().getProperty("line")).intValue(); + assertThat(line).isEqualTo(count + 1); + count++; + } + assertThat(count).isEqualTo(2); + } +} diff --git a/engine/src/test/java/com/arcadedb/query/opencypher/functions/OpenCypherMathLogarithmicFunctionsComprehensiveTest.java b/engine/src/test/java/com/arcadedb/query/opencypher/functions/OpenCypherMathLogarithmicFunctionsComprehensiveTest.java new file mode 100644 index 0000000000..4049f0ed30 --- /dev/null +++ b/engine/src/test/java/com/arcadedb/query/opencypher/functions/OpenCypherMathLogarithmicFunctionsComprehensiveTest.java @@ -0,0 +1,399 @@ +/* + * Copyright 2021-present Arcade Data Ltd (info@arcadedata.com) + * + * 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 (info@arcadedata.com) + * SPDX-License-Identifier: Apache-2.0 + */ +package com.arcadedb.query.opencypher.functions; + +import com.arcadedb.database.Database; +import com.arcadedb.database.DatabaseFactory; +import com.arcadedb.query.sql.executor.ResultSet; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import org.assertj.core.api.Assertions; +import static org.assertj.core.api.Assertions.within; + +/** + * Comprehensive tests for OpenCypher Mathematical Logarithmic functions based on Neo4j Cypher documentation. + * Tests cover: e(), exp(), log(), log10(), sqrt() + */ +class OpenCypherMathLogarithmicFunctionsComprehensiveTest { + private Database database; + + @BeforeEach + void setUp() { + final DatabaseFactory factory = new DatabaseFactory("./databases/test-cypher-math-log"); + if (factory.exists()) + factory.open().drop(); + database = factory.create(); + } + + @AfterEach + void tearDown() { + if (database != null) + database.drop(); + } + + // ==================== e() Tests ==================== + + @Test + void eBasic() { + final ResultSet result = database.command("opencypher", "RETURN e() AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Number resultNum = (Number) result.next().getProperty("result"); + assertThat(resultNum.doubleValue()).isCloseTo(Math.E, within(0.0000001)); + } + + @Test + void eConstant() { + final ResultSet result = database.command("opencypher", "RETURN e() AS e1, e() AS e2"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final var row = result.next(); + assertThat((Double) row.getProperty("e1")).isEqualTo((Double) row.getProperty("e2")); + } + + // ==================== exp() Tests ==================== + + @Test + void expZero() { + final ResultSet result = database.command("opencypher", "RETURN exp(0.0) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Number resultNum = (Number) result.next().getProperty("result"); + assertThat(resultNum.doubleValue()).isCloseTo(1.0, within(0.0001)); + } + + @Test + void expOne() { + final ResultSet result = database.command("opencypher", "RETURN exp(1.0) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Number resultNum = (Number) result.next().getProperty("result"); + assertThat(resultNum.doubleValue()).isCloseTo(Math.E, within(0.0001)); + } + + @Test + void expNegative() { + final ResultSet result = database.command("opencypher", "RETURN exp(-1.0) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Number resultNum = (Number) result.next().getProperty("result"); + assertThat(resultNum.doubleValue()).isCloseTo(1.0 / Math.E, within(0.0001)); + } + + @Test + void expLargeValue() { + final ResultSet result = database.command("opencypher", "RETURN exp(10.0) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Number resultNum = (Number) result.next().getProperty("result"); + assertThat(resultNum.doubleValue()).isGreaterThan(20000.0); + } + + @Test + void expOverflowReturnsInfinity() { + final ResultSet result = database.command("opencypher", "RETURN exp(1000.0) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final Double value = (Double) result.next().getProperty("result"); + assertThat(value.isInfinite()).isTrue(); + } + + @Test + void expNull() { + final ResultSet result = database.command("opencypher", "RETURN exp(null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + // ==================== log() Tests ==================== + + @Test + void logOne() { + final ResultSet result = database.command("opencypher", "RETURN log(1.0) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Number resultNum = (Number) result.next().getProperty("result"); + assertThat(resultNum.doubleValue()).isCloseTo(0.0, within(0.0001)); + } + + @Test + void logE() { + final ResultSet result = database.command("opencypher", "RETURN log(e()) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Number resultNum = (Number) result.next().getProperty("result"); + assertThat(resultNum.doubleValue()).isCloseTo(1.0, within(0.0001)); + } + + @Test + void logPositive() { + final ResultSet result = database.command("opencypher", "RETURN log(10.0) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Number resultNum = (Number) result.next().getProperty("result"); + assertThat(resultNum.doubleValue()).isCloseTo(Math.log(10.0), within(0.0001)); + } + + @Test + void logZeroReturnsNegativeInfinity() { + final ResultSet result = database.command("opencypher", "RETURN log(0.0) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final Double value = (Double) result.next().getProperty("result"); + assertThat(value).isEqualTo(Double.NEGATIVE_INFINITY); + } + + @Test + void logNegativeReturnsNaN() { + final ResultSet result = database.command("opencypher", "RETURN log(-1.0) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Number resultNum = (Number) result.next().getProperty("result"); + assertThat(resultNum.doubleValue()).isNaN(); + } + + @Test + void logNull() { + final ResultSet result = database.command("opencypher", "RETURN log(null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + // ==================== ln() Tests (alias of log()) ==================== + + @Test + void lnOne() { + final ResultSet result = database.command("opencypher", "RETURN ln(1.0) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Number resultNum = (Number) result.next().getProperty("result"); + assertThat(resultNum.doubleValue()).isCloseTo(0.0, within(0.0001)); + } + + @Test + void lnE() { + final ResultSet result = database.command("opencypher", "RETURN ln(e()) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Number resultNum = (Number) result.next().getProperty("result"); + assertThat(resultNum.doubleValue()).isCloseTo(1.0, within(0.0001)); + } + + @Test + void lnPositive() { + final ResultSet result = database.command("opencypher", "RETURN ln(10.0) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Number resultNum = (Number) result.next().getProperty("result"); + assertThat(resultNum.doubleValue()).isCloseTo(Math.log(10.0), within(0.0001)); + } + + @Test + void lnZeroReturnsNegativeInfinity() { + final ResultSet result = database.command("opencypher", "RETURN ln(0.0) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final Double value = (Double) result.next().getProperty("result"); + assertThat(value).isEqualTo(Double.NEGATIVE_INFINITY); + } + + @Test + void lnNegativeReturnsNaN() { + final ResultSet result = database.command("opencypher", "RETURN ln(-1.0) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Number resultNum = (Number) result.next().getProperty("result"); + assertThat(resultNum.doubleValue()).isNaN(); + } + + @Test + void lnNull() { + final ResultSet result = database.command("opencypher", "RETURN ln(null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + // ==================== log10() Tests ==================== + + @Test + void log10One() { + final ResultSet result = database.command("opencypher", "RETURN log10(1.0) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Number resultNum = (Number) result.next().getProperty("result"); + assertThat(resultNum.doubleValue()).isCloseTo(0.0, within(0.0001)); + } + + @Test + void log10Ten() { + final ResultSet result = database.command("opencypher", "RETURN log10(10.0) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Number resultNum = (Number) result.next().getProperty("result"); + assertThat(resultNum.doubleValue()).isCloseTo(1.0, within(0.0001)); + } + + @Test + void log10Hundred() { + final ResultSet result = database.command("opencypher", "RETURN log10(100.0) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Number resultNum = (Number) result.next().getProperty("result"); + assertThat(resultNum.doubleValue()).isCloseTo(2.0, within(0.0001)); + } + + @Test + void log10Thousand() { + final ResultSet result = database.command("opencypher", "RETURN log10(1000.0) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Number resultNum = (Number) result.next().getProperty("result"); + assertThat(resultNum.doubleValue()).isCloseTo(3.0, within(0.0001)); + } + + @Test + void log10ZeroReturnsNegativeInfinity() { + final ResultSet result = database.command("opencypher", "RETURN log10(0.0) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final Double value = (Double) result.next().getProperty("result"); + assertThat(value).isEqualTo(Double.NEGATIVE_INFINITY); + } + + @Test + void log10NegativeReturnsNaN() { + final ResultSet result = database.command("opencypher", "RETURN log10(-1.0) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Number resultNum = (Number) result.next().getProperty("result"); + assertThat(resultNum.doubleValue()).isNaN(); + } + + @Test + void log10Null() { + final ResultSet result = database.command("opencypher", "RETURN log10(null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + // ==================== sqrt() Tests ==================== + + @Test + void sqrtZero() { + final ResultSet result = database.command("opencypher", "RETURN sqrt(0.0) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Number resultNum = (Number) result.next().getProperty("result"); + assertThat(resultNum.doubleValue()).isCloseTo(0.0, within(0.0001)); + } + + @Test + void sqrtOne() { + final ResultSet result = database.command("opencypher", "RETURN sqrt(1.0) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Number resultNum = (Number) result.next().getProperty("result"); + assertThat(resultNum.doubleValue()).isCloseTo(1.0, within(0.0001)); + } + + @Test + void sqrtFour() { + final ResultSet result = database.command("opencypher", "RETURN sqrt(4.0) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Number resultNum = (Number) result.next().getProperty("result"); + assertThat(resultNum.doubleValue()).isCloseTo(2.0, within(0.0001)); + } + + @Test + void sqrtNine() { + final ResultSet result = database.command("opencypher", "RETURN sqrt(9.0) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Number resultNum = (Number) result.next().getProperty("result"); + assertThat(resultNum.doubleValue()).isCloseTo(3.0, within(0.0001)); + } + + @Test + void sqrtTwo() { + final ResultSet result = database.command("opencypher", "RETURN sqrt(2.0) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Number resultNum = (Number) result.next().getProperty("result"); + assertThat(resultNum.doubleValue()).isCloseTo(Math.sqrt(2.0), within(0.0001)); + } + + @Test + void sqrtNegativeReturnsNaN() { + final ResultSet result = database.command("opencypher", "RETURN sqrt(-1.0) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Number resultNum = (Number) result.next().getProperty("result"); + assertThat(resultNum.doubleValue()).isNaN(); + } + + @Test + void sqrtNull() { + final ResultSet result = database.command("opencypher", "RETURN sqrt(null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + // ==================== Combined/Integration Tests ==================== + + @Test + void logExpIdentity() { + // log(exp(x)) = x + final ResultSet result = database.command("opencypher", + "RETURN log(exp(2.0)) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Number resultNum = (Number) result.next().getProperty("result"); + assertThat(resultNum.doubleValue()).isCloseTo(2.0, within(0.0001)); + } + + @Test + void expLogIdentity() { + // exp(log(x)) = x + final ResultSet result = database.command("opencypher", + "RETURN exp(log(5.0)) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Number resultNum = (Number) result.next().getProperty("result"); + assertThat(resultNum.doubleValue()).isCloseTo(5.0, within(0.0001)); + } + + @Test + void sqrtSquareIdentity() { + // sqrt(x²) = x + final ResultSet result = database.command("opencypher", + "WITH 7.0 AS x RETURN sqrt(x * x) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Number resultNum = (Number) result.next().getProperty("result"); + assertThat(resultNum.doubleValue()).isCloseTo(7.0, within(0.0001)); + } + + @Test + void log10ConversionFromLog() { + // log10(x) = log(x) / log(10) + final ResultSet result = database.command("opencypher", + "WITH 100.0 AS x RETURN log10(x) AS log10_val, log(x) / log(10.0) AS log_ratio"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final var row = result.next(); + final Number log10Val = (Number) row.getProperty("log10_val"); + final Number logRatio = (Number) row.getProperty("log_ratio"); + assertThat(log10Val.doubleValue()).isCloseTo(logRatio.doubleValue(), within(0.0001)); + } + + @Test + void sqrtExp() { + // sqrt(x) = exp(log(x) / 2) + final ResultSet result = database.command("opencypher", + "WITH 16.0 AS x RETURN sqrt(x) AS sqrt_val, exp(log(x) / 2.0) AS exp_val"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final var row = result.next(); + final Number sqrtVal = (Number) row.getProperty("sqrt_val"); + final Number expVal = (Number) row.getProperty("exp_val"); + assertThat(sqrtVal.doubleValue()).isCloseTo(expVal.doubleValue(), within(0.0001)); + } + + @Test + void exponentialGrowth() { + // Test exponential growth formula + final ResultSet result = database.command("opencypher", + "WITH 100.0 AS initial, 0.05 AS rate, 10.0 AS time " + + "RETURN initial * exp(rate * time) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final Number growth = (Number) result.next().getProperty("result"); + assertThat(growth.doubleValue()).isGreaterThan(100.0); + assertThat(growth.doubleValue()).isCloseTo(164.87, within(0.1)); + } +} diff --git a/engine/src/test/java/com/arcadedb/query/opencypher/functions/OpenCypherMathNumericFunctionsComprehensiveTest.java b/engine/src/test/java/com/arcadedb/query/opencypher/functions/OpenCypherMathNumericFunctionsComprehensiveTest.java new file mode 100644 index 0000000000..ac2b670843 --- /dev/null +++ b/engine/src/test/java/com/arcadedb/query/opencypher/functions/OpenCypherMathNumericFunctionsComprehensiveTest.java @@ -0,0 +1,416 @@ +/* + * Copyright 2021-present Arcade Data Ltd (info@arcadedata.com) + * + * 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 (info@arcadedata.com) + * SPDX-License-Identifier: Apache-2.0 + */ +package com.arcadedb.query.opencypher.functions; + +import com.arcadedb.database.Database; +import com.arcadedb.database.DatabaseFactory; +import com.arcadedb.query.sql.executor.ResultSet; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import org.assertj.core.api.Assertions; +import static org.assertj.core.api.Assertions.within; + +/** + * Comprehensive tests for OpenCypher Mathematical Numeric functions based on Neo4j Cypher documentation. + * Tests cover: abs(), ceil(), floor(), isNaN(), rand(), round(), sign() + */ +class OpenCypherMathNumericFunctionsComprehensiveTest { + private Database database; + + @BeforeEach + void setUp() { + final DatabaseFactory factory = new DatabaseFactory("./databases/test-cypher-math-numeric"); + if (factory.exists()) + factory.open().drop(); + database = factory.create(); + } + + @AfterEach + void tearDown() { + if (database != null) + database.drop(); + } + + // ==================== abs() Tests ==================== + + @Test + void absPositiveInteger() { + final ResultSet result = database.command("opencypher", "RETURN abs(5) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat(((Number) result.next().getProperty("result")).intValue()).isEqualTo(5); + } + + @Test + void absNegativeInteger() { + final ResultSet result = database.command("opencypher", "RETURN abs(-5) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat(((Number) result.next().getProperty("result")).intValue()).isEqualTo(5); + } + + @Test + void absPositiveFloat() { + final ResultSet result = database.command("opencypher", "RETURN abs(3.14) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat(((Number) result.next().getProperty("result")).doubleValue()).isCloseTo(3.14, within(0.001)); + } + + @Test + void absNegativeFloat() { + final ResultSet result = database.command("opencypher", "RETURN abs(-3.14) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat(((Number) result.next().getProperty("result")).doubleValue()).isCloseTo(3.14, within(0.001)); + } + + @Test + void absZero() { + final ResultSet result = database.command("opencypher", "RETURN abs(0) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat(((Number) result.next().getProperty("result")).intValue()).isEqualTo(0); + } + + @Test + void absNull() { + final ResultSet result = database.command("opencypher", "RETURN abs(null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + // ==================== ceil() Tests ==================== + + @Test + void ceilPositive() { + final ResultSet result = database.command("opencypher", "RETURN ceil(3.14) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat(((Number) result.next().getProperty("result")).doubleValue()).isCloseTo(4.0, within(0.001)); + } + + @Test + void ceilNegative() { + final ResultSet result = database.command("opencypher", "RETURN ceil(-3.14) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat(((Number) result.next().getProperty("result")).doubleValue()).isCloseTo(-3.0, within(0.001)); + } + + @Test + void ceilWholeNumber() { + final ResultSet result = database.command("opencypher", "RETURN ceil(5.0) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat(((Number) result.next().getProperty("result")).doubleValue()).isCloseTo(5.0, within(0.001)); + } + + @Test + void ceilZero() { + final ResultSet result = database.command("opencypher", "RETURN ceil(0.0) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat(((Number) result.next().getProperty("result")).doubleValue()).isCloseTo(0.0, within(0.001)); + } + + @Test + void ceilNull() { + final ResultSet result = database.command("opencypher", "RETURN ceil(null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + // ==================== ceiling() Tests (alias of ceil()) ==================== + + @Test + void ceilingPositive() { + final ResultSet result = database.command("opencypher", "RETURN ceiling(3.14) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat(((Number) result.next().getProperty("result")).doubleValue()).isCloseTo(4.0, within(0.001)); + } + + @Test + void ceilingNegative() { + final ResultSet result = database.command("opencypher", "RETURN ceiling(-3.14) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat(((Number) result.next().getProperty("result")).doubleValue()).isCloseTo(-3.0, within(0.001)); + } + + @Test + void ceilingWholeNumber() { + final ResultSet result = database.command("opencypher", "RETURN ceiling(5.0) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat(((Number) result.next().getProperty("result")).doubleValue()).isCloseTo(5.0, within(0.001)); + } + + @Test + void ceilingZero() { + final ResultSet result = database.command("opencypher", "RETURN ceiling(0.0) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat(((Number) result.next().getProperty("result")).doubleValue()).isCloseTo(0.0, within(0.001)); + } + + @Test + void ceilingNull() { + final ResultSet result = database.command("opencypher", "RETURN ceiling(null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + // ==================== floor() Tests ==================== + + @Test + void floorPositive() { + final ResultSet result = database.command("opencypher", "RETURN floor(3.14) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat(((Number) result.next().getProperty("result")).doubleValue()).isCloseTo(3.0, within(0.001)); + } + + @Test + void floorNegative() { + final ResultSet result = database.command("opencypher", "RETURN floor(-3.14) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat(((Number) result.next().getProperty("result")).doubleValue()).isCloseTo(-4.0, within(0.001)); + } + + @Test + void floorWholeNumber() { + final ResultSet result = database.command("opencypher", "RETURN floor(5.0) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat(((Number) result.next().getProperty("result")).doubleValue()).isCloseTo(5.0, within(0.001)); + } + + @Test + void floorZero() { + final ResultSet result = database.command("opencypher", "RETURN floor(0.0) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat(((Number) result.next().getProperty("result")).doubleValue()).isCloseTo(0.0, within(0.001)); + } + + @Test + void floorNull() { + final ResultSet result = database.command("opencypher", "RETURN floor(null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + // ==================== isNaN() Tests ==================== + + @Test + void isNaNWithNaN() { + final ResultSet result = database.command("opencypher", "RETURN isNaN(0.0 / 0.0) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat((Boolean) result.next().getProperty("result")).isTrue(); + } + + @Test + void isNaNWithNumber() { + final ResultSet result = database.command("opencypher", "RETURN isNaN(5.0) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat((Boolean) result.next().getProperty("result")).isFalse(); + } + + @Test + void isNaNWithInfinity() { + final ResultSet result = database.command("opencypher", "RETURN isNaN(1.0 / 0.0) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat((Boolean) result.next().getProperty("result")).isFalse(); + } + + @Test + void isNaNWithInteger() { + final ResultSet result = database.command("opencypher", "RETURN isNaN(42) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat((Boolean) result.next().getProperty("result")).isFalse(); + } + + @Test + void isNaNNull() { + final ResultSet result = database.command("opencypher", "RETURN isNaN(null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + // ==================== rand() Tests ==================== + + @Test + void randBasic() { + final ResultSet result = database.command("opencypher", "RETURN rand() AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final Double value = (Double) result.next().getProperty("result"); + assertThat(value).isGreaterThanOrEqualTo(0.0); + assertThat(value).isLessThan(1.0); + } + + @Test + void randMultipleCalls() { + final ResultSet result = database.command("opencypher", + "RETURN rand() AS r1, rand() AS r2, rand() AS r3"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final var row = result.next(); + final Double r1 = (Double) row.getProperty("r1"); + final Double r2 = (Double) row.getProperty("r2"); + final Double r3 = (Double) row.getProperty("r3"); + // All should be in valid range + assertThat(r1).isGreaterThanOrEqualTo(0.0).isLessThan(1.0); + assertThat(r2).isGreaterThanOrEqualTo(0.0).isLessThan(1.0); + assertThat(r3).isGreaterThanOrEqualTo(0.0).isLessThan(1.0); + } + + // ==================== round() Tests ==================== + + @Test + void roundBasic() { + final ResultSet result = database.command("opencypher", "RETURN round(3.14) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat(((Number) result.next().getProperty("result")).doubleValue()).isCloseTo(3.0, within(0.001)); + } + + @Test + void roundHalfUp() { + final ResultSet result = database.command("opencypher", "RETURN round(3.5) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat(((Number) result.next().getProperty("result")).doubleValue()).isCloseTo(4.0, within(0.001)); + } + + @Test + void roundWithPrecision() { + final ResultSet result = database.command("opencypher", "RETURN round(3.14159, 2) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat(((Number) result.next().getProperty("result")).doubleValue()).isCloseTo(3.14, within(0.001)); + } + + @Test + void roundWithPrecisionAndMode() { + // Test different rounding modes + ResultSet result = database.command("opencypher", "RETURN round(3.145, 2, 'UP') AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat(((Number) result.next().getProperty("result")).doubleValue()).isCloseTo(3.15, within(0.001)); + + result = database.command("opencypher", "RETURN round(3.145, 2, 'DOWN') AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat(((Number) result.next().getProperty("result")).doubleValue()).isCloseTo(3.14, within(0.001)); + + result = database.command("opencypher", "RETURN round(3.145, 2, 'CEILING') AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat(((Number) result.next().getProperty("result")).doubleValue()).isCloseTo(3.15, within(0.001)); + + result = database.command("opencypher", "RETURN round(3.145, 2, 'FLOOR') AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat(((Number) result.next().getProperty("result")).doubleValue()).isCloseTo(3.14, within(0.001)); + } + + @Test + void roundWithHalfEvenMode() { + // HALF_EVEN mode (banker's rounding) + ResultSet result = database.command("opencypher", "RETURN round(2.5, 0, 'HALF_EVEN') AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat(((Number) result.next().getProperty("result")).doubleValue()).isCloseTo(2.0, within(0.001)); + + result = database.command("opencypher", "RETURN round(3.5, 0, 'HALF_EVEN') AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat(((Number) result.next().getProperty("result")).doubleValue()).isCloseTo(4.0, within(0.001)); + } + + @Test + void roundNegativeNumber() { + final ResultSet result = database.command("opencypher", "RETURN round(-3.14) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat(((Number) result.next().getProperty("result")).doubleValue()).isCloseTo(-3.0, within(0.001)); + } + + @Test + void roundZero() { + final ResultSet result = database.command("opencypher", "RETURN round(0.0) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat(((Number) result.next().getProperty("result")).doubleValue()).isCloseTo(0.0, within(0.001)); + } + + @Test + void roundNull() { + final ResultSet result = database.command("opencypher", "RETURN round(null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + // ==================== sign() Tests ==================== + + @Test + void signPositive() { + final ResultSet result = database.command("opencypher", "RETURN sign(42) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat(((Number) result.next().getProperty("result")).intValue()).isEqualTo(1); + } + + @Test + void signNegative() { + final ResultSet result = database.command("opencypher", "RETURN sign(-42) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat(((Number) result.next().getProperty("result")).intValue()).isEqualTo(-1); + } + + @Test + void signZero() { + final ResultSet result = database.command("opencypher", "RETURN sign(0) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat(((Number) result.next().getProperty("result")).intValue()).isEqualTo(0); + } + + @Test + void signPositiveFloat() { + final ResultSet result = database.command("opencypher", "RETURN sign(3.14) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat(((Number) result.next().getProperty("result")).intValue()).isEqualTo(1); + } + + @Test + void signNegativeFloat() { + final ResultSet result = database.command("opencypher", "RETURN sign(-3.14) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat(((Number) result.next().getProperty("result")).intValue()).isEqualTo(-1); + } + + @Test + void signNull() { + final ResultSet result = database.command("opencypher", "RETURN sign(null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + // ==================== Combined/Integration Tests ==================== + + @Test + void mathFunctionsCombined() { + final ResultSet result = database.command("opencypher", + "RETURN abs(ceil(-3.14)) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat(((Number) result.next().getProperty("result")).doubleValue()).isCloseTo(3.0, within(0.001)); + } + + @Test + void mathFunctionsChaining() { + final ResultSet result = database.command("opencypher", + "RETURN sign(floor(-3.7)) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat(((Number) result.next().getProperty("result")).intValue()).isEqualTo(-1); + } + + @Test + void mathFunctionsWithRound() { + final ResultSet result = database.command("opencypher", + "RETURN abs(round(-3.14159, 2)) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat(((Number) result.next().getProperty("result")).doubleValue()).isCloseTo(3.14, within(0.001)); + } +} diff --git a/engine/src/test/java/com/arcadedb/query/opencypher/functions/OpenCypherMathTrigonometricFunctionsComprehensiveTest.java b/engine/src/test/java/com/arcadedb/query/opencypher/functions/OpenCypherMathTrigonometricFunctionsComprehensiveTest.java new file mode 100644 index 0000000000..73f816f46e --- /dev/null +++ b/engine/src/test/java/com/arcadedb/query/opencypher/functions/OpenCypherMathTrigonometricFunctionsComprehensiveTest.java @@ -0,0 +1,596 @@ +/* + * Copyright 2021-present Arcade Data Ltd (info@arcadedata.com) + * + * 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 (info@arcadedata.com) + * SPDX-License-Identifier: Apache-2.0 + */ +package com.arcadedb.query.opencypher.functions; + +import com.arcadedb.database.Database; +import com.arcadedb.database.DatabaseFactory; +import com.arcadedb.query.sql.executor.ResultSet; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import org.assertj.core.api.Assertions; +import static org.assertj.core.api.Assertions.within; + +/** + * Comprehensive tests for OpenCypher Mathematical Trigonometric functions based on Neo4j Cypher documentation. + * Tests cover: acos(), asin(), atan(), atan2(), cos(), cosh(), cot(), coth(), degrees(), haversin(), pi(), radians(), sin(), sinh(), tan(), tanh() + */ +class OpenCypherMathTrigonometricFunctionsComprehensiveTest { + private Database database; + + @BeforeEach + void setUp() { + final DatabaseFactory factory = new DatabaseFactory("./databases/test-cypher-math-trig"); + if (factory.exists()) + factory.open().drop(); + database = factory.create(); + } + + @AfterEach + void tearDown() { + if (database != null) + database.drop(); + } + + // ==================== acos() Tests ==================== + + @Test + void acosBasic() { + final ResultSet result = database.command("opencypher", "RETURN acos(1.0) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Number numResult = (Number) result.next().getProperty("result"); + assertThat(numResult.doubleValue()).isCloseTo(0.0, within(0.0001)); + } + + @Test + void acosZero() { + final ResultSet result = database.command("opencypher", "RETURN acos(0.0) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Number numResult = (Number) result.next().getProperty("result"); + assertThat(numResult.doubleValue()).isCloseTo(Math.PI / 2, within(0.0001)); + } + + @Test + void acosNegativeOne() { + final ResultSet result = database.command("opencypher", "RETURN acos(-1.0) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Number numResult = (Number) result.next().getProperty("result"); + assertThat(numResult.doubleValue()).isCloseTo(Math.PI, within(0.0001)); + } + + @Test + void acosOutOfRangeReturnsNaN() { + ResultSet result = database.command("opencypher", "RETURN acos(2.0) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat((Double) result.next().getProperty("result")).isNaN(); + + result = database.command("opencypher", "RETURN acos(-2.0) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat((Double) result.next().getProperty("result")).isNaN(); + } + + @Test + void acosNull() { + final ResultSet result = database.command("opencypher", "RETURN acos(null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + // ==================== asin() Tests ==================== + + @Test + void asinBasic() { + final ResultSet result = database.command("opencypher", "RETURN asin(1.0) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Number numResult = (Number) result.next().getProperty("result"); + assertThat(numResult.doubleValue()).isCloseTo(Math.PI / 2, within(0.0001)); + } + + @Test + void asinZero() { + final ResultSet result = database.command("opencypher", "RETURN asin(0.0) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Number numResult = (Number) result.next().getProperty("result"); + assertThat(numResult.doubleValue()).isCloseTo(0.0, within(0.0001)); + } + + @Test + void asinNegativeOne() { + final ResultSet result = database.command("opencypher", "RETURN asin(-1.0) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Number numResult = (Number) result.next().getProperty("result"); + assertThat(numResult.doubleValue()).isCloseTo(-Math.PI / 2, within(0.0001)); + } + + @Test + void asinOutOfRangeReturnsNaN() { + ResultSet result = database.command("opencypher", "RETURN asin(2.0) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat((Double) result.next().getProperty("result")).isNaN(); + + result = database.command("opencypher", "RETURN asin(-2.0) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat((Double) result.next().getProperty("result")).isNaN(); + } + + @Test + void asinNull() { + final ResultSet result = database.command("opencypher", "RETURN asin(null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + // ==================== atan() Tests ==================== + + @Test + void atanBasic() { + final ResultSet result = database.command("opencypher", "RETURN atan(1.0) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Number numResult = (Number) result.next().getProperty("result"); + assertThat(numResult.doubleValue()).isCloseTo(Math.PI / 4, within(0.0001)); + } + + @Test + void atanZero() { + final ResultSet result = database.command("opencypher", "RETURN atan(0.0) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Number numResult = (Number) result.next().getProperty("result"); + assertThat(numResult.doubleValue()).isCloseTo(0.0, within(0.0001)); + } + + @Test + void atanNegative() { + final ResultSet result = database.command("opencypher", "RETURN atan(-1.0) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Number numResult = (Number) result.next().getProperty("result"); + assertThat(numResult.doubleValue()).isCloseTo(-Math.PI / 4, within(0.0001)); + } + + @Test + void atanNull() { + final ResultSet result = database.command("opencypher", "RETURN atan(null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + // ==================== atan2() Tests ==================== + + @Test + void atan2Basic() { + final ResultSet result = database.command("opencypher", "RETURN atan2(1.0, 1.0) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Number numResult = (Number) result.next().getProperty("result"); + assertThat(numResult.doubleValue()).isCloseTo(Math.PI / 4, within(0.0001)); + } + + @Test + void atan2Quadrants() { + ResultSet result = database.command("opencypher", "RETURN atan2(1.0, 1.0) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Number numResult = (Number) result.next().getProperty("result"); + assertThat(numResult.doubleValue()).isCloseTo(Math.PI / 4, within(0.0001)); + + result = database.command("opencypher", "RETURN atan2(1.0, -1.0) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + numResult = (Number) result.next().getProperty("result"); + assertThat(numResult.doubleValue()).isCloseTo(3 * Math.PI / 4, within(0.0001)); + + result = database.command("opencypher", "RETURN atan2(-1.0, -1.0) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + numResult = (Number) result.next().getProperty("result"); + assertThat(numResult.doubleValue()).isCloseTo(-3 * Math.PI / 4, within(0.0001)); + + result = database.command("opencypher", "RETURN atan2(-1.0, 1.0) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + numResult = (Number) result.next().getProperty("result"); + assertThat(numResult.doubleValue()).isCloseTo(-Math.PI / 4, within(0.0001)); + } + + @Test + void atan2Null() { + ResultSet result = database.command("opencypher", "RETURN atan2(null, 1.0) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + + result = database.command("opencypher", "RETURN atan2(1.0, null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + // ==================== cos() Tests ==================== + + @Test + void cosBasic() { + final ResultSet result = database.command("opencypher", "RETURN cos(0.0) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Number numResult = (Number) result.next().getProperty("result"); + assertThat(numResult.doubleValue()).isCloseTo(1.0, within(0.0001)); + } + + @Test + void cosPi() { + final ResultSet result = database.command("opencypher", "RETURN cos(pi()) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Number numResult = (Number) result.next().getProperty("result"); + assertThat(numResult.doubleValue()).isCloseTo(-1.0, within(0.0001)); + } + + @Test + void cosPiOver2() { + final ResultSet result = database.command("opencypher", "RETURN cos(pi() / 2) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Number numResult = (Number) result.next().getProperty("result"); + assertThat(numResult.doubleValue()).isCloseTo(0.0, within(0.0001)); + } + + @Test + void cosNull() { + final ResultSet result = database.command("opencypher", "RETURN cos(null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + // ==================== cosh() Tests ==================== + + @Test + void coshZero() { + final ResultSet result = database.command("opencypher", "RETURN cosh(0.0) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Number numResult = (Number) result.next().getProperty("result"); + assertThat(numResult.doubleValue()).isCloseTo(1.0, within(0.0001)); + } + + @Test + void coshSymmetry() { + final ResultSet result = database.command("opencypher", "RETURN cosh(2.0) AS x, cosh(-2.0) AS y"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final var row = result.next(); + final Double x = (Double) row.getProperty("x"); + final Double y = (Double) row.getProperty("y"); + assertThat(x).isCloseTo(y, within(0.0001)); + } + + @Test + void coshNull() { + final ResultSet result = database.command("opencypher", "RETURN cosh(null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + // ==================== cot() Tests ==================== + + @Test + void cotBasic() { + final ResultSet result = database.command("opencypher", "RETURN cot(pi() / 4) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Number numResult = (Number) result.next().getProperty("result"); + assertThat(numResult.doubleValue()).isCloseTo(1.0, within(0.001)); + } + + @Test + void cotZeroReturnsInfinity() { + final ResultSet result = database.command("opencypher", "RETURN cot(0.0) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final Double value = (Double) result.next().getProperty("result"); + assertThat(value.isInfinite()).isTrue(); + } + + @Test + void cotNull() { + final ResultSet result = database.command("opencypher", "RETURN cot(null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + // ==================== coth() Tests ==================== + + @Test + void cothBasic() { + final ResultSet result = database.command("opencypher", "RETURN coth(1.0) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat((Double) result.next().getProperty("result")).isNotNull(); + } + + @Test + void cothZeroReturnsInfinity() { + // coth(0) = cosh(0)/sinh(0) = 1/0 = +Infinity (mathematically correct) + final ResultSet result = database.command("opencypher", "RETURN coth(0.0) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat((Double) result.next().getProperty("result")).isInfinite(); + } + + @Test + void cothNull() { + final ResultSet result = database.command("opencypher", "RETURN coth(null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + // ==================== degrees() Tests ==================== + + @Test + void degreesFromPi() { + final ResultSet result = database.command("opencypher", "RETURN degrees(pi()) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Number numResult = (Number) result.next().getProperty("result"); + assertThat(numResult.doubleValue()).isCloseTo(180.0, within(0.0001)); + } + + @Test + void degreesFromZero() { + final ResultSet result = database.command("opencypher", "RETURN degrees(0.0) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Number numResult = (Number) result.next().getProperty("result"); + assertThat(numResult.doubleValue()).isCloseTo(0.0, within(0.0001)); + } + + @Test + void degreesFrom2Pi() { + final ResultSet result = database.command("opencypher", "RETURN degrees(2 * pi()) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Number numResult = (Number) result.next().getProperty("result"); + assertThat(numResult.doubleValue()).isCloseTo(360.0, within(0.001)); + } + + @Test + void degreesNull() { + final ResultSet result = database.command("opencypher", "RETURN degrees(null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + // ==================== haversin() Tests ==================== + + @Test + void haversinZero() { + final ResultSet result = database.command("opencypher", "RETURN haversin(0.0) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Number numResult = (Number) result.next().getProperty("result"); + assertThat(numResult.doubleValue()).isCloseTo(0.0, within(0.0001)); + } + + @Test + void haversinPi() { + final ResultSet result = database.command("opencypher", "RETURN haversin(pi()) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Number numResult = (Number) result.next().getProperty("result"); + assertThat(numResult.doubleValue()).isCloseTo(1.0, within(0.0001)); + } + + @Test + void haversinNull() { + final ResultSet result = database.command("opencypher", "RETURN haversin(null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + // ==================== pi() Tests ==================== + + @Test + void piBasic() { + final ResultSet result = database.command("opencypher", "RETURN pi() AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Number numResult = (Number) result.next().getProperty("result"); + assertThat(numResult.doubleValue()).isCloseTo(Math.PI, within(0.0000001)); + } + + @Test + void piConstant() { + final ResultSet result = database.command("opencypher", "RETURN pi() AS p1, pi() AS p2"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final var row = result.next(); + assertThat((Double) row.getProperty("p1")).isEqualTo((Double) row.getProperty("p2")); + } + + // ==================== radians() Tests ==================== + + @Test + void radiansFrom180() { + final ResultSet result = database.command("opencypher", "RETURN radians(180.0) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Number numResult = (Number) result.next().getProperty("result"); + assertThat(numResult.doubleValue()).isCloseTo(Math.PI, within(0.0001)); + } + + @Test + void radiansFromZero() { + final ResultSet result = database.command("opencypher", "RETURN radians(0.0) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Number numResult = (Number) result.next().getProperty("result"); + assertThat(numResult.doubleValue()).isCloseTo(0.0, within(0.0001)); + } + + @Test + void radiansFrom360() { + final ResultSet result = database.command("opencypher", "RETURN radians(360.0) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Number numResult = (Number) result.next().getProperty("result"); + assertThat(numResult.doubleValue()).isCloseTo(2 * Math.PI, within(0.001)); + } + + @Test + void radiansNull() { + final ResultSet result = database.command("opencypher", "RETURN radians(null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + // ==================== sin() Tests ==================== + + @Test + void sinZero() { + final ResultSet result = database.command("opencypher", "RETURN sin(0.0) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Number numResult = (Number) result.next().getProperty("result"); + assertThat(numResult.doubleValue()).isCloseTo(0.0, within(0.0001)); + } + + @Test + void sinPiOver2() { + final ResultSet result = database.command("opencypher", "RETURN sin(pi() / 2) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Number numResult = (Number) result.next().getProperty("result"); + assertThat(numResult.doubleValue()).isCloseTo(1.0, within(0.0001)); + } + + @Test + void sinPi() { + final ResultSet result = database.command("opencypher", "RETURN sin(pi()) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Number numResult = (Number) result.next().getProperty("result"); + assertThat(numResult.doubleValue()).isCloseTo(0.0, within(0.0001)); + } + + @Test + void sinNull() { + final ResultSet result = database.command("opencypher", "RETURN sin(null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + // ==================== sinh() Tests ==================== + + @Test + void sinhZero() { + final ResultSet result = database.command("opencypher", "RETURN sinh(0.0) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Number numResult = (Number) result.next().getProperty("result"); + assertThat(numResult.doubleValue()).isCloseTo(0.0, within(0.0001)); + } + + @Test + void sinhAntisymmetry() { + final ResultSet result = database.command("opencypher", "RETURN sinh(2.0) AS x, sinh(-2.0) AS y"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final var row = result.next(); + final Double x = (Double) row.getProperty("x"); + final Double y = (Double) row.getProperty("y"); + assertThat(x).isCloseTo(-y, within(0.0001)); + } + + @Test + void sinhNull() { + final ResultSet result = database.command("opencypher", "RETURN sinh(null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + // ==================== tan() Tests ==================== + + @Test + void tanZero() { + final ResultSet result = database.command("opencypher", "RETURN tan(0.0) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Number numResult = (Number) result.next().getProperty("result"); + assertThat(numResult.doubleValue()).isCloseTo(0.0, within(0.0001)); + } + + @Test + void tanPiOver4() { + final ResultSet result = database.command("opencypher", "RETURN tan(pi() / 4) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Number numResult = (Number) result.next().getProperty("result"); + assertThat(numResult.doubleValue()).isCloseTo(1.0, within(0.001)); + } + + @Test + void tanNull() { + final ResultSet result = database.command("opencypher", "RETURN tan(null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + // ==================== tanh() Tests ==================== + + @Test + void tanhZero() { + final ResultSet result = database.command("opencypher", "RETURN tanh(0.0) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Number numResult = (Number) result.next().getProperty("result"); + assertThat(numResult.doubleValue()).isCloseTo(0.0, within(0.0001)); + } + + @Test + void tanhLargePositive() { + final ResultSet result = database.command("opencypher", "RETURN tanh(10.0) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Number numResult = (Number) result.next().getProperty("result"); + assertThat(numResult.doubleValue()).isCloseTo(1.0, within(0.001)); + } + + @Test + void tanhLargeNegative() { + final ResultSet result = database.command("opencypher", "RETURN tanh(-10.0) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Number numResult = (Number) result.next().getProperty("result"); + assertThat(numResult.doubleValue()).isCloseTo(-1.0, within(0.001)); + } + + @Test + void tanhNull() { + final ResultSet result = database.command("opencypher", "RETURN tanh(null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + // ==================== Combined/Integration Tests ==================== + + @Test + void trigIdentitySinCos() { + // sin²(x) + cos²(x) = 1 + final ResultSet result = database.command("opencypher", + "WITH pi() / 3 AS x RETURN sin(x) * sin(x) + cos(x) * cos(x) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Number numResult = (Number) result.next().getProperty("result"); + assertThat(numResult.doubleValue()).isCloseTo(1.0, within(0.0001)); + } + + @Test + void degreesRadiansRoundtrip() { + final ResultSet result = database.command("opencypher", + "RETURN degrees(radians(90.0)) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Number numResult = (Number) result.next().getProperty("result"); + assertThat(numResult.doubleValue()).isCloseTo(90.0, within(0.001)); + } + + @Test + void hyperbolicIdentity() { + // cosh²(x) - sinh²(x) = 1 + final ResultSet result = database.command("opencypher", + "WITH 1.5 AS x RETURN cosh(x) * cosh(x) - sinh(x) * sinh(x) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Number numResult = (Number) result.next().getProperty("result"); + assertThat(numResult.doubleValue()).isCloseTo(1.0, within(0.0001)); + } + + @Test + void tanhIdentity() { + // tanh(x) = sinh(x) / cosh(x) + final ResultSet result = database.command("opencypher", + "WITH 1.5 AS x RETURN tanh(x) AS tanh_val, sinh(x) / cosh(x) AS ratio"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final var row = result.next(); + final Double tanhVal = (Double) row.getProperty("tanh_val"); + final Double ratio = (Double) row.getProperty("ratio"); + assertThat(tanhVal).isCloseTo(ratio, within(0.0001)); + } +} diff --git a/engine/src/test/java/com/arcadedb/query/opencypher/functions/OpenCypherPredicateFunctionsComprehensiveTest.java b/engine/src/test/java/com/arcadedb/query/opencypher/functions/OpenCypherPredicateFunctionsComprehensiveTest.java new file mode 100644 index 0000000000..72119d9d02 --- /dev/null +++ b/engine/src/test/java/com/arcadedb/query/opencypher/functions/OpenCypherPredicateFunctionsComprehensiveTest.java @@ -0,0 +1,400 @@ +/* + * Copyright 2021-present Arcade Data Ltd (info@arcadedata.com) + * + * 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 (info@arcadedata.com) + * SPDX-License-Identifier: Apache-2.0 + */ +package com.arcadedb.query.opencypher.functions; + +import com.arcadedb.database.Database; +import com.arcadedb.database.DatabaseFactory; +import com.arcadedb.query.sql.executor.ResultSet; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import org.assertj.core.api.Assertions; + +/** + * Comprehensive tests for OpenCypher Predicate functions based on Neo4j Cypher documentation. + * Tests cover: all(), allReduce(), any(), exists(), isEmpty(), none(), single() + */ +class OpenCypherPredicateFunctionsComprehensiveTest { + private Database database; + + @BeforeEach + void setUp() { + final DatabaseFactory factory = new DatabaseFactory("./databases/test-cypher-predicate-functions"); + if (factory.exists()) + factory.open().drop(); + database = factory.create(); + + // Create test graph matching Neo4j documentation + database.getSchema().createVertexType("Person"); + database.getSchema().createVertexType("Movie"); + database.getSchema().createEdgeType("KNOWS"); + database.getSchema().createEdgeType("ACTED_IN"); + + database.command("opencypher", + "CREATE " + + "(keanu:Person {name:'Keanu Reeves', age:58, nationality:'Canadian'}), " + + "(carrie:Person {name:'Carrie Anne Moss', age:55, nationality:'American'}), " + + "(liam:Person {name:'Liam Neeson', age:70, nationality:'Northern Irish'}), " + + "(guy:Person {name:'Guy Pearce', age:55, nationality:'Australian'}), " + + "(kathryn:Person {name:'Kathryn Bigelow', age:71, nationality:'American'}), " + + "(jessica:Person {name:'Jessica Chastain', age:45, address:''}), " + + "(theMatrix:Movie {title:'The Matrix'}), " + + "(keanu)-[:KNOWS {since: 1999}]->(carrie), " + + "(keanu)-[:KNOWS {since: 2005}]->(liam), " + + "(keanu)-[:KNOWS {since: 2010}]->(kathryn), " + + "(kathryn)-[:KNOWS {since: 2012}]->(jessica), " + + "(carrie)-[:KNOWS {since: 2008}]->(guy), " + + "(liam)-[:KNOWS {since: 2009}]->(guy), " + + "(keanu)-[:ACTED_IN]->(theMatrix), " + + "(carrie)-[:ACTED_IN]->(theMatrix)"); + } + + @AfterEach + void tearDown() { + if (database != null) + database.drop(); + } + + // ==================== all() Tests ==================== + + @Test + void allBasic() { + final ResultSet result = database.command("opencypher", + "RETURN all(x IN [1, 2, 3, 4, 5] WHERE x > 0) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat((Boolean) result.next().getProperty("result")).isTrue(); + } + + @Test + void allFalse() { + final ResultSet result = database.command("opencypher", + "RETURN all(x IN [1, 2, 3, 4, 5] WHERE x > 3) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat((Boolean) result.next().getProperty("result")).isFalse(); + } + + @Test + void allEmptyList() { + final ResultSet result = database.command("opencypher", + "WITH [] as emptyList RETURN all(i IN emptyList WHERE true) as result1, all(i IN emptyList WHERE false) as result2"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final var row = result.next(); + assertThat((Boolean) row.getProperty("result1")).isTrue(); + assertThat((Boolean) row.getProperty("result2")).isTrue(); + } + + @Test + void allWithPath() { + final ResultSet result = database.command("opencypher", + "MATCH p = (a:Person {name: 'Keanu Reeves'})-[]-{2}() " + + "WHERE all(x IN nodes(p) WHERE x.age < 60) " + + "RETURN count(p) AS pathCount"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat(((Number) result.next().getProperty("pathCount")).intValue()).isGreaterThan(0); + } + + @Test + void allWithNullList() { + final ResultSet result = database.command("opencypher", + "RETURN all(x IN null WHERE x > 0) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + // ==================== allReduce() Tests ==================== + + @Test + void allReduceBasic() { + final ResultSet result = database.command("opencypher", + "RETURN allReduce(sum = 0, x IN [1, 2, 3] | sum + x, sum < 10) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat((Boolean) result.next().getProperty("result")).isTrue(); + } + + @Test + void allReduceFalse() { + final ResultSet result = database.command("opencypher", + "RETURN allReduce(sum = 0, x IN [1, 2, 3, 10] | sum + x, sum < 10) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat((Boolean) result.next().getProperty("result")).isFalse(); + } + + @Test + void allReduceEmptyList() { + final ResultSet result = database.command("opencypher", + "RETURN allReduce(sum = 0, x IN [] | sum + x, sum < 10) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat((Boolean) result.next().getProperty("result")).isTrue(); + } + + @Test + void allReduceWithMap() { + final ResultSet result = database.command("opencypher", + "RETURN allReduce(span = {}, x IN [1, 2, 3] | {previous: span.current, current: x}, " + + "span.previous IS NULL OR span.previous < span.current) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat((Boolean) result.next().getProperty("result")).isTrue(); + } + + // ==================== any() Tests ==================== + + @Test + void anyBasic() { + final ResultSet result = database.command("opencypher", + "RETURN any(x IN [1, 2, 3, 4, 5] WHERE x > 3) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat((Boolean) result.next().getProperty("result")).isTrue(); + } + + @Test + void anyFalse() { + final ResultSet result = database.command("opencypher", + "RETURN any(x IN [1, 2, 3] WHERE x > 5) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat((Boolean) result.next().getProperty("result")).isFalse(); + } + + @Test + void anyEmptyList() { + final ResultSet result = database.command("opencypher", + "WITH [] as emptyList RETURN any(i IN emptyList WHERE true) as result1, any(i IN emptyList WHERE false) as result2"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final var row = result.next(); + assertThat((Boolean) row.getProperty("result1")).isFalse(); + assertThat((Boolean) row.getProperty("result2")).isFalse(); + } + + @Test + void anyWithPath() { + final ResultSet result = database.command("opencypher", + "MATCH p = (n:Person {name: 'Keanu Reeves'})-[:KNOWS]-{3}() " + + "WHERE any(rel IN relationships(p) WHERE rel.since < 2000) " + + "RETURN count(p) AS pathCount"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat(((Number) result.next().getProperty("pathCount")).intValue()).isGreaterThanOrEqualTo(0); + } + + @Test + void anyWithNullList() { + final ResultSet result = database.command("opencypher", + "RETURN any(x IN null WHERE x > 0) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + // ==================== exists() Tests ==================== + + @Test + void existsWithPattern() { + final ResultSet result = database.command("opencypher", + "MATCH (p:Person) " + + "RETURN p.name AS name, EXISTS { (p)-[:ACTED_IN]->() } AS hasActedIn " + + "ORDER BY name"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + // Keanu and Carrie acted in The Matrix + int actorsFound = 0; + while (result.hasNext()) { + final var row = result.next(); + final String name = (String) row.getProperty("name"); + final Boolean hasActedIn = (Boolean) row.getProperty("hasActedIn"); + if ("Keanu Reeves".equals(name) || "Carrie Anne Moss".equals(name)) { + assertThat(hasActedIn).isTrue(); + actorsFound++; + } + } + assertThat(actorsFound).isEqualTo(2); + } + + @Test + void existsNull() { + // exists(null) returns false: null is treated as a non-existent value + final ResultSet result = database.command("opencypher", + "RETURN exists(null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat((Boolean) result.next().getProperty("result")).isFalse(); + } + + // ==================== isEmpty() Tests ==================== + + @Test + void isEmptyString() { + final ResultSet result = database.command("opencypher", + "MATCH (p:Person) WHERE isEmpty(p.address) RETURN p.name AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat((String) result.next().getProperty("result")).isEqualTo("Jessica Chastain"); + } + + @Test + void isEmptyStringFalse() { + final ResultSet result = database.command("opencypher", + "MATCH (p:Person) WHERE NOT isEmpty(p.nationality) RETURN count(p) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat(((Number) result.next().getProperty("result")).intValue()).isEqualTo(5); + } + + @Test + void isEmptyList() { + final ResultSet result = database.command("opencypher", + "RETURN isEmpty([]) AS empty, isEmpty([1, 2, 3]) AS notEmpty"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final var row = result.next(); + assertThat((Boolean) row.getProperty("empty")).isTrue(); + assertThat((Boolean) row.getProperty("notEmpty")).isFalse(); + } + + @Test + void isEmptyMap() { + final ResultSet result = database.command("opencypher", + "RETURN isEmpty({}) AS empty, isEmpty({a: 1}) AS notEmpty"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final var row = result.next(); + assertThat((Boolean) row.getProperty("empty")).isTrue(); + assertThat((Boolean) row.getProperty("notEmpty")).isFalse(); + } + + @Test + void isEmptyNull() { + final ResultSet result = database.command("opencypher", + "RETURN isEmpty(null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + // ==================== none() Tests ==================== + + @Test + void noneBasic() { + final ResultSet result = database.command("opencypher", + "RETURN none(x IN [1, 2, 3] WHERE x > 5) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat((Boolean) result.next().getProperty("result")).isTrue(); + } + + @Test + void noneFalse() { + final ResultSet result = database.command("opencypher", + "RETURN none(x IN [1, 2, 3, 4, 5] WHERE x > 3) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat((Boolean) result.next().getProperty("result")).isFalse(); + } + + @Test + void noneEmptyList() { + final ResultSet result = database.command("opencypher", + "WITH [] as emptyList RETURN none(i IN emptyList WHERE true) as result1, none(i IN emptyList WHERE false) as result2"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final var row = result.next(); + assertThat((Boolean) row.getProperty("result1")).isTrue(); + assertThat((Boolean) row.getProperty("result2")).isTrue(); + } + + @Test + void noneWithPath() { + final ResultSet result = database.command("opencypher", + "MATCH p = (n:Person {name: 'Keanu Reeves'})-[]-{2}() " + + "WHERE none(x IN nodes(p) WHERE x.age > 60) " + + "RETURN count(p) AS pathCount"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat(((Number) result.next().getProperty("pathCount")).intValue()).isGreaterThanOrEqualTo(0); + } + + @Test + void noneWithNullList() { + final ResultSet result = database.command("opencypher", + "RETURN none(x IN null WHERE x > 0) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + // ==================== single() Tests ==================== + + @Test + void singleBasic() { + final ResultSet result = database.command("opencypher", + "RETURN single(x IN [1, 2, 3, 4, 5] WHERE x = 3) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat((Boolean) result.next().getProperty("result")).isTrue(); + } + + @Test + void singleFalseMultiple() { + final ResultSet result = database.command("opencypher", + "RETURN single(x IN [1, 2, 3, 4, 5] WHERE x > 3) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat((Boolean) result.next().getProperty("result")).isFalse(); + } + + @Test + void singleFalseNone() { + final ResultSet result = database.command("opencypher", + "RETURN single(x IN [1, 2, 3] WHERE x > 5) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat((Boolean) result.next().getProperty("result")).isFalse(); + } + + @Test + void singleEmptyList() { + final ResultSet result = database.command("opencypher", + "WITH [] as emptyList RETURN single(i IN emptyList WHERE true) as result1, single(i IN emptyList WHERE false) as result2"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final var row = result.next(); + assertThat((Boolean) row.getProperty("result1")).isFalse(); + assertThat((Boolean) row.getProperty("result2")).isFalse(); + } + + @Test + void singleWithNullList() { + final ResultSet result = database.command("opencypher", + "RETURN single(x IN null WHERE x > 0) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + // ==================== Combined/Integration Tests ==================== + + @Test + void predicatesCombined() { + final ResultSet result = database.command("opencypher", + "WITH [1, 2, 3, 4, 5] AS nums " + + "RETURN all(x IN nums WHERE x > 0) AS allPositive, " + + " any(x IN nums WHERE x > 3) AS anyAbove3, " + + " none(x IN nums WHERE x < 0) AS noneNegative, " + + " single(x IN nums WHERE x = 3) AS singleThree"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final var row = result.next(); + assertThat((Boolean) row.getProperty("allPositive")).isTrue(); + assertThat((Boolean) row.getProperty("anyAbove3")).isTrue(); + assertThat((Boolean) row.getProperty("noneNegative")).isTrue(); + assertThat((Boolean) row.getProperty("singleThree")).isTrue(); + } + + @Test + void predicatesWithIsEmpty() { + final ResultSet result = database.command("opencypher", + "WITH ['', 'hello', 'world'] AS strings " + + "RETURN any(s IN strings WHERE isEmpty(s)) AS anyEmpty, " + + " all(s IN strings WHERE NOT isEmpty(s)) AS allNonEmpty"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final var row = result.next(); + assertThat((Boolean) row.getProperty("anyEmpty")).isTrue(); + assertThat((Boolean) row.getProperty("allNonEmpty")).isFalse(); + } +} diff --git a/engine/src/test/java/com/arcadedb/query/opencypher/functions/OpenCypherScalarFunctionsComprehensiveTest.java b/engine/src/test/java/com/arcadedb/query/opencypher/functions/OpenCypherScalarFunctionsComprehensiveTest.java new file mode 100644 index 0000000000..e1fd360dee --- /dev/null +++ b/engine/src/test/java/com/arcadedb/query/opencypher/functions/OpenCypherScalarFunctionsComprehensiveTest.java @@ -0,0 +1,893 @@ +/* + * Copyright 2021-present Arcade Data Ltd (info@arcadedata.com) + * + * 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 (info@arcadedata.com) + * SPDX-License-Identifier: Apache-2.0 + */ +package com.arcadedb.query.opencypher.functions; + +import com.arcadedb.database.Database; +import com.arcadedb.database.DatabaseFactory; +import com.arcadedb.query.sql.executor.ResultSet; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import org.assertj.core.api.Assertions; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * Comprehensive tests for OpenCypher Scalar functions based on Neo4j Cypher documentation. + * Tests cover: char_length(), character_length(), coalesce(), elementId(), endNode(), + * head(), id(), last(), length(), nullIf(), properties(), randomUUID(), size(), + * startNode(), timestamp(), toBoolean(), toBooleanOrNull(), toFloat(), toFloatOrNull(), + * toInteger(), toIntegerOrNull(), type(), valueType() + */ +class OpenCypherScalarFunctionsComprehensiveTest { + private Database database; + + @BeforeEach + void setUp() { + final DatabaseFactory factory = new DatabaseFactory("./databases/test-cypher-scalar-functions"); + if (factory.exists()) + factory.open().drop(); + database = factory.create(); + + // Create test graph matching Neo4j documentation + database.getSchema().createVertexType("Developer"); + database.getSchema().createVertexType("Administrator"); + database.getSchema().createVertexType("Designer"); + database.getSchema().createVertexType("Person"); + database.getSchema().createEdgeType("KNOWS"); + database.getSchema().createEdgeType("MARRIED"); + + database.command("opencypher", + "CREATE " + + "(alice:Developer {name:'Alice', age: 38, eyes: 'Brown'}), " + + "(bob:Administrator {name: 'Bob', age: 25, eyes: 'Blue'}), " + + "(charlie:Administrator {name: 'Charlie', age: 53, eyes: 'Green'}), " + + "(daniel:Administrator {name: 'Daniel', age: 54, eyes: 'Brown'}), " + + "(eskil:Designer {name: 'Eskil', age: 41, eyes: 'blue', likedColors: ['Pink', 'Yellow', 'Black']}), " + + "(alice)-[:KNOWS]->(bob), " + + "(alice)-[:KNOWS]->(charlie), " + + "(bob)-[:KNOWS]->(daniel), " + + "(charlie)-[:KNOWS]->(daniel), " + + "(bob)-[:MARRIED]->(eskil)"); + } + + @AfterEach + void tearDown() { + if (database != null) + database.drop(); + } + + // ==================== char_length() Tests ==================== + + @Test + void charLengthBasic() { + final ResultSet result = database.command("opencypher", "RETURN char_length('Alice') AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat(((Number) result.next().getProperty("result")).intValue()).isEqualTo(5); + } + + @Test + void charLengthEmptyString() { + final ResultSet result = database.command("opencypher", "RETURN char_length('') AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat(((Number) result.next().getProperty("result")).intValue()).isEqualTo(0); + } + + @Test + void charLengthUnicode() { + final ResultSet result = database.command("opencypher", "RETURN char_length('Hello 世界') AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat(((Number) result.next().getProperty("result")).intValue()).isEqualTo(8); + } + + @Test + void charLengthNull() { + final ResultSet result = database.command("opencypher", "RETURN char_length(null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + // ==================== character_length() Tests ==================== + + @Test + void characterLengthBasic() { + final ResultSet result = database.command("opencypher", "RETURN character_length('Alice') AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat(((Number) result.next().getProperty("result")).intValue()).isEqualTo(5); + } + + @Test + void characterLengthNull() { + final ResultSet result = database.command("opencypher", "RETURN character_length(null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + // ==================== coalesce() Tests ==================== + + @Test + void coalesceBasic() { + final ResultSet result = database.command("opencypher", + "MATCH (a) WHERE a.name = 'Alice' RETURN coalesce(a.hairColor, a.eyes) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat((String) result.next().getProperty("result")).isEqualTo("Brown"); + } + + @Test + void coalesceMultipleArgs() { + final ResultSet result = database.command("opencypher", + "RETURN coalesce(null, null, 'third', 'fourth') AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat((String) result.next().getProperty("result")).isEqualTo("third"); + } + + @Test + void coalesceAllNull() { + final ResultSet result = database.command("opencypher", + "RETURN coalesce(null, null, null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + @Test + void coalesceFirstNonNull() { + final ResultSet result = database.command("opencypher", + "RETURN coalesce('first', null, 'third') AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat((String) result.next().getProperty("result")).isEqualTo("first"); + } + + // ==================== elementId() Tests ==================== + + @Test + void elementIdNode() { + final ResultSet result = database.command("opencypher", + "MATCH (n:Developer) RETURN elementId(n) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final String elementId = (String) result.next().getProperty("result"); + assertThat(elementId).isNotNull(); + assertThat(elementId).isNotEmpty(); + } + + @Test + void elementIdRelationship() { + final ResultSet result = database.command("opencypher", + "MATCH (:Developer)-[r]-() RETURN elementId(r) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final String elementId = (String) result.next().getProperty("result"); + assertThat(elementId).isNotNull(); + assertThat(elementId).isNotEmpty(); + } + + @Test + void elementIdNull() { + final ResultSet result = database.command("opencypher", "RETURN elementId(null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + // ==================== endNode() Tests ==================== + + @Test + void endNodeBasic() { + final ResultSet result = database.command("opencypher", + "MATCH (x:Developer)-[r]-() RETURN endNode(r).name AS result ORDER BY result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final var row1 = result.next(); + assertThat((String) row1.getProperty("result")).isIn("Bob", "Charlie"); + } + + @Test + void endNodeProperties() { + final ResultSet result = database.command("opencypher", + "MATCH (x:Developer)-[r]->() RETURN endNode(r).age AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final var row = result.next(); + final Integer age = ((Number) row.getProperty("result")).intValue(); + assertThat(age).isIn(25, 53); + } + + @Test + void endNodeNull() { + final ResultSet result = database.command("opencypher", "RETURN endNode(null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + // ==================== head() Tests ==================== + + @Test + void headBasic() { + final ResultSet result = database.command("opencypher", + "MATCH (a) WHERE a.name = 'Eskil' RETURN head(a.likedColors) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat((String) result.next().getProperty("result")).isEqualTo("Pink"); + } + + @Test + void headEmptyList() { + final ResultSet result = database.command("opencypher", "RETURN head([]) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + @Test + void headNull() { + final ResultSet result = database.command("opencypher", "RETURN head(null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + @Test + void headNullElement() { + final ResultSet result = database.command("opencypher", "RETURN head([null, 'second', 'third']) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + // ==================== id() Tests ==================== + + @Test + void idNode() { + final ResultSet result = database.command("opencypher", "MATCH (a) RETURN id(a) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + while (result.hasNext()) { + final var row = result.next(); + Assertions.assertThat(row.getProperty("result") != null).isTrue(); + } + } + + @Test + void idRelationship() { + final ResultSet result = database.command("opencypher", "MATCH ()-[r]->() RETURN id(r) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + while (result.hasNext()) { + final var row = result.next(); + Assertions.assertThat(row.getProperty("result") != null).isTrue(); + } + } + + @Test + void idNull() { + final ResultSet result = database.command("opencypher", "RETURN id(null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + // ==================== last() Tests ==================== + + @Test + void lastBasic() { + final ResultSet result = database.command("opencypher", + "MATCH (a) WHERE a.name = 'Eskil' RETURN last(a.likedColors) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat((String) result.next().getProperty("result")).isEqualTo("Black"); + } + + @Test + void lastEmptyList() { + final ResultSet result = database.command("opencypher", "RETURN last([]) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + @Test + void lastNull() { + final ResultSet result = database.command("opencypher", "RETURN last(null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + @Test + void lastNullElement() { + final ResultSet result = database.command("opencypher", "RETURN last(['first', 'second', null]) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + // ==================== length() Tests ==================== + + @Test + void lengthPath() { + final ResultSet result = database.command("opencypher", + "MATCH p = (a)-->(b)-->(c) WHERE a.name = 'Alice' RETURN length(p) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + while (result.hasNext()) { + final var row = result.next(); + assertThat(((Number) row.getProperty("result")).intValue()).isEqualTo(2); + } + } + + @Test + void lengthSingleHop() { + final ResultSet result = database.command("opencypher", + "MATCH p = (a)-->(b) WHERE a.name = 'Alice' RETURN length(p) AS result LIMIT 1"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat(((Number) result.next().getProperty("result")).intValue()).isEqualTo(1); + } + + @Test + void lengthNull() { + final ResultSet result = database.command("opencypher", "RETURN length(null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + // ==================== path_length() Tests (alias of length()) ==================== + + @Test + void pathLengthPath() { + final ResultSet result = database.command("opencypher", + "MATCH p = (a)-->(b)-->(c) WHERE a.name = 'Alice' RETURN path_length(p) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + while (result.hasNext()) { + final var row = result.next(); + assertThat(((Number) row.getProperty("result")).intValue()).isEqualTo(2); + } + } + + @Test + void pathLengthSingleHop() { + final ResultSet result = database.command("opencypher", + "MATCH p = (a)-->(b) WHERE a.name = 'Alice' RETURN path_length(p) AS result LIMIT 1"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat(((Number) result.next().getProperty("result")).intValue()).isEqualTo(1); + } + + @Test + void pathLengthNull() { + final ResultSet result = database.command("opencypher", "RETURN path_length(null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + // ==================== nullIf() Tests ==================== + + @Test + void nullIfEqual() { + final ResultSet result = database.command("opencypher", "RETURN nullIf(4, 4) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + @Test + void nullIfNotEqual() { + final ResultSet result = database.command("opencypher", "RETURN nullIf('abc', 'def') AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat((String) result.next().getProperty("result")).isEqualTo("abc"); + } + + @Test + void nullIfStrings() { + final ResultSet result = database.command("opencypher", "RETURN nullIf('same', 'same') AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + @Test + void nullIfWithCoalesce() { + final ResultSet result = database.command("opencypher", + "MATCH (a) WHERE a.name = 'Alice' " + + "RETURN a.name AS name, coalesce(nullIf(a.eyes, 'Brown'), 'Hazel') AS eyeColor"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final var row = result.next(); + assertThat((String) row.getProperty("name")).isEqualTo("Alice"); + assertThat((String) row.getProperty("eyeColor")).isEqualTo("Hazel"); + } + + // ==================== properties() Tests ==================== + + @Test + void propertiesNode() { + final ResultSet result = database.command("opencypher", + "MATCH (p:Developer) WHERE p.name = 'Alice' RETURN properties(p) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + @SuppressWarnings("unchecked") + final Map props = (Map) result.next().getProperty("result"); + assertThat(props).containsEntry("name", "Alice"); + assertThat(props).containsKey("age"); + assertThat(props).containsKey("eyes"); + } + + @Test + void propertiesRelationship() { + final ResultSet result = database.command("opencypher", + "CREATE (a)-[r:TEST {prop1: 'value1', prop2: 42}]->(b) RETURN properties(r) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + @SuppressWarnings("unchecked") + final Map props = (Map) result.next().getProperty("result"); + assertThat(props).containsEntry("prop1", "value1"); + assertThat(props).containsEntry("prop2", 42); + } + + @Test + void propertiesMap() { + final ResultSet result = database.command("opencypher", + "WITH {a: 1, b: 'test'} AS map RETURN properties(map) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + @SuppressWarnings("unchecked") + final Map props = (Map) result.next().getProperty("result"); + assertThat(props).containsEntry("a", 1L); + assertThat(props).containsEntry("b", "test"); + } + + @Test + void propertiesNull() { + final ResultSet result = database.command("opencypher", "RETURN properties(null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + // ==================== randomUUID() Tests ==================== + + @Test + void randomUUIDBasic() { + final ResultSet result = database.command("opencypher", "RETURN randomUUID() AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final String uuid = (String) result.next().getProperty("result"); + assertThat(uuid).isNotNull(); + assertThat(uuid).matches("[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"); + } + + @Test + void randomUUIDUnique() { + final ResultSet result = database.command("opencypher", + "RETURN randomUUID() AS uuid1, randomUUID() AS uuid2"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final var row = result.next(); + final String uuid1 = (String) row.getProperty("uuid1"); + final String uuid2 = (String) row.getProperty("uuid2"); + assertThat(uuid1).isNotEqualTo(uuid2); + } + + // ==================== size() Tests ==================== + + @Test + void sizeList() { + final ResultSet result = database.command("opencypher", "RETURN size(['Alice', 'Bob']) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat(((Number) result.next().getProperty("result")).intValue()).isEqualTo(2); + } + + @Test + void sizeString() { + final ResultSet result = database.command("opencypher", + "MATCH (a) WHERE size(a.name) > 6 RETURN size(a.name) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat(((Number) result.next().getProperty("result")).intValue()).isEqualTo(7); + } + + @Test + void sizeEmptyList() { + final ResultSet result = database.command("opencypher", "RETURN size([]) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat(((Number) result.next().getProperty("result")).intValue()).isEqualTo(0); + } + + @Test + void sizeNull() { + final ResultSet result = database.command("opencypher", "RETURN size(null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + // ==================== startNode() Tests ==================== + + @Test + void startNodeBasic() { + final ResultSet result = database.command("opencypher", + "MATCH (x:Developer)-[r]-() RETURN startNode(r).name AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + while (result.hasNext()) { + final var row = result.next(); + assertThat((String) row.getProperty("result")).isEqualTo("Alice"); + } + } + + @Test + void startNodeProperties() { + final ResultSet result = database.command("opencypher", + "MATCH (x:Developer)-[r]->() RETURN startNode(r).age AS result LIMIT 1"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat(((Number) result.next().getProperty("result")).intValue()).isEqualTo(38); + } + + @Test + void startNodeNull() { + final ResultSet result = database.command("opencypher", "RETURN startNode(null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + // ==================== timestamp() Tests ==================== + + @Test + void timestampBasic() { + final ResultSet result = database.command("opencypher", "RETURN timestamp() AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final Long ts = ((Number) result.next().getProperty("result")).longValue(); + assertThat(ts).isGreaterThan(0L); + assertThat(ts).isLessThan(System.currentTimeMillis() + 1000); + } + + @Test + void timestampConsistency() { + final ResultSet result = database.command("opencypher", + "RETURN timestamp() AS ts1, timestamp() AS ts2"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final var row = result.next(); + final Long ts1 = ((Number) row.getProperty("ts1")).longValue(); + final Long ts2 = ((Number) row.getProperty("ts2")).longValue(); + assertThat(ts1).isEqualTo(ts2); + } + + // ==================== toBoolean() Tests ==================== + + @Test + void toBooleanString() { + final ResultSet result = database.command("opencypher", + "RETURN toBoolean('true') AS t, toBoolean('false') AS f, toBoolean('not a boolean') AS invalid"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final var row = result.next(); + assertThat((Boolean) row.getProperty("t")).isTrue(); + assertThat((Boolean) row.getProperty("f")).isFalse(); + Assertions.assertThat(row.getProperty("invalid") == null).isTrue(); + } + + @Test + void toBooleanInteger() { + final ResultSet result = database.command("opencypher", + "RETURN toBoolean(0) AS zero, toBoolean(1) AS one, toBoolean(42) AS other"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final var row = result.next(); + assertThat((Boolean) row.getProperty("zero")).isFalse(); + assertThat((Boolean) row.getProperty("one")).isTrue(); + assertThat((Boolean) row.getProperty("other")).isTrue(); + } + + @Test + void toBooleanBoolean() { + final ResultSet result = database.command("opencypher", + "RETURN toBoolean(true) AS t, toBoolean(false) AS f"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final var row = result.next(); + assertThat((Boolean) row.getProperty("t")).isTrue(); + assertThat((Boolean) row.getProperty("f")).isFalse(); + } + + @Test + void toBooleanNull() { + final ResultSet result = database.command("opencypher", "RETURN toBoolean(null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + // ==================== toBooleanOrNull() Tests ==================== + + @Test + void toBooleanOrNullValid() { + final ResultSet result = database.command("opencypher", + "RETURN toBooleanOrNull('true') AS t, toBooleanOrNull(0) AS zero"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final var row = result.next(); + assertThat((Boolean) row.getProperty("t")).isTrue(); + assertThat((Boolean) row.getProperty("zero")).isFalse(); + } + + @Test + void toBooleanOrNullInvalid() { + final ResultSet result = database.command("opencypher", + "RETURN toBooleanOrNull('not a boolean') AS str, toBooleanOrNull(1.5) AS float"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final var row = result.next(); + Assertions.assertThat(row.getProperty("str") == null).isTrue(); + Assertions.assertThat(row.getProperty("float") == null).isTrue(); + } + + @Test + void toBooleanOrNullNull() { + final ResultSet result = database.command("opencypher", "RETURN toBooleanOrNull(null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + // ==================== toFloat() Tests ==================== + + @Test + void toFloatString() { + final ResultSet result = database.command("opencypher", + "RETURN toFloat('11.5') AS valid, toFloat('not a number') AS invalid"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final var row = result.next(); + assertThat(((Number) row.getProperty("valid")).doubleValue()).isEqualTo(11.5); + Assertions.assertThat(row.getProperty("invalid") == null).isTrue(); + } + + @Test + void toFloatInteger() { + final ResultSet result = database.command("opencypher", "RETURN toFloat(42) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat(((Number) result.next().getProperty("result")).doubleValue()).isEqualTo(42.0); + } + + @Test + void toFloatFloat() { + final ResultSet result = database.command("opencypher", "RETURN toFloat(3.14) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat(((Number) result.next().getProperty("result")).doubleValue()).isEqualTo(3.14); + } + + @Test + void toFloatNull() { + final ResultSet result = database.command("opencypher", "RETURN toFloat(null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + // ==================== toFloatOrNull() Tests ==================== + + @Test + void toFloatOrNullValid() { + final ResultSet result = database.command("opencypher", + "RETURN toFloatOrNull('11.5') AS str, toFloatOrNull(42) AS int"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final var row = result.next(); + assertThat(((Number) row.getProperty("str")).doubleValue()).isEqualTo(11.5); + assertThat(((Number) row.getProperty("int")).doubleValue()).isEqualTo(42.0); + } + + @Test + void toFloatOrNullInvalid() { + final ResultSet result = database.command("opencypher", + "RETURN toFloatOrNull('not a number') AS str, toFloatOrNull(true) AS bool"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final var row = result.next(); + Assertions.assertThat(row.getProperty("str") == null).isTrue(); + Assertions.assertThat(row.getProperty("bool") == null).isTrue(); + } + + @Test + void toFloatOrNullNull() { + final ResultSet result = database.command("opencypher", "RETURN toFloatOrNull(null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + // ==================== toInteger() Tests ==================== + + @Test + void toIntegerString() { + final ResultSet result = database.command("opencypher", + "RETURN toInteger('42') AS valid, toInteger('not a number') AS invalid"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final var row = result.next(); + assertThat(((Number) row.getProperty("valid")).intValue()).isEqualTo(42); + Assertions.assertThat(row.getProperty("invalid") == null).isTrue(); + } + + @Test + void toIntegerBoolean() { + final ResultSet result = database.command("opencypher", + "RETURN toInteger(true) AS t, toInteger(false) AS f"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final var row = result.next(); + assertThat(((Number) row.getProperty("t")).intValue()).isEqualTo(1); + assertThat(((Number) row.getProperty("f")).intValue()).isEqualTo(0); + } + + @Test + void toIntegerFloat() { + final ResultSet result = database.command("opencypher", "RETURN toInteger(3.14) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat(((Number) result.next().getProperty("result")).intValue()).isEqualTo(3); + } + + @Test + void toIntegerInteger() { + final ResultSet result = database.command("opencypher", "RETURN toInteger(42) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat(((Number) result.next().getProperty("result")).intValue()).isEqualTo(42); + } + + @Test + void toIntegerNull() { + final ResultSet result = database.command("opencypher", "RETURN toInteger(null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + // ==================== toIntegerOrNull() Tests ==================== + + @Test + void toIntegerOrNullValid() { + final ResultSet result = database.command("opencypher", + "RETURN toIntegerOrNull('42') AS str, toIntegerOrNull(true) AS bool"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final var row = result.next(); + assertThat(((Number) row.getProperty("str")).intValue()).isEqualTo(42); + assertThat(((Number) row.getProperty("bool")).intValue()).isEqualTo(1); + } + + @Test + void toIntegerOrNullInvalid() { + final ResultSet result = database.command("opencypher", + "RETURN toIntegerOrNull('not a number') AS str, toIntegerOrNull(['A', 'B', 'C']) AS list"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final var row = result.next(); + Assertions.assertThat(row.getProperty("str") == null).isTrue(); + Assertions.assertThat(row.getProperty("list") == null).isTrue(); + } + + @Test + void toIntegerOrNullNull() { + final ResultSet result = database.command("opencypher", "RETURN toIntegerOrNull(null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + // ==================== type() Tests ==================== + + @Test + void typeBasic() { + final ResultSet result = database.command("opencypher", + "MATCH (n)-[r]->() WHERE n.name = 'Alice' RETURN type(r) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + while (result.hasNext()) { + final var row = result.next(); + assertThat((String) row.getProperty("result")).isEqualTo("KNOWS"); + } + } + + @Test + void typeMultipleTypes() { + final ResultSet result = database.command("opencypher", + "MATCH (n)-[r]->() WHERE n.name = 'Bob' RETURN DISTINCT type(r) AS result ORDER BY result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final var types = new java.util.ArrayList(); + while (result.hasNext()) { + types.add((String) result.next().getProperty("result")); + } + assertThat(types).contains("KNOWS"); + } + + @Test + void typeNull() { + final ResultSet result = database.command("opencypher", "RETURN type(null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + // ==================== valueType() Tests ==================== + + @Test + void valueTypeBasic() { + final ResultSet result = database.command("opencypher", + "UNWIND ['abc', 1, 2.0, true] AS value RETURN valueType(value) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final var types = new java.util.ArrayList(); + while (result.hasNext()) { + types.add((String) result.next().getProperty("result")); + } + assertThat(types).hasSize(4); + assertThat(types).anyMatch(t -> t.contains("STRING")); + assertThat(types).anyMatch(t -> t.contains("INTEGER")); + assertThat(types).anyMatch(t -> t.contains("FLOAT")); + assertThat(types).anyMatch(t -> t.contains("BOOLEAN")); + } + + @Test + void valueTypeList() { + final ResultSet result = database.command("opencypher", + "RETURN valueType([1, 2, 3]) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final String type = (String) result.next().getProperty("result"); + assertThat(type).contains("LIST"); + } + + @Test + void valueTypeNull() { + final ResultSet result = database.command("opencypher", "RETURN valueType(null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final String type = (String) result.next().getProperty("result"); + assertThat(type).containsIgnoringCase("NULL"); + } + + // ==================== Combined/Integration Tests ==================== + + @Test + void sizeAndCharLengthEquivalent() { + final ResultSet result = database.command("opencypher", + "WITH 'Hello World' AS str " + + "RETURN size(str) AS sizeResult, char_length(str) AS charLenResult, character_length(str) AS charLenResult2"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final var row = result.next(); + final Integer sizeResult = ((Number) row.getProperty("sizeResult")).intValue(); + final Integer charLenResult = ((Number) row.getProperty("charLenResult")).intValue(); + final Integer charLenResult2 = ((Number) row.getProperty("charLenResult2")).intValue(); + assertThat(sizeResult).isEqualTo(charLenResult); + assertThat(sizeResult).isEqualTo(charLenResult2); + } + + @Test + void headAndLastOnSameList() { + final ResultSet result = database.command("opencypher", + "WITH ['first', 'middle', 'last'] AS list " + + "RETURN head(list) AS first, last(list) AS last, size(list) AS count"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final var row = result.next(); + assertThat((String) row.getProperty("first")).isEqualTo("first"); + assertThat((String) row.getProperty("last")).isEqualTo("last"); + assertThat(((Number) row.getProperty("count")).intValue()).isEqualTo(3); + } + + @Test + void startAndEndNodeSameRelationship() { + final ResultSet result = database.command("opencypher", + "MATCH (x:Developer)-[r]->(y) " + + "RETURN startNode(r).name AS start, endNode(r).name AS end, type(r) AS relType LIMIT 1"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final var row = result.next(); + assertThat((String) row.getProperty("start")).isEqualTo("Alice"); + assertThat((String) row.getProperty("end")).isIn("Bob", "Charlie"); + assertThat((String) row.getProperty("relType")).isEqualTo("KNOWS"); + } + + @Test + void typeConversionChain() { + final ResultSet result = database.command("opencypher", + "WITH '42' AS str " + + "RETURN str AS original, " + + " toInteger(str) AS asInt, " + + " toFloat(toInteger(str)) AS asFloat, " + + " toBoolean(toInteger(str)) AS asBool"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final var row = result.next(); + assertThat((String) row.getProperty("original")).isEqualTo("42"); + assertThat(((Number) row.getProperty("asInt")).intValue()).isEqualTo(42); + assertThat(((Number) row.getProperty("asFloat")).doubleValue()).isEqualTo(42.0); + assertThat((Boolean) row.getProperty("asBool")).isTrue(); + } + + @Test + void coalesceWithNullIf() { + final ResultSet result = database.command("opencypher", + "MATCH (a) " + + "RETURN a.name AS name, " + + " coalesce(nullIf(a.eyes, 'Brown'), 'Hazel') AS eyeColor " + + "ORDER BY name"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + int brownToHazelCount = 0; + while (result.hasNext()) { + final var row = result.next(); + final String name = (String) row.getProperty("name"); + final String eyeColor = (String) row.getProperty("eyeColor"); + if ("Alice".equals(name) || "Daniel".equals(name)) { + assertThat(eyeColor).isEqualTo("Hazel"); + brownToHazelCount++; + } else { + assertThat(eyeColor).isNotNull(); + } + } + assertThat(brownToHazelCount).isEqualTo(2); + } +} diff --git a/engine/src/test/java/com/arcadedb/query/opencypher/functions/OpenCypherSpatialFunctionsComprehensiveTest.java b/engine/src/test/java/com/arcadedb/query/opencypher/functions/OpenCypherSpatialFunctionsComprehensiveTest.java new file mode 100644 index 0000000000..1d8a90a347 --- /dev/null +++ b/engine/src/test/java/com/arcadedb/query/opencypher/functions/OpenCypherSpatialFunctionsComprehensiveTest.java @@ -0,0 +1,379 @@ +/* + * Copyright 2021-present Arcade Data Ltd (info@arcadedata.com) + * + * 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 (info@arcadedata.com) + * SPDX-License-Identifier: Apache-2.0 + */ +package com.arcadedb.query.opencypher.functions; + +import com.arcadedb.database.Database; +import com.arcadedb.database.DatabaseFactory; +import com.arcadedb.query.sql.executor.Result; +import com.arcadedb.query.sql.executor.ResultSet; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import org.assertj.core.api.Assertions; +import static org.assertj.core.api.Assertions.within; + +/** + * Comprehensive tests for OpenCypher Spatial functions based on Neo4j Cypher documentation. + * Tests cover all spatial functions: point(), point.distance(), point.withinBBox() + */ +class OpenCypherSpatialFunctionsComprehensiveTest { + private Database database; + + @BeforeEach + void setUp() { + final DatabaseFactory factory = new DatabaseFactory("./databases/test-cypher-spatial-functions"); + if (factory.exists()) + factory.open().drop(); + database = factory.create(); + } + + @AfterEach + void tearDown() { + if (database != null) + database.drop(); + } + + // ==================== point() Tests - WGS 84 2D ==================== + + @Test + void pointWGS84_2D_WithLongitudeLatitude() { + final ResultSet result = database.command("opencypher", + "RETURN point({longitude: 56.7, latitude: 12.78}) AS point"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final Object point = result.next().getProperty("point"); + assertThat(point).isNotNull(); + // Verify it's a point with the correct coordinates + assertThat(point.toString()).contains("56.7"); + assertThat(point.toString()).contains("12.78"); + } + + @Test + void pointWGS84_2D_WithXY() { + final ResultSet result = database.command("opencypher", + "RETURN point({x: 56.7, y: 12.78, crs: 'WGS-84'}) AS point"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final Object point = result.next().getProperty("point"); + assertThat(point).isNotNull(); + } + + @Test + void pointWGS84_2D_WithSRID() { + final ResultSet result = database.command("opencypher", + "RETURN point({longitude: 56.7, latitude: 12.78, srid: 4326}) AS point"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final Object point = result.next().getProperty("point"); + assertThat(point).isNotNull(); + } + + // ==================== point() Tests - WGS 84 3D ==================== + + @Test + void pointWGS84_3D_WithLongitudeLatitudeHeight() { + final ResultSet result = database.command("opencypher", + "RETURN point({longitude: 56.7, latitude: 12.78, height: 100.0}) AS point"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final Object point = result.next().getProperty("point"); + assertThat(point).isNotNull(); + assertThat(point.toString()).contains("56.7"); + assertThat(point.toString()).contains("12.78"); + assertThat(point.toString()).contains("100"); + } + + @Test + void pointWGS84_3D_WithXYZ() { + final ResultSet result = database.command("opencypher", + "RETURN point({x: 56.7, y: 12.78, z: 100.0, crs: 'WGS-84-3D'}) AS point"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final Object point = result.next().getProperty("point"); + assertThat(point).isNotNull(); + } + + @Test + void pointWGS84_3D_WithSRID() { + final ResultSet result = database.command("opencypher", + "RETURN point({longitude: 56.7, latitude: 12.78, height: 100.0, srid: 4979}) AS point"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final Object point = result.next().getProperty("point"); + assertThat(point).isNotNull(); + } + + // ==================== point() Tests - Cartesian 2D ==================== + + @Test + void pointCartesian2D_Basic() { + final ResultSet result = database.command("opencypher", + "RETURN point({x: 3.0, y: 4.0}) AS point"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final Object point = result.next().getProperty("point"); + assertThat(point).isNotNull(); + assertThat(point.toString()).contains("3"); + assertThat(point.toString()).contains("4"); + } + + @Test + void pointCartesian2D_WithCRS() { + final ResultSet result = database.command("opencypher", + "RETURN point({x: 3.0, y: 4.0, crs: 'cartesian'}) AS point"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final Object point = result.next().getProperty("point"); + assertThat(point).isNotNull(); + } + + @Test + void pointCartesian2D_WithSRID() { + final ResultSet result = database.command("opencypher", + "RETURN point({x: 3.0, y: 4.0, srid: 7203}) AS point"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final Object point = result.next().getProperty("point"); + assertThat(point).isNotNull(); + } + + // ==================== point() Tests - Cartesian 3D ==================== + + @Test + void pointCartesian3D_Basic() { + final ResultSet result = database.command("opencypher", + "RETURN point({x: 3.0, y: 4.0, z: 5.0}) AS point"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final Object point = result.next().getProperty("point"); + assertThat(point).isNotNull(); + assertThat(point.toString()).contains("3"); + assertThat(point.toString()).contains("4"); + assertThat(point.toString()).contains("5"); + } + + @Test + void pointCartesian3D_WithCRS() { + final ResultSet result = database.command("opencypher", + "RETURN point({x: 3.0, y: 4.0, z: 5.0, crs: 'cartesian-3D'}) AS point"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final Object point = result.next().getProperty("point"); + assertThat(point).isNotNull(); + } + + @Test + void pointCartesian3D_WithSRID() { + final ResultSet result = database.command("opencypher", + "RETURN point({x: 3.0, y: 4.0, z: 5.0, srid: 9157}) AS point"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final Object point = result.next().getProperty("point"); + assertThat(point).isNotNull(); + } + + // ==================== point() Tests - Null Handling ==================== + + @Test + void pointNullHandling() { + final ResultSet result = database.command("opencypher", + "RETURN point(null) AS point"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("point") == null).isTrue(); + } + + @Test + void pointWithNullCoordinate() { + final ResultSet result = database.command("opencypher", + "RETURN point({x: null, y: 4.0}) AS point"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("point") == null).isTrue(); + } + + // ==================== point.distance() Tests ==================== + + @Test + void pointDistanceCartesian2D() { + // Distance between (0,0) and (3,4) should be 5 (Pythagorean theorem) + final ResultSet result = database.command("opencypher", + "RETURN point.distance(point({x: 0.0, y: 0.0}), point({x: 3.0, y: 4.0})) AS distance"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final Double distance = (Double) result.next().getProperty("distance"); + assertThat(distance).isCloseTo(5.0, within(0.001)); + } + + @Test + void pointDistanceCartesian3D() { + // Distance between (0,0,0) and (1,1,1) should be sqrt(3) + final ResultSet result = database.command("opencypher", + "RETURN point.distance(point({x: 0.0, y: 0.0, z: 0.0}), point({x: 1.0, y: 1.0, z: 1.0})) AS distance"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final Double distance = (Double) result.next().getProperty("distance"); + assertThat(distance).isCloseTo(Math.sqrt(3), within(0.001)); + } + + @Test + void pointDistanceWGS84() { + // Geodesic distance between two geographic points + final ResultSet result = database.command("opencypher", + "RETURN point.distance(" + + "point({longitude: 12.564590, latitude: 55.672874}), " + + "point({longitude: 12.994341, latitude: 55.611784})) AS distance"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final Double distance = (Double) result.next().getProperty("distance"); + // Distance should be approximately 28 km (in meters) + assertThat(distance).isGreaterThan(25000.0); + assertThat(distance).isLessThan(32000.0); + } + + @Test + void pointDistanceSamePoint() { + final ResultSet result = database.command("opencypher", + "RETURN point.distance(point({x: 1.0, y: 2.0}), point({x: 1.0, y: 2.0})) AS distance"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final Double distance = (Double) result.next().getProperty("distance"); + assertThat(distance).isCloseTo(0.0, within(0.001)); + } + + @Test + void pointDistanceNullHandling() { + ResultSet result = database.command("opencypher", + "RETURN point.distance(null, point({x: 1.0, y: 2.0})) AS distance"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("distance") == null).isTrue(); + + result = database.command("opencypher", + "RETURN point.distance(point({x: 1.0, y: 2.0}), null) AS distance"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("distance") == null).isTrue(); + } + + @Test + void pointDistanceMixedDimensions() { + // Per Cypher spec, mixing 2D and 3D points returns null + final ResultSet result = database.command("opencypher", + "RETURN point.distance(point({x: 1.0, y: 2.0}), point({x: 1.0, y: 2.0, z: 3.0})) AS distance"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("distance") == null).isTrue(); + } + + // ==================== point.withinBBox() Tests ==================== + + @Test + void pointWithinBBoxInside() { + final ResultSet result = database.command("opencypher", + "RETURN point.withinBBox(" + + "point({x: 5.0, y: 5.0}), " + + "point({x: 0.0, y: 0.0}), " + + "point({x: 10.0, y: 10.0})) AS inside"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final Boolean inside = (Boolean) result.next().getProperty("inside"); + assertThat(inside).isTrue(); + } + + @Test + void pointWithinBBoxOutside() { + final ResultSet result = database.command("opencypher", + "RETURN point.withinBBox(" + + "point({x: 15.0, y: 15.0}), " + + "point({x: 0.0, y: 0.0}), " + + "point({x: 10.0, y: 10.0})) AS inside"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final Boolean inside = (Boolean) result.next().getProperty("inside"); + assertThat(inside).isFalse(); + } + + @Test + void pointWithinBBoxOnEdge() { + final ResultSet result = database.command("opencypher", + "RETURN point.withinBBox(" + + "point({x: 0.0, y: 5.0}), " + + "point({x: 0.0, y: 0.0}), " + + "point({x: 10.0, y: 10.0})) AS inside"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final Boolean inside = (Boolean) result.next().getProperty("inside"); + assertThat(inside).isTrue(); + } + + @Test + void pointWithinBBoxWGS84() { + // Test with geographic coordinates + final ResultSet result = database.command("opencypher", + "RETURN point.withinBBox(" + + "point({longitude: 12.8, latitude: 55.6}), " + + "point({longitude: 12.5, latitude: 55.5}), " + + "point({longitude: 13.0, latitude: 55.7})) AS inside"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final Boolean inside = (Boolean) result.next().getProperty("inside"); + assertThat(inside).isTrue(); + } + + @Test + void pointWithinBBoxNullHandling() { + ResultSet result = database.command("opencypher", + "RETURN point.withinBBox(null, point({x: 0.0, y: 0.0}), point({x: 10.0, y: 10.0})) AS inside"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("inside") == null).isTrue(); + + result = database.command("opencypher", + "RETURN point.withinBBox(point({x: 5.0, y: 5.0}), null, point({x: 10.0, y: 10.0})) AS inside"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("inside") == null).isTrue(); + + result = database.command("opencypher", + "RETURN point.withinBBox(point({x: 5.0, y: 5.0}), point({x: 0.0, y: 0.0}), null) AS inside"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("inside") == null).isTrue(); + } + + // ==================== Combined/Integration Tests ==================== + + @Test + void pointFunctionsCombined() { + // Create two points and verify they're at a specific distance + final ResultSet result = database.command("opencypher", + "WITH point({x: 0.0, y: 0.0}) AS p1, point({x: 3.0, y: 4.0}) AS p2 " + + "RETURN point.distance(p1, p2) AS dist, " + + "point.withinBBox(p2, point({x: 0.0, y: 0.0}), point({x: 10.0, y: 10.0})) AS inBox"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final Result row = result.next(); + assertThat((Double) row.getProperty("dist")).isCloseTo(5.0, within(0.001)); + assertThat((Boolean) row.getProperty("inBox")).isTrue(); + } + + @Test + void pointStoredInNode() { + database.getSchema().createVertexType("Location"); + database.command("opencypher", + "CREATE (loc:Location {name: 'Copenhagen', position: point({longitude: 12.564590, latitude: 55.672874})})"); + + final ResultSet result = database.command("opencypher", + "MATCH (loc:Location {name: 'Copenhagen'}) RETURN loc.position AS point"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final Object point = result.next().getProperty("point"); + assertThat(point).isNotNull(); + } + + @Test + void pointDistanceBetweenNodes() { + database.getSchema().createVertexType("Location"); + database.command("opencypher", + "CREATE (a:Location {name: 'A', pos: point({x: 0.0, y: 0.0})}), " + + "(b:Location {name: 'B', pos: point({x: 3.0, y: 4.0})})"); + + final ResultSet result = database.command("opencypher", + "MATCH (a:Location {name: 'A'}), (b:Location {name: 'B'}) " + + "RETURN point.distance(a.pos, b.pos) AS distance"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final Double distance = (Double) result.next().getProperty("distance"); + assertThat(distance).isCloseTo(5.0, within(0.001)); + } +} diff --git a/engine/src/test/java/com/arcadedb/query/opencypher/functions/OpenCypherStringFunctionsComprehensiveTest.java b/engine/src/test/java/com/arcadedb/query/opencypher/functions/OpenCypherStringFunctionsComprehensiveTest.java new file mode 100644 index 0000000000..f2f0b24638 --- /dev/null +++ b/engine/src/test/java/com/arcadedb/query/opencypher/functions/OpenCypherStringFunctionsComprehensiveTest.java @@ -0,0 +1,699 @@ +/* + * Copyright 2021-present Arcade Data Ltd (info@arcadedata.com) + * + * 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 (info@arcadedata.com) + * SPDX-License-Identifier: Apache-2.0 + */ +package com.arcadedb.query.opencypher.functions; + +import com.arcadedb.database.Database; +import com.arcadedb.database.DatabaseFactory; +import com.arcadedb.query.sql.executor.ResultSet; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import org.assertj.core.api.Assertions; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * Comprehensive tests for OpenCypher String functions based on Neo4j Cypher documentation. + * Tests cover all 18 string functions with their various parameters and edge cases. + */ +class OpenCypherStringFunctionsComprehensiveTest { + private Database database; + + @BeforeEach + void setUp() { + final DatabaseFactory factory = new DatabaseFactory("./databases/test-cypher-string-functions"); + if (factory.exists()) + factory.open().drop(); + database = factory.create(); + } + + @AfterEach + void tearDown() { + if (database != null) + database.drop(); + } + + // ==================== btrim() Tests ==================== + + @Test + void btrimBasicWhitespace() { + final ResultSet result = database.command("opencypher", "RETURN btrim(' hello ') AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat((String) result.next().getProperty("result")).isEqualTo("hello"); + } + + @Test + void btrimWithCustomCharacter() { + final ResultSet result = database.command("opencypher", "RETURN btrim('xxyyhelloxyxy', 'xy') AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat((String) result.next().getProperty("result")).isEqualTo("hello"); + } + + @Test + void btrimNullHandling() { + ResultSet result = database.command("opencypher", "RETURN btrim(null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + + result = database.command("opencypher", "RETURN btrim(null, null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + + result = database.command("opencypher", "RETURN btrim('hello', null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + + result = database.command("opencypher", "RETURN btrim(null, ' ') AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + @Test + void btrimEdgeCases() { + ResultSet result = database.command("opencypher", "RETURN btrim('') AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat((String) result.next().getProperty("result")).isEqualTo(""); + + result = database.command("opencypher", "RETURN btrim('hello') AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat((String) result.next().getProperty("result")).isEqualTo("hello"); + + result = database.command("opencypher", "RETURN btrim(' ') AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat((String) result.next().getProperty("result")).isEqualTo(""); + } + + // ==================== left() Tests ==================== + + @Test + void leftBasic() { + final ResultSet result = database.command("opencypher", "RETURN left('hello', 3) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat((String) result.next().getProperty("result")).isEqualTo("hel"); + } + + @Test + void leftExceedsLength() { + final ResultSet result = database.command("opencypher", "RETURN left('hi', 10) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat((String) result.next().getProperty("result")).isEqualTo("hi"); + } + + @Test + void leftZeroLength() { + final ResultSet result = database.command("opencypher", "RETURN left('hello', 0) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat((String) result.next().getProperty("result")).isEqualTo(""); + } + + @Test + void leftNullHandling() { + ResultSet result = database.command("opencypher", "RETURN left(null, 3) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + + result = database.command("opencypher", "RETURN left(null, null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + @Test + void leftNullLengthReturnsNull() { + // Neo4j returns null when length is null + final ResultSet result = database.command("opencypher", "RETURN left('hello', null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + @Test + void leftNegativeLengthRaisesError() { + assertThatThrownBy(() -> database.command("opencypher", "RETURN left('hello', -1) AS result").next()) + .hasMessageContaining("negative"); + } + + // ==================== lower() and toLower() Tests ==================== + + @Test + void lowerBasic() { + final ResultSet result = database.command("opencypher", "RETURN lower('HELLO') AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat((String) result.next().getProperty("result")).isEqualTo("hello"); + } + + @Test + void toLowerBasic() { + final ResultSet result = database.command("opencypher", "RETURN toLower('HELLO') AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat((String) result.next().getProperty("result")).isEqualTo("hello"); + } + + @Test + void lowerNullHandling() { + final ResultSet result = database.command("opencypher", "RETURN lower(null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + @Test + void lowerMixedCase() { + final ResultSet result = database.command("opencypher", "RETURN lower('HeLLo WoRLd') AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat((String) result.next().getProperty("result")).isEqualTo("hello world"); + } + + // ==================== ltrim() Tests ==================== + + @Test + void ltrimBasicWhitespace() { + final ResultSet result = database.command("opencypher", "RETURN ltrim(' hello') AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat((String) result.next().getProperty("result")).isEqualTo("hello"); + } + + @Test + void ltrimWithCustomCharacter() { + final ResultSet result = database.command("opencypher", "RETURN ltrim('xxyyhelloxyxy', 'xy') AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat((String) result.next().getProperty("result")).isEqualTo("helloxyxy"); + } + + @Test + void ltrimNullHandling() { + ResultSet result = database.command("opencypher", "RETURN ltrim(null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + + result = database.command("opencypher", "RETURN ltrim(null, null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + + result = database.command("opencypher", "RETURN ltrim('hello', null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + + result = database.command("opencypher", "RETURN ltrim(null, ' ') AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + // ==================== normalize() Tests ==================== + + @Test + void normalizeBasicNFC() { + // Unicode normalization: Angstrom sign (\u212B) should equal Latin capital letter A with ring above (\u00C5) + final ResultSet result = database.command("opencypher", "RETURN normalize('\\u212B') = '\\u00C5' AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat((Boolean) result.next().getProperty("result")).isTrue(); + } + + @Test + void normalizeWithNFKC() { + // Compatibility normalization: Small less-than sign (\uFE64) normalizes to less-than sign (\u003C) + final ResultSet result = database.command("opencypher", "RETURN normalize('\\uFE64', NFKC) = '\\u003C' AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat((Boolean) result.next().getProperty("result")).isTrue(); + } + + @Test + void normalizeWithNFD() { + final ResultSet result = database.command("opencypher", "RETURN normalize('é', NFD) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") != null).isTrue(); + } + + @Test + void normalizeWithNFKD() { + final ResultSet result = database.command("opencypher", "RETURN normalize('test', NFKD) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat((String) result.next().getProperty("result")).isEqualTo("test"); + } + + @Test + void normalizeNullHandling() { + final ResultSet result = database.command("opencypher", "RETURN normalize(null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + // ==================== replace() Tests ==================== + + @Test + void replaceBasic() { + final ResultSet result = database.command("opencypher", "RETURN replace('hello', 'l', 'w') AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat((String) result.next().getProperty("result")).isEqualTo("hewwo"); + } + + @Test + void replaceWithLimit() { + // Neo4j throws "Too many parameters for function 'replace'" - 4-arg form not supported + assertThatThrownBy(() -> database.command("opencypher", "RETURN replace('hello', 'l', 'w', 1) AS result").next()) + .isInstanceOf(Exception.class); + } + + @Test + void replaceNotFound() { + final ResultSet result = database.command("opencypher", "RETURN replace('hello', 'x', 'y') AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat((String) result.next().getProperty("result")).isEqualTo("hello"); + } + + @Test + void replaceNullHandling() { + ResultSet result = database.command("opencypher", "RETURN replace(null, 'a', 'b') AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + + result = database.command("opencypher", "RETURN replace('hello', null, 'b') AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + + result = database.command("opencypher", "RETURN replace('hello', 'a', null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + @Test + void replaceMultipleOccurrences() { + final ResultSet result = database.command("opencypher", "RETURN replace('banana', 'a', 'o') AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat((String) result.next().getProperty("result")).isEqualTo("bonono"); + } + + // ==================== reverse() Tests ==================== + + @Test + void reverseBasic() { + final ResultSet result = database.command("opencypher", "RETURN reverse('palindrome') AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat((String) result.next().getProperty("result")).isEqualTo("emordnilap"); + } + + @Test + void reverseSingleCharacter() { + final ResultSet result = database.command("opencypher", "RETURN reverse('a') AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat((String) result.next().getProperty("result")).isEqualTo("a"); + } + + @Test + void reverseEmpty() { + final ResultSet result = database.command("opencypher", "RETURN reverse('') AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat((String) result.next().getProperty("result")).isEqualTo(""); + } + + @Test + void reverseNullHandling() { + final ResultSet result = database.command("opencypher", "RETURN reverse(null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + // ==================== right() Tests ==================== + + @Test + void rightBasic() { + final ResultSet result = database.command("opencypher", "RETURN right('hello', 3) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat((String) result.next().getProperty("result")).isEqualTo("llo"); + } + + @Test + void rightExceedsLength() { + final ResultSet result = database.command("opencypher", "RETURN right('hi', 10) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat((String) result.next().getProperty("result")).isEqualTo("hi"); + } + + @Test + void rightZeroLength() { + final ResultSet result = database.command("opencypher", "RETURN right('hello', 0) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat((String) result.next().getProperty("result")).isEqualTo(""); + } + + @Test + void rightNullHandling() { + ResultSet result = database.command("opencypher", "RETURN right(null, 3) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + + result = database.command("opencypher", "RETURN right(null, null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + @Test + void rightNullLengthReturnsNull() { + // Neo4j returns null when length is null + final ResultSet result = database.command("opencypher", "RETURN right('hello', null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + @Test + void rightNegativeLengthRaisesError() { + assertThatThrownBy(() -> database.command("opencypher", "RETURN right('hello', -1) AS result").next()) + .hasMessageContaining("negative"); + } + + // ==================== rtrim() Tests ==================== + + @Test + void rtrimBasicWhitespace() { + final ResultSet result = database.command("opencypher", "RETURN rtrim('hello ') AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat((String) result.next().getProperty("result")).isEqualTo("hello"); + } + + @Test + void rtrimWithCustomCharacter() { + final ResultSet result = database.command("opencypher", "RETURN rtrim('xxyyhelloxyxy', 'xy') AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat((String) result.next().getProperty("result")).isEqualTo("xxyyhello"); + } + + @Test + void rtrimNullHandling() { + ResultSet result = database.command("opencypher", "RETURN rtrim(null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + + result = database.command("opencypher", "RETURN rtrim(null, null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + + result = database.command("opencypher", "RETURN rtrim('hello', null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + + result = database.command("opencypher", "RETURN rtrim(null, ' ') AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + // ==================== split() Tests ==================== + + @Test + void splitBasic() { + final ResultSet result = database.command("opencypher", "RETURN split('one,two', ',') AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + @SuppressWarnings("unchecked") + final List parts = (List) result.next().getProperty("result"); + assertThat(parts).containsExactly("one", "two"); + } + + @Test + void splitMultipleDelimiters() { + final ResultSet result = database.command("opencypher", "RETURN split('a,b,c,d', ',') AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + @SuppressWarnings("unchecked") + final List parts = (List) result.next().getProperty("result"); + assertThat(parts).containsExactly("a", "b", "c", "d"); + } + + @Test + void splitEmptyString() { + final ResultSet result = database.command("opencypher", "RETURN split('', ',') AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + @SuppressWarnings("unchecked") + final List parts = (List) result.next().getProperty("result"); + assertThat(parts).hasSize(1); + } + + @Test + void splitTrailingDelimiter() { + final ResultSet result = database.command("opencypher", "RETURN split('a,b,', ',') AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + @SuppressWarnings("unchecked") + final List parts = (List) result.next().getProperty("result"); + assertThat(parts).containsExactly("a", "b", ""); + } + + @Test + void splitNullHandling() { + ResultSet result = database.command("opencypher", "RETURN split(null, ',') AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + + result = database.command("opencypher", "RETURN split('hello', null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + // ==================== substring() Tests ==================== + + @Test + void substringBasic() { + final ResultSet result = database.command("opencypher", "RETURN substring('hello', 1, 3) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat((String) result.next().getProperty("result")).isEqualTo("ell"); + } + + @Test + void substringWithoutLength() { + final ResultSet result = database.command("opencypher", "RETURN substring('hello', 2) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat((String) result.next().getProperty("result")).isEqualTo("llo"); + } + + @Test + void substringFromStart() { + final ResultSet result = database.command("opencypher", "RETURN substring('hello', 0, 2) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat((String) result.next().getProperty("result")).isEqualTo("he"); + } + + @Test + void substringZeroLength() { + final ResultSet result = database.command("opencypher", "RETURN substring('hello', 1, 0) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat((String) result.next().getProperty("result")).isEqualTo(""); + } + + @Test + void substringNullHandling() { + final ResultSet result = database.command("opencypher", "RETURN substring(null, 1, 2) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + @Test + void substringNullStartReturnsNull() { + // Neo4j returns null when start is null + final ResultSet result = database.command("opencypher", "RETURN substring('hello', null, 2) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + @Test + void substringNegativeStartRaisesError() { + assertThatThrownBy(() -> database.command("opencypher", "RETURN substring('hello', -1, 2) AS result").next()) + .hasMessageContaining("negative"); + } + + @Test + void substringNegativeLengthRaisesError() { + assertThatThrownBy(() -> database.command("opencypher", "RETURN substring('hello', 1, -1) AS result").next()) + .hasMessageContaining("negative"); + } + + // ==================== toString() Tests ==================== + + @Test + void toStringFromInteger() { + final ResultSet result = database.command("opencypher", "RETURN toString(123) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat((String) result.next().getProperty("result")).isEqualTo("123"); + } + + @Test + void toStringFromFloat() { + final ResultSet result = database.command("opencypher", "RETURN toString(11.5) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat((String) result.next().getProperty("result")).isEqualTo("11.5"); + } + + @Test + void toStringFromBoolean() { + final ResultSet result = database.command("opencypher", "RETURN toString(true) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat((String) result.next().getProperty("result")).isEqualTo("true"); + } + + @Test + void toStringFromString() { + final ResultSet result = database.command("opencypher", "RETURN toString('already a string') AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat((String) result.next().getProperty("result")).isEqualTo("already a string"); + } + + @Test + void toStringNullHandling() { + final ResultSet result = database.command("opencypher", "RETURN toString(null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + // ==================== toStringOrNull() Tests ==================== + + @Test + void toStringOrNullFromInteger() { + final ResultSet result = database.command("opencypher", "RETURN toStringOrNull(123) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat((String) result.next().getProperty("result")).isEqualTo("123"); + } + + @Test + void toStringOrNullFromFloat() { + final ResultSet result = database.command("opencypher", "RETURN toStringOrNull(11.5) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat((String) result.next().getProperty("result")).isEqualTo("11.5"); + } + + @Test + void toStringOrNullFromBoolean() { + final ResultSet result = database.command("opencypher", "RETURN toStringOrNull(true) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat((String) result.next().getProperty("result")).isEqualTo("true"); + } + + @Test + void toStringOrNullFromInvalidType() { + final ResultSet result = database.command("opencypher", "RETURN toStringOrNull(['A', 'B', 'C']) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + @Test + void toStringOrNullNullHandling() { + final ResultSet result = database.command("opencypher", "RETURN toStringOrNull(null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + // ==================== toUpper() and upper() Tests ==================== + + @Test + void toUpperBasic() { + final ResultSet result = database.command("opencypher", "RETURN toUpper('hello') AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat((String) result.next().getProperty("result")).isEqualTo("HELLO"); + } + + @Test + void upperBasic() { + final ResultSet result = database.command("opencypher", "RETURN upper('hello') AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat((String) result.next().getProperty("result")).isEqualTo("HELLO"); + } + + @Test + void toUpperNullHandling() { + final ResultSet result = database.command("opencypher", "RETURN toUpper(null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + @Test + void toUpperMixedCase() { + final ResultSet result = database.command("opencypher", "RETURN toUpper('HeLLo WoRLd') AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat((String) result.next().getProperty("result")).isEqualTo("HELLO WORLD"); + } + + // ==================== trim() Tests ==================== + + @Test + void trimBasicWhitespace() { + final ResultSet result = database.command("opencypher", "RETURN trim(' hello ') AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat((String) result.next().getProperty("result")).isEqualTo("hello"); + } + + @Test + void trimWithBothSpecification() { + final ResultSet result = database.command("opencypher", "RETURN trim(BOTH 'x' FROM 'xxxhelloxxx') AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat((String) result.next().getProperty("result")).isEqualTo("hello"); + } + + @Test + void trimWithLeadingSpecification() { + final ResultSet result = database.command("opencypher", "RETURN trim(LEADING 'x' FROM 'xxxhelloxxx') AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat((String) result.next().getProperty("result")).isEqualTo("helloxxx"); + } + + @Test + void trimWithTrailingSpecification() { + final ResultSet result = database.command("opencypher", "RETURN trim(TRAILING 'x' FROM 'xxxhelloxxx') AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat((String) result.next().getProperty("result")).isEqualTo("xxxhello"); + } + + @Test + void trimNullHandling() { + ResultSet result = database.command("opencypher", "RETURN trim(null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + + result = database.command("opencypher", "RETURN trim(' ' FROM null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + + result = database.command("opencypher", "RETURN trim(null FROM 'hello') AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + + result = database.command("opencypher", "RETURN trim(BOTH null FROM null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + // ==================== Combined/Integration Tests ==================== + + @Test + void stringFunctionsCombination() { + final ResultSet result = database.command("opencypher", + "RETURN toUpper(left('hello world', 5)) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat((String) result.next().getProperty("result")).isEqualTo("HELLO"); + } + + @Test + void stringFunctionsChaining() { + final ResultSet result = database.command("opencypher", + "RETURN trim(toLower(' HELLO WORLD ')) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat((String) result.next().getProperty("result")).isEqualTo("hello world"); + } + + @Test + void stringFunctionsWithReplace() { + final ResultSet result = database.command("opencypher", + "RETURN toUpper(replace('hello world', 'world', 'cypher')) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat((String) result.next().getProperty("result")).isEqualTo("HELLO CYPHER"); + } +} diff --git a/engine/src/test/java/com/arcadedb/query/opencypher/functions/OpenCypherTemporalFunctionsComprehensiveTest.java b/engine/src/test/java/com/arcadedb/query/opencypher/functions/OpenCypherTemporalFunctionsComprehensiveTest.java new file mode 100644 index 0000000000..2ee86e8965 --- /dev/null +++ b/engine/src/test/java/com/arcadedb/query/opencypher/functions/OpenCypherTemporalFunctionsComprehensiveTest.java @@ -0,0 +1,1101 @@ +/* + * Copyright © 2021-present Arcade Data Ltd (info@arcadedata.com) + * + * 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. + */ +package com.arcadedb.query.opencypher.functions; + +import com.arcadedb.database.Database; +import com.arcadedb.database.DatabaseFactory; +import com.arcadedb.query.sql.executor.ResultSet; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import org.assertj.core.api.Assertions; + +/** + * Comprehensive tests for OpenCypher Temporal functions based on Neo4j Cypher documentation. + * Tests cover: duration(), duration.between(), duration.inDays(), duration.inMonths(), duration.inSeconds(), + * date(), date.realtime(), date.statement(), date.transaction(), date.truncate(), + * datetime(), datetime.fromEpoch(), datetime.fromEpochMillis(), datetime.realtime(), datetime.statement(), datetime.transaction(), datetime.truncate(), + * localdatetime(), localdatetime.realtime(), localdatetime.statement(), localdatetime.transaction(), localdatetime.truncate(), + * localtime(), localtime.realtime(), localtime.statement(), localtime.transaction(), localtime.truncate(), + * time(), time.realtime(), time.statement(), time.transaction(), time.truncate(), + * format() + */ +class OpenCypherTemporalFunctionsComprehensiveTest { + private Database database; + + @BeforeEach + void setUp() { + final DatabaseFactory factory = new DatabaseFactory("./target/databases/testOpenCypherTemporalFunctions"); + if (factory.exists()) + factory.open().drop(); + database = factory.create(); + } + + @AfterEach + void tearDown() { + if (database != null) + database.drop(); + } + + // ==================== duration() Tests ==================== + + @Test + void durationFromComponents() { + final ResultSet result = database.command("opencypher", + "RETURN duration({days: 14, hours: 16, minutes: 12}) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") != null).isTrue(); + } + + @Test + void durationFromString() { + final ResultSet result = database.command("opencypher", + "RETURN duration('P14DT16H12M') AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") != null).isTrue(); + } + + @Test + void durationWithDecimalComponents() { + final ResultSet result = database.command("opencypher", + "RETURN duration({months: 0.75}) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") != null).isTrue(); + } + + @Test + void durationWithNegativeComponents() { + final ResultSet result = database.command("opencypher", + "RETURN duration({days: -5, hours: -3}) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") != null).isTrue(); + } + + @Test + void durationNull() { + final ResultSet result = database.command("opencypher", "RETURN duration(null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + // ==================== duration.between() Tests ==================== + + @Test + void durationBetweenDates() { + final ResultSet result = database.command("opencypher", + "RETURN duration.between(date('1984-10-11'), date('1985-11-25')) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") != null).isTrue(); + } + + @Test + void durationBetweenNegative() { + final ResultSet result = database.command("opencypher", + "RETURN duration.between(date('1985-11-25'), date('1984-10-11')) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") != null).isTrue(); + } + + @Test + void durationBetweenDateAndDatetime() { + final ResultSet result = database.command("opencypher", + "RETURN duration.between(date('1984-10-11'), datetime('1984-10-12T21:40:32.142+0100')) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") != null).isTrue(); + } + + @Test + void durationBetweenLocalDatetimes() { + final ResultSet result = database.command("opencypher", + "RETURN duration.between(localdatetime('2015-07-21T21:40:32.142'), localdatetime('2016-07-21T21:45:22.142')) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") != null).isTrue(); + } + + // ==================== duration_between() Tests (alias of duration.between()) ==================== + + @Test + void durationBetweenAliasDates() { + final ResultSet result = database.command("opencypher", + "RETURN duration_between(date('1984-10-11'), date('1985-11-25')) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") != null).isTrue(); + } + + @Test + void durationBetweenAliasNegative() { + final ResultSet result = database.command("opencypher", + "RETURN duration_between(date('1985-11-25'), date('1984-10-11')) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") != null).isTrue(); + } + + @Test + void durationBetweenAliasDateAndDatetime() { + final ResultSet result = database.command("opencypher", + "RETURN duration_between(date('1984-10-11'), datetime('1984-10-12T21:40:32.142+0100')) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") != null).isTrue(); + } + + @Test + void durationBetweenAliasLocalDatetimes() { + final ResultSet result = database.command("opencypher", + "RETURN duration_between(localdatetime('2015-07-21T21:40:32.142'), localdatetime('2016-07-21T21:45:22.142')) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") != null).isTrue(); + } + + // ==================== duration.inDays() Tests ==================== + + @Test + void durationInDaysBasic() { + final ResultSet result = database.command("opencypher", + "RETURN duration.inDays(date('1984-10-11'), date('1985-11-25')) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") != null).isTrue(); + } + + @Test + void durationInDaysNegative() { + final ResultSet result = database.command("opencypher", + "RETURN duration.inDays(date('1985-11-25'), date('1984-10-11')) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") != null).isTrue(); + } + + @Test + void durationInDaysPartialDay() { + final ResultSet result = database.command("opencypher", + "RETURN duration.inDays(date('1984-10-11'), datetime('1984-10-12T21:40:32.142+0100')) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") != null).isTrue(); + } + + // ==================== duration.inMonths() Tests ==================== + + @Test + void durationInMonthsBasic() { + final ResultSet result = database.command("opencypher", + "RETURN duration.inMonths(date('1984-10-11'), date('1985-11-25')) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") != null).isTrue(); + } + + @Test + void durationInMonthsNegative() { + final ResultSet result = database.command("opencypher", + "RETURN duration.inMonths(date('1985-11-25'), date('1984-10-11')) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") != null).isTrue(); + } + + @Test + void durationInMonthsPartialMonth() { + final ResultSet result = database.command("opencypher", + "RETURN duration.inMonths(date('1984-10-11'), datetime('1984-10-12T21:40:32.142+0100')) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") != null).isTrue(); + } + + // ==================== duration.inSeconds() Tests ==================== + + @Test + void durationInSecondsBasic() { + final ResultSet result = database.command("opencypher", + "RETURN duration.inSeconds(date('1984-10-11'), date('1984-10-12')) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") != null).isTrue(); + } + + @Test + void durationInSecondsNegative() { + final ResultSet result = database.command("opencypher", + "RETURN duration.inSeconds(date('1984-10-12'), date('1984-10-11')) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") != null).isTrue(); + } + + @Test + void durationInSecondsWithTime() { + final ResultSet result = database.command("opencypher", + "RETURN duration.inSeconds(date('1984-10-11'), datetime('1984-10-12T01:00:32.142+0100')) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") != null).isTrue(); + } + + // ==================== date() Tests ==================== + + @Test + void dateNow() { + final ResultSet result = database.command("opencypher", "RETURN date() AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") != null).isTrue(); + } + + @Test + void dateFromComponents() { + final ResultSet result = database.command("opencypher", + "RETURN date({year: 1984, month: 10, day: 11}) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") != null).isTrue(); + } + + @Test + void dateFromString() { + final ResultSet result = database.command("opencypher", + "RETURN date('1984-10-11') AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") != null).isTrue(); + } + + @Test + void dateWithDefaultComponents() { + final ResultSet result = database.command("opencypher", + "RETURN date({year: 1984, month: 10}) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") != null).isTrue(); + } + + @Test + void dateWeekBased() { + final ResultSet result = database.command("opencypher", + "RETURN date({year: 1984, week: 10, dayOfWeek: 3}) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") != null).isTrue(); + } + + @Test + void dateQuarterBased() { + final ResultSet result = database.command("opencypher", + "RETURN date({year: 1984, quarter: 3, dayOfQuarter: 45}) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") != null).isTrue(); + } + + @Test + void dateOrdinal() { + final ResultSet result = database.command("opencypher", + "RETURN date({year: 1984, ordinalDay: 202}) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") != null).isTrue(); + } + + @Test + void dateNull() { + final ResultSet result = database.command("opencypher", "RETURN date(null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + // ==================== date.realtime() Tests ==================== + + @Test + void dateRealtime() { + final ResultSet result = database.command("opencypher", "RETURN date.realtime() AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") != null).isTrue(); + } + + @Test + void dateRealtimeWithTimezone() { + final ResultSet result = database.command("opencypher", + "RETURN date.realtime('America/Los_Angeles') AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") != null).isTrue(); + } + + // ==================== date.statement() Tests ==================== + + @Test + void dateStatement() { + final ResultSet result = database.command("opencypher", "RETURN date.statement() AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") != null).isTrue(); + } + + @Test + void dateStatementConsistency() { + final ResultSet result = database.command("opencypher", + "RETURN date.statement() AS d1, date.statement() AS d2"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final var row = result.next(); + Assertions.assertThat(row.getProperty("d1").equals(row.getProperty("d2"))).isTrue(); + } + + // ==================== date.transaction() Tests ==================== + + @Test + void dateTransaction() { + final ResultSet result = database.command("opencypher", "RETURN date.transaction() AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") != null).isTrue(); + } + + @Test + void dateTransactionConsistency() { + final ResultSet result = database.command("opencypher", + "RETURN date.transaction() AS d1, date.transaction() AS d2"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final var row = result.next(); + Assertions.assertThat(row.getProperty("d1").equals(row.getProperty("d2"))).isTrue(); + } + + // ==================== date.truncate() Tests ==================== + + @Test + void dateTruncateYear() { + final ResultSet result = database.command("opencypher", + "RETURN date.truncate('year', date('1984-10-11')) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") != null).isTrue(); + } + + @Test + void dateTruncateMonth() { + final ResultSet result = database.command("opencypher", + "RETURN date.truncate('month', date('1984-10-11')) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") != null).isTrue(); + } + + @Test + void dateTruncateDay() { + final ResultSet result = database.command("opencypher", + "RETURN date.truncate('day', date('1984-10-11')) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") != null).isTrue(); + } + + // ==================== datetime() Tests ==================== + + @Test + void datetimeNow() { + final ResultSet result = database.command("opencypher", "RETURN datetime() AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") != null).isTrue(); + } + + @Test + void datetimeFromComponents() { + final ResultSet result = database.command("opencypher", + "RETURN datetime({year: 1984, month: 10, day: 11, hour: 12, minute: 31, second: 14, timezone: '+01:00'}) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") != null).isTrue(); + } + + @Test + void datetimeFromString() { + final ResultSet result = database.command("opencypher", + "RETURN datetime('2015-07-21T21:40:32.142+0100') AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") != null).isTrue(); + } + + @Test + void datetimeNull() { + final ResultSet result = database.command("opencypher", "RETURN datetime(null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + // ==================== zoned_datetime() Tests (alias of datetime()) ==================== + + @Test + void zonedDatetimeNow() { + final ResultSet result = database.command("opencypher", "RETURN zoned_datetime() AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") != null).isTrue(); + } + + @Test + void zonedDatetimeFromComponents() { + final ResultSet result = database.command("opencypher", + "RETURN zoned_datetime({year: 1984, month: 10, day: 11, hour: 12, minute: 31, second: 14, timezone: '+01:00'}) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") != null).isTrue(); + } + + @Test + void zonedDatetimeFromString() { + final ResultSet result = database.command("opencypher", + "RETURN zoned_datetime('2015-07-21T21:40:32.142+0100') AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") != null).isTrue(); + } + + @Test + void zonedDatetimeNull() { + final ResultSet result = database.command("opencypher", "RETURN zoned_datetime(null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + // ==================== datetime.fromEpoch() Tests ==================== + + @Test + void datetimeFromEpochBasic() { + final ResultSet result = database.command("opencypher", + "RETURN datetime.fromEpoch(0, 0) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") != null).isTrue(); + } + + @Test + void datetimeFromEpochWithNanoseconds() { + final ResultSet result = database.command("opencypher", + "RETURN datetime.fromEpoch(1000000000, 123456789) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") != null).isTrue(); + } + + // ==================== datetime.fromEpochMillis() Tests ==================== + + @Test + void datetimeFromEpochMillisBasic() { + final ResultSet result = database.command("opencypher", + "RETURN datetime.fromEpochMillis(0) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") != null).isTrue(); + } + + @Test + void datetimeFromEpochMillisLargeValue() { + final ResultSet result = database.command("opencypher", + "RETURN datetime.fromEpochMillis(1000000000000) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") != null).isTrue(); + } + + // ==================== datetime.realtime() Tests ==================== + + @Test + void datetimeRealtime() { + final ResultSet result = database.command("opencypher", "RETURN datetime.realtime() AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") != null).isTrue(); + } + + @Test + void datetimeRealtimeWithTimezone() { + final ResultSet result = database.command("opencypher", + "RETURN datetime.realtime('America/Los_Angeles') AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") != null).isTrue(); + } + + // ==================== datetime.statement() Tests ==================== + + @Test + void datetimeStatement() { + final ResultSet result = database.command("opencypher", "RETURN datetime.statement() AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") != null).isTrue(); + } + + @Test + void datetimeStatementConsistency() { + final ResultSet result = database.command("opencypher", + "RETURN datetime.statement() AS dt1, datetime.statement() AS dt2"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final var row = result.next(); + Assertions.assertThat(row.getProperty("dt1").equals(row.getProperty("dt2"))).isTrue(); + } + + // ==================== datetime.transaction() Tests ==================== + + @Test + void datetimeTransaction() { + final ResultSet result = database.command("opencypher", "RETURN datetime.transaction() AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") != null).isTrue(); + } + + @Test + void datetimeTransactionConsistency() { + final ResultSet result = database.command("opencypher", + "RETURN datetime.transaction() AS dt1, datetime.transaction() AS dt2"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final var row = result.next(); + Assertions.assertThat(row.getProperty("dt1").equals(row.getProperty("dt2"))).isTrue(); + } + + // ==================== datetime.truncate() Tests ==================== + + @Test + void datetimeTruncateYear() { + final ResultSet result = database.command("opencypher", + "RETURN datetime.truncate('year', datetime('2015-07-21T21:40:32.142+0100')) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") != null).isTrue(); + } + + @Test + void datetimeTruncateDay() { + final ResultSet result = database.command("opencypher", + "RETURN datetime.truncate('day', datetime('2015-07-21T21:40:32.142+0100')) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") != null).isTrue(); + } + + @Test + void datetimeTruncateHour() { + final ResultSet result = database.command("opencypher", + "RETURN datetime.truncate('hour', datetime('2015-07-21T21:40:32.142+0100')) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") != null).isTrue(); + } + + // ==================== localdatetime() Tests ==================== + + @Test + void localdatetimeNow() { + final ResultSet result = database.command("opencypher", "RETURN localdatetime() AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") != null).isTrue(); + } + + @Test + void localdatetimeFromComponents() { + final ResultSet result = database.command("opencypher", + "RETURN localdatetime({year: 1984, month: 10, day: 11, hour: 12, minute: 31, second: 14}) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") != null).isTrue(); + } + + @Test + void localdatetimeFromString() { + final ResultSet result = database.command("opencypher", + "RETURN localdatetime('2015-07-21T21:40:32.142') AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") != null).isTrue(); + } + + @Test + void localdatetimeNull() { + final ResultSet result = database.command("opencypher", "RETURN localdatetime(null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + // ==================== local_datetime() Tests (alias of localdatetime()) ==================== + + @Test + void localDatetimeNow() { + final ResultSet result = database.command("opencypher", "RETURN local_datetime() AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") != null).isTrue(); + } + + @Test + void localDatetimeFromComponents() { + final ResultSet result = database.command("opencypher", + "RETURN local_datetime({year: 1984, month: 10, day: 11, hour: 12, minute: 31, second: 14}) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") != null).isTrue(); + } + + @Test + void localDatetimeFromString() { + final ResultSet result = database.command("opencypher", + "RETURN local_datetime('2015-07-21T21:40:32.142') AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") != null).isTrue(); + } + + @Test + void localDatetimeNull() { + final ResultSet result = database.command("opencypher", "RETURN local_datetime(null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + // ==================== localdatetime.realtime() Tests ==================== + + @Test + void localdatetimeRealtime() { + final ResultSet result = database.command("opencypher", "RETURN localdatetime.realtime() AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") != null).isTrue(); + } + + @Test + void localdatetimeRealtimeWithTimezone() { + final ResultSet result = database.command("opencypher", + "RETURN localdatetime.realtime('America/Los_Angeles') AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") != null).isTrue(); + } + + // ==================== localdatetime.statement() Tests ==================== + + @Test + void localdatetimeStatement() { + final ResultSet result = database.command("opencypher", "RETURN localdatetime.statement() AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") != null).isTrue(); + } + + @Test + void localdatetimeStatementConsistency() { + final ResultSet result = database.command("opencypher", + "RETURN localdatetime.statement() AS ldt1, localdatetime.statement() AS ldt2"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final var row = result.next(); + Assertions.assertThat(row.getProperty("ldt1").equals(row.getProperty("ldt2"))).isTrue(); + } + + // ==================== localdatetime.transaction() Tests ==================== + + @Test + void localdatetimeTransaction() { + final ResultSet result = database.command("opencypher", "RETURN localdatetime.transaction() AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") != null).isTrue(); + } + + @Test + void localdatetimeTransactionConsistency() { + final ResultSet result = database.command("opencypher", + "RETURN localdatetime.transaction() AS ldt1, localdatetime.transaction() AS ldt2"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final var row = result.next(); + Assertions.assertThat(row.getProperty("ldt1").equals(row.getProperty("ldt2"))).isTrue(); + } + + // ==================== localdatetime.truncate() Tests ==================== + + @Test + void localdatetimeTruncateYear() { + final ResultSet result = database.command("opencypher", + "RETURN localdatetime.truncate('year', localdatetime('2015-07-21T21:40:32.142')) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") != null).isTrue(); + } + + @Test + void localdatetimeTruncateDay() { + final ResultSet result = database.command("opencypher", + "RETURN localdatetime.truncate('day', localdatetime('2015-07-21T21:40:32.142')) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") != null).isTrue(); + } + + @Test + void localdatetimeTruncateMinute() { + final ResultSet result = database.command("opencypher", + "RETURN localdatetime.truncate('minute', localdatetime('2015-07-21T21:40:32.142')) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") != null).isTrue(); + } + + // ==================== localtime() Tests ==================== + + @Test + void localtimeNow() { + final ResultSet result = database.command("opencypher", "RETURN localtime() AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") != null).isTrue(); + } + + @Test + void localtimeFromComponents() { + final ResultSet result = database.command("opencypher", + "RETURN localtime({hour: 12, minute: 31, second: 14}) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") != null).isTrue(); + } + + @Test + void localtimeFromString() { + final ResultSet result = database.command("opencypher", + "RETURN localtime('21:40:32.142') AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") != null).isTrue(); + } + + @Test + void localtimeNull() { + final ResultSet result = database.command("opencypher", "RETURN localtime(null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + // ==================== local_time() Tests (alias of localtime()) ==================== + + @Test + void localTimeNow() { + final ResultSet result = database.command("opencypher", "RETURN local_time() AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") != null).isTrue(); + } + + @Test + void localTimeFromComponents() { + final ResultSet result = database.command("opencypher", + "RETURN local_time({hour: 12, minute: 31, second: 14}) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") != null).isTrue(); + } + + @Test + void localTimeFromString() { + final ResultSet result = database.command("opencypher", + "RETURN local_time('21:40:32.142') AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") != null).isTrue(); + } + + @Test + void localTimeNull() { + final ResultSet result = database.command("opencypher", "RETURN local_time(null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + // ==================== localtime.realtime() Tests ==================== + + @Test + void localtimeRealtime() { + final ResultSet result = database.command("opencypher", "RETURN localtime.realtime() AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") != null).isTrue(); + } + + @Test + void localtimeRealtimeWithTimezone() { + final ResultSet result = database.command("opencypher", + "RETURN localtime.realtime('America/Los_Angeles') AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") != null).isTrue(); + } + + // ==================== localtime.statement() Tests ==================== + + @Test + void localtimeStatement() { + final ResultSet result = database.command("opencypher", "RETURN localtime.statement() AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") != null).isTrue(); + } + + @Test + void localtimeStatementConsistency() { + final ResultSet result = database.command("opencypher", + "RETURN localtime.statement() AS lt1, localtime.statement() AS lt2"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final var row = result.next(); + Assertions.assertThat(row.getProperty("lt1").equals(row.getProperty("lt2"))).isTrue(); + } + + // ==================== localtime.transaction() Tests ==================== + + @Test + void localtimeTransaction() { + final ResultSet result = database.command("opencypher", "RETURN localtime.transaction() AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") != null).isTrue(); + } + + @Test + void localtimeTransactionConsistency() { + final ResultSet result = database.command("opencypher", + "RETURN localtime.transaction() AS lt1, localtime.transaction() AS lt2"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final var row = result.next(); + Assertions.assertThat(row.getProperty("lt1").equals(row.getProperty("lt2"))).isTrue(); + } + + // ==================== localtime.truncate() Tests ==================== + + @Test + void localtimeTruncateHour() { + final ResultSet result = database.command("opencypher", + "RETURN localtime.truncate('hour', localtime('21:40:32.142')) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") != null).isTrue(); + } + + @Test + void localtimeTruncateMinute() { + final ResultSet result = database.command("opencypher", + "RETURN localtime.truncate('minute', localtime('21:40:32.142')) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") != null).isTrue(); + } + + @Test + void localtimeTruncateSecond() { + final ResultSet result = database.command("opencypher", + "RETURN localtime.truncate('second', localtime('21:40:32.142')) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") != null).isTrue(); + } + + // ==================== time() Tests ==================== + + @Test + void timeNow() { + final ResultSet result = database.command("opencypher", "RETURN time() AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") != null).isTrue(); + } + + @Test + void timeFromComponents() { + final ResultSet result = database.command("opencypher", + "RETURN time({hour: 12, minute: 31, second: 14, timezone: '+01:00'}) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") != null).isTrue(); + } + + @Test + void timeFromString() { + final ResultSet result = database.command("opencypher", + "RETURN time('21:40:32.142+0100') AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") != null).isTrue(); + } + + @Test + void timeNull() { + final ResultSet result = database.command("opencypher", "RETURN time(null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + // ==================== zoned_time() Tests (alias of time()) ==================== + + @Test + void zonedTimeNow() { + final ResultSet result = database.command("opencypher", "RETURN zoned_time() AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") != null).isTrue(); + } + + @Test + void zonedTimeFromComponents() { + final ResultSet result = database.command("opencypher", + "RETURN zoned_time({hour: 12, minute: 31, second: 14, timezone: '+01:00'}) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") != null).isTrue(); + } + + @Test + void zonedTimeFromString() { + final ResultSet result = database.command("opencypher", + "RETURN zoned_time('21:40:32.142+0100') AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") != null).isTrue(); + } + + @Test + void zonedTimeNull() { + final ResultSet result = database.command("opencypher", "RETURN zoned_time(null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + // ==================== time.realtime() Tests ==================== + + @Test + void timeRealtime() { + final ResultSet result = database.command("opencypher", "RETURN time.realtime() AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") != null).isTrue(); + } + + @Test + void timeRealtimeWithTimezone() { + final ResultSet result = database.command("opencypher", + "RETURN time.realtime('America/Los_Angeles') AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") != null).isTrue(); + } + + // ==================== time.statement() Tests ==================== + + @Test + void timeStatement() { + final ResultSet result = database.command("opencypher", "RETURN time.statement() AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") != null).isTrue(); + } + + @Test + void timeStatementConsistency() { + final ResultSet result = database.command("opencypher", + "RETURN time.statement() AS t1, time.statement() AS t2"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final var row = result.next(); + Assertions.assertThat(row.getProperty("t1").equals(row.getProperty("t2"))).isTrue(); + } + + // ==================== time.transaction() Tests ==================== + + @Test + void timeTransaction() { + final ResultSet result = database.command("opencypher", "RETURN time.transaction() AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") != null).isTrue(); + } + + @Test + void timeTransactionConsistency() { + final ResultSet result = database.command("opencypher", + "RETURN time.transaction() AS t1, time.transaction() AS t2"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final var row = result.next(); + Assertions.assertThat(row.getProperty("t1").equals(row.getProperty("t2"))).isTrue(); + } + + // ==================== time.truncate() Tests ==================== + + @Test + void timeTruncateHour() { + final ResultSet result = database.command("opencypher", + "RETURN time.truncate('hour', time('21:40:32.142+0100')) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") != null).isTrue(); + } + + @Test + void timeTruncateMinute() { + final ResultSet result = database.command("opencypher", + "RETURN time.truncate('minute', time('21:40:32.142+0100')) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") != null).isTrue(); + } + + @Test + void timeTruncateSecond() { + final ResultSet result = database.command("opencypher", + "RETURN time.truncate('second', time('21:40:32.142+0100')) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") != null).isTrue(); + } + + // ==================== format() Tests ==================== + + @Test + void formatDatetime() { + final ResultSet result = database.command("opencypher", + "WITH datetime('1986-11-18T06:04:45.123456789+01:00') AS dt RETURN format(dt, 'MM/dd/yyyy') AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat((String) result.next().getProperty("result")).isEqualTo("11/18/1986"); + } + + @Test + void formatDatetimeDefault() { + final ResultSet result = database.command("opencypher", + "WITH datetime('1986-11-18T06:04:45.123456789+01:00') AS dt RETURN format(dt) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final String formatted = (String) result.next().getProperty("result"); + assertThat(formatted).isNotNull(); + assertThat(formatted).isNotEmpty(); + } + + @Test + void formatDatetimeComplexPattern() { + final ResultSet result = database.command("opencypher", + "WITH datetime('1986-11-18T06:04:45.123456789+01:00') AS dt RETURN format(dt, 'EEEE, MMMM d, yyyy') AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final String formatted = (String) result.next().getProperty("result"); + assertThat(formatted).isNotNull(); + assertThat(formatted).contains("1986"); + } + + @Test + void formatDate() { + final ResultSet result = database.command("opencypher", + "WITH date('1986-11-18') AS d RETURN format(d, 'yyyy-MM-dd') AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat((String) result.next().getProperty("result")).isEqualTo("1986-11-18"); + } + + @Test + void formatLocaltime() { + final ResultSet result = database.command("opencypher", + "WITH localtime('12:30:45') AS t RETURN format(t, 'HH:mm:ss') AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat((String) result.next().getProperty("result")).isEqualTo("12:30:45"); + } + + @Test + void formatDuration() { + final ResultSet result = database.command("opencypher", + "WITH duration('P1Y2M3DT4H5M6S') AS dur RETURN format(dur) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final String formatted = (String) result.next().getProperty("result"); + assertThat(formatted).isNotNull(); + assertThat(formatted).isNotEmpty(); + } + + @Test + void formatNull() { + final ResultSet result = database.command("opencypher", "RETURN format(null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + // ==================== Combined/Integration Tests ==================== + + @Test + void temporalTypeConversion() { + final ResultSet result = database.command("opencypher", + "WITH datetime('2015-07-21T21:40:32.142+0100') AS dt " + + "RETURN date(dt) AS dateOnly, localtime(dt) AS timeOnly, localdatetime(dt) AS localDt"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final var row = result.next(); + Assertions.assertThat(row.getProperty("dateOnly") != null).isTrue(); + Assertions.assertThat(row.getProperty("timeOnly") != null).isTrue(); + Assertions.assertThat(row.getProperty("localDt") != null).isTrue(); + } + + @Test + void durationArithmetic() { + final ResultSet result = database.command("opencypher", + "WITH duration({days: 10}) AS dur1, duration({hours: 24}) AS dur2 " + + "RETURN dur1 AS d1, dur2 AS d2"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final var row = result.next(); + Assertions.assertThat(row.getProperty("d1") != null).isTrue(); + Assertions.assertThat(row.getProperty("d2") != null).isTrue(); + } + + @Test + void clockConsistencyComparison() { + final ResultSet result = database.command("opencypher", + "RETURN date.statement() AS s1, date.statement() AS s2, " + + "date.transaction() AS t1, date.transaction() AS t2"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final var row = result.next(); + Assertions.assertThat(row.getProperty("s1").equals(row.getProperty("s2"))).isTrue(); + Assertions.assertThat(row.getProperty("t1").equals(row.getProperty("t2"))).isTrue(); + } + + @Test + void truncateAndFormat() { + final ResultSet result = database.command("opencypher", + "WITH datetime('2015-07-21T21:40:32.142+0100') AS dt " + + "RETURN format(datetime.truncate('day', dt), 'yyyy-MM-dd') AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final String formatted = (String) result.next().getProperty("result"); + assertThat(formatted).isEqualTo("2015-07-21"); + } +} diff --git a/engine/src/test/java/com/arcadedb/query/opencypher/functions/OpenCypherVectorFunctionsComprehensiveTest.java b/engine/src/test/java/com/arcadedb/query/opencypher/functions/OpenCypherVectorFunctionsComprehensiveTest.java new file mode 100644 index 0000000000..06fde813e2 --- /dev/null +++ b/engine/src/test/java/com/arcadedb/query/opencypher/functions/OpenCypherVectorFunctionsComprehensiveTest.java @@ -0,0 +1,420 @@ +/* + * Copyright © 2021-present Arcade Data Ltd (info@arcadedata.com) + * + * 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. + */ +package com.arcadedb.query.opencypher.functions; + +import com.arcadedb.database.Database; +import com.arcadedb.database.DatabaseFactory; +import com.arcadedb.query.sql.executor.ResultSet; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import org.assertj.core.api.Assertions; +import static org.assertj.core.api.Assertions.within; + +/** + * Comprehensive tests for OpenCypher Vector functions based on Neo4j Cypher documentation. + * Tests cover: vector(), vector.similarity.cosine(), vector.similarity.euclidean(), + * vector_dimension_count(), vector_distance(), vector_norm() + */ +class OpenCypherVectorFunctionsComprehensiveTest { + private Database database; + + @BeforeEach + void setUp() { + final DatabaseFactory factory = new DatabaseFactory("./target/databases/testOpenCypherVectorFunctions"); + if (factory.exists()) + factory.open().drop(); + database = factory.create(); + } + + @AfterEach + void tearDown() { + if (database != null) + database.drop(); + } + + // ==================== vector() Tests ==================== + + @Test + void vectorFromList() { + final ResultSet result = database.command("opencypher", + "RETURN vector([1, 2, 3], 3, INTEGER) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") != null).isTrue(); + } + + @Test + void vectorFromString() { + final ResultSet result = database.command("opencypher", + "RETURN vector('[1.05000e+00, 0.123, 5]', 3, FLOAT) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") != null).isTrue(); + } + + @Test + void vectorInteger8() { + final ResultSet result = database.command("opencypher", + "RETURN vector([1, 2, 3], 3, INTEGER8) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") != null).isTrue(); + } + + @Test + void vectorFloat32() { + final ResultSet result = database.command("opencypher", + "RETURN vector([1.0, 2.5, 3.7], 3, FLOAT32) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") != null).isTrue(); + } + + @Test + void vectorFloat64() { + final ResultSet result = database.command("opencypher", + "RETURN vector([1.0, 2.5, 3.7], 3, FLOAT64) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") != null).isTrue(); + } + + @Test + void vectorNullValue() { + final ResultSet result = database.command("opencypher", + "RETURN vector(null, 3, FLOAT32) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + @Test + void vectorNullDimension() { + final ResultSet result = database.command("opencypher", + "RETURN vector([1, 2, 3], null, INTEGER8) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + // ==================== vector.similarity.cosine() Tests ==================== + + @Test + void vectorSimilarityCosineIdentical() { + final ResultSet result = database.command("opencypher", + "RETURN vector.similarity.cosine([1, 2, 3], [1, 2, 3]) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final Double similarity = (Double) result.next().getProperty("result"); + assertThat(similarity).isCloseTo(1.0, within(0.0001)); + } + + @Test + void vectorSimilarityCosineOrthogonal() { + final ResultSet result = database.command("opencypher", + "RETURN vector.similarity.cosine([1, 0, 0], [0, 1, 0]) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final Double similarity = (Double) result.next().getProperty("result"); + assertThat(similarity).isCloseTo(0.0, within(0.0001)); + } + + @Test + void vectorSimilarityCosineWithVectorType() { + final ResultSet result = database.command("opencypher", + "RETURN vector.similarity.cosine(vector([1, 2, 3], 3, FLOAT32), vector([1, 2, 4], 3, FLOAT32)) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final Double similarity = (Double) result.next().getProperty("result"); + Assertions.assertThat(similarity != null).isTrue(); + assertThat(similarity).isGreaterThan(0.0); + assertThat(similarity).isLessThanOrEqualTo(1.0); + } + + @Test + void vectorSimilarityCosineNull() { + ResultSet result = database.command("opencypher", + "RETURN vector.similarity.cosine(null, [1, 2, 3]) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + + result = database.command("opencypher", + "RETURN vector.similarity.cosine([1, 2, 3], null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + // ==================== vector.similarity.euclidean() Tests ==================== + + @Test + void vectorSimilarityEuclideanIdentical() { + final ResultSet result = database.command("opencypher", + "RETURN vector.similarity.euclidean([1, 2, 3], [1, 2, 3]) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final Double similarity = (Double) result.next().getProperty("result"); + assertThat(similarity).isCloseTo(1.0, within(0.0001)); + } + + @Test + void vectorSimilarityEuclideanDifferent() { + final ResultSet result = database.command("opencypher", + "RETURN vector.similarity.euclidean([1, 2, 3], [4, 5, 6]) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final Double similarity = (Double) result.next().getProperty("result"); + Assertions.assertThat(similarity != null).isTrue(); + assertThat(similarity).isGreaterThan(0.0); + assertThat(similarity).isLessThan(1.0); + } + + @Test + void vectorSimilarityEuclideanWithVectorType() { + final ResultSet result = database.command("opencypher", + "RETURN vector.similarity.euclidean(vector([1.0, 4.0, 2.0], 3, FLOAT32), vector([3.0, -2.0, 1.0], 3, FLOAT32)) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final Double similarity = (Double) result.next().getProperty("result"); + Assertions.assertThat(similarity != null).isTrue(); + assertThat(similarity).isGreaterThan(0.0); + assertThat(similarity).isLessThanOrEqualTo(1.0); + } + + @Test + void vectorSimilarityEuclideanNull() { + ResultSet result = database.command("opencypher", + "RETURN vector.similarity.euclidean(null, [1, 2, 3]) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + + result = database.command("opencypher", + "RETURN vector.similarity.euclidean([1, 2, 3], null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + // ==================== vector_dimension_count() Tests ==================== + + @Test + void vectorDimensionCountBasic() { + final ResultSet result = database.command("opencypher", + "RETURN vector_dimension_count(vector([1, 2, 3], 3, INTEGER8)) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat(((Number) result.next().getProperty("result")).intValue()).isEqualTo(3); + } + + @Test + void vectorDimensionCountLargeDimension() { + final ResultSet result = database.command("opencypher", + "RETURN vector_dimension_count(vector([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 10, FLOAT32)) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat(((Number) result.next().getProperty("result")).intValue()).isEqualTo(10); + } + + @Test + void vectorDimensionCountSingleDimension() { + final ResultSet result = database.command("opencypher", + "RETURN vector_dimension_count(vector([42], 1, INTEGER)) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat(((Number) result.next().getProperty("result")).intValue()).isEqualTo(1); + } + + // ==================== vector_distance() Tests ==================== + + @Test + void vectorDistanceEuclidean() { + final ResultSet result = database.command("opencypher", + "RETURN vector_distance(vector([1.0, 5.0, 3.0, 6.7], 4, FLOAT32), vector([5.0, 2.5, 3.1, 9.0], 4, FLOAT32), EUCLIDEAN) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final Number distance = (Number) result.next().getProperty("result"); + assertThat(distance.doubleValue()).isCloseTo(5.248, within(0.01)); + } + + @Test + void vectorDistanceEuclideanSquared() { + final ResultSet result = database.command("opencypher", + "RETURN vector_distance(vector([1, 2, 3], 3, INTEGER8), vector([4, 5, 6], 3, INTEGER8), EUCLIDEAN_SQUARED) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final Number distance = (Number) result.next().getProperty("result"); + assertThat(distance.doubleValue()).isCloseTo(27.0, within(0.01)); + } + + @Test + void vectorDistanceManhattan() { + final ResultSet result = database.command("opencypher", + "RETURN vector_distance(vector([1, 2, 3], 3, INTEGER8), vector([4, 5, 6], 3, INTEGER8), MANHATTAN) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final Number distance = (Number) result.next().getProperty("result"); + assertThat(distance.doubleValue()).isCloseTo(9.0, within(0.01)); + } + + @Test + void vectorDistanceCosine() { + final ResultSet result = database.command("opencypher", + "RETURN vector_distance(vector([1, 2, 3], 3, INTEGER8), vector([1, 2, 4], 3, INTEGER8), COSINE) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final Number distance = (Number) result.next().getProperty("result"); + assertThat(distance.doubleValue()).isCloseTo(0.008539, within(0.001)); + } + + @Test + void vectorDistanceDot() { + final ResultSet result = database.command("opencypher", + "RETURN vector_distance(vector([1, 2, 3], 3, INTEGER8), vector([4, 5, 6], 3, INTEGER8), DOT) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final Number distance = (Number) result.next().getProperty("result"); + Assertions.assertThat(distance != null).isTrue(); + } + + @Test + void vectorDistanceHamming() { + final ResultSet result = database.command("opencypher", + "RETURN vector_distance(vector([1, 2, 3], 3, INTEGER8), vector([1, 2, 4], 3, INTEGER8), HAMMING) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final Number distance = (Number) result.next().getProperty("result"); + assertThat(distance.doubleValue()).isCloseTo(1.0, within(0.01)); + } + + @Test + void vectorDistanceIdenticalVectors() { + final ResultSet result = database.command("opencypher", + "RETURN vector_distance(vector([1, 2, 3], 3, INTEGER8), vector([1, 2, 3], 3, INTEGER8), EUCLIDEAN) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final Number distance = (Number) result.next().getProperty("result"); + assertThat(distance.doubleValue()).isCloseTo(0.0, within(0.0001)); + } + + // ==================== vector_norm() Tests ==================== + + @Test + void vectorNormEuclidean() { + final ResultSet result = database.command("opencypher", + "RETURN vector_norm(vector([1.0, 5.0, 3.0, 6.7], 4, FLOAT32), EUCLIDEAN) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final Number norm = (Number) result.next().getProperty("result"); + assertThat(norm.doubleValue()).isCloseTo(8.938, within(0.01)); + } + + @Test + void vectorNormManhattan() { + final ResultSet result = database.command("opencypher", + "RETURN vector_norm(vector([1.0, 5.0, 3.0, 6.7], 4, FLOAT32), MANHATTAN) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final Number norm = (Number) result.next().getProperty("result"); + assertThat(norm.doubleValue()).isCloseTo(15.7, within(0.01)); + } + + @Test + void vectorNormZeroVector() { + final ResultSet result = database.command("opencypher", + "RETURN vector_norm(vector([0, 0, 0], 3, FLOAT32), EUCLIDEAN) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final Number norm = (Number) result.next().getProperty("result"); + assertThat(norm.doubleValue()).isCloseTo(0.0, within(0.0001)); + } + + @Test + void vectorNormUnitVector() { + final ResultSet result = database.command("opencypher", + "RETURN vector_norm(vector([1, 0, 0], 3, FLOAT32), EUCLIDEAN) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final Number norm = (Number) result.next().getProperty("result"); + assertThat(norm.doubleValue()).isCloseTo(1.0, within(0.0001)); + } + + // ==================== Combined/Integration Tests ==================== + + @Test + void vectorSimilarityAndDistanceComparison() { + // For identical vectors, similarity should be 1 and distance should be 0 + final ResultSet result = database.command("opencypher", + "WITH vector([1, 2, 3], 3, FLOAT32) AS v " + + "RETURN vector.similarity.cosine(v, v) AS cosSim, " + + " vector.similarity.euclidean(v, v) AS eucSim, " + + " vector_distance(v, v, EUCLIDEAN) AS eucDist"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final var row = result.next(); + final Number cosSim = (Number) row.getProperty("cosSim"); + final Number eucSim = (Number) row.getProperty("eucSim"); + final Number eucDist = (Number) row.getProperty("eucDist"); + assertThat(cosSim.doubleValue()).isCloseTo(1.0, within(0.0001)); + assertThat(eucSim.doubleValue()).isCloseTo(1.0, within(0.0001)); + assertThat(eucDist.doubleValue()).isCloseTo(0.0, within(0.0001)); + } + + @Test + void vectorDimensionAndSizeConsistency() { + final ResultSet result = database.command("opencypher", + "WITH vector([1, 2, 3, 4, 5], 5, INTEGER) AS v " + + "RETURN vector_dimension_count(v) AS dimCount, size(v) AS sizeResult"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final var row = result.next(); + assertThat(((Number) row.getProperty("dimCount")).intValue()).isEqualTo(5); + assertThat(((Number) row.getProperty("sizeResult")).intValue()).isEqualTo(5); + } + + @Test + void vectorNormAndDistanceRelationship() { + // For a vector, its norm should equal the distance from origin + final ResultSet result = database.command("opencypher", + "WITH vector([3, 4], 2, FLOAT32) AS v " + + "RETURN vector_norm(v, EUCLIDEAN) AS norm, " + + " vector_distance(v, vector([0, 0], 2, FLOAT32), EUCLIDEAN) AS distFromOrigin"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final var row = result.next(); + final Number norm = (Number) row.getProperty("norm"); + final Number distFromOrigin = (Number) row.getProperty("distFromOrigin"); + assertThat(norm.doubleValue()).isCloseTo(distFromOrigin.doubleValue(), within(0.0001)); + } + + @Test + void vectorKNearestNeighbors() { + // Create sample vectors and find k-nearest neighbors + database.command("opencypher", + "CREATE (:Node {id: 1, vector: vector([1.0, 4.0, 2.0], 3, FLOAT32)})"); + database.command("opencypher", + "CREATE (:Node {id: 2, vector: vector([3.0, -2.0, 1.0], 3, FLOAT32)})"); + database.command("opencypher", + "CREATE (:Node {id: 3, vector: vector([2.0, 8.0, 3.0], 3, FLOAT32)})"); + + final ResultSet result = database.command("opencypher", + "MATCH (node:Node) " + + "WITH node, vector.similarity.euclidean([4.0, 5.0, 6.0], node.vector) AS score " + + "RETURN node.id AS id, score " + + "ORDER BY score DESC " + + "LIMIT 2"); + + Assertions.assertThat(result.hasNext() != false).isTrue(); + final var row1 = result.next(); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final var row2 = result.next(); + + // Verify we got 2 results + Assertions.assertThat(row1.getProperty("id") != null).isTrue(); + Assertions.assertThat(row2.getProperty("id") != null).isTrue(); + Assertions.assertThat(row1.getProperty("score") != null).isTrue(); + Assertions.assertThat(row2.getProperty("score") != null).isTrue(); + } + + @Test + void vectorMultipleDistanceMetrics() { + final ResultSet result = database.command("opencypher", + "WITH vector([1, 2, 3], 3, FLOAT32) AS v1, vector([4, 5, 6], 3, FLOAT32) AS v2 " + + "RETURN vector_distance(v1, v2, EUCLIDEAN) AS euclidean, " + + " vector_distance(v1, v2, MANHATTAN) AS manhattan, " + + " vector_distance(v1, v2, COSINE) AS cosine"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + final var row = result.next(); + Assertions.assertThat(row.getProperty("euclidean") != null).isTrue(); + Assertions.assertThat(row.getProperty("manhattan") != null).isTrue(); + Assertions.assertThat(row.getProperty("cosine") != null).isTrue(); + + final Number euclidean = (Number) row.getProperty("euclidean"); + final Number manhattan = (Number) row.getProperty("manhattan"); + assertThat(euclidean.doubleValue()).isGreaterThan(0.0); + assertThat(manhattan.doubleValue()).isGreaterThan(0.0); + } +} diff --git a/engine/src/test/resources/cypher/test_load_csv.csv b/engine/src/test/resources/cypher/test_load_csv.csv new file mode 100644 index 0000000000..b8dac442fd --- /dev/null +++ b/engine/src/test/resources/cypher/test_load_csv.csv @@ -0,0 +1,4 @@ +name,age +Alice,30 +Bob,25 +Charlie,35