diff --git a/engine/src/main/java/com/arcadedb/query/opencypher/executor/CypherExecutionPlan.java b/engine/src/main/java/com/arcadedb/query/opencypher/executor/CypherExecutionPlan.java index 51c3936a70..d593ed6ffd 100644 --- a/engine/src/main/java/com/arcadedb/query/opencypher/executor/CypherExecutionPlan.java +++ b/engine/src/main/java/com/arcadedb/query/opencypher/executor/CypherExecutionPlan.java @@ -599,32 +599,35 @@ private AbstractExecutionStep buildExecutionStepsWithOrder(final CommandContext if (typeCountStep != null) return typeCountStep; - // Special case: RETURN without MATCH (standalone expressions) - // E.g., RETURN abs(-42), RETURN 1+1 - if (statement.getMatchClauses().isEmpty() && statement.getReturnClause() != null && - clausesInOrder.stream().noneMatch(c -> c.getType() == ClauseEntry.ClauseType.UNWIND)) { - // Create a dummy row to evaluate expressions against - final ResultInternal dummyRow = new ResultInternal(); - final List singleRow = List.of(dummyRow); - - // Return the single row via an initial step - currentStep = new AbstractExecutionStep(context) { - private boolean consumed = false; - - @Override - public ResultSet syncPull(final CommandContext ctx, final int nRecords) { - if (consumed) { - return new IteratorResultSet(List.of().iterator()); + // Special case: Standalone WITH, UNWIND, or RETURN + if (!clausesInOrder.isEmpty()) { + final ClauseEntry.ClauseType firstClauseType = clausesInOrder.get(0).getType(); + if (firstClauseType == ClauseEntry.ClauseType.WITH || + firstClauseType == ClauseEntry.ClauseType.UNWIND || + (firstClauseType == ClauseEntry.ClauseType.RETURN && statement.getMatchClauses().isEmpty())) { + // Create a dummy row to evaluate expressions against + final ResultInternal dummyRow = new ResultInternal(); + final List singleRow = List.of(dummyRow); + + // Return the single row via an initial step + currentStep = new AbstractExecutionStep(context) { + private boolean consumed = false; + + @Override + public ResultSet syncPull(final CommandContext ctx, final int nRecords) { + if (consumed) { + return new IteratorResultSet(List.of().iterator()); + } + consumed = true; + return new IteratorResultSet(singleRow.iterator()); } - consumed = true; - return new IteratorResultSet(singleRow.iterator()); - } - @Override - public String prettyPrint(final int depth, final int indent) { - return " ".repeat(Math.max(0, depth * indent)) + "+ DUMMY ROW (for standalone expressions)"; - } - }; + @Override + public String prettyPrint(final int depth, final int indent) { + return " ".repeat(Math.max(0, depth * indent)) + "+ DUMMY ROW (for standalone expressions)"; + } + }; + } } // Process clauses in order diff --git a/engine/src/main/java/com/arcadedb/query/opencypher/executor/steps/CreateStep.java b/engine/src/main/java/com/arcadedb/query/opencypher/executor/steps/CreateStep.java index 4776f89821..8881219aa9 100644 --- a/engine/src/main/java/com/arcadedb/query/opencypher/executor/steps/CreateStep.java +++ b/engine/src/main/java/com/arcadedb/query/opencypher/executor/steps/CreateStep.java @@ -31,6 +31,7 @@ import com.arcadedb.query.opencypher.ast.NodePattern; import com.arcadedb.query.opencypher.ast.PathPattern; import com.arcadedb.query.opencypher.ast.RelationshipPattern; +import com.arcadedb.query.opencypher.ast.Direction; import com.arcadedb.query.opencypher.parser.CypherASTBuilder; import com.arcadedb.query.sql.executor.*; @@ -205,8 +206,16 @@ private void createPath(final PathPattern pathPattern, final ResultInternal resu // Create relationships between vertices for (int i = 0; i < pathPattern.getRelationshipCount(); i++) { final RelationshipPattern relPattern = pathPattern.getRelationship(i); - final Vertex fromVertex = vertices.get(i); - final Vertex toVertex = vertices.get(i + 1); + final Vertex fromVertex; + final Vertex toVertex; + + if (relPattern.getDirection() == Direction.IN) { + fromVertex = vertices.get(i + 1); + toVertex = vertices.get(i); + } else { + fromVertex = vertices.get(i); + toVertex = vertices.get(i + 1); + } final Edge edge = createEdge(fromVertex, toVertex, relPattern, result); if (relPattern.getVariable() != null) { diff --git a/engine/src/main/java/com/arcadedb/query/opencypher/executor/steps/MergeStep.java b/engine/src/main/java/com/arcadedb/query/opencypher/executor/steps/MergeStep.java index a23cc72c0a..489fb8b404 100644 --- a/engine/src/main/java/com/arcadedb/query/opencypher/executor/steps/MergeStep.java +++ b/engine/src/main/java/com/arcadedb/query/opencypher/executor/steps/MergeStep.java @@ -31,6 +31,7 @@ import com.arcadedb.query.opencypher.ast.NodePattern; import com.arcadedb.query.opencypher.ast.PathPattern; import com.arcadedb.query.opencypher.ast.RelationshipPattern; +import com.arcadedb.query.opencypher.ast.Direction; import com.arcadedb.query.opencypher.ast.SetClause; import com.arcadedb.query.opencypher.executor.CypherFunctionFactory; import com.arcadedb.query.opencypher.executor.ExpressionEvaluator; @@ -278,8 +279,16 @@ private boolean mergePath(final PathPattern pathPattern, final ResultInternal re // Merge relationships between vertices for (int i = 0; i < pathPattern.getRelationshipCount(); i++) { final RelationshipPattern relPattern = pathPattern.getRelationship(i); - final Vertex fromVertex = vertices.get(i); - final Vertex toVertex = vertices.get(i + 1); + final Vertex fromVertex; + final Vertex toVertex; + + if (relPattern.getDirection() == Direction.IN) { + fromVertex = vertices.get(i + 1); + toVertex = vertices.get(i); + } else { + fromVertex = vertices.get(i); + toVertex = vertices.get(i + 1); + } // Try to find existing relationship Edge edge = findEdge(fromVertex, toVertex, relPattern, result); diff --git a/engine/src/test/java/com/arcadedb/query/opencypher/OpenCypherIssuesTest.java b/engine/src/test/java/com/arcadedb/query/opencypher/OpenCypherIssuesTest.java new file mode 100644 index 0000000000..4838a2a49e --- /dev/null +++ b/engine/src/test/java/com/arcadedb/query/opencypher/OpenCypherIssuesTest.java @@ -0,0 +1,61 @@ +/* + * 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; + +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 static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class OpenCypherIssuesTest { + private Database database; + + @BeforeEach + void setUp() { + database = new DatabaseFactory("./target/databases/testopencypher-issues").create(); + } + + @AfterEach + void tearDown() { + if (database != null) { + database.drop(); + database = null; + } + } + + @Test + void unwindWithNestedList() { + final String query = "WITH [[1, 2], [3, 4], []] AS nested\n" + + "UNWIND nested AS x\n" + + "UNWIND x AS y\n" + + "RETURN count(y) AS total"; + final ResultSet result = database.query("opencypher", query); + assertTrue(result.hasNext()); + final Result row = result.next(); + assertEquals(4L, (long) row.getProperty("total")); + assertFalse(result.hasNext()); + } +}