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..fc9b9ccad5 --- /dev/null +++ b/engine/src/test/java/com/arcadedb/query/opencypher/functions/OpenCypherAggregatingFunctionsComprehensiveTest.java @@ -0,0 +1,436 @@ +/* + * 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); + } +} 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..8ebf97a0e5 --- /dev/null +++ b/engine/src/test/java/com/arcadedb/query/opencypher/functions/OpenCypherListFunctionsComprehensiveTest.java @@ -0,0 +1,796 @@ +/* + * 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")) + .hasMessageContaining("negative"); + } + + @Test + void collInsertIndexTooLargeRaisesError() { + assertThatThrownBy(() -> database.command("opencypher", "RETURN coll.insert([1, 2], 10, 0) AS result")) + .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() { + final ResultSet result = database.command("opencypher", + "RETURN coll.min([true, 'a', 1, 5.4]) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat((Boolean) result.next().getProperty("result")).isTrue(); + } + + @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")) + .hasMessageContaining("negative"); + } + + @Test + void collRemoveIndexTooLargeRaisesError() { + assertThatThrownBy(() -> database.command("opencypher", "RETURN coll.remove([1, 2], 10) AS result")) + .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/OpenCypherMathLogarithmicFunctionsComprehensiveTest.java b/engine/src/test/java/com/arcadedb/query/opencypher/functions/OpenCypherMathLogarithmicFunctionsComprehensiveTest.java new file mode 100644 index 0000000000..472e6bc656 --- /dev/null +++ b/engine/src/test/java/com/arcadedb/query/opencypher/functions/OpenCypherMathLogarithmicFunctionsComprehensiveTest.java @@ -0,0 +1,350 @@ +/* + * 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(); + } + + // ==================== 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..2ac4688a1e --- /dev/null +++ b/engine/src/test/java/com/arcadedb/query/opencypher/functions/OpenCypherMathNumericFunctionsComprehensiveTest.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.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(); + } + + // ==================== 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..8011de5484 --- /dev/null +++ b/engine/src/test/java/com/arcadedb/query/opencypher/functions/OpenCypherMathTrigonometricFunctionsComprehensiveTest.java @@ -0,0 +1,595 @@ +/* + * 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 cothZeroReturnsNaN() { + final ResultSet result = database.command("opencypher", "RETURN coth(0.0) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat((Double) result.next().getProperty("result")).isNaN(); + } + + @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..a1b79170ca --- /dev/null +++ b/engine/src/test/java/com/arcadedb/query/opencypher/functions/OpenCypherPredicateFunctionsComprehensiveTest.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; + +/** + * 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() { + final ResultSet result = database.command("opencypher", + "RETURN exists(null) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + Assertions.assertThat(result.next().getProperty("result") == null).isTrue(); + } + + // ==================== 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..252bce543d --- /dev/null +++ b/engine/src/test/java/com/arcadedb/query/opencypher/functions/OpenCypherScalarFunctionsComprehensiveTest.java @@ -0,0 +1,865 @@ +/* + * 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(); + } + + // ==================== 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..4d12d21784 --- /dev/null +++ b/engine/src/test/java/com/arcadedb/query/opencypher/functions/OpenCypherSpatialFunctionsComprehensiveTest.java @@ -0,0 +1,370 @@ +/* + * 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 35 km (in meters) + assertThat(distance).isGreaterThan(30000.0); + assertThat(distance).isLessThan(40000.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(); + } + + // ==================== 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..c3395b2348 --- /dev/null +++ b/engine/src/test/java/com/arcadedb/query/opencypher/functions/OpenCypherStringFunctionsComprehensiveTest.java @@ -0,0 +1,684 @@ +/* + * 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 leftNullLengthRaisesError() { + assertThatThrownBy(() -> database.command("opencypher", "RETURN left('hello', null) AS result")) + .hasMessageContaining("null"); + } + + @Test + void leftNegativeLengthRaisesError() { + assertThatThrownBy(() -> database.command("opencypher", "RETURN left('hello', -1) AS result")) + .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() { + final ResultSet result = database.command("opencypher", "RETURN replace('hello', 'l', 'w', 1) AS result"); + Assertions.assertThat(result.hasNext() != false).isTrue(); + assertThat((String) result.next().getProperty("result")).isEqualTo("hewlo"); + } + + @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 rightNullLengthRaisesError() { + assertThatThrownBy(() -> database.command("opencypher", "RETURN right('hello', null) AS result")) + .hasMessageContaining("null"); + } + + @Test + void rightNegativeLengthRaisesError() { + assertThatThrownBy(() -> database.command("opencypher", "RETURN right('hello', -1) AS result")) + .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 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 substringNullStartRaisesError() { + assertThatThrownBy(() -> database.command("opencypher", "RETURN substring('hello', null, 2) AS result")) + .hasMessageContaining("null"); + } + + @Test + void substringNegativeStartRaisesError() { + assertThatThrownBy(() -> database.command("opencypher", "RETURN substring('hello', -1, 2) AS result")) + .hasMessageContaining("negative"); + } + + @Test + void substringNegativeLengthRaisesError() { + assertThatThrownBy(() -> database.command("opencypher", "RETURN substring('hello', 1, -1) AS result")) + .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..9d52f4020d --- /dev/null +++ b/engine/src/test/java/com/arcadedb/query/opencypher/functions/OpenCypherTemporalFunctionsComprehensiveTest.java @@ -0,0 +1,939 @@ +/* + * 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.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(); + } + + // ==================== 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(); + } + + // ==================== 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(); + } + + // ==================== 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(); + } + + // ==================== 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); + } +}